<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Rectcircle Blog</title><link>https://www.rectcircle.cn/</link><description>Recent content in Home on Rectcircle Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><managingEditor>rectcircle96@gmail.com (Rectcircle)</managingEditor><webMaster>rectcircle96@gmail.com (Rectcircle)</webMaster><copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright><lastBuildDate>Wed, 01 May 2019 01:29:29 +0800</lastBuildDate><atom:link href="https://www.rectcircle.cn/index.xml" rel="self" type="application/rss+xml"/><item><title>内容组织</title><link>https://www.rectcircle.cn/series/hugo/content-management/organization/</link><pubDate>Wed, 01 May 2019 01:13:42 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/content-management/organization/</guid><description type="html"><![CDATA[

<h2 id="page-bundles">Page Bundles</h2>

<p>自Hugo 0.32开始，界面和其他资源都会打包到Page Bundles</p>

<p><a href="../page-resources/">页面资源</a> 和 <a href="../image-processing/">图像处理</a> 和本节内容是相关的，阅读这些内容可以得到完整的理解</p>


    <figure class="center" >
        <img src="../1-featured-content-bundles.png"  alt="Hello Friend"   />

        
            <figcaption class="center" >该图显示了3个Bundles。请注意，主页Bundles不能包含其他内容页，但允许包含其他内容（图像等），如果包含了其他内容页，在主页中拿不到相关参数</figcaption>
        
    </figure>



<p>更多参见 <a href="../page-bundles/">Page Bundles</a></p>

<h2 id="内容的组织">内容的组织</h2>

<p>在Hugo中，您的内容应以反映所呈现网站的方式进行组织。</p>

<p>虽然Hugo支持嵌套在任何级别的内容，但顶级（即 <code>content/&lt;DIRECTORIES&gt;</code>）在Hugo中是特殊的，并且被认为是用于确定布局等的内容类型。要阅读有关部分的更多信息，包括如何嵌套它们，请参阅 <a href="../sections/">section</a>。</p>

<p>没有任何其他配置，以下也能工作：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-tree" data-lang="tree">.
└── content
    └── about
    |   └── _index.md  // &lt;- https://example.com/about/
    ├── posts
    |   ├── firstpost.md   // &lt;- https://example.com/posts/firstpost/
    |   ├── happy
    |   |   └── ness.md  // &lt;- https://example.com/posts/happy/ness/
    |   └── secondpost.md  // &lt;- https://example.com/posts/secondpost/
    └── quote
        ├── first.md       // &lt;- https://example.com/quote/first/
        └── second.md      // &lt;- https://example.com/quote/second/</code></pre></div>
<h2 id="hugo中路径分析">Hugo中路径分析</h2>

<p>下面演示了Hugo网站呈现时内容组织与输出URL结构之间的关系。这些示例假设您使用的是 <code>pretty URLs</code>(<a href="https://gohugo.io/content-management/urls/#pretty-urls">https://gohugo.io/content-management/urls/#pretty-urls</a>) ，这是Hugo的默认行为。这些示例还假设您 <a href="../../getting-started/configuration/">站点的配置文件</a> 中存在 <code>baseurl =&quot;https://example.com&quot;</code> 键值。</p>

<h3 id="索引页面-index-md">索引页面：<code>_index.md</code></h3>

<p><code>_index.md</code>在hugo中扮演着特殊的角色。它允许您将 front matter 和内容添加到 list 模板中。list 模板 又包括 <a href="https://gohugo.io/templates/section-templates/">section templates</a>, <a href="https://gohugo.io/templates/taxonomy-templates/">taxonomy templates</a>, <a href="https://gohugo.io/templates/taxonomy-templates/">taxonomy terms templates</a>, <a href="https://gohugo.io/templates/homepage/">homepage template</a>.</p>

<blockquote>
<p><strong>提示</strong>：您可以使用<a href="https://gohugo.io/functions/getpage/"><code>.Site.GetPage</code></a>函数获取<code>_index.md</code>中的内容和元数据的引用。</p>
</blockquote>

<p>您可以为主页保留一个 <code>_index.md</code>，在每个content section，taxonomies 和 taxonomies 中保留一个<code>_index.md</code>。下面显示了一个<code>_index.md</code>的典型位置，该位置包含Hugo网站上 post section 页面的内容和 ront matter 内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.         url
.       ⊢--^-⊣
.        path    slug
.       ⊢--^-⊣⊢---^---⊣
.           filepath
.       ⊢------^------⊣
content/posts/_index.md</pre></div>
<p>在构建时，这将使用关联的值输出到以下目标：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                     url (&#34;/posts/&#34;)
                    ⊢-^-⊣
       baseurl      section (&#34;posts&#34;)
⊢--------^---------⊣⊢-^-⊣
        permalink
⊢----------^-------------⊣
https://example.com/posts/index.html</pre></div>
<p>这些部分可以根据需要嵌套。要理解的重要部分是，为了使节树完全导航，至少最下面的部分需要一个内容文件。（即<code>_index.md</code>）。</p>

<h3 id="sections-中的单个页面">Sections 中的单个页面</h3>

<p>每个Section中的单个内容文件将使用single模板渲染。以下是帖子中单个帖子的示例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                   path (&#34;posts/my-first-hugo-post.md&#34;)
.       ⊢-----------^------------⊣
.      section        slug
.       ⊢-^-⊣⊢--------^----------⊣
content/posts/my-first-hugo-post.md</pre></div>
<p>在构建时，这将使用关联的值输出到以下目标：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                               url (&#34;/posts/my-first-hugo-post/&#34;)
                   ⊢------------^----------⊣
       baseurl     section     slug
⊢--------^--------⊣⊢-^--⊣⊢-------^---------⊣
                 permalink
⊢--------------------^---------------------⊣
https://example.com/posts/my-first-hugo-post/index.html</pre></div>
<h2 id="路径解释">路径解释</h2>

<p>以下概念将在构建输出网站时更深入地了解项目组织与Hugo的默认行为之间的关系。</p>

<h3 id="section"><code>section</code></h3>

<p>默认内容类型由一段内容 section 确定。section由其在 content 目录中的位置决定。section 不能在 front matter 中进行覆盖</p>

<h3 id="slug-1"><code>slug</code> <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></h3>

<p>内容的slug的默认值 是 <code>name.extension</code> 或 <code>name/</code> 之一，slug的值由以下规则决定</p>

<ul>
<li>内容文件的名称（例如，<code>lollapalooza.md</code>）或</li>
<li>在 <code>front matter</code> 中重写</li>
</ul>

<h3 id="path"><code>path</code></h3>

<p>内容的路径由该section的文件路径决定。其中文件路径指：</p>

<ul>
<li>基于在 content 目录中的相对路径 并</li>
<li>排除掉slug</li>
</ul>

<h3 id="url"><code>url</code></h3>

<p>url是内容的相对URL。url：</p>

<ul>
<li>基于内容在目录结构中的位置 或</li>
<li>在 <code>front matter</code> 中重写</li>
</ul>

<h2 id="通过front-matter覆盖目标路径">通过Front Matter覆盖目标路径</h2>

<p>hugo认为，您有目的地组织您的内容。用于组织源内容的相同结构用于组织呈现的站点。如上所示，源内容的组织将在目标中进行镜像。</p>

<p>有时您可能需要对内容进行更多控制。在这些情况下，可以在前面的内容中指定字段以确定特定内容的目的地。</p>

<p>出于特定原因，此顺序中定义了以下项目：列表中进一步说明的项目将覆盖之前的项目，并且并非所有这些项目都可以在前面的事项中定义：</p>

<h3 id="filename"><code>filename</code></h3>

<p>这不在front matter中，而是文件的实际名称减去扩展名。这将是目标中文件的名称（例如，<code>content/posts/my-post.md</code>变为<code>example.com/posts/my-post/</code>）</p>

<h3 id="slug"><code>slug</code></h3>

<p>当在front matter定义时，slug可以取代输出的文件名。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
title: New Post
slug: &#34;new-post&#34;
---</code></pre></div>
<p>这将根据Hugo的默认行为将渲染到如下路径：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">example.com/posts/new-post/</pre></div>
<h3 id="section-1"><code>section</code></h3>

<p>section由内容在磁盘上的位置决定，不能在front matter中指定。有关更多信息，请参阅 <a href="../sections/">部分</a></p>

<h3 id="type"><code>type</code></h3>

<p>内容的类型也取决于它在磁盘上的位置，但与section不同，它可以在front matter中指定。参阅 <a href="../types/">type</a>。当您希望使用不同的布局呈现内容时，这可以特别方便。在下面的示例中，您可以在 <code>layouts/new/mylayout.html</code> 中创建一个布局，Hugo将使用该布局来呈现此内容，即使在许多其他帖子中也是如此。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
title: My Post
type: new
layout: mylayout
---</code></pre></div>
<h3 id="url-1">url</h3>

<p>可以提供完整的URL。这将覆盖所有上述内容，因为它与最终输出关联。这必须是baseURL的路径（以/开头）。url将完全按照前面提供的内容使用，并忽略站点配置中的<code>--uglyURLs</code>设置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
title: Old URL
url: /blog/new-url/
---</code></pre></div>
<p>假设您的baseURL配置为 <code>https://example.com</code> ，则在前面添加url将使<code>old-url.md</code> 渲染到如下路径：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">https://example.com/blog/new-url/</pre></div>
<p>您可以在 <a href="../urls/">URL管理</a> 中查看有关如何控制输出路径的更多信息。</p>
<div class="footnotes">

<hr />

<ol>
<li id="fn:1">译者注：经测试，<code>.Slug</code>始终为空字符串，版本:<code>Hugo Static Site Generator v0.55.4/extended darwin/amd64 BuildDate: unknown</code>。<code>.Slug</code> 指的应该是Front Matter中定义的<code>slug</code>。而此处指的是逻辑上的slug
 <a class="footnote-return" href="#fnref:1"><sup>[return]</sup></a></li>
</ol>
</div>
]]></description></item><item><title>安装并使用主题</title><link>https://www.rectcircle.cn/series/hugo/themes/installing-and-using-themes/</link><pubDate>Tue, 30 Apr 2019 19:28:44 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/themes/installing-and-using-themes/</guid><description type="html"><![CDATA[

<p>Hugo目前没有附带“默认”主题。这个决定是故意的。我们由您决定哪个主题最适合您的Hugo项目。</p>

<h2 id="假设">假设</h2>

<ul>
<li>您已在开发计算机上<a href="../../getting-started/installing">安装Hugo</a></li>
<li>您已在计算机上安装了git，并且熟悉基本的git用法</li>
</ul>

<h2 id="安装所有主题">安装所有主题</h2>

<p>您可以通过在工作目录中克隆GitHub上的整个Hugo Theme存储库来安装所有可用的Hugo主题。根据您的互联网连接，所有主题的下载可能需要一段时间。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone --depth <span style="color:#ae81ff">1</span> --recursive https://github.com/gohugoio/hugoThemes.git themes</code></pre></div>
<p>在你使用主题之前，请删除主题目录根目录下的<code>.git</code>目录。如果不，使用git部署将报错</p>

<h2 id="安装单个主题">安装单个主题</h2>

<p>cd 到themes目录下载指定主题</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd themes
git clone URL_TO_THEME</code></pre></div>
<p>以下示例显示如何使用“Hyde”主题，其主题源位于<a href="https://github.com/spf13/hyde：">https://github.com/spf13/hyde：</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd themes
git clone https://github.com/spf13/hyde</code></pre></div>
<p>或者，您可以将主题下载为.zip文件，解压缩主题内容，然后将解压缩的源移动到主题目录中。</p>

<blockquote>
<p>始终查看主题附带的README.md文件。通常，这些文件包含主题设置所需的进一步说明；例如，从示例配置文件复制值。</p>
</blockquote>

<h2 id="主题放置">主题放置</h2>

<p>请确保您已在 <code>/themes</code> 目录中安装了要使用的主题。这是Hugo使用的默认目录。Hugo具有通过您的站点配置中的 <code>themesDir</code> 变量更改主题目录的功能，但不建议这样做。</p>

<h2 id="使用主题">使用主题</h2>

<p>Hugo首先应用已确定的主题，然后应用本地目录中的任何内容。这样可以更轻松地进行自定义，同时保持与主题上游版本的兼容性。要了解更多信息，请转到 <a href="../customizing/">自定义主题</a>。</p>

<h3 id="命令行">命令行</h3>

<p>在Hugo网站上使用主题有两种不同的方法：通过Hugo CLI或作为<a href="../../getting-started/configuration/">站点配置文件</a>的一部分。</p>

<p>要通过Hugo CLI更改主题，您可以在构建站点时传递-t标志：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo -t themename</code></pre></div>
<p>可能，您需要在运行Hugo本地服务器时添加主题，特别是如果您要自定义主题：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo server -t themename</code></pre></div>
<h3 id="config-文件"><code>config</code> 文件</h3>

<p>如果您已经确定了网站的主题并且不想使用命令行，则可以将主题直接添加到站点配置文件中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">theme</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#a6e22e">themename</span></code></pre></div>
<blockquote>
<p>上面示例中的<code>themename</code>必须与<code>/themes</code>中的特定主题目录的名称相匹配；即目录名称，而不是主题展示站点中显示的主题名称。</p>
</blockquote>
]]></description></item><item><title>快速入门</title><link>https://www.rectcircle.cn/series/hugo/getting-started/quick-start/</link><pubDate>Tue, 30 Apr 2019 11:43:56 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/getting-started/quick-start/</guid><description type="html"><![CDATA[

<blockquote>
<p>这个快速入门在示例中使用了macOS。有关如何在其他操作系统上安装Hugo的说明，请参阅 <a href="https://gohugo.io/getting-started/installing">安装</a></p>
</blockquote>

<h2 id="第1步-安装hugo">第1步：安装Hugo</h2>

<blockquote>
<p>Homebrew是macOS的包管理器，可以通过 <a href="https://brew.sh/">brew.sh</a> 安装。如果您正在运行Windows等，请参阅 <a href="https://gohugo.io/getting-started/installing">安装</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install hugo</code></pre></div>
<p>验证</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo version</code></pre></div>
<h2 id="第2步-创建一个新网站">第2步：创建一个新网站</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo new site quickstart</code></pre></div>
<p>以上将在名为quickstart的文件夹中创建一个新的Hugo网站项目</p>

<h2 id="第3步-添加一个主题">第3步：添加一个主题</h2>

<p>请参阅 <a href="https://themes.gohugo.io/">themes.gohugo.io</a> 以获取要考虑的主题列表。本快速入门使用了美丽的Ananke主题。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd quickstart

<span style="color:#75715e"># 下载主题</span>
git init
git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke
<span style="color:#75715e"># 非git用户请注意：</span>
<span style="color:#75715e">#   - 如果您没有安装git，可以从 https://github.com/budparr/gohugo-theme-ananke/archive/master.zip 下载这个主题的最新版本的压缩包</span>
<span style="color:#75715e">#   - 解压缩这个 .zip 文件到 &#34;gohugo-theme-ananke-master&#34; 目录</span>
<span style="color:#75715e">#   - 重命名这个目录为 &#34;ananke&#34; ，并移动到 &#34;themes&#34; 目录</span>

<span style="color:#75715e"># 编辑config.toml配置文件</span>
<span style="color:#75715e"># 并添加Ananke主题</span>
echo <span style="color:#e6db74">&#39;theme = &#34;ananke&#34;&#39;</span> &gt;&gt; config.toml</code></pre></div>
<h2 id="第4步-添加一些content">第4步：添加一些Content</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo new posts/my-first-post.md</code></pre></div>
<p>如果需要，编辑新创建的内容文件</p>

<h2 id="第5步-启动hugo服务器">第5步：启动Hugo服务器</h2>

<p>使用启用草稿选项启动Hugo服务</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">▶ hugo server -D

                   | EN
+------------------+----+
  Pages            | <span style="color:#ae81ff">10</span>
  Paginator pages  |  <span style="color:#ae81ff">0</span>
  Non-page files   |  <span style="color:#ae81ff">0</span>
  Static files     |  <span style="color:#ae81ff">3</span>
  Processed images |  <span style="color:#ae81ff">0</span>
  Aliases          |  <span style="color:#ae81ff">1</span>
  Sitemaps         |  <span style="color:#ae81ff">1</span>
  Cleaned          |  <span style="color:#ae81ff">0</span>

Total in <span style="color:#ae81ff">11</span> ms
Watching <span style="color:#66d9ef">for</span> changes in /Users/bep/quickstart/<span style="color:#f92672">{</span>content,data,layouts,static,themes<span style="color:#f92672">}</span>
Watching <span style="color:#66d9ef">for</span> config changes in /Users/bep/quickstart/config.toml
Environment: <span style="color:#e6db74">&#34;development&#34;</span>
Serving pages from memory
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ <span style="color:#f92672">(</span>bind address <span style="color:#ae81ff">127</span>.0.0.1<span style="color:#f92672">)</span>
Press Ctrl+C to stop</code></pre></div>
<p><strong>访问你的新网站： <a href="http://localhost:1313/">http://localhost:1313/</a></strong></p>

<h2 id="第6步-自定义主题">第6步：自定义主题</h2>

<p>您的新网站看起来很棒，但在向公众发布之前，您需要稍微调整一下</p>

<h3 id="网站配置">网站配置</h3>

<p>在一个文本编辑器中打开 <code>config.toml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://example.org/&#34;</span>
<span style="color:#a6e22e">languageCode</span> = <span style="color:#e6db74">&#34;en-us&#34;</span>
<span style="color:#a6e22e">title</span> = <span style="color:#e6db74">&#34;My New Hugo Site&#34;</span>
<span style="color:#a6e22e">theme</span> = <span style="color:#e6db74">&#34;ananke&#34;</span></code></pre></div>
<p>用更个性化的东西替换上面的标题。此外，如果您已准备好域名，请设置baseURL。请注意，运行本地开发服务器时不需要此值。</p>

<blockquote>
<p>提示：在Hugo服务器运行时对站点配置或站点中的任何其他文件进行更改，您将立即看到浏览器中的更改，但您可能需要清除缓存。</p>
</blockquote>

<p>有关主题特定的配置选项，请参阅 <a href="https://github.com/budparr/gohugo-theme-ananke">主题网站</a></p>

<p><strong>有关进一步的主题自定义，请参阅 <a href="https://gohugo.io/themes/customizing/">自定义主题</a>。</strong></p>
]]></description></item><item><title>Page Bundles</title><link>https://www.rectcircle.cn/series/hugo/content-management/page-bundles/</link><pubDate>Wed, 01 May 2019 01:41:26 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/content-management/page-bundles/</guid><description type="html"><![CDATA[

<p>Page Bundles 是一种对 <a href="https://gohugo.io/content-management/page-resources/">页面资源</a> 进行分组的方法</p>

<p>一个 Page Bundles 有两种类型：</p>

<ul>
<li>Leaf Bundle (叶子意味着没有孩子)</li>
<li>Branch Bundle (主页、section、类别项、类别列表)</li>
</ul>

<table>
<thead>
<tr>
<th></th>
<th>Leaf Bundle</th>
<th>Branch Bundle</th>
</tr>
</thead>

<tbody>
<tr>
<td>用法</td>
<td>单页内容和附件的集合</td>
<td>section页面附件的集合 (home page, section, taxonomy terms, taxonomy list)</td>
</tr>

<tr>
<td>Index文件名</td>
<td><code>index.md</code> <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></td>
<td><code>_index.md</code> <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></td>
</tr>

<tr>
<td>允许的资源</td>
<td>页面或非页面类型</td>
<td>只允许非页面类型</td>
</tr>

<tr>
<td>资源在哪里可以使用？</td>
<td>包含leaf bundle 目录的任一目录层次</td>
<td>仅在branch bundle目录的目录级别，即包含_index.md（<a href="https://discourse.gohugo.io/t/question-about-content-folder-structure/11822/4?u=kaushalmodi">ref</a>）的目录中</td>
</tr>

<tr>
<td>布局类型</td>
<td><code>single</code></td>
<td><code>list</code></td>
</tr>

<tr>
<td>嵌套</td>
<td>不允许在其下嵌套更多的Bundle</td>
<td>允许嵌套 leaf 或 branch bundles</td>
</tr>

<tr>
<td>例子</td>
<td><code>content/posts/my-post/index.md</code></td>
<td><code>content/posts/_index.md</code></td>
</tr>

<tr>
<td>非Index文件的内容的用途</td>
<td>仅作为页面资源访问 <sup class="footnote-ref" id="fnref:2"><a href="#fn:2">2</a></sup></td>
<td>仅作为常规页面访问 <sup class="footnote-ref" id="fnref:3"><a href="#fn:3">3</a></sup></td>
</tr>
</tbody>
</table>

<h2 id="leaf-bundles">Leaf Bundles</h2>

<p>Leaf Bundle是 <code>content/</code> 目录中任何层次结构的目录，只要包含index.md文件。</p>

<h3 id="leaf-bundle-的组织方式的例子">Leaf Bundle 的组织方式的例子</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-tree" data-lang="tree">content/
├── about
│   ├── index.md
├── posts
│   ├── my-post
│   │   ├── content1.md
│   │   ├── content2.md
│   │   ├── image1.jpg
│   │   ├── image2.png
│   │   └── index.md
│   └── my-other-post
│       └── index.md
│
└── another-section
    ├── ..
    └── not-a-leaf-bundle
        ├── ..
        └── another-leaf-bundle
            └── index.md</code></pre></div>
<p>在上面的示例 <code>content/</code> 目录中，有四个leaf bundle：</p>

<dl>
<dt>about</dt>
<dd>位于root级别（直接位于内容目录下），并且只有index.md。</dd>
<dt>my-post</dt>
<dd>具有index.md，另外两个内容Markdown文件和两个图像文件。</dd>
<dt>my-other-post</dt>
<dd>只有index.md。</dd>
<dt>another-leaf-bundle</dt>
<dd>嵌套在几个目录下。该捆绑包也只有index.md。</dd>
</dl>

<blockquote>
<p>创建leaf bundle的层次结构深度无关紧要，只要它不在另一个leaf bundle内</p>
</blockquote>

<h3 id="无头bundle">无头bundle</h3>

<p>无头bundle 是一个配置为不在任何地方发布的 bundle：</p>

<ul>
<li>它没有固定链接（Permalink），也不会渲染到 <code>public</code> 中</li>
<li>它不会是.Site.RegularPages等的一部分。</li>
</ul>

<p>但你可以通过.Site.GetPage获得它。这是一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">{{ $headless := .Site.GetPage &#34;/some-headless-bundle&#34; }}
{{ $reusablePages := $headless.Resources.Match &#34;author*&#34; }}
&lt;<span style="color:#f92672">h2</span>&gt;Authors&lt;/<span style="color:#f92672">h2</span>&gt;
{{ range $reusablePages }}
    &lt;<span style="color:#f92672">h3</span>&gt;{{ .Title }}&lt;/<span style="color:#f92672">h3</span>&gt;
    {{ .Content }}
{{ end }}</code></pre></div>
<p>在这个例子中，我们假设 <code>some-headless-bundle</code> 是一个 无头bundle，包含一个或多个<code>.Name</code> 与 <code>&quot;author *&quot;</code>页面资源。</p>

<p>上面例子的解释：</p>

<ul>
<li>获取 <code>some-headless-bundle</code> Page <code>&quot;object&quot;</code></li>
<li>使用 <code>.Resources.Match</code> 在此页面包中收集与 <code>&quot;author *&quot;</code> 匹配的资源片段</li>
<li>遍历该片段的嵌套页面，并输出它们的<code>.Title</code>和<code>.Content</code></li>
</ul>

<p>通过在Front Matter中添加以下内容（在index.md中），可以使叶子束无头：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">headless</span> = <span style="color:#66d9ef">true</span></code></pre></div>
<blockquote>
<p>只有可以 Leaf Bundles 可以设置为无头</p>
</blockquote>

<p>这种无头页面包很有用，比如：</p>

<ul>
<li>共享媒体画廊</li>
<li>可重复使用的页面内&rdquo;片段&rdquo;</li>
</ul>

<h2 id="branch-bundles">Branch Bundles</h2>

<p>分支包是 <code>content/</code> 目录中任何层次结构的任何目录，至少包含一个 <code>_index.md</code> 文件。</p>

<p>这个 <code>_index.md</code> 也可以直接在 <code>content/</code> 目录下。</p>

<blockquote>
<p>这里以md（markdown）为例。您可以将任何文件类型用作内容资源，只要它是Hugo可识别的内容类型即可。</p>
</blockquote>

<h3 id="branch-bundle组织的示例">Branch Bundle组织的示例</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-tree" data-lang="tree">content/
├── branch-bundle-1
│   ├── branch-content1.md
│   ├── branch-content2.md
│   ├── image1.jpg
│   ├── image2.png
│   └── _index.md
└── branch-bundle-2
    ├── _index.md
    └── a-leaf-bundle
        └── index.md</code></pre></div>
<p>在上面的示例 <code>content/</code> 目录中，有两个 Branch Bundle（和一个 Leaf Bundle）：</p>

<dl>
<dt>branch-bundle-1</dt>
<dd>此 Branch Bundle 具有_index.md，另外两个内容Markdown文件和两个图像文件。</dd>
<dt>branch-bundle-2</dt>
<dd>此 Branch Bundle 具有_index.md，内部嵌套了一个leaf bundle</dd>
</dl>

<blockquote>
<p>创建Branch Bundle的层次结构深度无关紧要。</p>
</blockquote>
<div class="footnotes">

<hr />

<ol>
<li id="fn:1"><code>.md</code> 扩展名只是一个例子。扩展名可以是.html，.json或任何有效的MIME类型。
 <a class="footnote-return" href="#fnref:1"><sup>[return]</sup></a></li>
<li id="fn:2">译者注：非Index的内容实际上也是可以作为普通页面资源访问的，这里的仅作为资源指的是，调用模板在处理index.md时， <code>.Pages</code> 是拿不到其他页面的，除非使用<code>.Resources</code>
 <a class="footnote-return" href="#fnref:2"><sup>[return]</sup></a></li>
<li id="fn:3">译者注：非Index内容在Index内容可以通过 <code>.Pages</code> 获取到。因此 Leaf Bundle 和 Branch Bundle 区别体现在：非Index内容是当做<code>.Pages</code>还是<code>.Resources</code>
 <a class="footnote-return" href="#fnref:3"><sup>[return]</sup></a></li>
</ol>
</div>
]]></description></item><item><title>主题组件</title><link>https://www.rectcircle.cn/series/hugo/themes/theme-components/</link><pubDate>Tue, 30 Apr 2019 19:46:40 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/themes/theme-components/</guid><description type="html"><![CDATA[<p>从Hugo 0.42开始，项目可以将主题配置为您需要的主题组件的组合：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">theme</span> = [<span style="color:#e6db74">&#34;my-shortcodes&#34;</span>, <span style="color:#e6db74">&#34;base-theme&#34;</span>, <span style="color:#e6db74">&#34;hyde&#34;</span>]</code></pre></div>
<p>你甚至可以嵌套它，让主题组件config.toml中的theme指向自己（主题继承）<sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></p>

<p>config.toml中的主题定义示例创建了一个主题，其中包含3个主题组件，从左到右依次为优先级。</p>

<p>对于任何给定的文件，数据输入等，Hugo将首先查看项目，然后是<code>my-shortcode</code>，<code>base-theme</code>，最后是<code>hyde</code>。</p>

<p>Hugo使用两种不同的算法来合并文件系统，具体取决于文件类型：</p>

<ul>
<li>对于<code>i18n</code>和<code>data</code>文件，Hugo使用文件中的翻译ID和数据键深入合并</li>
<li>对于静态，布局（模板）和原型文件，这些文件在文件级别合并。因此，将选择最左侧的文件。</li>
</ul>

<p>这里定义的主题列表必须与<code>/your-site/themes</code>中的目录名严格匹配</p>

<p>还要注意，作为主题一部分的组件可以具有其自己的配置文件，例如，config.toml。目前，主题组件可以配置一些限制：</p>

<ul>
<li><code>params</code>（全局和每种语言）</li>
<li><code>menu</code>（全局和每种语言）</li>
<li><code>outputformats</code> and <code>mediatypes</code></li>
</ul>

<p>这里适用相同的规则：具有相同ID的最左边的参数/菜单等将获胜。上面有一些隐藏的和实验性的命名空间支持，我们将来会努力改进它们，但是鼓励主题作者创建自己的命名空间以避免命名冲突。</p>
<div class="footnotes">

<hr />

<ol>
<li id="fn:1">对于在 <a href="https://themes.gohugo.io/">Hugo Themes Showcase</a> 上托管的主题，需要将组件添加为指向目录<code>exampleSite/themes</code>的git子模块
 <a class="footnote-return" href="#fnref:1"><sup>[return]</sup></a></li>
</ol>
</div>
]]></description></item><item><title>安装</title><link>https://www.rectcircle.cn/series/hugo/getting-started/installing/</link><pubDate>Tue, 30 Apr 2019 12:05:29 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/getting-started/installing/</guid><description type="html"><![CDATA[

<blockquote>
<p>有很多关于“hugo由Go语言编写”的讨论，但你不需要安装Go就能使用Hugo。只需安装预编译的二进制文件！</p>
</blockquote>

<p>Hugo是用 <a href="https://golang.org/">Go</a> 编写的，支持多个平台。最新版本可以在 <a href="https://github.com/gohugoio/hugo/releases">Hugo Releases</a> 上找到。</p>

<p>Hugo目前为以下内容提供预构建的二进制文件：</p>

<ul>
<li>macOS (Darwin) 的 x64, i386, ARM 架构</li>
<li>Windows</li>
<li>Linux</li>
<li>OpenBSD</li>
<li>FreeBSD</li>
</ul>

<p>Hugo可以在任何可以运行Go工具链的平台编译Hugo；例如DragonFly BSD，OpenBSD，Plan 9，Solaris等。有关目标操作系统和编译体系结构的全套支持组合，请参阅 <a href="https://golang.org/doc/install/source">https://golang.org/doc/install/source</a> 。</p>

<h2 id="快速安装">快速安装</h2>

<h3 id="二进制-跨平台">二进制（跨平台）</h3>

<p>从 <a href="https://github.com/gohugoio/hugo/releases">Hugo Releases</a> 下载适用于您的平台的版本。下载后，二进制文件可以从任何地方运行。您无需将其安装到全局位置。这适用于您没有特权帐户的共享主机和其他系统。</p>

<h3 id="homebrew-macos">Homebrew (macOS)</h3>

<p>如果您使用的是macOS并使用 <a href="https://brew.sh/">Homebrew</a>，则可以使用以下一行命令安装Hugo：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install hugo</code></pre></div>
<p>有关更详细的说明，请阅读以下安装指南，以便在macOS和Windows上进行安装。</p>

<h3 id="chocolatey-windows">Chocolatey (Windows)</h3>

<p>如果您使用的是Windows机器并使用Chocolatey进行包管理，则可以使用以下一行命令安装Hugo：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">choco install hugo -confirm</code></pre></div>
<h3 id="scoop-windows">Scoop (Windows)</h3>

<p>如果您使用的是Windows计算机并使用Scoop进行包管理，则可以使用以下一行命令安装Hugo：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">scoop install hugo</code></pre></div>
<h3 id="源码">源码</h3>

<h4 id="必备工具">必备工具</h4>

<ul>
<li><a href="http://git-scm.com/">Git</a></li>
<li><a href="https://golang.org/dl/">Go (1.11或更新版本)</a></li>
</ul>

<h4 id="从github获取">从GitHub获取</h4>

<p>从Hugo 0.48起，Hugo使用Go 1.11内置的Go Modules支持来构建。最简单的入门方法是将Hugo克隆到GOPATH之外的目录中，如下例所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir $HOME/src
cd $HOME/src
git clone https://github.com/gohugoio/hugo.git
cd hugo
go install --tags extended</code></pre></div>
<p>如果您不需要/需要Sass/SCSS支持，请删除&ndash;tags扩展。</p>

<blockquote>
<p>如果您是Windows用户，请使用<code>％USERPROFILE％</code>替换上面的<code>$HOME</code>环境变量。</p>
</blockquote>

<h2 id="其他内容">其他内容</h2>

<p><a href="https://gohugo.io/getting-started/installing/#macos">参见</a></p>
]]></description></item><item><title>创建一个主题</title><link>https://www.rectcircle.cn/series/hugo/themes/creating/</link><pubDate>Tue, 30 Apr 2019 20:07:29 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/themes/creating/</guid><description type="html"><![CDATA[

<blockquote>
<p>如果您正在创建一个主题，并计划在Hugo主题网站上分享，请注意以下事项：</p>

<ul>
<li>如果使用内联样式，则需要使用绝对URL，以便正确提供链接资产，例如：<code>&lt;div style =&quot;background：url（'{{&quot;images/background.jpg&quot;| absURL}}'）&quot;&gt;</code></li>
<li>确保不要在URL的开头使用正斜杠<code>/</code>因为它将指向主机根。您的主题演示将在Hugo网站的子目录中提供，在这种情况下，Hugo将不会为主题资产生成正确的URL</li>
<li>如果使用CDN中的外部CSS和JS，请确保通过https加载这些资产。请不要在主题模板中使用相对协议URL。</li>
</ul>
</blockquote>

<p>Hugo可以使用<code>hugo new</code>命令在现有主题中初始化一个新的空白主题目录：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo new theme <span style="color:#f92672">[</span>name<span style="color:#f92672">]</span></code></pre></div>
<h2 id="主题文件夹">主题文件夹</h2>

<p>主题组件可以提供以下一个或多个标准Hugo文件夹中的文件：</p>

<dl>
<dt><code>layouts</code></dt>
<dd>用于在Hugo中呈现内容的模板。另请 <a href="https://gohugo.io/templates/lookup-order/">参阅模板查找顺序</a>。</dd>
<dt><code>static</code></dt>
<dd>静态文件例如：logos、CSS、JavaScript</dd>
<dt><code>i18n</code></dt>
<dd>语言包</dd>
<dt><code>data</code></dt>
<dd>数据文件</dd>
<dt><code>archetypes</code></dt>
<dd><code>hugo new</code> 中使用的内容模板</dd>
</dl>

<h2 id="主题配置文件">主题配置文件</h2>

<p>主题组件还可以提供其自己的配置文件，例如，config.toml。可以在主题组件中配置的内容有一些限制，并且无法覆盖项目中的设置。</p>

<p>可以设置的内容如下</p>

<ul>
<li><code>params</code>（全局和每种语言）</li>
<li><code>menu</code>（全局和每种语言）</li>
<li><code>outputformats</code> and <code>mediatypes</code></li>
</ul>

<h2 id="主题描述文件">主题描述文件</h2>

<p>除了配置文件之外，主题还可以提供描述主题，作者和原点等的theme.toml文件。请参阅 <a href="https://gohugo.io/contribute/themes/">将您的Hugo主题添加到Showcase</a></p>

<blockquote>
<p><a href="https://gohugo.io/variables/hugo/">.Hugo.Generator</a> 标签包含在<a href="http://themes.gohugo.io/">Hugo Themes Showcase</a> 中的所有主题中。我们要求您在Hugo创建的所有网站和主题中包含生成器标签，以帮助核心团队跟踪Hugo的使用情况和受欢迎程度。</p>
</blockquote>
]]></description></item><item><title>基本用法</title><link>https://www.rectcircle.cn/series/hugo/getting-started/usage/</link><pubDate>Tue, 30 Apr 2019 12:37:24 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/getting-started/usage/</guid><description type="html"><![CDATA[

<p>以下是您在开发Hugo项目时将使用的最常用命令的说明。更多有关Hugo CLI的，请 <a href="https://gohugo.io/commands/">参阅命令行参考</a>。</p>

<h2 id="测试安装">测试安装</h2>

<p><a href="installing">安装Hugo</a> 后，请确保它位于PATH中。您可以通过<code>help</code>命令测试Hugo是否已正确安装：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo help</code></pre></div>
<p>您在控制台中看到的输出应类似于以下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo is the main command, used to build your Hugo site.

Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.

Complete documentation is available at http://gohugo.io/.

Usage:
  hugo <span style="color:#f92672">[</span>flags<span style="color:#f92672">]</span>
  hugo <span style="color:#f92672">[</span>command<span style="color:#f92672">]</span>

Available Commands:
  check       Contains some verification checks
  config      Print the site configuration
  convert     Convert your content to different formats
  env         Print Hugo version and environment info
  gen         A collection of several useful generators.
  help        Help about any command
  import      Import your site from others.
  list        Listing out various types of content
  new         Create new content <span style="color:#66d9ef">for</span> your site
  server      A high performance webserver
  version     Print the version number of Hugo

Flags:
  -b, --baseURL string         hostname <span style="color:#f92672">(</span>and path<span style="color:#f92672">)</span> to the root, e.g. http://spf13.com/
  -D, --buildDrafts            include content marked as draft
  -E, --buildExpired           include expired content
  -F, --buildFuture            include content with publishdate in the future
      --cacheDir string        filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/
      --cleanDestinationDir    remove files from destination not found in static directories
      --config string          config file <span style="color:#f92672">(</span>default is path/config.yaml|json|toml<span style="color:#f92672">)</span>
      --configDir string       config dir <span style="color:#f92672">(</span>default <span style="color:#e6db74">&#34;config&#34;</span><span style="color:#f92672">)</span>
  -c, --contentDir string      filesystem path to content directory
      --debug                  debug output
  -d, --destination string     filesystem path to write files to
      --disableKinds strings   disable different kind of pages <span style="color:#f92672">(</span>home, RSS etc.<span style="color:#f92672">)</span>
      --enableGitInfo          add Git revision, date and author info to the pages
  -e, --environment string     build environment
      --forceSyncStatic        copy all files when static is changed.
      --gc                     enable to run some cleanup tasks <span style="color:#f92672">(</span>remove unused cache files<span style="color:#f92672">)</span> after the build
  -h, --help                   help <span style="color:#66d9ef">for</span> hugo
      --i18n-warnings          print missing translations
      --ignoreCache            ignores the cache directory
  -l, --layoutDir string       filesystem path to layout directory
      --log                    enable Logging
      --logFile string         log File path <span style="color:#f92672">(</span><span style="color:#66d9ef">if</span> set, logging enabled automatically<span style="color:#f92672">)</span>
      --minify                 minify any supported output format <span style="color:#f92672">(</span>HTML, XML etc.<span style="color:#f92672">)</span>
      --noChmod                don<span style="color:#e6db74">&#39;t sync permission mode of files
</span><span style="color:#e6db74">      --noTimes                don&#39;</span>t sync modification time of files
      --path-warnings          print warnings on duplicate target paths etc.
      --quiet                  build in quiet mode
      --renderToMemory         render to memory <span style="color:#f92672">(</span>only useful <span style="color:#66d9ef">for</span> benchmark testing<span style="color:#f92672">)</span>
  -s, --source string          filesystem path to read files relative from
      --templateMetrics        display metrics about template executions
      --templateMetricsHints   calculate some improvement hints when combined with --templateMetrics
  -t, --theme strings          themes to use <span style="color:#f92672">(</span>located in /themes/THEMENAME/<span style="color:#f92672">)</span>
      --themesDir string       filesystem path to themes directory
      --trace file             write trace to file <span style="color:#f92672">(</span>not useful in general<span style="color:#f92672">)</span>
  -v, --verbose                verbose output
      --verboseLog             verbose logging
  -w, --watch                  watch filesystem <span style="color:#66d9ef">for</span> changes and recreate as needed

Use <span style="color:#e6db74">&#34;hugo [command] --help&#34;</span> <span style="color:#66d9ef">for</span> more information about a command.</code></pre></div>
<h2 id="hugo命令">Hugo命令</h2>

<p>最常见的用法就是直接运行 <code>hugo</code>，当前目录是输入目录。默认情况下，这会将您的网站生成到 <code>public/</code> 目录，当然可以通过更改 <code>publishDir</code> 字段的输出目录（<a href="configuration">站点配置</a>）。</p>

<p>命令hugo将网站渲染到 <code>public/</code> 目录，并准备好部署到您的Web服务器：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo
<span style="color:#ae81ff">0</span> draft content
<span style="color:#ae81ff">0</span> future content
<span style="color:#ae81ff">99</span> pages created
<span style="color:#ae81ff">0</span> paginator pages created
<span style="color:#ae81ff">16</span> tags created
<span style="color:#ae81ff">0</span> groups created
in <span style="color:#ae81ff">90</span> ms</code></pre></div>
<h2 id="draft-future和expired内容">Draft，Future和Expired内容</h2>

<p>Hugo允许你在每篇内容的 <a href="/content-management/front-matter">front matter</a> 设置 <code>draft</code> （是否是草稿）、 <code>publishdate</code> （发布时间）、 <code>expirydate</code> （过期时间）。默认情况下Hugo不会发布：</p>

<ul>
<li><code>publishdate</code> 值在未来的内容</li>
<li><code>draft: true</code> 的内容</li>
<li><code>expirydate</code>值在过去的内容</li>
</ul>

<p>通过在 <code>hugo</code> 和 <code>hugo server</code> 中分别添加以下标志，或者在 <a href="configuration">配置</a> 中更改分配给同名字段（不带 &ndash; ）的布尔值，可以在本地开发和部署期间覆盖所有这三个：</p>

<ul>
<li><code>--buildFuture</code></li>
<li><code>--buildDrafts</code></li>
<li><code>--buildExpired</code></li>
</ul>

<h2 id="自动重新载入-livereload">自动重新载入（LiveReload）</h2>

<p>Hugo内置了LiveReload。且不需要额外安装软件包。在开发网站时使用Hugo的一种常用方法是让Hugo使用 <code>hugo server</code> 命令运行服务器并监视更改：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo server
<span style="color:#ae81ff">0</span> draft content
<span style="color:#ae81ff">0</span> future content
<span style="color:#ae81ff">99</span> pages created
<span style="color:#ae81ff">0</span> paginator pages created
<span style="color:#ae81ff">16</span> tags created
<span style="color:#ae81ff">0</span> groups created
in <span style="color:#ae81ff">120</span> ms
Watching <span style="color:#66d9ef">for</span> changes in /Users/yourname/sites/yourhugosite/<span style="color:#f92672">{</span>data,content,layouts,static<span style="color:#f92672">}</span>
Serving pages from /Users/yourname/sites/yourhugosite/public
Web Server is available at http://localhost:1313/
Press Ctrl+C to stop</code></pre></div>
<p>这将运行功能完备的Web服务器，同时在项目录的以下区域内监视文件系统的添加、删除或更改：</p>

<ul>
<li><code>/static/*</code></li>
<li><code>/content/*</code></li>
<li><code>/data/*</code></li>
<li><code>/i18n/*</code></li>
<li><code>/layouts/*</code></li>
<li><code>/themes/&lt;CURRENT-THEME&gt;/*</code></li>
<li><code>config</code></li>
</ul>

<p>每当您进行更改时，Hugo将同时重建网站并继续提供内容。一旦构建完成，LiveReload就会告诉浏览器静默重新加载页面。</p>

<p>大多数Hugo构建都非常快，除非直接在浏览器中查看站点，否则您可能不会注意到更改。这意味着在第二台显示器（或当前显示器的另一半）上保持站点打开，可以让您查看最新版本的网站，而无需离开文本编辑器。</p>

<blockquote>
<p>Hugo在模板中 <code>&lt;/body&gt;</code> 之前注入了LiveReload <code>&lt;script&gt;</code>，因此如果此标记不存在，则LiveReload将不起作用。</p>
</blockquote>

<h3 id="禁用livereload">禁用LiveReload</h3>

<p>LiveReload通过将JavaScript注入Hugo生成的页面来工作。该脚本创建从浏览器的Web套接字客户端到Hugo Web套接字服务器的连接。</p>

<p>LiveReload非常适合开发。但是，一些雨果用户可能会在生产中使用hugo服务器来即时显示更新的内容。以下方法可以轻松禁用LiveReload：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo server --watch<span style="color:#f92672">=</span>false</code></pre></div>
<p>或者</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo server --disableLiveReload</code></pre></div>
<p>通过将以下键值分别添加到 <code>config.toml</code> 或 <code>config.yml</code> 文件中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">disableLiveReload</span> = <span style="color:#66d9ef">true</span></code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yml" data-lang="yml">disableLiveReload: <span style="color:#66d9ef">true</span></code></pre></div>
<h2 id="部署您的网站">部署您的网站</h2>

<p>在运行 <code>hugo server</code> 进行本地Web开发之后，最终的需要使用 <code>hugo</code> 命令（不要 <code>server</code> 部分）来重建您的站点。然后，您可以通过将 <code>public/</code> 目录复制到生产Web服务器来部署站点。</p>

<p>由于Hugo生成静态网站，您的网站可以使用任何Web服务器托管在任何地方。有关由Hugo社区提供的托管和自动化部署的方法，请参阅 <a href="https://gohugo.io/hosting-and-deployment/">主机和部署</a> 。</p>

<blockquote>
<p>运行hugo删除上次构建的文件。这意味着您应该在运行hugo命令之前删除您的 <code>/public</code>（或通过标志或配置文件指定的发布目录）。如果您不删除这些文件，则存在不应该出现的文件（例如，drafts或future的帖子），却留在生成的网站中的风险。</p>
</blockquote>

<h3 id="开发和部署输出目录">开发和部署输出目录</h3>

<p>Hugo在构建之前不会删除生成的文件。一个简单的解决方法是针对开发和部署使用不同的输出目录。</p>

<p>要启动构建草稿内容的服务器（有助于编辑），您可以指定不同的目标;例如，<code>dev/</code> 目录：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo server -wDs ~/Code/hugo/docs -d dev</code></pre></div>
<p>当内容准备好发布时，使用默认的 <code>public/</code> 目录：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo -s ~/Code/hugo/docs</code></pre></div>
<p>这可以防止草稿内容意外变为可用。</p>
]]></description></item><item><title>目录结构</title><link>https://www.rectcircle.cn/series/hugo/getting-started/directory-structure/</link><pubDate>Tue, 30 Apr 2019 14:07:51 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/getting-started/directory-structure/</guid><description type="html"><![CDATA[

<h2 id="新网站脚手架">新网站脚手架</h2>

<p>运行 <code>hugo new site</code> 命令 将创建一个包含以下元素的目录结构：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-tree" data-lang="tree">.
├── archetypes
├── assets
├── config
├── content
├── data
├── layouts
├── static
└── themes</code></pre></div>
<h2 id="目录结构解释">目录结构解释</h2>

<p>以下是每个目录的高级概述，其中包含指向Hugo文档中各个部分的链接</p>

<p><strong><a href="/content-management/archetypes/">archetypes</a></strong></p>

<p>您可以使用<code>hugo new</code>命令在Hugo中创建新的内容文件。默认情况下，Hugo将创建至少包含 <code>data</code> (日期)，<code>title</code> （标题（从文件名推断））和 <code>draft=true</code> 的新内容文件。You can create your own archetypes with custom preconfigured front matter fields as well.你可以自定义front matter来创建自己的archetypes。</p>

<p><strong><a href="https://gohugo.io/hugo-pipes/introduction/#asset-directory">assets</a></strong></p>

<p>存储Hugo Pipes需要处理的所有文件。只有使用<code>.Permalink</code>或<code>.RelPermalink</code>的文件才会发布到公共目录。</p>

<p><strong><a href="getting-started/configuration">config</a></strong></p>

<p>Hugo附带了大量配置项。config目录是存储格式为JSON，YAML或TOML配置文件的位置。Every root setting object can stand as its own file and structured by environments。如果只需要使用一套配置，可以直接使用项目根目录的单个 <code>config.toml</code> 配置文件</p>

<p>许多站点可能几乎不需要任何配置，但Hugo附带了大量 <a href="(getting-started/configuration)">配置指令</a>，可以更详细地说明您希望Hugo如何构建您的网站。</p>

<p><strong><a href="/content-management/organization">content</a></strong></p>

<p>您网站的所有内容都将位于此目录中。content中的所有顶级目录都被叫做 <a href="https://gohugo.io/content-management/sections/">content section</a> 例如，如果您的网站有三个 content section 分别是 <code>content/blog</code>, <code>content/articles</code>, 和 <code>content/tutorials</code>。 Hugo使用sections来分配 <a href="/content-management/types/">默认内容类型</a></p>

<p><strong><a href="/content-management/static-files">static</a></strong></p>

<p>存储所有静态内容：图像，CSS，JavaScript等。当Hugo构建您的站点时，静态目录中的所有资源都将按原样复制。使用静态文件夹的一个很好的示例是在Google Search Console上验证网站所有权，您希望Hugo在其中复制整个HTML文件而不修改其内容。</p>

<p>从Hugo 0.31开始，您可以拥有多个静态目录。</p>
]]></description></item><item><title>介绍</title><link>https://www.rectcircle.cn/series/vscode/intro/</link><pubDate>Wed, 15 Apr 2020 00:34:41 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/intro/</guid><description type="html"><![CDATA[

<h3 id="历程">历程</h3>

<p>从开始学习 Coding 开始，我深入使用过如下几种通用的编辑器/IDE （至于其他专有的IDE比如 Dev C++，不做讨论）</p>

<ul>
<li>Eclipse</li>
<li>Visual Studio</li>
<li>IntelliJ 系列 (IDEA 等 包括 Android Studio)</li>
<li>Sublime</li>
<li>VSCode</li>
</ul>

<p>根据个人体验上来说，最完美的就是 VSCode。</p>

<p>目前在工作/生活中，所有的与文本文件编辑相关的内容，均采用 VSCode 进行，包括但不限于如下场景：</p>

<ul>
<li>工作计划/每日TODO</li>
<li>博客撰写</li>
<li>Java 项目开发</li>
<li>React 前端项目开发</li>
<li>Python 开发</li>
<li>Scala 开发</li>
<li>Spark SQL 开发</li>
<li>Rust 开发（学习阶段）</li>
</ul>

<h3 id="优势">优势</h3>

<p>接下来按照如下几个维度来对 VSCode 的优势进行分析</p>

<ul>
<li>市场指标</li>
<li>开源</li>
<li>性能</li>
<li>外观</li>
<li>核心功能</li>
</ul>

<h4 id="市场指标">市场指标</h4>

<blockquote>
<p>截止: 2020年4月</p>
</blockquote>

<p>在 <a href="https://pypl.github.io/IDE.html">https://pypl.github.io/IDE.html</a> 榜单中，VSCode 排名在第 4。较去年2019年4月份上涨 2.2 %。</p>

<p>从趋势上看 自 2015 年发布起，VSCode 持续增长，且增长势头良好。</p>

<p>在VSCode 的 github 仓库，star 数量达到 <code>94.3k</code>。</p>

<p>Sublime 属于个人开发的收费软件。</p>

<p>在 <a href="https://insights.stackoverflow.com/survey/2019">Stack Overflow Developer Survey Results 2019</a> 中 VSCode 在 <a href="https://insights.stackoverflow.com/survey/2019#technology-_-most-popular-development-environments">Most Popular Development Environments</a> 榜单中排名第一</p>

<h4 id="开源">开源</h4>

<p>完全开源，开源协议为 MIT，与之类似的就只有 Eclipse了。</p>

<p>IntelliJ 属于部分开源，其专业版属于收费软件。</p>

<p>VSCode 的主要贡献团队 是微软，近几年微软在开源项目上的投入有目共睹。</p>

<p>团队负责人：<a href="https://zh.wikipedia.org/zh/%E5%9F%83%E9%87%8C%E5%B8%8C%C2%B7%E4%BC%BD%E7%91%AA">Erich Gamma</a> JUnit 作者之一，《设计模式》作者之一， Eclipse 架构师。2011 加入微软，在瑞士苏黎世组建团队开发基于 web 技术的编辑器，也就是后来的 monaco-editor。VSCode 开发团队从 10 来个人开始，早期成员大多有 Eclipse 开发团队的背景。</p>

<h4 id="性能">性能</h4>

<p><strong>启动速度</strong></p>

<p>VSCode 从设计上之处就追求极致的性能，本质上是一个轻量级 编辑器，因此可以在性能较弱的设备中实现秒级打开。不同于 Atom，其扩展机制保证了在安装大量扩展的情况下，VSCode仍然有较快的启动速度。在启动速度上可能只有 Sublime 可以与之相提并论。</p>

<p>相较于 Eclipse 、 Visual Studio 和 IntelliJ 之类的重量级 IDE来说，其启动速度是绝对的领先。</p>

<p><strong>空间占用</strong></p>

<p>目前（2020-04-11），Mac端的Zip文件大小仅 82.9MB，app文件大小，200MB左右。在空间方面，只有 Sublime 可以与之相提并论。</p>

<p>相较于 Eclipse 、 Visual Studio 和 IntelliJ 之类的重量级 IDE来说可谓非常小</p>

<p><strong>响应速度</strong></p>

<p>由于轻量级的特性（基于 Electron），响应速度上非常快。而基于 Java 开发的 UI（Eclipse、IntelliJ 系列），响应缓慢，有时会遇到卡顿。</p>

<h4 id="外观">外观</h4>

<p>VSCode 在 UI 上相对来说比较现代化，各种第三方主题几乎可以满足所有 程序员对 UI 上的执念。</p>

<p>扩展虽然无法修改 VSCode 的 UI 结构（仅可以更改主题，未来，正在预览阶段的 <a href="https://code.visualstudio.com/api/extension-guides/custom-editors">Custom text editors</a> 可以做更深层次的定制），看似定制程度低，但从另一方面看，这显著降低了用户在UI认知上的心智成本，也就是说，一旦梳理了VSCode交互的套路，所有扩展程序的表现都是可以预测的。</p>

<h4 id="核心功能">核心功能</h4>

<p>VSCode 的核心竞争力是设计优良架构和扩展机制</p>

<ul>
<li>极强的扩展能力，几乎编辑器的所有方面都可以进行扩展</li>
<li>丰富的扩展商店，你想要的几乎都有，即使没有自己动手实现一个呗</li>
<li>扩展与编辑器内核的松耦合（通过RPC进行通信），因此可以实现独一份的 <a href="https://code.visualstudio.com/docs/remote/remote-overview">Remote Develop</a></li>
<li>扩展几乎不会拖慢启动速度</li>
<li>UI 基于 Electron

<ul>
<li>原则上支持所有平台</li>
<li>因为 Electron 本质上是基于 Chromium，因此可以实现 在浏览器上进行开发的 <a href="https://code.visualstudio.com/docs/remote/vsonline">Visual Studio Online</a></li>
</ul></li>
</ul>

<p>在扩展开发技术栈方面，原生支持 通过 JavaScript（TypeScript）开发，因此可以享受 前端生态的红利。对于其他开发语言，可以通过语言服务器的方式进行扩展开发。</p>

<h4 id="总结">总结</h4>

<ul>
<li>性能优秀</li>
<li>外观现代化，定制化程度高</li>
<li>丰富强大扩展机制</li>
<li>独有的全功能的远程开发与浏览器开发能力</li>
</ul>

<h3 id="站点">站点</h3>

<ul>
<li><a href="https://code.visualstudio.com/">官方网站</a></li>
<li><a href="https://code.visualstudio.com/docs">官方文档</a></li>
<li><a href="https://marketplace.visualstudio.com/VSCode">扩展商店</a></li>
<li><a href="https://geek-docs.com/vscode">极客教程-VSCode</a> （推荐）</li>
</ul>
]]></description></item><item><title>配置 Hugo</title><link>https://www.rectcircle.cn/series/hugo/getting-started/configuration/</link><pubDate>Tue, 30 Apr 2019 14:31:49 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/getting-started/configuration/</guid><description type="html"><![CDATA[

<h2 id="配置文件">配置文件</h2>

<p>Hugo使用 <code>config.toml</code>，<code>config.yaml</code>或<code>config.json</code>（如果在站点根目录中找到）作为默认站点配置文件。</p>

<p>用户可以使用命令行 <code>--config</code> 开关选择使用一个或多个站点配置文件覆盖该默认值。</p>

<p>例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hugo --config debugconfig.toml
hugo --config a.toml,b.toml,c.toml</code></pre></div>
<blockquote>
<p>可以将多个站点配置文件指定为&ndash;config开关的逗号分隔字符串。</p>
</blockquote>

<h2 id="配置目录">配置目录</h2>

<p>除了使用单个站点配置文件之外，还可以使用 <code>configDir</code>目录（默认为 <code>config/</code> ）来维护更轻松的组织和环境特定设置。</p>

<ul>
<li>每个文件代表一个配置根对象，例如<code>Params</code>，<code>Menus</code>，<code>Languages</code>等&hellip;&hellip;</li>
<li>每个目录包含一组包含环境特有的设置的文件。</li>

<li><p>文件可以本地化为特定于语言。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-tree" data-lang="tree">config
├── _default
│   ├── config.toml
│   ├── languages.toml
│   ├── menus.en.toml
│   ├── menus.zh.toml
│   └── params.toml
├── staging
│   ├── config.toml
│   └── params.toml
└── production
├── config.toml
└── params.toml</code></pre></div></li>
</ul>

<p>考虑到上面的结构，当运行 <code>hugo --environment staging</code> 时，Hugo将使用<code>config/_default</code> 中的每个设置并在这些设置之上合并 <code>staging</code>。</p>

<blockquote>
<p><code>hugo server</code> 的默认环境是 <code>development</code>，<code>hugo</code> 的默认环境是 <code>production</code></p>
</blockquote>

<h2 id="所有配置设置">所有配置设置</h2>

<p>以下是Hugo定义的变量的完整列表，其默认值在括号中。用户可以选择在其站点配置文件中覆盖这些值。</p>

<dl>
<dt><code>archetypeDir (&quot;archetypes&quot;)</code></dt>
<dd>内容模板目录配置</dd>
<dt><code>assetDir (&quot;assets&quot;)</code></dt>
<dd>参见 <a href="https://gohugo.io/hugo-pipes/">Hugo Pipes</a>.</dd>
<dt><code>baseURL</code></dt>
<dd>网站根URL, e.g. <code>http://bep.is/</code></dd>
<dt><code>blackfriday</code></dt>
<dd>Markdown解析器配置参见 <a href="https://gohugo.io/getting-started/configuration/#configure-blackfriday">Configure Blackfriday</a></dd>
<dt><code>buildDrafts(false)</code></dt>
<dd>构建时包含草稿(<code>draft: true</code>)</dd>
<dt><code>buildExpired (false)</code></dt>
<dd>构建时包含已过期(<code>expirydate</code> 在过去)的内容</dd>
<dt><code>buildFuture (false)</code></dt>
<dd>构建时包含 <code>publishdate</code> 时间在未来的内容</dd>
<dt><code>caches</code></dt>
<dd>参见 <a href="https://gohugo.io/getting-started/configuration/#configure-file-caches">Configure File Caches</a></dd>
<dt><code>canonifyURLs (false)</code></dt>
<dd>启用将相对URL转换为绝对URL</dd>
<dt><code>contentDir (&quot;content&quot;)</code></dt>
<dd>内容目录设置</dd>
<dt><code>dataDir (&quot;data&quot;)</code></dt>
<dd>数据目录设置</dd>
<dt><code>defaultContentLanguage (&quot;en&quot;)</code></dt>
<dd>内容的默认语言</dd>
<dt><code>defaultContentLanguageInSubdir (false)</code></dt>
<dd>在子目录中渲染默认语言的内容，例如 <code>/</code> 重定向到 <code>/en/</code></dd>
<dt><code>disableAliases (false)</code></dt>
<dd>将禁用别名重定向的生成。请注意，即使设置了disableAliases，也会在页面上保留别名本身。这样做的动机是能够使用自定义输出格式在.htacess，Netlify _redirects文件或类似文件中生成301重定向。</dd>
<dt><code>disableHugoGeneratorInject (false)</code></dt>
<dd>禁用hugo生成器头</dd>
<dt><code>disableKinds ([])</code></dt>
<dd>启用禁用指定种类的所有页面，允许列表如下 <code>[&quot;page&quot;, &quot;home&quot;, &quot;section&quot;, &quot;taxonomy&quot;, &quot;taxonomyTerm&quot;, &quot;RSS&quot;, &quot;sitemap&quot;, &quot;robotsTXT&quot;, &quot;404&quot;]</code></dd>
<dt><code>disableLiveReload (false)</code></dt>
<dd>禁用浏览器窗口的自动实时重新加载。</dd>
<dt><code>disablePathToLower (false)</code></dt>
<dd>不要将<code>url/path</code>转换为小写。</dd>
<dt><code>enableEmoji (false)</code></dt>
<dd>启用 <a href="https://www.webpagefx.com/tools/emoji-cheat-sheet/">Emoji</a></dd>
<dt><code>enableGitInfo (false)</code></dt>
<dd>为每个页面启用.GitInfo对象（如果Hugo站点由Git进行版本控制）。然后，这将使用该内容文件的最后一个git提交日期更新每个页面的Lastmod参数。</dd>
<dt><code>enableInlineShortcodes</code></dt>
<dd>参见 <a href="https://gohugo.io/templates/shortcode-templates/#inline-shortcodes">Inline Shortcodes</a>.</dd>
<dt><code>enableMissingTranslationPlaceholders (false)</code></dt>
<dd>如果缺少翻译，则显示占位符而不是默认值或空字符串</dd>
<dt><code>enableRobotsTXT (false)</code></dt>
<dd>启用robots.txt文件的生成</dd>
<dt><code>frontmatter</code></dt>
<dd>参见 <a href="#configure-front-matter">Front matter Configuration</a></dd>
<dt><code>footnoteAnchorPrefix (&quot;&quot;)</code></dt>
<dd>脚注锚点的前缀</dd>
<dt><code>footnoteReturnLinkContents (&quot;&quot;)</code></dt>
<dd>脚注返回链接显示的文本。</dd>
<dt><code>googleAnalytics (&quot;&quot;)</code></dt>
<dd>Google Analytics跟踪ID</dd>
<dt><code>hasCJKLanguage (false)</code></dt>
<dd>如果为true，则在内容中自动检测中文/日文/韩文语言。这将使.Summary和.WordCount在CJK语言中正常运行。</dd>
<dt><code>imaging</code></dt>
<dd>参见 <a href="https://gohugo.io/content-management/image-processing/#image-processing-config">Image Processing Config</a></dd>
<dt><code>languages</code></dt>
<dd>参见 <a href="https://gohugo.io/content-management/multilingual/#configure-languages">Configure Languages</a></dd>
<dt><code>languageCode (&quot;&quot;)</code></dt>
<dd>设置站点语言代码</dd>
<dt><code>languageName (&quot;&quot;)</code></dt>
<dd>设置站点语言名</dd>
<dt><code>disableLanguages</code></dt>
<dd>参见 <a href="https://gohugo.io/content-management/multilingual/#disable-a-language">https://gohugo.io/content-management/multilingual/#disable-a-language</a></dd>
<dt><code>layoutDir (&quot;layouts&quot;)</code></dt>
<dd>布局（模板）目录</dd>
<dt><code>log (false)</code></dt>
<dd>使能日志</dd>
<dt><code>logFile (&quot;&quot;)</code></dt>
<dd>配置日志文件</dd>
<dt><code>menu</code></dt>
<dd>参见 <a href="https://gohugo.io/content-management/menus/#add-non-content-entries-to-a-menu">Add Non-content Entries to a Menu</a></dd>
<dt><code>metaDataFormat (&quot;toml&quot;)</code></dt>
<dd>Front matter格式. 允许值为: &ldquo;toml&rdquo;, &ldquo;yaml&rdquo;, or &ldquo;json&rdquo;.</dd>
<dt><code>newContentEditor (&quot;&quot;)</code></dt>
<dd>创建新内容时使用的编辑器。</dd>
<dt><code>noChmod (false)</code></dt>
<dd>不要同步文件的权限模式</dd>
<dt><code>noTimes (false)</code></dt>
<dd>不要同步文件的修改时间</dd>
<dt><code>paginate (10)</code>
:配置分页数 <a href="https://gohugo.io/templates/pagination/">pagination</a></dt>
<dt><code>paginatePath (&quot;page&quot;)</code></dt>
<dd>分页路径 (例如<code>https://example.com/page/2</code>).</dd>
<dt><code>permalinks</code></dt>
<dd>参见 <a href="https://gohugo.io/content-management/urls/#permalinks">Content Management</a></dd>
<dt><code>pluralizeListTitles (true)</code></dt>
<dd>如果某个目录没有_index.md，此列表页页面标题将会转换为复数形式（针对英语）</dd>
<dt><code>publishDir (&quot;public&quot;)</code></dt>
<dd>Hugo将编写最终静态站点（HTML文件等）的目录。</dd>
<dt><code>pygmentsCodeFencesGuessSyntax (false)</code></dt>
<dd>在没有指定语言的情况下为代码栅栏启用语法猜测。</dd>
<dt><code>pygmentsStyle (&quot;monokai&quot;)</code></dt>
<dd>用于语法突出显示的颜色主题或样式。请参阅<a href="https://help.farbox.com/pygments.html">Pygments Color Themes</a></dd>
<dt><code>pygmentsUseClasses (false)</code></dt>
<dd>启用外部CSS以进行语法突出显示。</dd>
<dt><code>related</code></dt>
<dd>请参阅 <a href="https://gohugo.io/content-management/related/#configure-related-content">Related Content</a></dd>
<dt><code>relativeURLs (false)</code></dt>
<dd>启用此选项可使所有相对URL相对于内容根目录。请注意，这不会影响绝对URL</dd>
<dt><code>refLinksErrorLevel (&quot;ERROR&quot;)</code></dt>
<dd>使用ref或relref解析页面链接并且无法解析链接时，将使用此logg级别记录该链接。有效值为ERROR（默认值）或WARNING。任何ERROR都将使构建失败（退出-1）。</dd>
<dt><code>refLinksNotFoundURL</code></dt>
<dd>使用ref或relref解析页面链接并且无法解析链接时，将使用此logg级别记录该链接。有效值为ERROR（默认值）或WARNING。任何ERROR都将使构建失败（退出-1）</dd>
<dt><code>rssLimit (unlimited)</code></dt>
<dd>RSS源中的最大项目数</dd>
<dt><code>sectionPagesMenu (&quot;&quot;)</code></dt>
<dd>参见 <a href="https://gohugo.io/templates/menu-templates/#section-menu-for-lazy-bloggers">Section Menu for Lazy Bloggers</a>.</dd>
<dt><code>sitemap</code></dt>
<dd>参见 <a href="https://gohugo.io/templates/sitemap-template/#configure-sitemap-xml">sitemap configuration</a></dd>
<dt><code>staticDir (&quot;static&quot;)</code></dt>
<dd>静态文件目录 参见 <a href="https://gohugo.io/content-management/static-files/">静态文件</a></dd>
<dt><code>stepAnalysis (false)</code></dt>
<dd>显示程序的不同步骤的存储器和时序</dd>
<dt><code>summaryLength (70)</code></dt>
<dd>摘要长度</dd>
<dt><code>taxonomies</code></dt>
<dd>分类，参阅 <a href="https://gohugo.io/content-management/taxonomies#configure-taxonomies">Configure Taxonomies</a></dd>
<dt><code>theme (&quot;&quot;)</code></dt>
<dd>主题 要使用的主题（默认位于/themes/THEMENAME/）</dd>
<dt><code>themesDir (&quot;themes&quot;)</code></dt>
<dd>主题目录</dd>
<dt><code>timeout (10000)</code>
:生成页面内容的超时，以毫秒为单位（默认为10秒）。注意：这用于避免递归内容生成，如果您的页面生成缓慢（例如，因为它们需要大型图像处理或依赖于远程内容），您可能需要提高此限制。</dt>
<dt><code>title (&quot;&quot;)</code></dt>
<dd>站点标题</dd>
<dt><code>uglyURLs (false)</code></dt>
<dd>启用后，创建格式为<code>/filename.html</code>而不是<code>/filename/</code>的URL。</dd>
<dt><code>verbose (false)</code></dt>
<dd>启用详细输出</dd>
<dt><code>verboseLog (false)</code></dt>
<dd>启用详细日志输出</dd>
<dt><code>watch (false)</code></dt>
<dd>监视文件系统以进行更改并根据需要重新创建</dd>
</dl>

<blockquote>
<p>如果您在* nix机器上开发站点，这是从命令行查找配置选项的便捷快捷方式：</p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd ~/sites/yourhugosite
hugo config | grep emoji</code></pre></div>
<p>输出类似如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">enableemoji: true</pre></div>
<h2 id="配置环境变量">配置环境变量</h2>

<dl>
<dt><code>HUGO_NUMWORKERMULTIPLIER</code></dt>
<dd>可以设置为增加或减少Hugo中并行处理中使用的工作者数量。如果未设置，将使用逻辑CPU的数量。</dd>
</dl>

<h2 id="配置查找顺序">配置查找顺序</h2>

<p>与模板查找顺序类似，Hugo有一组默认规则，用于在网站源目录的根目录中搜索配置文件，作为默认行为：</p>

<ul>
<li><code>./config.toml</code></li>
<li><code>./config.yaml</code></li>
<li><code>./config.json</code></li>
</ul>

<p>在您的配置文件中，您可以指导Hugo了解您希望网站呈现的方式，控制网站的菜单，以及任意定义特定于项目的网站范围参数。</p>

<h2 id="示例配置">示例配置</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://yoursite.example.com/&#34;</span>
<span style="color:#a6e22e">footnoteReturnLinkContents</span> = <span style="color:#e6db74">&#34;↩&#34;</span>
<span style="color:#a6e22e">title</span> = <span style="color:#e6db74">&#34;My Hugo Site&#34;</span>

[<span style="color:#a6e22e">params</span>]
  <span style="color:#a6e22e">AuthorName</span> = <span style="color:#e6db74">&#34;Jon Doe&#34;</span>
  <span style="color:#a6e22e">GitHubUser</span> = <span style="color:#e6db74">&#34;spf13&#34;</span>
  <span style="color:#a6e22e">ListOfFoo</span> = [<span style="color:#e6db74">&#34;foo1&#34;</span>, <span style="color:#e6db74">&#34;foo2&#34;</span>]
  <span style="color:#a6e22e">SidebarRecentLimit</span> = <span style="color:#ae81ff">5</span>
  <span style="color:#a6e22e">Subtitle</span> = <span style="color:#e6db74">&#34;Hugo is Absurdly Fast!&#34;</span>

[<span style="color:#a6e22e">permalinks</span>]
  <span style="color:#a6e22e">posts</span> = <span style="color:#e6db74">&#34;/:year/:month/:title/&#34;</span></code></pre></div>
<h2 id="使用环境变量配置">使用环境变量配置</h2>

<p>除了已经提到的3个配置选项之外，还可以通过操作系统环境变量定义配置键值。</p>

<p>例如，以下命令将在类Unix系统上有效地设置网站标题：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">env HUGO_TITLE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Some Title&#34;</span> hugo</code></pre></div>
<p>如果您使用Netlify等服务部署您的站点，这非常有用。查看 [Netlify configuration file] 示例。</p>

<blockquote>
<p>名称必须以HUGO_为前缀，并且在设置操作系统环境变量时必须将配置键设置为大写。</p>
</blockquote>

<h2 id="渲染时忽略文件">渲染时忽略文件</h2>

<p><code>./config.toml</code>中的以下语句将导致Hugo在呈现时忽略以<code>.foo</code>和<code>.boo</code>结尾的文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">ignoreFiles</span> = [ <span style="color:#e6db74">&#34;\\.foo$&#34;</span>, <span style="color:#e6db74">&#34;\\.boo$&#34;</span> ]</code></pre></div>
<p>以上是正则表达式列表。请注意，在此示例中对反斜杠（\）字符进行了转义，以使TOML能够理解。</p>

<h2 id="配置front-matter">配置Front Matter</h2>

<h3 id="配置日期">配置日期</h3>

<p>日期在Hugo中非常重要，您可以配置Hugo如何为您的内容页面分配日期。您可以通过向config.toml添加frontmatter部分来完成此操作。</p>

<p>默认配置如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">frontmatter</span>]
<span style="color:#a6e22e">date</span> = [<span style="color:#e6db74">&#34;date&#34;</span>, <span style="color:#e6db74">&#34;publishDate&#34;</span>, <span style="color:#e6db74">&#34;lastmod&#34;</span>]
<span style="color:#a6e22e">lastmod</span> = [<span style="color:#e6db74">&#34;:git&#34;</span>, <span style="color:#e6db74">&#34;lastmod&#34;</span>, <span style="color:#e6db74">&#34;date&#34;</span>, <span style="color:#e6db74">&#34;publishDate&#34;</span>]
<span style="color:#a6e22e">publishDate</span> = [<span style="color:#e6db74">&#34;publishDate&#34;</span>, <span style="color:#e6db74">&#34;date&#34;</span>]
<span style="color:#a6e22e">expiryDate</span> = [<span style="color:#e6db74">&#34;expiryDate&#34;</span>]</code></pre></div>
<p>例如，如果您在某些内容中包含非标准日期参数，则可以覆盖日期设置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">frontmatter</span>]
<span style="color:#a6e22e">date</span> = [<span style="color:#e6db74">&#34;myDate&#34;</span>, <span style="color:#e6db74">&#34;:default&#34;</span>]</code></pre></div>
<p><code>:default</code>是默认设置的快捷方式。如果<code>myDate</code>存在，上面将设置<code>.Date</code>为<code>myDate</code>中的日期值，如果不存在，我们将查看 <code>[&quot;date&quot;, &quot;publishDate&quot;, &quot;lastmod&quot;]</code> 并选择第一个有效日期。</p>

<p>在列表中，以<code>&quot;:&quot;</code>开头的值是具有特殊含义的日期处理程序（见下文）。其他只是前端配置中日期参数的名称（不区分大小写）。另请注意，Hugo上面有一些内置别名：lastmod =&gt; modified，publishDate =&gt; pubdate，published和expiryDate =&gt; unpublishdate。以此为例，默认情况下，将pubDate用作前面的日期，将分配给.PublishDate。</p>

<p>特殊日期处理程序是：</p>

<p><strong><code>:fileModTime</code></strong></p>

<p>从内容文件的上次修改时间戳中获取日期。</p>

<p>一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">frontmatter</span>]
<span style="color:#a6e22e">lastmod</span> = [<span style="color:#e6db74">&#34;lastmod&#34;</span>, <span style="color:#e6db74">&#34;:fileModTime&#34;</span>, <span style="color:#e6db74">&#34;:default&#34;</span>]</code></pre></div>
<p>上面将首先尝试从<code>front matter</code>参数中的 <code>lastmod</code> 提取.Lastmod的值，然后是内容文件的修改时间戳。最后，在这里不需要默认，但Hugo最终会在：git，date和publishDate中寻找有效的日期。</p>

<p><strong><code>:filename</code></strong></p>

<p>从内容文件的文件名中获取日期。例如，<code>2018-02-22-mypage.md</code>将提取日期<code>2018-02-22</code>。此外，如果未设置slug，<code>mypage</code>将用作.Slug的值。</p>

<p>一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">frontmatter</span>]
<span style="color:#a6e22e">date</span>  = [<span style="color:#e6db74">&#34;:filename&#34;</span>, <span style="color:#e6db74">&#34;:default&#34;</span>]</code></pre></div>
<p>上面将首先尝试从文件名中提取.Date的值，然后它将查看前面的参数date，publishDate和last last last。</p>

<p><strong><code>:git</code></strong></p>

<p>这是此内容文件的最新修订版的Git作者日期。只有在设置了<code>--enableGitInfo</code>或在站点配置中设置了<code>enableGitInfo = true</code>时才会设置此项。</p>

<h2 id="配置blackfriday">配置Blackfriday</h2>

<p><a href="https://github.com/russross/blackfriday">Blackfriday</a> 是Hugo内置的Markdown渲染引擎。</p>

<p>Hugo通常使用理智的默认值配置Blackfriday，这些默认值应该适合大多数用例。</p>

<p>但是，如果您对Markdown有特殊需求，Hugo会公开其部分Blackfriday行为选项供您更改。下表列出了这些Hugo选项，与Blackfriday的源代码（ <a href="https://github.com/russross/blackfriday/blob/master/html.go">html.go</a> 和 <a href="https://github.com/russross/blackfriday/blob/master/html.go">markdown.go</a> ）中的相应标志配对。</p>

<h3 id="blackfriday选项">Blackfriday选项</h3>

<p><a href="https://gohugo.io/getting-started/configuration/#configure-blackfriday">参见</a></p>

<h2 id="配置其他输出格式">配置其他输出格式</h2>

<p>Hugo v0.20引入了将内容呈现为多种输出格式（例如，JSON，AMP html或CSV）的功能。有关如何将这些值添加到Hugo项目的配置文件的信息，请参阅 <a href="https://gohugo.io/templates/output-formats/">输出格式</a>。</p>

<h3 id="配置文件缓存">配置文件缓存</h3>

<p>从Hugo 0.52开始，您可以配置的不仅仅是 <code>cacheDir</code>。这是默认配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">caches</span>]
[<span style="color:#a6e22e">caches</span>.<span style="color:#a6e22e">getjson</span>]
<span style="color:#a6e22e">dir</span> = <span style="color:#e6db74">&#34;:cacheDir/:project&#34;</span>
<span style="color:#a6e22e">maxAge</span> = <span style="color:#ae81ff">-1</span>
[<span style="color:#a6e22e">caches</span>.<span style="color:#a6e22e">getcsv</span>]
<span style="color:#a6e22e">dir</span> = <span style="color:#e6db74">&#34;:cacheDir/:project&#34;</span>
<span style="color:#a6e22e">maxAge</span> = <span style="color:#ae81ff">-1</span>
[<span style="color:#a6e22e">caches</span>.<span style="color:#a6e22e">images</span>]
<span style="color:#a6e22e">dir</span> = <span style="color:#e6db74">&#34;:resourceDir/_gen&#34;</span>
<span style="color:#a6e22e">maxAge</span> = <span style="color:#ae81ff">-1</span>
[<span style="color:#a6e22e">caches</span>.<span style="color:#a6e22e">assets</span>]
<span style="color:#a6e22e">dir</span> = <span style="color:#e6db74">&#34;:resourceDir/_gen&#34;</span>
<span style="color:#a6e22e">maxAge</span> = <span style="color:#ae81ff">-1</span></code></pre></div>
<p>您可以在自己的config.toml中覆盖任何这些缓存设置。</p>

<h4 id="关键词解释">关键词解释</h4>

<dl>
<dt><code>:cacheDir</code></dt>
<dd>这是cacheDir配置选项的值（如果已设置）（也可以通过OS env变量HUGO_CACHEDIR设置）。它将回退到Netlify上的 <code>/opt/build/cache/ hugo_cache/</code>，或其他人的OS temp dir下面的hugo_cache目录。这意味着如果在Netlify上运行构建，则将在下一个构建中保存和恢复所有配置有：cacheDir的缓存。对于其他CI供应商，请阅读他们的文档。对于CircleCI示例，请参阅此配置。</dd>
<dt><code>:project</code></dt>
<dd>当前Hugo项目的基本目录名称。这意味着，在其默认设置中，每个项目都将具有单独的文件缓存，这意味着当您执行hugo &ndash;gc时，您将不会触摸与在同一台PC上运行的其他Hugo项目相关的文件。</dd>
<dt><code>:resourceDir</code></dt>
<dd>这是resourceDir配置选项的值。</dd>
<dt><code>maxAge</code></dt>
<dd>这是缓存条目被逐出之前的持续时间，-1表示永远，0有效地关闭该特定缓存。使用Go的time.Duration，因此有效值为“10s”（10秒），“10m”（10分钟）和“10h”（10小时）。</dd>
<dt><code>dir</code></dt>
<dd>将存储此缓存的文件的绝对路径。允许的起始占位符是：cacheDir和：resourceDir（见上文）。</dd>
</dl>

<h2 id="配置文件语法">配置文件语法</h2>

<ul>
<li><a href="https://github.com/toml-lang/toml">TOML Spec</a></li>
<li><a href="http://yaml.org/spec/">YAML Spec</a></li>
<li><a href="https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf" title="Specification for JSON, JavaScript Object Notation">JSON Spec</a></li>
</ul>
]]></description></item><item><title>Rust RFC 项目管理调研</title><link>https://www.rectcircle.cn/series/software-project-management/rust-rfc/</link><pubDate>Sun, 21 Mar 2021 14:20:13 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/software-project-management/rust-rfc/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://github.com/rust-lang/rfcs">rust-lang/rfcs</a></p>
</blockquote>

<h2 id="rust-rfc-现状">Rust rfc 现状</h2>

<h3 id="rust-lang-rfcs-代码库概览">rust-lang/rfcs 代码库概览</h3>

<ul>
<li><code>README.md</code>，说明 RFC 的目的是什么，何时需要提交 RFC（何时不需要），RFC 如何编写，如何提交 RFC，RFC 生命周期如何管理</li>
<li><code>style-guide</code> RFC 中代码样式规范</li>
<li><code>text</code> RFC 文档的存储目录</li>
</ul>

<h3 id="简述">简述</h3>

<p>大多数变更，比如 bugfix 、文档，通过 Pull Request 完成</p>

<p>但是某些 “实质性” 的变更，需要通过一些设计过程，让社区达成共识</p>

<p>RFC (request for comments) 流程，旨在为新特性进入语言和标准库提供一致和受控的路径。帮助所有的利益相关方都可以对语言在不断发展的方向上有信心</p>

<h3 id="什么场景触发-rfc-流程">什么场景触发 RFC 流程</h3>

<p>如果您打算对Rust，Cargo，CrateS.IO 或 RFC 过程本身进行“实质性”更改，则需要遵循此过程。基于社区规范的 “实质性” 变更是什么构成的，并根据您提出改变的生态系统的哪个部分而变化，但可能包括以下内容。</p>

<ul>
<li>任何非 bugfix 的 想进入语言的 语义或语法更改</li>
<li>删除语言功能，包括 “feature-gated”</li>
<li>编译器和库之间的接口，包括LANG项目和内部 的更改</li>
<li>添加到 std</li>
</ul>

<p>不需要RFC的变更：</p>

<ul>
<li>重建，重组，重构或以其他方式 “变化形态但不会改变意义” 的变更</li>
<li>添加严格改善客观，数值质量标准（警告移除，加速，更好的跨平台新，并发度，陷阱和错误等）</li>
<li>只对开发人员可见对 rust 用户不可见的变更</li>
</ul>

<h3 id="提交须知">提交须知</h3>

<p>如下提案可能被快速拒绝</p>

<ul>
<li>低质量的提案</li>
<li>以前拒绝的 feature 的提案</li>
<li>不适合近期路线图的提案</li>
</ul>

<p>官方论坛</p>

<ul>
<li>在官方 <a href="https://discord.gg/rust-lang">Discord</a> 上讨论了讨论我们开发人员讨论<a href="https://internals.rust-lang.org/">论坛的主题</a></li>
<li>偶尔在开发人员论坛上发布 &ldquo;pre-RFCs&rdquo; 。您可以在此仓库中提交问题进行讨论，但这些次数不会被团队积极地查看。</li>
</ul>

<p>作为经验丰富的规则，接受了从长远的项目开发人员的鼓励反馈，特别是相关子团队的成员是一个很好的迹象表明RFC值得追求。</p>

<h3 id="rfc-流程">RFC 流程</h3>

<ul>
<li>Fork <a href="https://github.com/rust-lang/rfcs">rfc repo</a></li>
<li>将 <code>0000-template.md</code> 复制到 <code>text/0000-my-feature.md</code>（其中 &ldquo;my-feature&rdquo; 是描述性的）。不要分配RFC号码；这将是PR号码，如果接受RFC，我们将相应地重命名文件。</li>
<li>填写RFC，需要注意细节：没有提出令人信服的动机、对设计的影响缺乏了解、或对缺点或替代方案不诚实的建议性文件往往不受欢迎。</li>
<li>提交一个 PR 请求。作为一个 PR，RFC将从更大的社区收到设计反馈，作者应该准备好修改它作为回应。</li>
<li>该 RFC 的编号将是 PR 的 ID</li>
<li>每个 PR 将添加上最相关的 子团队 的 label</li>
<li>建立共识，整合反馈。获得广泛支持的RFC比那些没有收到任何意见的RFC更有可能取得进展。特别是随时可以联系RFC受让人，以获得确定利益相关者和障碍的帮助。</li>
<li>子团队将在 RFC 尽量在 PR 下中进行讨论，离线讨论的总结将发表到 PR 中</li>
<li>RFC很少会一成不变地通过这个过程，特别是当替代方案和缺点被展示出来的时候。你可以对 RFC 进行大大小小的修改，以澄清或改变设计，但要以新 commit 的方式提交到 PR 中，并在 PR 中留下评论，解释你的修改。具体来说，不要在 PR 上可见后再 Squash 或 Rebase 提交。</li>
<li>在某些时候，小组的成员将提出 “最后论期动议” （FCP），以及RFC的处理（合并，关闭或推迟）

<ul>
<li>当讨论了足够多的权衡，小组能够做出决定时，就会采取这一步骤。这并不要求RFC线的所有参与者达成共识（这通常是不可能的）。然而，支持 RFC 处置的论点需要已经被清楚地表达出来，而且在子团队之外不应该有强烈的反对该立场的共识。分组成员在采取这一步骤时，要运用他们的最佳判断力，而FCP本身也要确保有足够的时间和通知，让利益相关者在过早做出决定的情况下进行反击。</li>
<li>对于讨论时间较长的RFC，在提出FCP动议之前，通常会有一个总结性意见，试图说明讨论的现状和主要的权衡/分歧点。</li>
<li>在真正进入FCP之前，所有的子团队成员必须签字；这往往是许多子团队成员第一次全面深入审查RFC的时刻。</li>
</ul></li>
<li>在大多数情况下，FCP期间是平静的，RFC要么被合并，要么被关闭。然而，有时会有大量新的论点或想法被提出，FCP被取消，RFC又回到发展模式。</li>
</ul>

<h3 id="rfc-生命周期">RFC 生命周期</h3>

<ul>
<li>一旦一个RFC变得 &ldquo;活跃&rdquo;，那么作者就可以实现它，并将该功能作为 PR 提交到Rust repo。&rdquo;活跃&rdquo; 并不是橡皮图章，尤其是并不意味着该功能最终会被合并；它确实意味着原则上所有主要的利益相关者都同意该功能，并且愿意合并它。</li>
<li>此外，一个给定的RFC已经被接受并且是 &ldquo;活跃的&rdquo; 这一事实并不意味着它的实现被分配了什么优先级，也不意味着是否有一个Rust开发者被分配了实现该功能的任务。虽然RFC的作者不一定要写实现，但这是迄今为止看到RFC完成的最有效的方式：作者不应该期望其他项目开发者会承担实现他们所接受的功能的责任。</li>
<li>对 &ldquo;活动&rdquo; RFC的修改可以在后续的 PR 中进行。我们努力以反映该功能最终设计的方式来编写每一个RFC；但这个过程的性质意味着我们不能期望每一个合并的RFC都能实际反映下一个主要版本时的最终结果。</li>
<li>一般来说，RFC一旦被接受，就不应该进行实质性的修改。只有非常小的改动才应作为修正案提交。更多的实质性修改应该是新的RFC，并在原RFC上添加注释。确切地说，什么是 &ldquo;非常小的改动&rdquo; 是由分团队决定的；更多细节请查看分团队的具体指南。</li>
</ul>

<h3 id="审查-rfc">审查 RFC</h3>

<ul>
<li>在 RFC PR 发布的同时，子团队可能会安排与作者 and/or 相关利益相关者的会议，以更详细地讨论问题，在某些情况下，该主题可能会在分小组会议上讨论。不管是哪种情况，会议的摘要都会发布到RFC PR 中。</li>
<li>在充分了解利弊之后，子团队做出最终决定。这些决定可以在任何时候做出，但分团队会定期发布决定。当做出决定后，RFC拉动请求将被合并或关闭。无论哪种情况，如果在线的 PR 讨论区中没有讨论清楚，子团队需要加上评论，并说明决定的理由。</li>
</ul>

<h3 id="实现-rfc">实现 RFC</h3>

<ul>
<li>一些被接受的RFC代表了需要立即实施的重要功能。其他被接受的RFC可以代表一些可以等到一些任意的开发者觉得喜欢做的功能。每一个被接受的RFC都有一个相关的问题，跟踪它在Rust仓库中的实现情况；因此，相关的问题可以通过团队对Rust仓库中所有问题的分流过程分配一个优先级。</li>
<li>RFC的作者没有义务去实现它。当然，欢迎RFC作者(像其他开发者一样)在RFC被接受后将其实施情况张贴出来供审查。</li>
<li>如果你对一个 &ldquo;活跃的&rdquo; RFC 的实现感兴趣，但又不能确定是否已经有人在做，请随时提出问题（例如，在相关 issue 上留下评论）。</li>
</ul>

<h3 id="延期-rfc">延期 RFC</h3>

<ul>
<li>一些 RFC 拉取请求在关闭时（作为拒绝过程的一部分）会被贴上 &ldquo;推迟 &ldquo;标签。用 &ldquo;postponed&rdquo; 标签关闭的RFC，是因为我们既不想评估这个提案，也不想在未来的某个时候实现所描述的特性，而且我们相信我们有能力等到那个时候再去做。历史上，&rdquo;postponed&rdquo; 是用来推迟到1.0之后的特性。推迟的拉取请求可能会在时机成熟时重新开放。我们没有任何正式的流程，你应该询问相关子团队的成员。</li>
<li>通常，标记为 &ldquo;推迟&rdquo; 的RFC拉取请求已经通过了非正式的第一轮评估，即 &ldquo;我们是否认为我们可能会考虑做出RFC拉取请求中概述的这一改变，或者它的一些半明显的变体&rdquo;。(当后一个问题的答案是 &ldquo;不 &ldquo;时，适当的回应是关闭RFC，而不是推迟它)。</li>
</ul>

<h3 id="help-this-is-all-too-informal">Help this is all too informal</h3>

<p>这个过程的目的是为了在目前的情况下尽可能的轻量化。一如既往，我们试图让这个过程由共识和社区规范驱动，而不是强加更多不必要的结构。</p>

<h2 id="rust-rfc-模板">Rust rfc 模板</h2>

<h3 id="整体结构">整体结构</h3>

<ul>
<li>头部元信息</li>
<li>摘要</li>
<li>动机</li>
<li>指南级别的解释</li>
<li>参考级别解释</li>
<li>缺点</li>
<li>理由和替代方案</li>
<li>现有技术</li>
<li>未解决的问题</li>
<li>未来的可能性</li>
</ul>

<h3 id="文件名">文件名</h3>

<p><code>0000-feature-name.md</code></p>

<h3 id="头部元信息">头部元信息</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">Feature Name: (fill me in with a unique ident, my_awesome_feature)
Start Date: (fill me in with today&#39;s date, YYYY-MM-DD)
RFC PR: rust-lang/rfcs#0000
Rust Issue: rust-lang/rust#0000</code></pre></div>
<ul>
<li>Feature Name: 特性唯一标识（和文件名没有关系）</li>
<li>Start Date: 启动时间</li>
<li>RFC PR: 当前 PR 的链接，PR 的 id 将作为 RFC 的编号</li>
</ul>

<h3 id="摘要">摘要</h3>

<p>我们为什么要这样做？它支持哪些用例？预期的结果是什么？</p>

<h3 id="指南级别的解释">指南级别的解释</h3>

<p>解释该提案，好像它已被包含在语言中，并且您正在向另一个 Rust 程序员传授它。这通常意味着：</p>

<ul>
<li>引入新的命名概念</li>
<li>主要用例子来解释这个特性</li>
<li>解释 Rust 程序员应该如何思考这个特性，以及它应该如何影响他们使用 Rust 的方式。它应该尽可能具体地解释影响。</li>
<li>如果适用，提供错误信息、废弃警告或迁移指导的示例。</li>
<li>如果适用，描述向现有的 Rust 程序员和新的 Rust 程序员教授这个功能的区别。</li>
</ul>

<p>对于以实现为导向的RFC（例如，编译器内部），本节应专注于编译贡献者如何考虑变革，并举例说明其具体影响。对于政策RFC，本节应提供对政策的示例驱动的介绍，并在具体方面解释其影响。</p>

<h3 id="参考级别解释">参考级别解释</h3>

<p>这是RFC的技术部分。以足够的细节解释设计：</p>

<ul>
<li>该功能与其他功能的相互作用是明确的。</li>
<li>合理地明确了如何实现该功能。</li>
<li>通过示例解剖视角案例。</li>
</ul>

<p>本节应回到上一节所举的例子，并更充分地解释详细提案如何使这些例子发挥作用。</p>

<h3 id="缺点">缺点</h3>

<p>存在缺点</p>

<h3 id="理由和替代方案">理由和替代方案</h3>

<ul>
<li>为什么在可能的设计中，这种设计是最好的？</li>
<li>还考虑过哪些其他设计，不选择这些设计的理由是什么？</li>
<li>不这样做的影响是什么？</li>
</ul>

<h3 id="现有技术">现有技术</h3>

<p>讨论与本提案有关的现有技术，包括好的和坏的。其中可包括以下几个例子：</p>

<ul>
<li>对于语言、库、crate、工具和编译器的建议。这个功能在其他编程语言中是否存在？ 他们的社区有什么经验？</li>
<li>对于社区的建议。是否有其他社区做了这个功能，他们有什么经验？</li>
<li>对于其他团队来说。我们可以从其他社区在这里做的事情中学到什么经验？</li>
<li>论文。有没有发表过的论文或者是讨论这个问题的好帖子？如果你有一些相关的论文可以参考，这可以作为一个更详细的理论背景。</li>
</ul>

<p>本节旨在鼓励您作为作者思考其他语言的经验教训，为您的RFC读者提供更全面的信息。如果没有先例，那也没关系&ndash;无论你的想法是全新的，还是从其他语言改编而来，我们都会感兴趣。</p>

<p>请注意，虽然其他语言创造的先例是一些动力，但它本身并不能成为RFC的动力。也请考虑到rust有时会故意偏离通用语言的特性。</p>

<h3 id="未解决的问题">未解决的问题</h3>

<ul>
<li>在这个功能被合并之前，你期望通过RFC程序解决设计中的哪些部分？</li>
<li>在稳定化之前，你期望通过这个功能的实现来解决设计中的哪些部分？</li>
<li>您认为哪些相关问题不在本 RFC 的范围内，可以在未来独立于本 RFC 的解决方案解决？</li>
</ul>

<h3 id="未来的可能性">未来的可能性</h3>

<p>想一想你的提案的自然延伸和演变会是什么，以及它将如何整体地影响语言和项目。试着把这部分作为一个工具，在你的提案中更全面地考虑所有可能与项目和语言的互动。同时考虑这一切如何与项目和相关子团队的路线图相适应。</p>

<p>这也是一个 &ldquo;倾倒想法 &ldquo;的好地方，如果这些想法不在你正在编写的RFC范围内，但在其他方面是相关的。</p>

<p>如果你已经尝试过，但想不出任何未来的可能性，你可以简单地声明你想不出任何东西。</p>

<p>请注意，在未来可能性部分写下一些东西并不是接受当前或未来 RFC 的理由；这样的说明应该在本 RFC 或后续 RFC 的动机或理由部分。这一节只是提供补充信息。</p>

<h2 id="rust-一个-rfc-的例子">Rust 一个 rfc 的例子</h2>

<p><a href="https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md">1210-impl-specialization.md</a></p>

<h2 id="rust-rfc-自描述-rfc">Rust rfc 自描述 rfc</h2>

<p><a href="https://github.com/rust-lang/rfcs/blob/master/text/0002-rfc-process.md">0002-rfc-process.md</a></p>
]]></description></item><item><title>如何学习扩展开发</title><link>https://www.rectcircle.cn/series/vscode/extension-develop/how-to-learn/</link><pubDate>Wed, 29 Apr 2020 15:24:31 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/extension-develop/how-to-learn/</guid><description type="html"></description></item><item><title>Remote Development</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/remote-development/</link><pubDate>Sat, 18 Apr 2020 19:38:24 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/remote-development/</guid><description type="html"><![CDATA[

<h2 id="前言">前言</h2>

<h3 id="简介">简介</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/remote/remote-overview">https://code.visualstudio.com/docs/remote/remote-overview</a>
发行日志 <a href="https://github.com/microsoft/vscode-docs/tree/master/remote-release-notes">https://github.com/microsoft/vscode-docs/tree/master/remote-release-notes</a></p>
</blockquote>

<p>远程开发是，19年 VSCode 推出的一项重大特性。因此将 《远程开发》 章节作为 VSCode 优质扩展的第一篇。</p>

<p>近几年来 云 十分火爆，因此各种 云 IDE 应运而生。但是这也云 IDE 在功能上与本地IDE无法相提并论。</p>

<p>而 VSCode 的 Remote Development，真正实现了不损失功能的情况下实现了远程开发，而且配置简单，在使用上和本地 VSCode 几乎没有差别，这对于开发者来说是非常大的福音。</p>

<p>Visual Studio Code远程开发允许您将容器，远程主机或<a href="https://docs.microsoft.com/windows/wsl">Windows子系统</a>（WSL）用作完整功能的开发环境。您可以：</p>

<ul>
<li>在您部署到的同一操作系统上进行开发，或使用更大或更专业的硬件。</li>
<li>将开发环境沙盒化，以避免影响本地计算机配置。</li>
<li>使新贡献者易于上手，并使每个人都保持一致的环境。</li>
<li>使用本地操作系统上不可用的工具或运行时，或管理它们的多个版本。</li>
<li>使用Linux的Windows子系统开发部署了Linux的应用程序。</li>
<li>从多台机器或位置访问现有的开发环境。</li>
<li>调试在其他位置（例如客户站点或云中）运行的应用程序。</li>
</ul>

<h3 id="场景">场景</h3>

<h4 id="远程开发">远程开发</h4>

<p>有时，迫于现状，正在开发的项目依赖特定的环境（比如：操作系统/网络隔离/宿主环境），导致在本地运行和调试程序及其困难甚至不可能。另外，在和线上环境相同的环境下开发，可以杜绝因为环境不同导致的问题。</p>

<p>此时每次开发可能这样的流程：</p>

<ul>
<li>本地开发</li>
<li>利用git、ftp、sftp、scp等手段同步到开发机</li>
<li>在开发机启动程序，观察日志输出</li>
</ul>

<p>一般情况下，以上流程虽然较长，但是可以接受，但是在 Debug 的时候需要频繁修改代码 各种 print。开发效率及其低下的。</p>

<p>在使用 VSCode Remote Development 后，只需要做一次前序操作：</p>

<ul>
<li>安装 Remote Development 扩展</li>
<li>连接到远端主机，并打开项目</li>
<li>在远端安装扩展</li>
</ul>

<p>此后，就可以像在本地开发一样进行开发。</p>

<p>利用 Remote Development 开发还有如下好处</p>

<ul>
<li>如果远程主机性能很强，则可以提高编译速度和扩展的响应速度</li>
<li>自己的本地机器不会因为扩展的语言服务器在进行CPU密集的运算而造成风扇狂响了</li>
</ul>

<p>远程开发需要付出的代价</p>

<ul>
<li>与远程主机较好的网络连接（带宽不必要求太高，延迟越低越好）</li>
</ul>

<h4 id="一致性开发环境-敏捷">一致性开发环境（敏捷）</h4>

<p>在 敏捷开发/持续交付 要求可以快速的搭建开发环境， Docker 容器化是实现该需求的关键。</p>

<p>当容器技术和VSCode结合，不管项目如何复杂，都可以真正实现</p>

<ul>
<li>所有成员开发环境一致</li>
<li>开发环境快速搭建</li>
<li>开发环境统一管理</li>
</ul>

<h3 id="原理">原理</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/remote/remote-overview">https://code.visualstudio.com/docs/remote/remote-overview</a></p>
</blockquote>

<p>整体架构</p>

<p><img src="https://code.visualstudio.com/assets/docs/remote/remote-overview/architecture.png" alt="architecture" /></p>

<p>Remote - SSH 架构</p>

<p><img src="https://code.visualstudio.com/assets/docs/remote/ssh/architecture-ssh.png" alt="architecture-ssh" /></p>

<h2 id="安装">安装</h2>

<h3 id="命令安装">命令安装</h3>

<ul>
<li><code>ext install ms-vscode-remote.vscode-remote-extensionpack</code>

<ul>
<li><code>ext install ms-vscode-remote.remote-ssh</code>

<ul>
<li><code>ext install ms-vscode-remote.remote-ssh-edit</code></li>
</ul></li>
<li><code>ext install ms-vscode-remote.remote-wsl</code></li>
<li><code>ext install ms-vscode-remote.remote-containers</code></li>
</ul></li>
<li><code>ext install ms-vsonline.vsonline</code></li>
</ul>

<h3 id="商店链接">商店链接</h3>

<ul>
<li>扩展包 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh">Remote - SSH</a>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh-edit">Remote - SSH: Editing Configuration Files</a></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl">Remote - WSL</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Remote - Containers</a></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vsonline.vsonline">Visual Studio Online</a></li>
</ul>

<h2 id="特性">特性</h2>

<p>远程开发</p>

<h2 id="remote-ssh">Remote - SSH</h2>

<h3 id="相关链接">相关链接</h3>

<ul>
<li><a href="https://code.visualstudio.com/docs/remote/ssh">Remote Development using SSH</a></li>
</ul>

<h3 id="使用说明">使用说明</h3>

<p>常用命令</p>

<ul>
<li><code>&gt;remote ssh: connect to host</code> 连接到远端Server</li>
<li><code>&gt;remote ssh: open configuration file</code> 打开 ssh 配置文件</li>
<li><code>&gt;remote ssh: settings</code> 打开设置</li>
<li><code>&gt;remote-ssh:show log</code> 打开日志</li>
</ul>

<p>基本使用</p>

<ul>
<li>进入 <code>&gt;remote ssh: connect to host</code> 命令</li>
<li>选择 ssh config 已经配置好的主机或者输入用户名主机信息，形如 <code>username@host</code></li>
</ul>

<p>技巧</p>

<ul>
<li>通过 <code>~/.ssh/config</code> 可以方便的管理 server 列表，可通过 <code>&gt;remote ssh: open configuration file</code> 命令打开</li>
<li>通过 活动栏 图标打开 远程资源管理器，可以查看所有可以历史打开的远程工作区，方便一键打开远程开发</li>
<li>远程开发的窗口，与本地开发窗口不同点在于

<ul>
<li>扩展视图：添加了安装到远程的特性</li>
<li>设置视图：添加了远程主机的设置，优先级 在 工作区 和 用户设置之间。</li>
</ul></li>
</ul>

<p><a href="https://code.visualstudio.com/docs/remote/troubleshooting#_ssh-tips">提示</a></p>

<ul>
<li>通过 <code>remote SSH</code> 连接主机，需要进行如下配置

<ul>
<li>远程主机开启 sshd 服务，且开启ssh端口</li>
<li>（不强制，单非常建议）配置 主机 通过 key-based 验证（所谓的免密登录），最简单的方式是在本地设备执行如下命令 （<a href="https://code.visualstudio.com/docs/remote/troubleshooting#_quick-start-using-ssh-keys">参考</a>）

<ul>
<li>如果本地设备没有公私钥需要执行 <code>ssh-keygen -t rsa -C &quot;your_email@example.com&quot;</code></li>
<li>将公钥拷贝到远端 <code>ssh-copy-id username@host</code></li>
</ul></li>
</ul></li>

<li><p>连接失败，安装如下步骤进行检查</p>

<ul>
<li>配置 <code>&quot;remote.SSH.showLoginTerminal&quot;: true,</code> 观察是否需要输入密码</li>
<li>在连接远端的VSCode 窗口执行 <code>&gt;remote-ssh:show log</code> 观察输出</li>

<li><p>进入远端主机可以尝试，杀死进程并清理文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kill -9 <span style="color:#e6db74">`</span>ps ax | grep <span style="color:#e6db74">&#34;remoteExtensionHostAgent.js&#34;</span> | grep -v grep | awk <span style="color:#e6db74">&#39;{print $1}&#39;</span><span style="color:#e6db74">`</span>
kill -9 <span style="color:#e6db74">`</span>ps ax | grep <span style="color:#e6db74">&#34;watcherService&#34;</span> | grep -v grep | awk <span style="color:#e6db74">&#39;{print $1}&#39;</span><span style="color:#e6db74">`</span>
rm -rf ~/.vscode-server <span style="color:#75715e"># Or ~/.vscode-server-insiders</span></code></pre></div></li>
</ul></li>
</ul>

<h3 id="常用配置">常用配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">连接远端自动安装的扩展列表</span>
    <span style="color:#f92672">&#34;remote.SSH.defaultExtensions&#34;</span>: [
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">常用工具</span>
        <span style="color:#e6db74">&#34;editorconfig.editorconfig&#34;</span>,
        <span style="color:#e6db74">&#34;donjayamanne.githistory&#34;</span>,
        <span style="color:#e6db74">&#34;codezombiech.gitignore&#34;</span>,
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Java</span>
        <span style="color:#e6db74">&#34;vscjava.vscode-java-pack&#34;</span>,
        <span style="color:#e6db74">&#34;gabrielbb.vscode-lombok&#34;</span>,
        <span style="color:#e6db74">&#34;pivotal.vscode-boot-dev-pack&#34;</span>,
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Python</span>
        <span style="color:#e6db74">&#34;ms-python.python&#34;</span>,
    ],
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置端口转发</span>
    <span style="color:#f92672">&#34;remote.SSH.defaultForwardedPorts&#34;</span>: [
        {
            <span style="color:#f92672">&#34;localPort&#34;</span>: <span style="color:#ae81ff">8080</span>,
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;example&#34;</span>,
            <span style="color:#f92672">&#34;remotePort&#34;</span>: <span style="color:#ae81ff">8080</span>
        }
    ],
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">使用密码登录的时候需要</span>
    <span style="color:#f92672">&#34;remote.SSH.showLoginTerminal&#34;</span>: <span style="color:#66d9ef">true</span>
}</code></pre></div>
<h2 id="remote-containers">Remote - Containers</h2>

<p>Remote - SSH 解决了远程开发的问题，但是本质上还是 个人 的 定制的环境，仍然不够敏捷。</p>

<p>通过 Remote - Containers 可以做到团队项目成员一致性的环境、真正快速的搭建开发环境、开发环境统一管理。可以极大的降低大型项目的环境搭建问题。</p>

<h2 id="其他扩展">其他扩展</h2>

<p>参见：<a href="https://code.visualstudio.com/docs/remote/remote-overview">官网文档</a></p>
]]></description></item><item><title>快速开始</title><link>https://www.rectcircle.cn/series/vscode/get-started/</link><pubDate>Sat, 11 Apr 2020 14:40:51 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/get-started/</guid><description type="html"><![CDATA[

<h2 id="安装">安装</h2>

<p>和普通软件安装方式一致，进入<a href="https://code.visualstudio.com/">官方网站</a>后，点击 <code>Download for xxx</code>，安装即可，在此不做赘述。</p>

<p>详细文档参见 <a href="https://code.visualstudio.com/docs/setup/setup-overview">setup</a></p>

<h2 id="ui">UI</h2>

<p>更多参见 <a href="https://code.visualstudio.com/docs/getstarted/userinterface">User Interface</a></p>

<p>本质上，Visual Studio Code是代码编辑器。其基本布局如下：</p>

<p><img src="https://code.visualstudio.com/assets/docs/getstarted/userinterface/hero.png" alt="ui" /></p>

<ul>
<li>A - Activity Bar 最左侧为活动栏，由于切换 Side Bar</li>
<li>B - Side Bar 侧边栏，包含诸如资源管理器之类的不同视图，可在您处理项目时为您提供帮助。</li>
<li>C - Editor Group 编辑器组，最大的主要区域。您可以在垂直和水平方向上并排打开任意多个编辑器。</li>
<li>D - Panel 面板，包含四种页面，问题、输出、调试控制台、终端</li>
<li>E - Status Bar 最下方为状态栏，展示 打开的项目和编辑的文件的状态信息。</li>
</ul>

<h3 id="活动栏">活动栏</h3>

<ul>
<li>右击活动栏，隐藏活动栏的图标</li>
<li>活动栏图标可以拖动</li>
</ul>

<h3 id="侧边栏">侧边栏</h3>

<p>侧边栏基本UI元素为可折叠视图，拖动可折叠视图可以调整顺序，右击可以隐藏视图</p>

<h4 id="资源管理器">资源管理器</h4>

<p>内置三个可折叠视图</p>

<ul>
<li>打开的编辑器</li>
<li>文件夹</li>
<li>大纲</li>
</ul>

<h4 id="搜索">搜索</h4>

<p>搜索视图可以实现多文件查找替换</p>

<h4 id="源代码管理">源代码管理</h4>

<p>支持对 git/svn 等代码仓库的管理</p>

<h4 id="run">Run</h4>

<p>运行/调试应用程序</p>

<h4 id="扩展商店">扩展商店</h4>

<p>管理扩展</p>

<h3 id="编辑器组">编辑器组</h3>

<p>编辑器可以细分为如下部分</p>

<ul>
<li>标签 位于最上方的</li>
<li>快捷操作按钮 位于最上方右侧</li>
<li>编辑区域</li>
<li>最右侧的小地图和滚动条</li>
</ul>

<h3 id="面板">面板</h3>

<ul>
<li>问题：展示各种Lint扩展检查出的问题信息</li>
<li>输出：展示扩展的输出，一般用于扩展调试</li>
<li>调试控制台：用于代码调试过程中查看变量等操作</li>
<li>终端：执行一些命令</li>
</ul>

<h3 id="状态栏">状态栏</h3>

<p>展示状态信息</p>

<h2 id="命令面板">命令面板</h2>

<h3 id="执行命令">执行命令</h3>

<p>在 VSCode 中所有操作都对应一个命令，触发这些命令的方式主要有两种</p>

<ul>
<li>键盘快捷键</li>
<li>命令面板手动触发</li>
</ul>

<p>唤起命令面板的默认快捷键为 <code>command + shift + p</code></p>

<p>如何查看所有的命名呢，一个技巧就是打开快捷键配置视图进行搜索 （ <code>command + shift + p</code> 输入键盘 <code>keyboard shortcuts</code> 选择 <code>首选项: 打开键盘快捷方式</code> ）</p>

<h3 id="其他类型命令面板">其他类型命令面板</h3>

<p>VSCode 的命令面板以前缀来区分功能，具体可以通过如下方式列出</p>

<ul>
<li><code>?</code> 列出所有前缀，<code>⌘P</code> 并输入 <code>?</code></li>
</ul>

<h2 id="常用命令和快捷键">常用命令和快捷键</h2>

<blockquote>
<p>本部分仅列出常用的命令和默认快捷键，并提供命令名称，这样就可以在命令面板查看自己平台的快捷键和自定义配置快捷键</p>
</blockquote>

<h3 id="编辑器相关">编辑器相关</h3>

<h4 id="光标移动">光标移动</h4>

<ul>
<li><code>上下左右</code> 方向键移动光标一个行/列

<ul>
<li><code>cursorUp</code></li>
<li><code>cursorDown</code></li>
<li><code>cursorLeft</code></li>
<li><code>cursorRight</code></li>
</ul></li>
<li><code>command+上下左右</code> 光标移动到行尾行首列首列尾

<ul>
<li><code>cursorEnd</code></li>
<li><code>cursorHome</code></li>
<li><code>cursorTop</code></li>
<li><code>cursorBottom</code></li>
</ul></li>
<li><code>option+左右</code> 光标移动一个单词

<ul>
<li><code>cursorWordStartLeft</code></li>
<li><code>cursorWordEndRight</code></li>
</ul></li>
</ul>

<h4 id="选择">选择</h4>

<p>在光标移动的基础上多按住 <code>shift</code> 键</p>

<ul>
<li><code>command + a</code> 选择全部</li>
<li><code>control+shift+左右</code> 根据代码语义进行选择与去取消

<ul>
<li><code>editor.action.smartSelect.shrink</code></li>
<li><code>editor.action.smartSelect.expand</code></li>
</ul></li>
</ul>

<h4 id="多光标">多光标</h4>

<ul>
<li><code>option+command+上下</code> 在当前上面一行添加光标</li>
<li>一直添加光标到底部顶部

<ul>
<li><code>editor.action.addCursorsToBottom</code></li>
<li><code>editor.action.addCursorsToTop</code></li>
</ul></li>
<li>根据单词匹配添加光标

<ul>
<li>向下搜索 <code>editor.action.addSelectionToNextFindMatch</code></li>
<li>向上搜索 <code>editor.action.addSelectionToPreviousFindMatch</code></li>
</ul></li>
<li><code>option + shift + i</code> 在选中文文本的行尾部添加光标 <code>editor.action.insertCursorAtEndOfEachLineSelected</code></li>
<li><code>option + enter</code> 在搜索窗口，选中命中的搜索目标</li>
</ul>

<h4 id="行操作">行操作</h4>

<ul>
<li>删除行 <code>editor.action.deleteLines</code> Eclipse 建议配置成 <code>command + d</code></li>
<li><code>option + 上下</code> 整行移动

<ul>
<li><code>editor.action.moveLinesUpAction</code></li>
<li><code>editor.action.moveLinesDownAction</code></li>
</ul></li>
<li><code>option + shift + 上下</code> 整行复制

<ul>
<li><code>editor.action.copyLinesUpAction</code></li>
<li><code>editor.action.copyLinesDownAction</code></li>
</ul></li>
<li><code>command + enter</code> 在下方加入一行 <code>editor.action.insertCursorAbove</code></li>
<li><code>command + shift + enter</code> 在上方加入一行 <code>editor.action.insertLineBefore</code></li>
</ul>

<h4 id="智能编辑">智能编辑</h4>

<ul>
<li>智能提示（触发建议） <code>editor.action.triggerSuggest</code> Eclipse转来的用户建议配置成了 <code>option + /</code></li>
<li><code>command + /</code> 切换注释 <code>editor.action.commentLine</code></li>
<li><code>shift + tab</code> 去除缩进 <code>outdent</code></li>
<li><code>command + .</code> 快速修复 <code>editor.action.quickFix</code></li>
<li><code>command + option + .</code> 快速修复 <code>editor.action.autoFix</code></li>
<li>显示悬停 <code>editor.action.showHover</code>，建议配置成 <code>command + h</code>（hover）</li>
<li><code>command + F2</code> 修改所有匹配项（相当于查找替换）</li>
<li>格式化文档，Eclipse 转来建议改成 <code>ctrl+shift+f</code>

<ul>
<li><code>editor.action.formatDocument</code> 格式化整个文档</li>
<li><code>editor.action.formatSelection</code> 格式化选中内容</li>
</ul></li>
<li><code>option + shift + o</code> 组织导入 <code>editor.action.organizeImports</code></li>
<li><code>ctrl + shift + r</code> 重构 <code>editor.action.refactor</code></li>
<li><code>editor.action.rename</code> 重命名 <code>editor.action.refactor</code></li>
</ul>

<h4 id="跳转">跳转</h4>

<ul>
<li><code>F12</code> 跳转到定义 <code>editor.action.revealDefinition</code></li>
<li><code>command + F12</code> 跳转到实现 <code>editor.action.goToImplementation</code></li>
<li><code>shift + F12</code> 转到引用 <code>editor.action.goToReferences</code></li>
<li><code>command + shift + \</code> 跳转到括号  <code>editor.action.jumpToBracket</code></li>
<li><code>F7</code> 跳转到下一个匹配项 <code>editor.action.wordHighlight.next</code></li>
<li><code>shift + F7</code> 跳转到上一个匹配项 <code>editor.action.wordHighlight.prev</code></li>
<li><code>F8</code> 跳转到文件中下一个问题 <code>editor.action.marker.nextInFiles</code></li>
<li><code>shift + F8</code> 跳转到文件中上一个问题 <code>editor.action.marker.prevInFiles</code></li>
<li>符号跳转

<ul>
<li><code>command+t</code> 搜索工作空间符号 <code>workbench.action.showAllSymbols</code></li>
<li><code>shift+command+o</code> 搜索文件符号 <code>workbench.action.gotoSymbol</code></li>
</ul></li>
<li>前进后退

<ul>
<li><code>workbench.action.navigateForward</code> 前进 建议配置成 <code>ctrl + =</code></li>
<li><code>workbench.action.navigateBack</code> 后退 建议配置成 <code>ctrl + -</code></li>
</ul></li>
</ul>

<h4 id="折叠">折叠</h4>

<ul>
<li>全部折叠 <code>editor.foldAll</code></li>
<li>全部展开 <code>editor.unfoldAll</code></li>
</ul>

<h3 id="窗口切换">窗口切换</h3>

<ul>
<li><code>command + n</code> 新建文件窗口 <code>workbench.action.files.newUntitledFile</code></li>
<li><code>command + w</code> 关闭窗口 <code>workbench.action.closeWindow</code></li>
<li><code>command + ,</code> 打开设置窗口 <code>workbench.action.openSettings</code></li>
<li>编辑器窗口之间的切换

<ul>
<li><code>alt+cmd+right</code> 下一个编辑器 <code>workbench.action.nextEditor</code></li>
<li><code>alt+cmd+left</code> 上一个编辑器 <code>workbench.action.previousEditor</code></li>
<li><code>ctrl + tab</code> 顺序切换 <code>workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup</code></li>
</ul></li>
<li>编辑器与其他窗口切换

<ul>
<li><code>ctrl + 反引号</code> 切换到终端 <code>workbench.action.terminal.toggleTerminal</code></li>
<li>切换到编辑器 <code>workbench.action.focusActiveEditorGroup</code> 建议配置成 <code>option + e</code></li>
<li><code>command + shift + e</code> 切换到资源管理器 <code>workbench.view.explorer</code></li>
</ul></li>
<li>终端

<ul>
<li>新建终端  <code>workbench.action.terminal.new</code> 建议配置为 <code>command + t</code></li>
<li>上一个/下一个终端 <code>workbench.action.terminal.focusPrevious</code> 与 <code>workbench.action.terminal.focusNext</code> 建议配置为 <code>command+option+上下</code></li>
</ul></li>
</ul>

<h3 id="调试">调试</h3>

<ul>
<li><code>F5</code> 启动调试/继续

<ul>
<li><code>workbench.action.debug.start</code></li>
<li><code>workbench.action.debug.continue</code></li>
</ul></li>
<li><code>shift + F5</code> 停止调试 <code>workbench.action.debug.stop</code></li>
<li><code>F6</code> 暂停 <code>workbench.action.debug.pause</code></li>
<li><code>command + shift + F5</code> 重启调试 <code>workbench.action.debug.restart</code></li>
<li><code>ctrl + F5</code> 运行，不调试 <code>workbench.action.debug.run</code></li>
<li><code>F10</code> 单步跳过 <code>workbench.action.debug.stepOver</code></li>
<li><code>F11</code> 单步进入 <code>workbench.action.debug.stepInto</code></li>
<li><code>shift + F11</code> 单步跳出 <code>workbench.action.debug.stepOut</code></li>
</ul>

<h3 id="终端相关">终端相关</h3>

<ul>
<li><code>command + k</code> 清屏 (类似的有 <code>clear</code> 命令)</li>
<li><code>ctrl + a</code> 光标切换到行首</li>
<li><code>ctrl + e</code> 光标切换到行尾</li>
<li><code>ctrl + u</code> 或者 <code>command + 退格</code> 删除整行</li>
<li><code>option + 退格</code> 删除一个单词</li>
<li><code>command + 上/下</code> 滚动到上一条/下一条命令位置</li>
</ul>

<h2 id="配置">配置</h2>

<h3 id="配置机制">配置机制</h3>

<p>使用 <code>command + ,</code> 或者 <code>command + shift + p</code> 搜索 <code>首选项</code> 打开配置面板。</p>

<p>VSCode 的配置是基于文件的配置（格式为JSON，允许包含注释），有三个级别的配置优先级从高到低排序分别为：</p>

<ul>
<li>工作空间配置 位于 <code>.vscode/settings.json</code></li>
<li>用户配置 位于安装目录</li>
<li>默认配置</li>
</ul>

<p>优先级高的配置覆盖优先级低的配置。当然VSCode提供可视化的配置页面，通过如下方式可以打开</p>

<p>使用 <code>command + ,</code> 或者 <code>command + shift + p</code> 搜索 <code>首选项</code> 打开配置面板。</p>

<h3 id="常见内置配置">常见内置配置</h3>

<p>本小结，将展示可能需要更改的配置。其他的配置建议保持默认即可</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;files.autoSave&#34;</span>: <span style="color:#66d9ef">false</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">自动保存，建议不要开启，经常</span> <span style="color:#960050;background-color:#1e0010">command</span> <span style="color:#960050;background-color:#1e0010">+</span> <span style="color:#960050;background-color:#1e0010">s</span> <span style="color:#960050;background-color:#1e0010">是好习惯</span>
    <span style="color:#f92672">&#34;editor.fontSize&#34;</span>: <span style="color:#ae81ff">16</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启老年人字体😂</span>
    <span style="color:#f92672">&#34;editor.fontFamily&#34;</span>: <span style="color:#e6db74">&#34;consolas, Menlo, Monaco, &#39;Courier New&#39;, monospace, &#39;文泉驿等宽微米黑&#39;&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">更换为自己喜欢的字体</span>
    <span style="color:#f92672">&#34;editor.renderWhitespace&#34;</span>: <span style="color:#e6db74">&#34;all&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">显示空格和Tab键</span>
    <span style="color:#f92672">&#34;editor.codeActionsOnSave&#34;</span>: [  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">如果是新项目建议开启，老项目建议关闭，保存时自动进行导入，自动修复</span>
        <span style="color:#e6db74">&#34;source.organizeImports&#34;</span>,
        <span style="color:#e6db74">&#34;source.fixAll&#34;</span>,
    ],
    <span style="color:#f92672">&#34;editor.smoothScrolling&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">炫酷的平滑滚动，在性能较好的设备中建议启用</span>
    <span style="color:#f92672">&#34;editor.cursorSmoothCaretAnimation&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span>  <span style="color:#960050;background-color:#1e0010">炫酷的光标平滑滚动，在性能较好的设备中建议启用</span>
    <span style="color:#f92672">&#34;editor.formatOnPaste&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启，不满足</span> <span style="color:#960050;background-color:#1e0010">直接</span> <span style="color:#960050;background-color:#1e0010">command</span> <span style="color:#960050;background-color:#1e0010">+</span> <span style="color:#960050;background-color:#1e0010">z</span> <span style="color:#960050;background-color:#1e0010">撤销即可</span>
    <span style="color:#f92672">&#34;editor.formatOnSave&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启，保存自动格式化</span>
    <span style="color:#f92672">&#34;editor.formatOnType&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启，在回车后自动格式化</span>
    <span style="color:#f92672">&#34;diffEditor.ignoreTrimWhitespace&#34;</span>: <span style="color:#66d9ef">false</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启空白字符diff</span>
    <span style="color:#f92672">&#34;editor.quickSuggestions&#34;</span>: {  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在所有位置输入所有字符都显示提示</span>
        <span style="color:#f92672">&#34;other&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;comments&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;strings&#34;</span>: <span style="color:#66d9ef">true</span>,
    },
    <span style="color:#f92672">&#34;files.exclude&#34;</span>: {  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议显示</span> <span style="color:#960050;background-color:#1e0010">.git</span> <span style="color:#960050;background-color:#1e0010">文件</span>
        <span style="color:#f92672">&#34;**/.git&#34;</span>: <span style="color:#66d9ef">false</span>,
    },
    <span style="color:#f92672">&#34;files.insertFinalNewline&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在保存时，自动插入一行</span>
    <span style="color:#f92672">&#34;workbench.editor.wrapTabs&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">编辑器组标签溢出后自动换行</span>
    <span style="color:#f92672">&#34;workbench.commandPalette.preserveInput&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">再次打开命令面板恢复上次输入的内容</span>
    <span style="color:#f92672">&#34;workbench.quickOpen.closeOnFocusLost&#34;</span>: <span style="color:#66d9ef">false</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">失去焦点时，命令面板也不自动关闭</span>
    <span style="color:#f92672">&#34;workbench.quickOpen.preserveInput&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">再次打开快速打开板恢复上次输入的内容</span>
    <span style="color:#f92672">&#34;workbench.colorTheme&#34;</span>: <span style="color:#e6db74">&#34;Monokai&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">主题，在扩展商城挑选</span>
    <span style="color:#f92672">&#34;workbench.iconTheme&#34;</span>: <span style="color:#e6db74">&#34;vscode-icons&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">图标，在扩展商城挑选</span>
    <span style="color:#f92672">&#34;breadcrumbs.enabled&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">建议开启，是否显示编辑器顶部的导航路径</span>
    <span style="color:#f92672">&#34;workbench.editor.tabSizing&#34;</span>: <span style="color:#e6db74">&#34;shrink&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">编辑器</span> <span style="color:#960050;background-color:#1e0010">tab</span> <span style="color:#960050;background-color:#1e0010">紧凑模式</span>
    <span style="color:#f92672">&#34;window.autoDetectColorScheme&#34;</span>: <span style="color:#66d9ef">false</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">主题跟随系统变化</span>
    <span style="color:#f92672">&#34;terminal.integrated.copyOnSelection&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">将终端选中内容复制到剪切板</span>
    <span style="color:#f92672">&#34;terminal.integrated.fontFamily&#34;</span>: <span style="color:#e6db74">&#34;Menlo, Monaco, &#39;Courier New&#39;, monospace,&#39;Roboto Mono Medium for Powerline&#39;&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置终端字体</span>
    <span style="color:#f92672">&#34;terminal.integrated.scrollback&#34;</span>: <span style="color:#ae81ff">100000</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">终端缓冲区大小，建议调大</span>
    <span style="color:#f92672">&#34;terminal.integrated.shell.osx&#34;</span>: <span style="color:#e6db74">&#34;/bin/zsh&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">mac</span> <span style="color:#960050;background-color:#1e0010">的默认终端</span>
    <span style="color:#f92672">&#34;terminal.integrated.shell.linux&#34;</span>: <span style="color:#e6db74">&#34;/bin/zsh&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">Linux</span> <span style="color:#960050;background-color:#1e0010">的默认终端</span>
    <span style="color:#f92672">&#34;problems.showCurrentInStatus&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在状态连显示错误信息</span>

}</code></pre></div>
<h2 id="扩展">扩展</h2>

<p>以上部分仅介绍了 VSCode 的基本功能。VSCode真正强大之处在于他的扩展，因此一些优质扩展的安装是必不可少的。关于扩展相关内容，请参考</p>

<p>TODO 章节</p>
]]></description></item><item><title>通用规范</title><link>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E9%80%9A%E7%94%A8%E8%A7%84%E8%8C%83/</link><pubDate>Sat, 04 Apr 2020 21:16:21 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E9%80%9A%E7%94%A8%E8%A7%84%E8%8C%83/</guid><description type="html"><![CDATA[

<blockquote>
<p>更新时间：2020-04-04</p>
</blockquote>

<h2 id="目标">目标</h2>

<ul>
<li>杜绝人员单点依赖风险（杜绝项目依赖特定个人，杜绝只有某个人才知道某个项目的关键点）</li>
<li>降低代码失控风险（降低代码千人千面情况）</li>
<li>快速入手（拥有开发能力的个人，可以短时间内，无需指点的自助进入开发状态）</li>
<li>代码整洁（让拥有代码洁癖的人，参与旧项目也有宾至如归的感觉）</li>
</ul>

<h2 id="原则">原则</h2>

<ul>
<li><code>code-repo-has-max-context</code> 代码仓库拥有最大的上下文信息。该原则指，一个项目的全部信息（包括但不限于：PRD、部署信息、设计文档、测试、开发入门手册）可以通过代码仓库在明确的有限次跳后可以到达，不允许出现代码仓库无法触达信息孤岛</li>
<li><code>develop-docs-all-in-code-repo</code> 所有与开发相关的文档维护在代码仓库中（包括但不限于：系统设计文档、技术分享、开发环境搭建指南、开发规范）</li>
<li><code>specification-by-tool-constraint</code> 所有实施的规范必须通过工具进行约束，口头规范约束是无效的</li>
<li><code>simple-local-run</code> 代码仓库必须是可以在本地环境（个人电脑）根据文档在简单的配置后可运行的</li>
<li><code>use-newest-lts-version</code> 编程语言和依赖库使用最新的 lts 版本（或 <code>stable</code>），在新版本兼容性不足时或者生态受限时可以使用 <code>latest - 1</code> 版本</li>
<li><code>not-dependent-specific-env</code> 不依赖特定环境，所有依赖均可已通过 包管理工具（如 apt、mvn、pip）等安装；针对特定的没有被托管在包管理工具的依赖，必须放置与代码仓库中</li>
</ul>

<h2 id="开发环境规范">开发环境规范</h2>

<ul>
<li>保证项目在 <code>Mac</code> 和 <code>Linux</code> 环境下可以正常开发，<code>Windows</code> 环境不考虑</li>
<li>不强制要求使用特定集成开发环境</li>
<li>不强制依赖某集成开发环境的特定功能</li>
<li>强制依赖cli环境的开发工具链</li>
</ul>

<h2 id="通用文件目录">通用文件目录</h2>

<ul>
<li><code>.gitignore</code></li>
<li><code>**/.gitkeep</code></li>
<li><code>cli/</code></li>
<li><code>docs/</code></li>
<li><code>README.md</code></li>
<li><code>.editorconfig</code></li>
</ul>

<h3 id="git">git</h3>

<p>约定使用 <code>git</code> 做代码版本管理工具，必须包含如下文件</p>

<ul>
<li><code>.gitignore</code> 忽略文件</li>
<li><code>**/.gitkeep</code> 空目录建议添加一个 <code>.gitkeep</code> 文件以保证目录也被提交到 <code>git</code> 仓库中</li>
</ul>

<h3 id="cli">cli</h3>

<p><code>Shell</code> 脚本，需要托管的二进制文件，等可执行文件放置于 <code>cli</code> 目录下，其目录结构如下</p>

<ul>
<li><code>cli/</code>

<ul>
<li><code>cli/*.sh</code> Shell 脚本</li>
<li><code>cli/lib/</code> Shell 脚本依赖的库，比如 jar 包等</li>
</ul></li>
</ul>

<h3 id="docs">docs</h3>

<p>项目的各种文档，强制使用 <code>markdown</code> 格式进行编写，文档必须放在 <code>markdown</code> 目录下的二级目录下，命名格式为 <code>%d%d.文件名.md</code>，图片放置于 <code>docs/asserts/images</code> 目录下，一个例子如下：</p>

<ul>
<li><code>docs/</code>

<ul>
<li><code>docs/asserts/images</code></li>
<li><code>docs/01.环境搭建/01.通用环境.md</code></li>
<li><code>docs/01.环境搭建/02.VSCode开发环境.md</code></li>
</ul></li>
</ul>

<h3 id="readme-md">README.md</h3>

<p>项目梗概，同时可以包含 get started，非常重要，相当于项目主页</p>

<h3 id="editorconfig-等其他配置文件">.editorconfig 等其他配置文件</h3>

<ul>
<li><a href="https://editorconfig.org/">.editorconfig</a> 是一种跨编辑器的通用文件格式配置文件，支持大多数主流编辑器和集成开发环境</li>
</ul>

<h2 id="分支管理与devops">分支管理与Devops</h2>

<p>可以根据项目复杂度和人员规模进行选择，本规范采用单主干模式</p>

<ul>
<li>一个主干分支</li>
<li>多个其他分支</li>
</ul>

<p>准入流程（开发）</p>

<ul>
<li>其他分支向 主干分支 提交 <code>merge request</code> 请求（下文简称 <code>mr</code>）</li>
<li>（可选）准入工作流 拦截 该 <code>mr</code>，进入准入工作流

<ul>
<li>进行代码规范检查</li>
<li>单元测试</li>
<li>功能测试</li>
<li>以上各个阶段均成功后，进入下一阶段</li>
</ul></li>
<li>邀请 Reviewer 进行 Review 和 合并</li>
</ul>

<p>主干流程（上线）</p>

<ul>
<li>Merge 成功后进入 该流程（可选的每日定时集成）

<ul>
<li>项目编译</li>
<li>制作镜像</li>
<li>基准测试</li>
<li>人工卡点</li>
<li>K8S上线</li>
</ul></li>
</ul>

<p>P.S.</p>

<ul>
<li>准入工作流需要在本地的 <code>git hook</code> 脚本 <code>pre_commit</code> 中进行配置</li>
</ul>
]]></description></item><item><title>上海居住证</title><link>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E4%B8%8A%E6%B5%B7%E5%B1%85%E4%BD%8F%E8%AF%81/</link><pubDate>Sat, 28 Dec 2019 19:21:10 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E4%B8%8A%E6%B5%B7%E5%B1%85%E4%BD%8F%E8%AF%81/</guid><description type="html"><![CDATA[

<blockquote>
<p>更新时间：2019-12-28</p>

<p>相关网站和公众号
* <a href="https://www.962222.net/index.html">上海市市民信息服务网</a>
* 公众号：上海公安人口管理（shgarkb）</p>
</blockquote>

<h2 id="上海居住证用途">上海居住证用途</h2>

<p>居住证是给外地人在上海享受社会公共福利的基本条件。</p>

<p>法规原文参见 <a href="http://www.shanghai.gov.cn/nw2/nw2314/nw2319/nw12344/u26aw54292.html">《上海市居住证管理办法》（沪府令58号）</a></p>

<p>简单摘要如下几个作用（必备条件）：</p>

<blockquote>
<p>参考： <a href="http://sh.bendibao.com/zffw/2018820/196865.shtm">http://sh.bendibao.com/zffw/2018820/196865.shtm</a></p>
</blockquote>

<ul>
<li>办证

<ul>
<li>护照（已经不需要了、可以选择全国通办）</li>
<li>驾驶证</li>
<li>参与沪牌拍卖资格（居住证满1年）</li>
</ul></li>
<li>子女教育

<ul>
<li>幼儿园、义务教育（小学初中）</li>
<li>中考、高考、高中（积分120、不满无法参见）</li>
</ul></li>
<li>子女社保（积分120）</li>
<li>子女公共卫生服务

<ul>
<li>疫苗接种</li>
</ul></li>
<li>资格评定、考试和鉴定</li>
</ul>

<h2 id="什么人需要上海居住证">什么人需要上海居住证</h2>

<p>想要在上海长期发展（一定要办）或者目前在上海但是未来没有规划的（以防万一）的在沪外地户口人员。</p>

<p>注意如果本科生或研究生通过积分落户的则不需要居住证（你已经是上海市民了）</p>

<h2 id="办理流程和方式">办理流程和方式</h2>

<p>居住证办理需要两个步骤，耗时半年。居住证办理下来后开可以办理居住证积分（委托单位办理）和居转户（7年），则不再本文讨论范围。</p>

<h3 id="第一步-居住登记">第一步：居住登记</h3>

<p>居住登记需要跨部门办理，所以各个部门可能都不知道全部流程。且各个区也有可能不同，但是按照如下顺序办理，会省很多时间。当然也可以委托中介进行办理（可能会多花一些钱）。以下说明是个人办理的流程（亲身经历：宝山顾村公园附近）。</p>

<h4 id="花费时间">花费时间</h4>

<ul>
<li><strong>实有人口信息采集</strong> 可能需要等待一段时间</li>
<li>其他步骤 一个上午搞定</li>
</ul>

<h4 id="各部门上班时间">各部门上班时间</h4>

<ul>
<li>居委会可能只有周一到周五上班</li>
<li>其他部门周一到周五上午8点30到下午16点30，节假日（周六日）只上午上班8点到11点半</li>
</ul>

<h4 id="居住登记的需要的材料和人">居住登记的需要的材料和人</h4>

<ul>
<li>人员

<ul>
<li>房东</li>
<li>租客</li>
</ul></li>
<li>材料

<ul>
<li>房东的房屋所有权证/上海市房地产权证/不动产权证</li>
<li>房东的身份证</li>
<li>房客的身份证</li>
</ul></li>
</ul>

<p>注意：</p>

<ul>
<li>此处只陈述租房场景的</li>
<li><strong>房产证的如果有多人所有人都要到场或者至少来一个（需要其他人写一个委托书、非本人经历不一定可行）</strong>

<ul>
<li><a href="https://wenku.baidu.com/view/4872c7d5bf23482fb4daa58da0116c175e0e1e68.html">https://wenku.baidu.com/view/4872c7d5bf23482fb4daa58da0116c175e0e1e68.html</a></li>
</ul></li>
<li>申请表等各个部门都有不要自己打印</li>
<li>材料复印件可以准备一份，各个部门也可以打印</li>
<li>如果是二房东的话或者房东不愿意给你办或者房东没有房产证等证件的话，你可以按如下办法做

<ul>
<li>找房产中介（不限于自己租的房子的中介）说明花钱办理居住证</li>
<li>如果自己有亲戚朋友在上海有房产，那么可以麻烦亲戚朋友按照如下步骤走（个人做法）</li>
</ul></li>
</ul>

<h4 id="居委会">居委会</h4>

<p>居委会一般每个小区都有一个，一定要先到居委会去。</p>

<p>到居委会后告诉工作人员你要办居住证，他会给你做 <strong>实有人口信息采集</strong>（拍照、上传身份证、居住地址），这些信息会上传到公安系统。工作人员会告诉你几天后可以去办居住证了，接下来的地点也会和你说的（或者问他），以前需要领个证明材料现在已经不需要了。</p>

<p>这一步骤自己去就行了。</p>

<h4 id="居住证受理网点1">居住证受理网点1</h4>

<p>全部网点如下：</p>

<p><a href="https://www.962222.net/pages/sbk/sbkfw.html">https://www.962222.net/pages/sbk/sbkfw.html</a></p>

<p>一般是社区受理中心（也可能是公安局，和社区受理中心不再一个地方），先到这里来的原因是需要问明你签的合同的房租是多少，他们需要收房屋租赁税，为了创收他们不允许你的合同租金过低。</p>

<p>所以该步骤就是<strong>咨询</strong></p>

<p>告诉工作人员你要办理居住证，他会告诉你却什么材料，一般是《房租赁合同通知书》，并咨询清楚房屋房租应该是多少。并问清楚去哪里办《房租赁合同备案通知书》。</p>

<p>需要和房东一起携带上述材料前往</p>

<h4 id="社区受理中心">社区受理中心</h4>

<p>地址： 地图应用搜 社区受理中心</p>

<p>告诉工作人员要办理居住证，他会现场给你办理网签（租赁合同网签备案，原来需要跑房产中心的，2019-12-26日起在社区受理中心就可以一站式办理了🎉）（填写申请表，注意房租金额，签约时间，建议长一点比如3年，省得再跑一遍），然后出具一个《房租赁合同备案通知书》。</p>

<p>最后询问后续步骤的地点（再次确认流程）</p>

<h4 id="居住证受理网点2">居住证受理网点2</h4>

<p>注意这一步可以尝试在网上办理通过《上海公安人口管理》公众号进行办理（本文未尝试过，不知道是否可以跳过交税）</p>

<p>回到此处，他会审查你的材料，并可能让你去交房屋租赁税（2019-12-27房租税率为2.5%）。然后填写材料拍照，给你一个《居住登记凭证》</p>

<p>至此居住登记办理完成。</p>

<h4 id="注意-后续"><strong>注意&amp;后续</strong></h4>

<ul>
<li>半年内不要变更《实有人口信息申报》，否则无法办理居住证

<ul>
<li>在这半年内，如果换房子了，房东大概率会向你要身份证时，这时候和他说明你在班居住证，不能让他去给你登记。</li>
<li>即使被登记了，也没关系，按照如下步骤操作：

<ul>
<li>关注微信公众号：上海公安人口管理（shgarkb）</li>
<li>选择：业务办理 -&gt; 业务办理</li>
<li>点击：实有人口信息自助申报，重新填写会之前登记的地址区县</li>
<li>一般不会有人电话核实</li>
<li>等待3个工作日，公众号推送“业务办理公知”状态为“已采集”即可</li>
</ul></li>
</ul></li>
<li>妥善保管好

<ul>
<li>《房租赁合同备案通知书》</li>
<li>《居住登记凭证》</li>
</ul></li>
<li>税款仅支持现金支付</li>
<li>定好闹钟日程，半年后申领居住证</li>
</ul>

<h3 id="第二步-申领居住证">第二步：申领居住证</h3>

<h4 id="申请">申请</h4>

<ul>
<li>时间：居住登记半年后</li>
<li>地点：居住证受理网点

<ul>
<li>工作时间：工作日8:30~16:30，周末8:30~11:30</li>
</ul></li>
<li>准备好如下材料

<ul>
<li>税单</li>
<li>《上海市住房租赁合同备案通知书》</li>
<li>《居住登记凭证》</li>
<li>办证者本人身份证</li>
<li>零钱/身份证复印件</li>
</ul></li>
<li>到场人员

<ul>
<li>办证者本人</li>
<li>深色上衣（可以重新拍摄居住证证件照片）</li>
</ul></li>
<li>耗费时间：10分钟内</li>
</ul>

<p>回执材料</p>

<ul>
<li>《上海市居住证受理回执》</li>
</ul>

<h4 id="领取">领取</h4>

<ul>
<li>时间：申请后20天</li>
<li>地点：居住证受理网点

<ul>
<li>工作时间：工作日8:30~16:30，周末8:30~11:30</li>
</ul></li>
<li>准备好如下材料

<ul>
<li>《上海市居住证受理回执》</li>
</ul></li>
<li>到场人员

<ul>
<li>办证者本人</li>
</ul></li>
<li>耗费时间：5分钟内</li>
</ul>
]]></description></item><item><title>URL 管理</title><link>https://www.rectcircle.cn/series/hugo/content-management/urls/</link><pubDate>Wed, 01 May 2019 16:46:35 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/hugo/content-management/urls/</guid><description type="html"><![CDATA[

<h2 id="固定链接">固定链接</h2>

<p>您构建的网站的默认Hugo输出目录是 <code>public/</code>。但是，您可以通过在 <a href="../../getting-started/configuration">站点配置</a> 中指定其他<code>publishDir</code>来更改此值。在构建时为节创建的目录反映了内容文件夹中内容目录的位置以及与contentdir层次结构中的布局匹配的命名空间。</p>

<p><a href="../../getting-started/configuration">站点配置</a> 中的 <code>permalinks</code> 选项允许您基于每个部分调整目录路径（即URL）。这将更改文件写入的位置，并将更改页面的内部“规范”位置，以便对 <code>.RelPermalink</code> 的模板引用将遵循由此选项中的映射所做的调整。</p>

<blockquote>
<p>这些示例使用<code>publishDir</code>和<code>contentDir</code>的默认值；即，分别是 <code>public</code> 和 <code>content</code>。您可以覆盖 <a href="../../getting-started/configuration">站点配置文件</a> 中的默认值。</p>
</blockquote>

<p>例如，如果您的某个 <code>sections</code> 被称为帖子，并且您希望根据年份，月份和帖子标题将规范路径调整为分层，则可以分别在YAML和TOML中设置以下配置。</p>

<h3 id="永久链接配置示例">永久链接配置示例</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">permalinks</span>]
  <span style="color:#a6e22e">posts</span> = <span style="color:#e6db74">&#34;/:year/:month/:title/&#34;</span></code></pre></div>
<p>只有 <code>posts/</code> 下的内容才会有新的URL结构。例如，文件内容 <code>/posts/ sample-entry.md</code> 中 front matter 的<code>data</code>为：<code>2017-02-27T19:20:00-05:00</code>，则这个内容在构建中将渲染到 <code>public/2017/02/ sample-entry/index.html</code> 中，因此可以通过<code>https://example.com/2017/02/sample-entry/</code>访问。</p>

<p>您还可以使用相同的语法配置按照分类来配置的永久链接来取代默认的section，您可能只想使用配置值 <code>:slug</code> 或 <code>:title</code>。</p>

<h3 id="永久链接配置值">永久链接配置值</h3>

<p>以下是可在站点配置文件中的<code>permalink</code>定义中使用的值列表。所有对时间的引用都取决于内容的日期。</p>

<dl>
<dt><code>:year</code></dt>
<dd>4位年</dd>
<dt><code>:month</code></dt>
<dd>2位月</dd>
<dt><code>:monthname</code></dt>
<dd>英文月</dd>
<dt><code>:day</code></dt>
<dd>2位天</dd>
<dt><code>:weekday</code></dt>
<dd>1位周 (Sunday = 0)</dd>
<dt><code>:weekdayname</code></dt>
<dd>英文周</dd>
<dt><code>:yearday</code></dt>
<dd>1到3位，今年中的第几天</dd>
<dt><code>:section</code></dt>
<dd>所在目录</dd>
<dt><code>:sections</code></dt>
<dd>content到文件所有层次的目录</dd>
<dt><code>:title</code></dt>
<dd>内容标题</dd>
<dt><code>:slug</code></dt>
<dd>内容的slug</dd>
<dt><code>:filename</code></dt>
<dd>文件名不带扩展名</dd>
</dl>

<h2 id="别名">别名</h2>

<p>别名可用于从其他URL创建重定向到您的页面。别名有两种形式：</p>

<ul>
<li>以<code>/</code>开头，表示相对于<code>BaseURL</code>，例如 <code>/posts/my-blogpost/</code></li>
<li>相对路径，相对于这个Page，例如<code>../blog/my-blogpost</code>  Hugo 0.55 新增</li>
</ul>

<h3 id="别名的例子">别名的例子</h3>

<p>假设您在 <code>content/posts/my-awesome-blog-post.md</code> 中创建了一段新内容。该内容是您在 <code>previous/posts/my-original-url.md</code> 上发布的上一篇文章的修订版。您可以在新 <code>my-awesome-blog-post.md</code> 的 front matter 中创建别名字段，您可以在其中添加以前的路径。以下示例分别显示如何在TOML和YAML前端内容中创建此字段。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">+++
aliases = [
    &#34;/posts/my-original-url/&#34;,
    &#34;/2010/01/01/even-earlier-url.html&#34;
]
+++</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
aliases:
    <span style="color:#66d9ef">-</span> /posts/my-original-url/
    <span style="color:#66d9ef">-</span> /2010/01/01/even-earlier-url.html
---</code></pre></div>
<p>现在，当您访问别名中指定的任何位置时，即假设相同的站点域 —— 您将被重定向到指定的页面。例如，<code>example.com/posts/my-original-url/</code> 的访问者将立即重定向到<code>example.com/posts/my-awesome-post/</code>。</p>

<h3 id="多语言中的别名的例子">多语言中的别名的例子</h3>

<p>在 <a href="../multilingual/">多语言网站</a> 上，帖子的每个翻译都可以有唯一的别名。要在多种语言中使用相同的别名，请在其前面加上语言代码。在<code>/posts/my-new-post.es.md</code>中：</p>

<p>在<code>/posts/my-new-post.es.md</code>中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
aliases:
    <span style="color:#66d9ef">-</span> /es/posts/my-original-post/
---</code></pre></div>
<p>从Hugo 0.55其，你也可以有页面相对别名，所以<code>/es/posts/my-original-post/</code>可以简化为更便携的 <code>my-original-post/</code></p>

<h2 id="hugo别名如何运作">Hugo别名如何运作</h2>

<p>指定别名时，Hugo会创建一个与别名条目匹配的目录。在目录中，Hugo创建了一个.html文件，指定页面的规范URL和新的重定向目标。</p>

<p>例如，<code>posts/my-intended-url.md</code> 中的内容文件，front matter有以下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">---
title: My New post
aliases: [/posts/my-old-url/]
---</code></pre></div>
<p>假设<code>example.com</code>为baseURL，在<code>https://example.com/posts/my-old-url/</code>上找到的自动生成的别名.html的内容将包含以下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#75715e">&lt;!DOCTYPE html&gt;</span>
&lt;<span style="color:#f92672">html</span>&gt;
  &lt;<span style="color:#f92672">head</span>&gt;
    &lt;<span style="color:#f92672">title</span>&gt;https://example.com/posts/my-intended-url&lt;/<span style="color:#f92672">title</span>&gt;
    &lt;<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;canonical&#34;</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://example.com/posts/my-intended-url&#34;</span>/&gt;
    &lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;robots&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noindex&#34;</span>&gt;
    &lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">http-equiv</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;content-type&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text/html; charset=utf-8&#34;</span>/&gt;
    &lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">http-equiv</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;refresh&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0; url=https://example.com/posts/my-intended-url&#34;</span>/&gt;
  &lt;/<span style="color:#f92672">head</span>&gt;
&lt;/<span style="color:#f92672">html</span>&gt;</code></pre></div>
<p><code>http-equiv=&quot;refresh&quot;</code>是执行重定向的代码，在这种情况下为0秒。如果您网站的最终用户访问<code>https://example.com/posts/my-old-url</code>，他们现在会自动重定向到更新，更正确的网址。添加<code>&lt;meta name =&quot;robots&quot; content =&quot;noindex&quot;&gt;</code>可让搜索引擎机器人知道他们不应抓取您的新别名页并将其编入索引。</p>

<h3 id="自定义">自定义</h3>

<p>您可以通过在站点的layouts文件夹中创建alias.html模板来自定义此别名页面（即<code>layouts/alias.html</code>）。在这种情况下，传递给模板的数据是：</p>

<dl>
<dt>Permalink</dt>
<dd>指向别名的页面的链接</dd>
<dt>Page</dt>
<dd>该别名指向的页面的数据</dd>
</dl>

<h3 id="别名的重要行为">别名的重要行为</h3>

<ul>
<li>hugo对别名没有任何假设。它们也不会根据您的UglyURL设置进行更改。您需要提供Web根目录的完整路径以及完整的文件名或目录。</li>
<li>别名在渲染任何其他内容之前渲染，因此将被具有相同位置的任何内容覆盖。</li>
</ul>
]]></description></item><item><title>使用 RFC 流程管理项目</title><link>https://www.rectcircle.cn/series/software-project-management/use-rfc-manage/</link><pubDate>Sun, 21 Mar 2021 17:51:35 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/software-project-management/use-rfc-manage/</guid><description type="html"><![CDATA[

<blockquote>
<p>该文档处于设想阶段，并未落地实践</p>
</blockquote>

<h2 id="参考">参考</h2>

<ul>
<li><a href="/series/software-project-management/rust-rfc/">Rust RFC 项目管理调研</a></li>
</ul>

<h2 id="简介">简介</h2>

<p>RFC 全称 （Request for Comments），征求意见。</p>

<p>RFC 概念最早在 互联网 标准指定领域出现。在互联网技术标准领域，RFC 是技术标准文档的代名词，业界公认的主流的互联网技术，比如 TCP/IP 都编入了 IETF 的 RFC 文档中。换句话说，想成为互联网技术方面的标准或者基础设施，都需要进入 IETF 的 RFC 文档</p>

<p>在开源技术领域，古老的开源项目，比如 Linux，采用的是 邮件组的 方式来进行讨论及项目管理。</p>

<p>较新的开源项目，会考虑使用 Github + RFC + IM/论坛 机制来实现 Feature 管理。比如 Rust、React。</p>

<p>关于 Github 和 mail list 参见：<a href="https://begriffs.com/posts/2018-06-05-mailing-list-vs-github.html">博客</a></p>

<h2 id="rfc-内容">RFC 内容</h2>

<p>参考：<a href="/series/software-project-management/rust-rfc/#rust-rfc-模板">Rust RFC 项目管理调研 / 模板</a></p>

<h2 id="开源项目的-rfc-使用姿势">开源项目的 RFC 使用姿势</h2>

<h3 id="开源项目的特点">开源项目的特点</h3>

<ul>
<li>任何人都可以贡献代码</li>
<li>贡献者间不熟悉，来自全球各地</li>
</ul>

<h3 id="开源项目贡献者角色分析">开源项目贡献者角色分析</h3>

<ul>
<li>意愿贡献者，想为开源项目贡献代码，但是还没有贡献（多数）</li>
<li>普通贡献者，偶尔为开源项目贡献代码</li>
<li>项目组核心成员，经常为项目贡献代码，同时负责开源项目管理，参与技术讨论，决策</li>
</ul>

<h3 id="开源项目管理的诉求">开源项目管理的诉求</h3>

<ul>
<li>新人能更容易的参与到迭代中</li>
<li>核心成员对项目的迭代方向、代码质量有控制力</li>
</ul>

<h3 id="开源项目-rfc-的基本流程">开源项目 RFC 的基本流程</h3>

<p>物料准备</p>

<ul>
<li>rfc 仓库，用来维护所有的 rfc 文档</li>
<li>rfc 仓库 PR 讨论区，作为 rfc 设计评审的讨论地点</li>
<li>项目代码库</li>
<li>项目代码库代码库的 Issue 讨论区，作为该 rfc 实现过程的讨论地点</li>
</ul>

<p>角色（一人可能身兼数职）</p>

<ul>
<li>核心项目组成员</li>
<li>rfc 作者</li>
<li>rfc 实现者</li>
</ul>

<p>RFC 创建、讨论、评审流程（工作在 rfc 仓库）</p>

<ul>
<li>确认如下问题

<ul>
<li>查看所有历史 RFC，确认待创建的 RFC 不存在或者没有被拒绝</li>
<li>该 Feature 不是 Bugfix、重构类的需求</li>
</ul></li>
<li>Fork 代码库，根据模板编写 RFC</li>
<li>提交 PR</li>
<li>初步评审，项目组核心成员对该 RFC 质量，是否有重复等问题，决定是否进入下一阶段</li>
<li>讨论阶段，相关利益相关方在此 PR 处进行讨论，作者根据讨论进行修改并 commit。讨论参与者如下

<ul>
<li>RFC 作者</li>
<li>相关项目组核心成员</li>
<li>所有感兴趣的其他人员</li>
</ul></li>
<li>最终动议阶段，RFC 作者 和 相关项目组核心成员认为可以进入最终动议阶段，则可以发起最终动议，公示一段时间后，没有问题，则激活 RFC。同时 该 RFC 的 PR 将合入 RFC 仓库</li>
</ul>

<p>RFC 开发阶段（工作在 项目代码库）</p>

<ul>
<li>在项目代码库创建一个 Issue 用来追踪 RFC 的实现情况</li>
<li>实现者（当然欢迎 RFC 作者作为实现者）认领该任务，进入开发阶段</li>
</ul>

<h2 id="商业项目管理-结合-rfc-机制">商业项目管理 结合 RFC 机制</h2>

<h3 id="商业项目的特点">商业项目的特点</h3>

<p>与开源项目不同，商业项目</p>

<ul>
<li>研发成员相互之间在同一地点办公，彼此熟悉</li>
<li>能参与讨论的成员相对较少，核心开发一般较少</li>
<li>存在 PM 、测试 角色，分工更细，需求一般偏业务</li>
<li>迭代速度要求尽量快，代码质量要求相对迭代速度优先级低一些</li>
</ul>

<h3 id="流程设计">流程设计</h3>

<blockquote>
<p>仅设想阶段</p>
</blockquote>

<p>使用 RFC 管理 需求</p>

<ul>
<li>参考 <a href="/series/software-project-management/rust-rfc/#rust-rfc-模板">Rust RFC 项目管理调研 / 模板</a> 给出 RFC 模板，该模板需要按照业务类型需求重新设计，主要包含 PRD、 技术方案、测试验收方案的内容</li>
<li>一个 RFC 一般需要三种角色

<ul>
<li>PM</li>
<li>技术（两人）</li>
<li>测试</li>
</ul></li>
<li>RFC 文档 可以使用公司内部的 在线 Doc 平台 （需附上 IM 群） 或者 Gitlab （推荐） 中编写，使用标签或者在文章中添加状态或者使用需求管理软件管理状态，来管理 RFC 生命周期</li>
<li>RFC 评审

<ul>
<li>评审的形式不一定是会议，可以是评论、论坛、群聊的方式</li>
<li>一般有两次：产品方案评审、技术方案评审</li>
<li>更新 RFC 的状态</li>
</ul></li>
<li>开发阶段

<ul>
<li>更新 RFC 的状态，并添加一个 Issue 的链接</li>
<li>使用 Issue 管理 RFC 的实现，在描述上关联上 RFC 的链接</li>
<li>Gitlab 的多个 MR 上关联上 Issue，一个技术提交必须要另外的一个技术的 Review</li>
</ul></li>
<li>验收阶段，测试进行测试，验收阶段结束后，关闭 Issue，更新 RFC 状态，并最后校验文档和实现是否脱节</li>
</ul>

<h3 id="优缺点">优缺点</h3>

<p>优点</p>

<ul>
<li>更标准化的管理方案文档和代码实现以及两者相互索引，提高项目质量</li>
<li>因为存在众多 RFC 文档，且 RFC 文档的讨论和 MR 或者 PR 都有管理，所以新人更容易快速进入状态，了解历史进度</li>
<li>扩大所有人对项目的 context 的了解，不至于出现人员点单，降低人员变更带来的风险</li>
<li>方案指导开发，给开发人员一种普遍的方案设计思路，提高开发人员的核心素质，培养开发人员工程师思维（做 Engineer、Developer 摆脱 coder）</li>
</ul>

<p>缺点</p>

<ul>
<li>部分程序员无法将设计和开发彻底分离，设计后置到开发阶段，也就是说该这种程序员在设计阶段的想法无法落实在开发上。这将导致

<ul>
<li>存在阵痛期，认为写 RFC 是浪费时间，心理上会有抵触情绪</li>
<li>产出的 RFC 和代码不符，导致 RFC 文档反而误导其他成员</li>
<li>需要要求开发者有 Owner 精神</li>
</ul></li>
<li>一定程度降低迭代速度</li>
</ul>
]]></description></item><item><title>美化</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/beautify/</link><pubDate>Fri, 24 Apr 2020 19:38:24 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/beautify/</guid><description type="html"><![CDATA[

<h2 id="前言">前言</h2>

<p>VSCode 基本页面布局目前无法通过扩展进行定制，但是官方提供了两种主题定制机制</p>

<ul>
<li>颜色主题，可以定制定制

<ul>
<li>页面各个部分的颜色</li>
<li>编辑器各种语法高亮的颜色</li>
</ul></li>
<li>图标主题

<ul>
<li>定制各种文件的图标样式</li>
</ul></li>
</ul>

<p>以上这些主题可以通过 <a href="https://marketplace.visualstudio.com/search?sortBy=Installs&amp;category=Themes&amp;target=VSCode">商店</a> 进行安装</p>

<h2 id="颜色主题">颜色主题</h2>

<h3 id="切换方式">切换方式</h3>

<p>打开命令面板 <code>&gt;Color Theme</code> 即可进行颜色主题切换</p>

<h3 id="流行的颜色主题">流行的颜色主题</h3>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=AdamCaviness.theme-monokai-dark-soda">Monokai Dark Soda</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=AdamCaviness.theme-monokai-dark-soda">Night Owl</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme">One Dark Pro</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=azemoh.one-monokai">One Monokai Theme</a></li>
</ul>

<h2 id="图标主题">图标主题</h2>

<h3 id="切换方式-1">切换方式</h3>

<p>打开命令面板 <code>&gt;File Icon Theme</code> 即可进行颜色主题切换</p>

<h3 id="流行的图标主题">流行的图标主题</h3>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme">Material Icon Theme</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons">vscode-icons</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme-icons">Material Theme Icons</a></li>
</ul>

<h2 id="better-comments">Better Comments</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments">Better Comments</a> 是一个美化注释颜色的扩展，原生的注释的描述性文字颜色比较单一，而该扩展作用是根据特殊某些规则给注释文字染色以达到突出显示和美化的效果。内置5种高亮例子如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * 正常注释
</span><span style="color:#75715e"> * * 重要信息高亮
</span><span style="color:#75715e"> * ! 危险信息提示
</span><span style="color:#75715e"> * ? 疑问和不确定
</span><span style="color:#75715e"> * TODO TODO 信息
</span><span style="color:#75715e"> * // 被删除的注释
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">App</span> {

}</code></pre></div>
<p>该扩展同样支持通过 配置自定义颜色效果</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;better-comments.tags&#34;</span>:   {
        <span style="color:#f92672">&#34;tag&#34;</span>: <span style="color:#e6db74">&#34;!&#34;</span>,
        <span style="color:#f92672">&#34;color&#34;</span>: <span style="color:#e6db74">&#34;#FF2D00&#34;</span>,
        <span style="color:#f92672">&#34;strikethrough&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;backgroundColor&#34;</span>: <span style="color:#e6db74">&#34;transparent&#34;</span>
    }
}</code></pre></div>
<h2 id="bracket-pair-colorizer-2">Bracket Pair Colorizer 2</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2">Bracket Pair Colorizer 2</a> 是一个对括号匹配高亮的扩展，可以让我们一眼识别括号匹配情况。之前有一个V1的版本，V2版本提升了性能。</p>
]]></description></item><item><title>核心机制</title><link>https://www.rectcircle.cn/series/vscode/core-mechanism/</link><pubDate>Sat, 11 Apr 2020 14:50:50 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/core-mechanism/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考文章</p>

<ul>
<li><a href="https://zhuanlan.zhihu.com/p/96041706">从 VSCode 看大型 IDE 技术架构</a></li>
<li><a href="https://codeteenager.github.io/vscode-analysis/">VSCode技术揭秘</a></li>
</ul>
</blockquote>

<h2 id="基本情况">基本情况</h2>

<h3 id="代码仓库">代码仓库</h3>

<p><a href="https://github.com/microsoft/vscode">https://github.com/microsoft/vscode</a></p>

<h3 id="技术栈">技术栈</h3>

<ul>
<li>采用了 基于 Chromium 的 <a href="https://www.electronjs.org/">Electron</a> 框架（JavaScript，HTML 和 CSS 构建跨平台的桌面应用程序）</li>
<li>编程语言上采用微软自家的 <a href="https://www.typescriptlang.org/">TypeScript</a> （JavaScript 的超集，有类型，与JavaScript互操作兼容）</li>
<li>编辑器UI上采用 <a href="https://microsoft.github.io/monaco-editor/">Monaco</a></li>
</ul>

<h3 id="定位">定位</h3>

<p>位于 IDE 和 轻量级编辑器之间。更加偏向 编辑器 一侧。其核心是 编辑器 + 代码理解 + 调试。围绕这个关键路径做深做透，其他东西非常克制，产品保持轻量与高性能。</p>

<h3 id="进程架构">进程架构</h3>

<p><img src="https://img.geek-docs.com/vscode/plugin-dev/plugin-dev-overview-1.png" alt="进程架构图" /></p>

<p>多进程架构。（可以通过帮助菜单 -&gt; 打开进程管理器 查看，可以看到进程的父子关系等信息）</p>

<ul>
<li>主进程：VSCode 的入口进程，负责一些类似窗口管理、进程间通信、自动更新等全局任务</li>
<li>渲染进程：负责一个 Web 页面的渲染</li>
<li>扩展宿主进程：扩展运行在 独立的扩展宿主进程中，不允许访问 UI</li>
<li>Debug 进程：Debugger 相比普通扩展做了特殊化</li>
<li>Search 进程：搜索是一类计算密集型的任务，单开进程保证软件整体体验与性能</li>
</ul>

<p>VSCode 采用松散的架构，模块间的耦合很小。</p>

<h2 id="扩展机制">扩展机制</h2>

<ul>
<li>扩展API 相对克制，仅暴露核心 API，不暴露编辑器 DOM UI，提供有限的 UI 组件API，这样做的好处

<ul>
<li>对于扩展来说基本够用</li>
<li>对于VSCode内核来说可以从容重构，不会太多受到扩展API的牵制</li>
<li>对于用户来说，始终有统一的UI交互风格，降低心智成本</li>
</ul></li>
<li>扩展与 编辑器 核心强隔离，耦合程度小，扩展运行在对立的扩展进程中，扩展API与内核通信的通过RPC服务进行

<ul>
<li>保持编辑器的性能不受 扩展的影响</li>
<li>为 Remote Develop 打下基础</li>
</ul></li>
<li>不同编程语言的支持，通过语言服务器协议（LSP）实现，扩展和语言服务器之间通过 <code>JSON PRC</code> 进行通信

<ul>
<li>更好的复用其他语言开发的相关工具</li>
<li>保持开放，使一个语言服务器可以支持多种编辑器/开发环境</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/lsp-languages-editors.png" alt="lsp" /></p>
]]></description></item><item><title>后端-Java-Spring-企业内部系统项目规范</title><link>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E5%90%8E%E7%AB%AF-java-spring-%E9%80%9A%E7%94%A8%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/</link><pubDate>Sat, 04 Apr 2020 14:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E5%90%8E%E7%AB%AF-java-spring-%E9%80%9A%E7%94%A8%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/</guid><description type="html"><![CDATA[

<blockquote>
<p>更新时间： 2020-04-04</p>

<p>本文可能疏于更新：最新情况请参考 <a href="https://github.com/rectcircle/rectcircle-project-template">Github Repo</a></p>
</blockquote>

<p>前置文章：<a href="/series/项目规范/通用规范/">通用规范</a></p>

<h2 id="版本选择与依赖管理">版本选择与依赖管理</h2>

<h3 id="主编程语言java">主编程语言Java</h3>

<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Java_version_history">维基百科: Java Version History</a></p>
</blockquote>

<p>选择 目前 Java 最新的 TLS 版本 Java11 （发布于 2018 年 9 月，下一个 Java TLS 版本为 Java17，预计于 2021 年 9 月发布）</p>

<p>相关 SDK 安装( <a href="https://sdkman.io/install">SDKMAN 方式</a> )</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl -s <span style="color:#e6db74">&#34;https://get.sdkman.io&#34;</span> | bash
<span style="color:#75715e"># 重新打开终端</span>
<span style="color:#75715e"># 如果你的系统安装了Java，最好该Java交由SDK管理</span>
sdk install java <span style="color:#ae81ff">8</span>.0.191-local $JAVA_HOME
sdk install java <span style="color:#ae81ff">11</span>.0.6.hs-adpt
<span style="color:#75715e"># 仅当前shell生效</span>
sdk use java <span style="color:#ae81ff">11</span>.0.6.hs-adpt
sdk default java <span style="color:#ae81ff">11</span>.0.6.hs-adpt
<span style="color:#75715e"># 恢复 java8</span>
<span style="color:#75715e"># sdk default java 8.0.191-local</span>
java -version</code></pre></div>
<h3 id="spring-boot">Spring Boot</h3>

<p>采用 最新的 <code>2.2.6.RELEASE</code> 版本</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;dependency&gt;</span>
    <span style="color:#f92672">&lt;groupId&gt;</span>org.springframework.boot<span style="color:#f92672">&lt;/groupId&gt;</span>
    <span style="color:#f92672">&lt;artifactId&gt;</span>spring-boot<span style="color:#f92672">&lt;/artifactId&gt;</span>
    <span style="color:#f92672">&lt;version&gt;</span>2.2.6.RELEASE<span style="color:#f92672">&lt;/version&gt;</span>
<span style="color:#f92672">&lt;/dependency&gt;</span></code></pre></div>
<h3 id="依赖管理">依赖管理</h3>

<ul>
<li>使用分模块的Maven项目</li>
<li>被大于一个模块使用过的依赖在根 <code>pom.xml</code> 中的 <code>dependencyManagement</code> 进行版本管理</li>
</ul>

<h3 id="maven-wrapper">Maven Wrapper</h3>

<p>为方便构建镜像，应使用 <code>./mvnw</code> 命令替代 <code>mvn</code> 命令，创建方式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mvn -N io.takari:maven:wrapper</code></pre></div>
<p>将创建如文件和目录</p>

<ul>
<li><code>.mvn/</code></li>
<li><code>mvnw</code></li>
<li><code>mvnw.cmd</code></li>
</ul>

<h2 id="模块设计">模块设计</h2>

<p>依赖关系</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">template-dto        template-scala
    ^                ^
    |                |
    ------------------
            |
        template-core
            ^
            |
    ------------------
    |                |
template-web        template-batch</pre></div>
<ul>
<li>template-web 负责提供web服务，主要包含如下模块

<ul>
<li><code>controller</code></li>
<li><code>config</code></li>
<li><code>filter</code></li>
<li><code>security</code></li>
<li><code>utils</code></li>
</ul></li>
<li>template-batch 负责执行某些定时/异步任务</li>
<li>template-core 核心业务逻辑，主要包含如下模块（细节参见 <a href="https://developer.aliyun.com/special/tech-java">阿里Java开发手册</a>）

<ul>
<li>DO</li>
<li>BO</li>
<li>DAO</li>
<li>Service</li>
</ul></li>
<li>template-dto 数据传输对象，service层的参数与返回值，单独抽出来的原因是该部分可能被复用，比如

<ul>
<li>OpenAPI 的 Client</li>
<li>RPC 的 Client</li>
</ul></li>
<li>template-scala 可选，当与其他 JVM 语言交互式，建议独立出一个模块</li>
</ul>

<h2 id="编码规范">编码规范</h2>

<p>以如下两个规范作为蓝本，根据情况进行定制化的选择</p>

<ul>
<li><a href="https://developer.aliyun.com/special/tech-java">阿里Java开发手册</a></li>
<li><a href="https://www.jianshu.com/p/c0e5a4a896be">Google Java Style 指南</a></li>
</ul>

<p>将结果配置到配置文件中，并在如下两个地方应用配置</p>

<ul>
<li>配置 Maven Plugin 进行检查，调用 <code>mvn verify</code> 进行检查，该部分参见脚本管理

<ul>
<li>应用在 <code>.git/hooks/pre_commit</code> 在提交之前进行严格的规范检查</li>
<li>应用在 准入工作流</li>
</ul></li>
<li>配置集成开发环境插件，进行实时检查</li>
</ul>

<p>规范检查</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">./cli/verify.sh</code></pre></div>
<p>配置方式参见：<a href="/posts/java-code-style-check-implement/">Java 代码样式检查落地</a></p>

<h2 id="文档设计">文档设计</h2>

<ul>
<li><code>README.md</code></li>
<li><code>docs/01.环境搭建</code></li>
<li><code>docs/02.规范约定</code></li>
<li><code>docs/03.技术方案</code></li>
<li><code>docs/04.模块设计</code></li>
</ul>

<h2 id="脚本管理">脚本管理</h2>

<ul>
<li><code>./cli/verify.sh</code> CI 的 代码规范检查 步骤</li>
<li><code>./cli/unit_test.sh</code> CI 的 单元测试 步骤</li>
<li><code>./cli/build.sh</code> 构建二进制包</li>
<li><code>./cli/pre_commit</code> 为 <code>.git/hooks/pre_commit</code> 软链或者调用的脚本</li>
</ul>

<h2 id="数据库migrations">数据库migrations</h2>

<p>所有数据库版本脚本均托管到 git 仓库中，建议手动管理（因为企业级系统一般均有DBA进行限制DDL操作），目录位于 <code>template-core/src/main/resources/db/migrations</code> 目录。</p>

<p>该目录下包含多个目录，每个目录就是一个数据库变更版本，这些目录的命名方式为 <code>YYYY-mm-DD-%d%d%d%d%d%d-name</code>，每个目录下包含两个文件 <code>up.sql</code> 和 <code>down.sql</code>（可选）如下</p>

<ul>
<li><code>2020-04-02-000000-init</code>

<ul>
<li><code>up.sql</code></li>
</ul></li>
<li><code>2020-04-02-000001-new-core</code>

<ul>
<li><code>up.sql</code></li>
</ul></li>
</ul>

<h2 id="配置管理">配置管理</h2>

<h3 id="配置文件约定">配置文件约定</h3>

<ul>
<li>格式统一为 <code>*.yml</code>，语法参见 <a href="https://www.ruanyifeng.com/blog/2016/07/yaml.html">https://www.ruanyifeng.com/blog/2016/07/yaml.html</a></li>
<li>线上环境命名规则为 <code>application-prod-$region.yml</code>，例如

<ul>
<li><code>application-prod-cn.yml</code></li>
<li><code>application-prod-jp.yml</code></li>
</ul></li>
<li>个人开发环境命名规则为 <code>application-dev-$username.yml</code></li>
<li>所有自定义配置项在 <code>application.yml</code> 中有默认值</li>
<li>配置文件仅允许出现在 <code>template-web</code> 和 <code>template-batch</code> 模块下</li>
</ul>

<h2 id="配置项约定">配置项约定</h2>

<p>所有自定义配置项（非框架配置项，都定义在 <code>template</code> 域的一个子域）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">template:
    region: cn
    xxx:
        id: xxx
        key: xxx</code></pre></div>
<p>配置项禁止直接通过 <code>@Value</code> 注入，通过 <code>template-core</code> 下的 <code>TemplateConfiguration</code> 类 访问配置，该类添加 <code>@ConfigurationProperties(prefix = &quot;template&quot;)</code> 注解；对于其他子域也创建相关类，并作为 <code>TemplateConfiguration</code> 的成员。</p>
]]></description></item><item><title>如何在团队中落实一件具体的事项</title><link>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%9B%A2%E9%98%9F%E4%B8%AD%E8%90%BD%E5%AE%9E%E4%B8%80%E4%BB%B6%E5%85%B7%E4%BD%93%E4%BA%8B%E9%A1%B9/</link><pubDate>Sat, 04 Apr 2020 14:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%9B%A2%E9%98%9F%E4%B8%AD%E8%90%BD%E5%AE%9E%E4%B8%80%E4%BB%B6%E5%85%B7%E4%BD%93%E4%BA%8B%E9%A1%B9/</guid><description type="html"><![CDATA[

<blockquote>
<p>更新时间：2020-04-04</p>
</blockquote>

<h2 id="leader安排下来的事项">Leader安排下来的事项</h2>

<h3 id="利益相关">利益相关</h3>

<ul>
<li>Leader 不关系你是如何实现的，他们一般只看结果</li>
<li>团队成员配合你的工作对于他来说没有太大收益，只是一种义务</li>
<li>个人作为一个承上启下的作用</li>
<li>Context 方面

<ul>
<li>在做这件事情的原因和背景方面 Leader &gt; 个人 &gt; 其他成员</li>
<li>在具体落实方案和实施后的收益方面 个人 &gt; 其他成员 &gt; Leader</li>
</ul></li>
</ul>

<h3 id="明确事项">明确事项</h3>

<p>当 Leader 给你分配这件事情的时候，他一般只会告诉你要目标是什么，但是由于信息偏差，他可能不会告诉你为什么做这件事情（这件事情的背景）。因此，在你给出具体实施方案之前，一定要向 Leader 明确如下事项：</p>

<ul>
<li>这件事项完成后如何评估收益，是否可以量化？</li>
<li>为什么要做这件是，背景是什么？</li>
<li>如果是一线 Leader，他应该有更多的经验，可以询问下做这件事情的有没有什么推荐的实施手段</li>
</ul>

<h3 id="实施计划">实施计划</h3>

<p>制定实施方案的时候，需要注意如下事项</p>

<ul>
<li>明确背景/目标，让大家思想上达成一致和认可</li>
<li>如果需要成员讨论决策的话，作为实施负责人

<ul>
<li><strong>事先必须给出方案</strong>（因为其他成员没有义务帮你出主意），让大家在这个草案的范围内进行讨论，否则讨论可能会变成太过于发散</li>
<li>制定方案的之后，在讨论之前，最好交给 Leader 和 团队核心成员（经验丰富者） Review，确认方案可行性和改进点，这样可以避免在由于个人经验不足导致方案不足，在会议中被 Challenge 甚至 Diss</li>
</ul></li>
<li>方案必须具有简单性和可操作性，主要体现在两个方面

<ul>
<li>需要你做的部分技术上可行，且不会很复杂，这样可以节省你的时间，做更有意义的事情</li>
<li>需要其他成员做的部分尽量的少，尽量的简化；否则大家都有自己要忙的事情，会很不情愿做你推动的事情

<ul>
<li>不要给成员太多自由的选择和决策的可能性，如果这样，大家会倾向于按自己的利益方向进行考量，可能无法完成设定的目标</li>
<li>建议给一个在大家能忍受的，对达成目标方向最优的默认值，这样才能掌握主动</li>
</ul></li>
</ul></li>
<li>确保方案可以达到 Leader 的目标，在这个阶段必须考虑如何量化工作成果</li>
<li>一定要明确方案 Deadline，且 Deadline 的指定一定要留有余地，否则自己会很被动</li>
<li>方案一定要文档化，表格化，不能只停留在脑子中</li>
</ul>

<p>如何更好的推动方案的实施</p>

<ul>
<li>最好的方式是在，会议上提出这件事情，描述清楚事情的背景，简述下方案的重点，将方案文档发给大家。并让 Leader 发言强调下事情的重要性，同样做如下的事情</li>
<li>其次的方式是，建立一个IM群组，@Leader @所有人，然后比较正式的发出一个消息，文字控制在100字内，大概格式如下

<ul>
<li>标题</li>
<li>背景</li>
<li>需要大家做的事情</li>
<li>影响/收益</li>
<li>Deadline</li>
<li>附件：方案文档/表格/其他资源链接</li>
</ul></li>
<li>每天发送下大家的处理进度和距离 Deadline 的时间</li>
<li>在接近 Deadline 的时候单独找 进度缓慢的成员聊下，让他关注下这个事情</li>
</ul>

<h3 id="收尾">收尾</h3>

<ul>
<li>统计收益，并将收益文档化，且有数据支撑</li>
<li>将收益，发送给 Leader 和 团队成员，并 感谢大家的 付出</li>
</ul>

<h2 id="个人的想法">个人的想法</h2>

<p>比如想在团队中推动某些流程的标准化，推动某些机制等</p>

<p>TODO</p>
]]></description></item><item><title>Git 和 工作流</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/git-and-workflow/</link><pubDate>Tue, 26 May 2020 23:27:41 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/git-and-workflow/</guid><description type="html"><![CDATA[

<h2 id="前言">前言</h2>

<p>如果熟悉 git 命令，使用命令操作可能更高效且知识的迁移性更高。</p>

<p>另一方面，以下扩展如果熟练使用的话可能更直观快捷，如下扩展在功能上可能存在重复，可以对比选择使用。</p>

<h2 id="git-graph">Git Graph</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph">Git Graph</a> 专注于可视化 git 的 分支图。通过该扩展可以直观的看到分支合并情况，代码变更与比较</p>

<p>使用方式</p>

<ul>
<li>通过 <code>&gt;git graph: View Git Graph</code> 命令</li>
<li>通过 状态栏 的 <code>Git Graph</code> 按钮</li>
</ul>

<h2 id="github-pull-requests-and-issue">GitHub Pull Requests and Issue</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a> 微软 Github 官方的 Github 管理工具，用于托管在 Github 上的 项目的工作流管理。</p>

<h2 id="gitlab-workflow">GitLab Workflow</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=fatihacet.gitlab-workflow">GitLab Workflow</a></p>

<p>类似于 GitHub Pull Requests and Issue 扩展。</p>

<p>Gitlab 一般用于企业级 项目管理，因此此扩展在日常搬砖过程中非常有用。</p>

<h3 id="基本配置">基本配置</h3>

<ul>
<li>前往 profile/personal_access_tokens 页面生成 Token 勾选如下权限，然后点击生成，然后复制备用

<ul>
<li><code>api</code></li>
<li><code>read_user</code></li>
</ul></li>
<li>打开 VSCode，输入 命令 <code>&gt;GitLab: Set GitLab Personal Access Token</code>

<ul>
<li>首先输入 Gitlab 的首页</li>
<li>然后输入 粘贴 刚刚复制的 token</li>
</ul></li>
</ul>

<p>如果是私有部署的Gitlab，需要在设置中配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;gitlab.instanceUrl&#34;</span>: <span style="color:#e6db74">&#34;https://my-gitlab-domain.com&#34;</span>
}</code></pre></div>
<h3 id="功能">功能</h3>

<ul>
<li>状态栏显示流水线/MR数目和基本状态</li>
<li>活动栏查看 Issue / MR 列表</li>
<li>一些命令，通过 <code>&gt;Gitlab:</code> 前缀可以查看，这里展示几个常用的

<ul>
<li><code>&gt;gitlab create snippet</code> 快速创建 代码片段</li>
<li><code>&gt;gitlab create new issue on current project</code> 快速打开创建 issue 页面</li>
<li><code>&gt;gitlab create new merge request on current project</code> 快速为当前分支创建 MR</li>
<li><code>&gt;gitlab open</code> 快速打开系列</li>
</ul></li>
</ul>

<h2 id="git-history">Git History</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory">Git History</a></p>

<p>类似于 Git Graph，可以通过 命令 <code>&gt; Git: View History</code> 打开，或者通过 源代码管理 侧边栏的标题上的图标进入</p>

<h2 id="project-manager">Project Manager</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager">Project Manager</a></p>

<p>工作空间管理，在项目非常多的情况可以使用。类似于项目收藏夹的功能，可以快速一键打开项目。具体功能如下</p>

<ul>
<li>通过 <code>&gt; Project Manager:</code> 前缀 可以列出所有命令列表</li>
<li>提供一个侧边栏，通过活动栏的按钮打开侧边栏</li>

<li><p>项目管理主要通过JSON配置文件的方式进行管理，可以通过 <code>&gt;project manager: edit projects</code> 打开</p>

<ul>
<li>目前仅 <code>name</code>, <code>rootPath</code>, and <code>enabled</code> 字段被使用</li>

<li><p><code>$home</code> 将会替换为用户家目录</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Numbered Bookmarks&#34;</span>,
<span style="color:#f92672">&#34;rootPath&#34;</span>: <span style="color:#e6db74">&#34;$home\\Documents\\GitHub\\vscode-numbered-bookmarks&#34;</span>,
<span style="color:#f92672">&#34;paths&#34;</span>: [],
<span style="color:#f92672">&#34;group&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
<span style="color:#f92672">&#34;enabled&#34;</span>: <span style="color:#66d9ef">true</span>
}</code></pre></div></li>
</ul></li>
</ul>

<h2 id="gitlens-git-supercharged">GitLens — Git supercharged</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens — Git supercharged</a></p>

<p>GitLens 功能十分强大，基本特性如下</p>

<ul>
<li>在当前行尾以浅色字体显示

<ul>
<li>当前行的作者，最后提交时间，提交消息</li>
</ul></li>
<li>在文件头部显示作者，最新更新时间，点击作者可以打开 File blame annotation 视图</li>
<li>提供一个功能丰富的侧边栏

<ul>
<li>代码仓库视图</li>
<li>文件历史视图</li>
<li>行历史视图</li>
<li>自定义搜索视图</li>
<li>比较视图</li>
</ul></li>
<li>👍File blame annotation 视图，展示文件每一行的提交信息

<ul>
<li>通过 <code>&gt;gitlens.toggleFileBlame</code> 命令或者左上角 图标打开</li>
</ul></li>
<li>👍文件热度图

<ul>
<li>通过 <code>&gt;gitlens.toggleFileHeatmap</code> 命令打开</li>
<li>通过 <code>&quot;gitlens.heatmap.toggleMode&quot;: &quot;window&quot;</code> 配置命令适用于整个窗口</li>
</ul></li>
<li>👍高亮显示当前文件最后一次更改内容

<ul>
<li>通过 <code>&gt;gitlens.toggleFileRecentChanges</code> 命令打开</li>
<li>通过 <code>&quot;gitlens.recentChanges.toggleMode&quot;: &quot;window&quot;</code> 配置命令适用于整个窗口</li>
</ul></li>
</ul>

<p>配置说明： <a href="https://github.com/eamodio/vscode-gitlens#gitlens-settings-">https://github.com/eamodio/vscode-gitlens#gitlens-settings-</a></p>

<h2 id="gitignore">gitignore</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=codezombiech.gitignore">gitignore</a></p>

<p>一个很有用的小扩展，根据类型给你的 项目添加 gitignore <code>&gt;add gitignore</code></p>

<h2 id="open-in-github-bitbucket-gitlab-visualstudio-com">Open in GitHub, Bitbucket, Gitlab, VisualStudio.com</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=ziyasal.vscode-open-in-github">Open in GitHub, Bitbucket, Gitlab, VisualStudio.com !</a></p>

<p>通过 命令 <code>&gt; open in</code> 快速打开当前文件在 github 类网站的页面</p>
]]></description></item><item><title>基本流程</title><link>https://www.rectcircle.cn/series/vscode/extension-develop/workflow/</link><pubDate>Wed, 29 Apr 2020 15:24:31 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/extension-develop/workflow/</guid><description type="html"><![CDATA[

<p>参考：<a href="https://code.visualstudio.com/api/get-started/your-first-extension">官方文档-快速开始</a></p>

<h2 id="相关-context">相关 context</h2>

<ul>
<li>运行环境：VScode 内部 Electron 依赖的 Node 版本</li>
<li>开发语言：TypeScript （官方推荐）、JavaScript 等</li>
<li>开发环境：VSCode</li>
<li>语言服务器LSP：支持任意编程语言</li>
</ul>

<h2 id="创建">创建</h2>

<p>VSCode 官方提供了 相关的 yo 模板，安装</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装模板生成器</span>
npm install -g yo generator-code
yo code</code></pre></div>
<h2 id="开发调试">开发调试</h2>

<p>为了调试时启动速度，建议修改调试配置 <code>.vscode/launch.json</code>，添加 <code>&quot;--disable-extensions&quot;</code> 启动参数</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
	<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
	<span style="color:#f92672">&#34;configurations&#34;</span>: [
		{
			<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Run Extension&#34;</span>,
			<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;extensionHost&#34;</span>,
			<span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
			<span style="color:#f92672">&#34;runtimeExecutable&#34;</span>: <span style="color:#e6db74">&#34;${execPath}&#34;</span>,
			<span style="color:#f92672">&#34;args&#34;</span>: [
				<span style="color:#e6db74">&#34;--disable-extensions&#34;</span>,
				<span style="color:#e6db74">&#34;--extensionDevelopmentPath=${workspaceFolder}&#34;</span>
			],
			<span style="color:#f92672">&#34;outFiles&#34;</span>: [
				<span style="color:#e6db74">&#34;${workspaceFolder}/out/**/*.js&#34;</span>
			],
			<span style="color:#f92672">&#34;preLaunchTask&#34;</span>: <span style="color:#e6db74">&#34;${defaultBuildTask}&#34;</span>
        },
    ]
}</code></pre></div>
<h2 id="发布">发布</h2>
]]></description></item><item><title>搬砖计划制定和实施历程</title><link>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E6%90%AC%E7%A0%96%E8%AE%A1%E5%88%92%E5%88%B6%E5%AE%9A%E5%92%8C%E5%AE%9E%E6%96%BD%E5%8E%86%E7%A8%8B/</link><pubDate>Tue, 07 Apr 2020 23:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E6%90%AC%E7%A0%96%E8%AE%A1%E5%88%92%E5%88%B6%E5%AE%9A%E5%92%8C%E5%AE%9E%E6%96%BD%E5%8E%86%E7%A8%8B/</guid><description type="html"><![CDATA[

<h2 id="2019-07-01">2019-07-01</h2>

<h3 id="方案">方案</h3>

<ul>
<li>粒度：双月级/日级别</li>
<li>每双月初指定双月OKR（借公司系统）</li>
<li>每天指定当天计划

<ul>
<li>前一天晚上/当天早上，将前一天未完成的任务放置到当天计划中，并将当天新的任务添加到计划中</li>
<li>按照优先级排序</li>
<li>没完成一项后勾选掉</li>
<li>依次循环</li>
</ul></li>
</ul>

<h3 id="失控">失控</h3>

<ul>
<li>初期，任务较简单，时间周期较短，每天任务不超过10行</li>
<li>2020年起，逐步失控，非重要任务开始挤压，每日任务列表开始膨胀（膨胀到64行），导致

<ul>
<li>无法一眼看清楚要做什么，非常不明确，等于没有计划</li>
<li>低优任务得不到调度</li>
</ul></li>
</ul>

<h2 id="2020-04-07">2020-04-07</h2>

<h3 id="方案-1">方案</h3>

<ul>
<li>粒度：双月级别/周级别/日级别</li>
<li>每双月初指定双月OKR（借公司系统）</li>
<li>每周制定当周的任务清单

<ul>
<li>每周日/周一，将上周未完成的任务搬移到本周任务中</li>
<li>当周有新增任务，添加到当周的任务列表中</li>
<li>当周每完成一项任务，将该任务标记为已完成</li>
<li>依次循环</li>
</ul></li>
<li>每天制定和实施当天计划

<ul>
<li>前一天晚上/当天早上，从周任务清单中抽取每个大项中的子项目到当天计划中，需要注意

<ul>
<li>任务数目视当天时间情况进行评估（避免无法完成）</li>
<li>要保持 低优任务 可以得到调度，<strong>避免饥饿</strong>（低优任务得不到实施）</li>
</ul></li>
<li>当天每完成一项任务，将该任务标记为已完成，同时将对应的周任务项目标记为完成</li>
</ul></li>
</ul>
]]></description></item><item><title>前端-JavaScript-React-企业内部系统项目规范</title><link>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E5%89%8D%E7%AB%AF-javascript-react-%E9%80%9A%E7%94%A8%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/</link><pubDate>Sat, 04 Apr 2020 18:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/%E5%89%8D%E7%AB%AF-javascript-react-%E9%80%9A%E7%94%A8%E9%A1%B9%E7%9B%AE%E8%A7%84%E8%8C%83/</guid><description type="html"><![CDATA[<blockquote>
<p>更新时间：2020-04-04
本文可能疏于更新：最新情况请参考 <a href="https://github.com/rectcircle/rectcircle-project-template">Github Repo</a></p>
</blockquote>
]]></description></item><item><title>优质扩展总览</title><link>https://www.rectcircle.cn/series/vscode/extension-overview/</link><pubDate>Sat, 11 Apr 2020 14:50:53 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/extension-overview/</guid><description type="html"><![CDATA[

<h2 id="约定">约定</h2>

<h3 id="推荐等级">推荐等级</h3>

<ul>
<li>必备：如果从事该类别的工作，该扩展应该是必备扩展，使用该扩展可以极大的提高效率</li>
<li>推荐：建议安装，锦上添花，不适用也不影响效率</li>
<li>知悉：告知存在此类扩展，使用与否视情况而定</li>
</ul>

<!-- ### 分类原则

* 当某个扩展专为某个编程语言或者某类开发设计，则该扩展将直接划分到其类别
* 当某个扩展属于特殊功能组，则将该功能组提升到顶级分类中，比如【远程开发】
* 不属于以上两类的且属于通用工具的，划分在【通用】类别
* 不属于以上类别的，划分在【其他】类别 -->

<h2 id="远程开发">远程开发</h2>

<p>远程开发是 VSCode 最重大的特性，是其最具有竞争力的地方。因此，在推荐列表的第一部分，详细参见： <a href="/series/vscode/good-extensions/remote-development/">优质扩展/远程开发</a></p>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>Remote Development</td>
<td>必备</td>
<td>远程开发扩展包，包含如下 4 个扩展</td>
</tr>

<tr>
<td>Remote - SSH</td>
<td>必备</td>
<td>远程开发，通过 SSH 隧道连接（同时包含如下扩展）</td>
</tr>

<tr>
<td>Remote - SSH: Editing Configuration Files</td>
<td>必备</td>
<td>编辑 SSH Config 文件</td>
</tr>

<tr>
<td>Remote - Containers</td>
<td>必备</td>
<td>远程开发，通过暴露端口和挂载实现</td>
</tr>

<tr>
<td>Remote - WSL</td>
<td>必备</td>
<td>远程开发，通过暴露端口和挂载实现</td>
</tr>

<tr>
<td>Visual Studio Online</td>
<td>知悉</td>
<td>远程开发</td>
</tr>
</tbody>
</table>

<h2 id="语言包与翻译">语言包与翻译</h2>

<p>VSCode 的国际化支持是通过语言包扩展实现的，同时商店中有一个辅助翻译的扩展以帮助大家阅读英文注释和变量命名的含义</p>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-zh-hans">Chinese (Simplified) Language Pack for Visual Studio Code</a></td>
<td>必备</td>
<td>简体中文语言包</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=intellsmi.comment-translate">Comment Translate</a></td>
<td>必备</td>
<td>国人开发的辅助翻译工具</td>
</tr>
</tbody>
</table>

<p>说明</p>

<ul>
<li>更多语言包参见 <a href="https://marketplace.visualstudio.com/search?target=VSCode&amp;category=Extension%20Packs&amp;sortBy=Installs">扩展商店</a></li>
<li>语言切换方式 <code>&gt;configure display language</code></li>
<li>Comment Translate 的使用方式：鼠标选中需要翻译的文本上，针对注释，直接放置在注释上即可

<ul>
<li>命令前缀 <code>&gt;Comment Translate:</code></li>
<li>快速选择 目标语言，点击状态栏的地球图标</li>
</ul></li>
</ul>

<h2 id="来自其他编辑器">来自其他编辑器</h2>

<p>如果真心想将 VSCode 作为开发主力，建议还是接受 VSCode 的快捷键逻辑；针对部分是在用惯的快捷键，可以通过 快捷键配置 来进行 自定义。最好不要使用如下扩展，全部映射。</p>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=vscodevim.vim">Vim</a></td>
<td>知悉</td>
<td>VSCode 上实现大部分 vim 特性</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.sublime-keybindings">Sublime Text Keymap and Settings Importer</a></td>
<td>知悉</td>
<td>VSCode Sublime 快捷键绑定</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.atom-keybindings">Atom Keymap</a></td>
<td>知悉</td>
<td>VSCode Atom 快捷键绑定</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=alphabotsec.vscode-eclipse-keybindings">Eclipse Keymap</a></td>
<td>知悉</td>
<td>VSCode Eclipse 快捷键绑定</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=k--kato.intellij-idea-keybindings">IntelliJ IDEA Keybindings</a></td>
<td>知悉</td>
<td>VSCode IntelliJ 家族 快捷键绑定</td>
</tr>
</tbody>
</table>

<p>更多快捷键绑定参见： <a href="https://marketplace.visualstudio.com/search?sortBy=Installs&amp;category=Keymaps&amp;target=VSCode">商店</a></p>

<p>关于 VIM 扩展的额外说明：</p>

<ul>
<li>特有命令

<ul>
<li><code>gd</code> 相当于vim中的<code>ctrl+]</code> 跳转到定义</li>
<li><code>gb</code> 多光标模式，找到下一个和当前单词匹配的单词并添加光标</li>
<li><code>gh</code> 显示当前位置的悬浮提示框</li>
</ul></li>
<li>打通系统剪切板 配置  &ldquo;vim.useSystemClipboard&rdquo;: true,`</li>

<li><p>输入法</p>

<ul>
<li><a href="https://github.com/daipeihust/im-select#installation">安装 <code>im-select</code></a>
  *MAC 安装 <code>curl -Ls https://raw.githubusercontent.com/daipeihust/im-select/master/install_mac.sh | sh</code></li>

<li><p>配置如下（中文）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;vim.autoSwitchInputMethod.defaultIM&#34;</span>: <span style="color:#e6db74">&#34;com.apple.keylayout.ABC&#34;</span>,
<span style="color:#f92672">&#34;vim.autoSwitchInputMethod.obtainIMCmd&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/bin/im-select&#34;</span>,
<span style="color:#f92672">&#34;vim.autoSwitchInputMethod.switchIMCmd&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/bin/im-select {im}&#34;</span>,
<span style="color:#f92672">&#34;vim.autoSwitchInputMethod.enable&#34;</span>: <span style="color:#66d9ef">true</span>,
}</code></pre></div></li>
</ul></li>
</ul>

<h2 id="美化">美化</h2>

<p>详细参见： <a href="/series/vscode/good-extensions/beautify/">优质扩展/美化</a></p>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>Monokai Dark Soda</td>
<td>推荐</td>
<td>颜色主题</td>
</tr>

<tr>
<td>Night Owl</td>
<td>推荐</td>
<td>颜色主题</td>
</tr>

<tr>
<td>One Dark Pro</td>
<td>推荐</td>
<td>颜色主题</td>
</tr>

<tr>
<td>One Monokai Theme</td>
<td>推荐</td>
<td>颜色主题</td>
</tr>

<tr>
<td>Material Icon Theme</td>
<td>推荐</td>
<td>图标主题</td>
</tr>

<tr>
<td>vscode-icons</td>
<td>推荐</td>
<td>图标主题</td>
</tr>

<tr>
<td>Material Theme Icons</td>
<td>推荐</td>
<td>图标主题</td>
</tr>

<tr>
<td>Better Comments</td>
<td>必备</td>
<td>注释美化</td>
</tr>

<tr>
<td>Bracket Pair Colorizer 2</td>
<td>必备</td>
<td>括号美化</td>
</tr>
</tbody>
</table>

<h2 id="git和工作流">Git和工作流</h2>

<p>详细参见： <a href="/series/vscode/good-extensions/git-and-workflow/">优质扩展/Git和工作流</a></p>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph">Git Graph</a></td>
<td>必备</td>
<td>清晰的查看分支合并图</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a></td>
<td>知悉</td>
<td>适合开源项目管理者使用</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=fatihacet.gitlab-workflow">GitLab Workflow</a></td>
<td>知悉</td>
<td>适合公司内项目管理</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=donjayamanne.git-extension-pack">Git Extension Pack</a></td>
<td>推荐</td>
<td>git扩展包，包含如下 5 个 扩展</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory">Git History</a></td>
<td>推荐</td>
<td>通过 <code>&gt; git view</code> 命令唤醒 History 操作视图</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager">Project Manager</a></td>
<td>推荐</td>
<td>项目（目录）收藏，通过状态栏快速打开</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens — Git supercharged</a></td>
<td>必备</td>
<td>提供大量好用Git相关功能</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=codezombiech.gitignore">gitignore</a></td>
<td>必备</td>
<td>快速为项目添加gitignore文件</td>
</tr>

<tr>
<td><a href="https://marketplace.visualstudio.com/items?itemName=ziyasal.vscode-open-in-github">Open in GitHub, Bitbucket, Gitlab, VisualStudio.com !</a></td>
<td>知悉</td>
<td>快速在代码仓库打开当前文件</td>
</tr>
</tbody>
</table>

<h2 id="通用">通用</h2>

<ul>
<li>Bookmarks</li>
<li>Code Runner</li>
<li>Code Spell Checker</li>
<li>Comment Translate</li>
<li>EditorConfig for VS Code</li>
<li>i18n Ally</li>
<li>LeetCode</li>
<li>New File by Type</li>
<li>Open in GitHub, Bitbucket, Gitlab, VisualStudio.com !</li>
<li>Path Intellisense</li>
<li>Project Manager</li>
<li>REST Client</li>
<li>SonarLint</li>
<li>Settings Sync</li>
<li>TabNine</li>
<li>TODO Highlight</li>
<li>Todo Tree</li>
<li>Visual Studio IntelliCode</li>
<li>CodeQL</li>
<li>Reference Search View</li>
<li>Archiver</li>
<li>Zip Preview</li>
</ul>

<h2 id="sql">SQL</h2>

<ul>
<li>SQLTools - Database tools</li>
<li>PostgreSQL</li>
</ul>

<h2 id="协作">协作</h2>

<ul>
<li>CodeTour</li>
<li>CodeStream</li>
<li>Live Share</li>
<li>Live Share Audio</li>
</ul>

<h2 id="docker-k8s">Docker&amp;K8S</h2>

<ul>
<li>Docker Extension Pack</li>
<li>Kubernetes</li>
<li>Docker Explorer</li>
</ul>

<h2 id="服务提供商">服务提供商</h2>

<ul>
<li>Travis CI Status</li>
<li>Salesforce CLI Integration</li>
<li>Azure Account</li>
<li>Cloudfoundry Manifest YML Support</li>
<li>Concourse CI Pipeline Editor</li>
<li>Spark &amp; Hive Tools</li>
<li>Azure Virtual Machines</li>
<li>Azure CLI Tools</li>
</ul>

<h2 id="web前端开发-nodejs">Web前端开发&amp;NodeJS</h2>

<ul>
<li>Debugger for Chrome</li>
<li>ESLint</li>
<li>JavaScript Booster</li>
<li>npm</li>
<li>npm Intellisense</li>
<li>open in browser</li>
<li>TSLint</li>
<li>yo</li>
<li>Node Debug</li>
</ul>

<h2 id="c-c">C/C++</h2>

<ul>
<li>C/C++</li>
</ul>

<h2 id="java">Java</h2>

<ul>
<li>Java P3C Checker</li>
<li>Java Properties</li>
<li>Java Extension Pack</li>
<li>Language Support for Java&trade; by Red Hat</li>
<li>Lombok Annotations Support for VS Code</li>
<li>Checkstyle for Java</li>
<li>Debugger for Java</li>
<li>Java Decompiler</li>
<li>Java Dependency Viewer</li>
<li>Java Extension Pack</li>
<li>Java Test Runner</li>
<li>Jetty for Java</li>
<li>Maven for Java</li>
<li>Spring Boot Dashboard</li>
<li>Spring Boot Extension Pack</li>
<li>Spring Boot Tools</li>
<li>Spring Initializr Java Support</li>
<li>Tomcat for Java</li>
</ul>

<h2 id="python">Python</h2>

<ul>
<li>MagicPython</li>
<li>Python</li>
</ul>

<h2 id="rust">Rust</h2>

<ul>
<li>CodeLLDB</li>
<li>Rust (rls)</li>
<li>rust-analyzer</li>
<li>crates</li>
<li>Rust Test Explorer</li>
</ul>

<h2 id="go">Go</h2>

<ul>
<li>Go</li>
</ul>

<h2 id="dart-flutter">Dart&amp;Flutter</h2>

<ul>
<li>Dart</li>
<li>Flutter</li>
</ul>

<h2 id="文档-绘图-数据">文档&amp;绘图&amp;数据</h2>

<ul>
<li>Excel Viewer</li>
<li>Data Preview</li>
<li>PlantUML</li>
<li>Markdown Extension Pack</li>
<li>Markdown All in One</li>
<li>Markdown Emoji</li>
<li>Markdown PDF</li>
<li>Markdown Preview Enhanced</li>
<li>Markdown TOC</li>
<li>Markdown+Math</li>
<li>markdownlint</li>
</ul>

<h2 id="scala">Scala</h2>

<ul>
<li>Scala Syntax (official)</li>
<li>Dotty Language Server</li>
<li>Scala (Metals)</li>
<li>Scala (sbt)</li>
<li>Scala Language Server</li>
</ul>

<h2 id="shell">Shell</h2>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>BASH Extension Pack</td>
<td>必备</td>
<td>BASH 优质扩展全家桶</td>
</tr>

<tr>
<td>Bash Debug</td>
<td>必备</td>
<td>Bash Debug 工具 <a href="https://itnext.io/upgrading-bash-on-macos-7138bd1066ba">https://itnext.io/upgrading-bash-on-macos-7138bd1066ba</a></td>
</tr>

<tr>
<td>Bash IDE</td>
<td>必备</td>
<td>Bash 语言服务器</td>
</tr>
</tbody>
</table>

<ul>
<li>Shebang Snippets</li>
<li>shell-format</li>
<li>shellcheck</li>
<li>shellman</li>
</ul>

<h2 id="其他语言配置文件支持">其他语言配置文件支持</h2>

<table>
<thead>
<tr>
<th>扩展名</th>
<th>推荐级别</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>ANTLR4 grammar syntax support</td>
<td>必备</td>
<td>ANTLR4 语言服务器。语法描述文件高亮、智能提示、可视化、CLI工具封装</td>
</tr>
</tbody>
</table>

<ul>
<li>Better TOML</li>
<li>FreeMarker</li>
<li>SSH Tooling</li>
<li>systemd-unit-file</li>
<li>Thrift</li>
<li>vscode-proto3</li>
<li>XML</li>
<li>XML Tools</li>
<li>YAML</li>
<li>DotENV</li>
<li>LaTeX Workshop</li>
<li>SVG Viewer</li>
<li>Output Colorizer</li>
</ul>
]]></description></item><item><title>理发店事件——维护消费者权益</title><link>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E7%90%86%E5%8F%91%E5%BA%97%E4%BA%8B%E4%BB%B6%E7%BB%B4%E6%8A%A4%E6%B6%88%E8%B4%B9%E8%80%85%E6%9D%83%E7%9B%8A/</link><pubDate>Sat, 28 Dec 2019 19:21:10 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/%E7%A4%BE%E4%BC%9A%E7%94%9F%E5%AD%98/%E7%90%86%E5%8F%91%E5%BA%97%E4%BA%8B%E4%BB%B6%E7%BB%B4%E6%8A%A4%E6%B6%88%E8%B4%B9%E8%80%85%E6%9D%83%E7%9B%8A/</guid><description type="html"><![CDATA[

<h2 id="维权过程">维权过程</h2>

<h3 id="相关平台">相关平台</h3>

<ul>
<li><a href="https://aiqicha.baidu.com/">爱企查</a> —— 免费 企业信息 查询（天眼查和企查查需要收费）</li>
<li><a href="http://www.12315.cn/">全国12315平台</a></li>
</ul>

<h3 id="投诉内容">投诉内容</h3>

<p>2019年8月2日，在位于 合川路2850号的 【鑫尚国际】 充值 1000 元五折会员卡，并以手机号作为凭证，此后可以30元进行理发，并且未提供实体卡。2020年4月左右，合川路2850号 停业装修 更名为 【维纳斯】，并承诺仍可以继续使用旧卡，理发涨价到34元，此后每次前往，都以各种理由让你续费（威胁说不续费就不能使用）。2020年8月24日，理发店称必须提供实体卡，但是凭上次小票可以作为凭据，此次仍使用了会员卡。2020年9月28日，理发店称，以持卡消费为由，拒绝我凭手机号使用会员卡，于是付款原价 68元（收款码显示为【上海威天美容美发有限公司】）。既然无法使用，期望可以退款卡中余额621元，并退换本次理发多收的34元。经查询公司实体为【上海威天美容美发有限公司】，法定代表人在2020-03-26日发生变更。类似事件：微博卡里余额2.5万继续用得再充2.5万。法条：消协法五十三条</p>

<h3 id="结果">结果</h3>

<p>几个工作日内，工商管理局介入协商，理发店最终同意继续使用该卡。</p>

<h2 id="总结">总结</h2>

<ul>
<li>在保证自己在法律上有法可依的情况下，遇到类似事件可进行投诉</li>
<li>协商过程尽量提出自己的诉求，这次处理个人不是很满意。我主张进行退款，但是工商管理局给的方案是可继续使用而已，没有完全达成目标，还要继续到该店中进行理发，继续忍受理发店人员的语言轰炸</li>
</ul>
]]></description></item><item><title>Go 语言开发</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/go-language/</link><pubDate>Mon, 29 Nov 2021 22:27:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/go-language/</guid><description type="html"><![CDATA[

<h2 id="导读">导读</h2>

<blockquote>
<p>VSCode Go  扩展版本 0.29.0</p>
</blockquote>

<p>阅读本章节，可以了解到如何使用 VSCode 开发 Go 语言项目，并可以获取到基本不输于 Goland 的体验。</p>

<h2 id="特性速览">特性速览</h2>

<ul>
<li>智能感知（自动完成、Hover、函数签名）</li>
<li>代码导航（调转定义、查找引用、查找实现、调用层次）</li>
<li>代码编辑（代码片段、包导入、格式化）</li>
<li>重构和代码生成</li>
<li>构建、测试和运行调试</li>
<li>问题诊断</li>
<li>其他（Go Playground）</li>
</ul>

<p>关于这些特性，介绍，参见 <a href="https://code.visualstudio.com/docs/languages/go">VSCode 官方文档</a></p>

<h2 id="快速开始">快速开始</h2>

<blockquote>
<p>参考：<a href="https://github.com/golang/vscode-go/blob/master/README.md#quick-start">官方文档</a></p>
</blockquote>

<ul>
<li>安装 Go，参见 <a href="https://go.dev/doc/install">Go 官方文档</a></li>
<li><a href="https://code.visualstudio.com/download">安装 VSCode</a>，并在 VSCode 中，安装 <a href="https://marketplace.visualstudio.com/items?itemName=golang.Go">Go 扩展</a></li>
<li>使用 VSCode 打开包含一个 Go 项目目录 （包含 <code>go.mod</code> 的目录），按照提示安装依赖工具即可</li>
<li>Enjoy it!</li>
</ul>

<p><img src="/image/vscode/golang/installtools.gif" alt="image" /></p>

<h2 id="使用指南">使用指南</h2>

<h3 id="状态和环境管理">状态和环境管理</h3>

<blockquote>
<p>参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/ui.md">官方文档</a></p>
</blockquote>

<p>安装完 <a href="https://marketplace.visualstudio.com/items?itemName=golang.Go">Go 扩展</a>，并打开一个 Go 项目后。可以通过左下角状态栏，观察到当前系统的 Go 环境和状态。例如 <code>Go 1.17.1 ⚡</code>，表示当前系统 Go 版本，<code>⚡</code> 代表已经使用 <code>gopls</code> 提供能力。</p>

<p><img src="/image/vscode/golang/statusbarmenu.png" alt="image" /></p>

<p>点击状态栏按钮，可以做如下事情</p>

<ul>
<li>打印当前系统的详细状态信息（go env），以及安装的命令行工具。</li>
<li>Choose Go Environment，可以快速选择或者安装其他版本的 Go。</li>
<li>显示 gopls 的日志</li>
</ul>

<h3 id="依赖的命令行工具">依赖的命令行工具</h3>

<blockquote>
<p>参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md">官方文档</a></p>
</blockquote>

<p><a href="https://marketplace.visualstudio.com/items?itemName=golang.Go">Go 扩展</a> 依赖一些社区开发的命令行工具。通过 <code>&gt;Go: Install/Update Tools</code> 命令进行安装。</p>

<p>使用 gopls 模式的依赖工具如下表所示（默认）</p>

<table>
<thead>
<tr>
<th>命令名</th>
<th>说明</th>
<th>何时需要</th>
<th>手动安装命令</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#go">go</a></td>
<td>Go 语言</td>
<td>必装</td>
<td><a href="https://go.dev/doc/install">Go 官方文档</a></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#gopls">gopls</a></td>
<td>Go 语言的 LSP 实现，提供智能感知、代码导航等核心能力</td>
<td>必装</td>
<td><code>GO111MODULE=on go get golang.org/x/tools/gopls</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#dlv">dlv</a></td>
<td>断点调试（legacy 模式）</td>
<td>必装</td>
<td>go1.14 即之前的版本 <code>GO111MODULE=on go get github.com/go-delve/delve/cmd/dlv@v1.4.1</code>，新版本 <code>GO111MODULE=on go get github.com/go-delve/delve/cmd/dlv</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#dlv-dap">dlv-dap</a></td>
<td>断点调试（默认）</td>
<td>必装</td>
<td><code>GO111MODULE=on GOBIN=/tmp/ go get github.com/go-delve/delve/cmd/dlv@master &amp;&amp; mv /tmp/dlv $GOPATH/bin/dlv-dap</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#gopkgs">gopkgs</a></td>
<td>为未导入的包提供自动完成功能</td>
<td>必装</td>
<td><code>GO111MODULE=on go get github.com/uudashr/gopkgs/cmd/gopkgs@v2</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#go-outline">go-outline</a></td>
<td>为测试的 code lens 提供信息</td>
<td>必装</td>
<td><code>GO111MODULE=on go get github.com/ramya-rao-a/go-outline</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#goplay">goplay</a></td>
<td>为 <code>Go: Run on Go Playground</code> 命令提供支持</td>
<td>可选</td>
<td><code>GO111MODULE=on go get github.com/haya14busa/goplay/cmd/goplay</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#gomodifytags">gomodifytags</a></td>
<td>为  <code>Go: Add Tags to Struct Fields</code> 和 <code>Go: Remove Tags From Struct Fields</code> 命令提供支持</td>
<td>可选</td>
<td><code>GO111MODULE=on go get github.com/fatih/gomodifytags</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#impl">impl</a></td>
<td>为 <code>Go: Generate Interface Stubs</code> 命令提供支持</td>
<td>可选</td>
<td><code>GO111MODULE=on go get github.com/josharian/impl</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#gotests">gotests</a></td>
<td>为 <code>Go: Generate Unit Tests</code> 命令提供支持</td>
<td>可选</td>
<td><code>go get github.com/cweill/gotests/...</code></td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#staticcheck">staticcheck</a></td>
<td>默认的 lint 工具，可以通过 <code>&quot;go.lintFlags&quot;</code> 配置选项</td>
<td>可选</td>
<td>略</td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#staticcheck">golangci-lint</a></td>
<td>可选的 lint 工具，通过 <code>&quot;go.lintTool&quot;</code> 配置项配置，可以通过 <code>&quot;go.lintFlags&quot;</code> 配置选项</td>
<td>可选</td>
<td>略</td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#staticcheck">revive</a></td>
<td>可选的 lint 工具，通过 <code>&quot;go.lintTool&quot;</code> 配置项配置，可以通过 <code>&quot;go.lintFlags&quot;</code> 配置选项</td>
<td>可选</td>
<td>略</td>
</tr>

<tr>
<td><a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#staticcheck">golint</a></td>
<td>可选的 lint 工具，通过 <code>&quot;go.lintTool&quot;</code> 配置项配置</td>
<td>可选</td>
<td>略</td>
</tr>
</tbody>
</table>

<p>使用 legacy 模式的依赖工具如下表所示（通过 <code>&quot;go.useLanguageServer&quot;: false</code> 启用该模式），不建议使用，具体参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/tools.md#misc-tools-used-in-the-legacy-mode">官方文档</a></p>

<p>如果想把命令行工具安装到指定位置，可以通过 <code>&quot;go.toolsGopath&quot;</code> 配置项指定。更多关于安装位置，参见<a href="https://github.com/golang/vscode-go/blob/master/docs/advanced.md#configuring-the-installation-of-command-line-tools">官方文档</a></p>

<h3 id="特性详解">特性详解</h3>

<blockquote>
<p>参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/features.md">官方文档</a></p>
</blockquote>

<h4 id="智能感知">智能感知</h4>

<ul>
<li>建议列表 <code>editor.action.triggerSuggest</code>，快捷键 <code>cmd + i</code></li>
<li>显示悬浮文档 <code>editor.action.showHover</code>，快捷键 <code>cmd+k cmd+i</code></li>
<li>显示参数和参数位置提示 <code>editor.action.triggerParameterHints</code>，快捷键 <code>cmd+shift+space</code></li>
</ul>

<p><img src="/image/vscode/golang/completion-signature-help.gif" alt="image" /></p>

<h4 id="代码导航">代码导航</h4>

<p>代码编辑器鼠标右击，打开上下文菜单，可以调转定义、查找引用、查找实现、调用层次</p>

<ul>
<li>调转定义 <code>editor.action.revealDefinition</code> ，快捷键 <code>F12</code></li>
</ul>

<p><img src="/image/vscode/golang/gotodefinition.gif" alt="image" /></p>

<ul>
<li>查找引用 <code>editor.action.goToReferences</code>，快捷键 <code>shift+F12</code></li>
</ul>

<p><img src="/image/vscode/golang/findallreferences.gif" alt="image" /></p>

<ul>
<li>查找实现 <code>editor.action.goToImplementation</code>，快捷键 <code>cmd+F12</code>，光标在接口上</li>
</ul>

<p><img src="/image/vscode/golang/implementations.gif" alt="image" /></p>

<ul>
<li>查找工作空间所有符号</li>
</ul>

<p><img src="/image/vscode/golang/workspace-symbols.gif" alt="image" /></p>

<ul>
<li>调用层次 <code>references-view.showCallHierarchy</code>，快捷键 <code>shift+option+H</code>，命令名 <code>&gt;Calls: Show Call Hierarchy</code></li>
</ul>

<p><img src="/image/vscode/golang/callhierarchy.gif" alt="image" /></p>

<ul>
<li>Explorer （资源管理器）的 Outline （大纲），可以看到当前编辑器打开文件的符号列表</li>
</ul>

<p><img src="/image/vscode/golang/outline.png" alt="image" /></p>

<ul>
<li><code>&gt;Go: Toggle Test File</code> 命令可以快速切换到当前文件的测试文件</li>
</ul>

<p><img src="/image/vscode/golang/toggletestfile.gif" alt="image" /></p>

<h4 id="代码编辑">代码编辑</h4>

<p><img src="/image/vscode/golang/snippets-tys.gif" alt="image" /></p>

<h5 id="动态代码片段">动态代码片段</h5>

<p>在一个表达式后输入 <code>.</code>，有两种常用操作</p>

<ul>
<li><code>.var!</code>，将该函数赋值给一个变量 <code>x, x := 表达式</code></li>
<li><code>.print!</code>，将生成<code>fmt.Printf(&quot;c: %v\n&quot;, 表达式)</code></li>
</ul>

<h5 id="静态代码片段">静态代码片段</h5>

<blockquote>
<p>完整列表，参见：<a href="https://github.com/golang/vscode-go/blob/master/snippets/go.json">配置文件</a></p>
</blockquote>

<table>
<thead>
<tr>
<th>分类</th>
<th>代码片段</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>package</td>
<td>pkgm</td>
<td>main 包和 main 函数</td>
</tr>

<tr>
<td>import</td>
<td>im</td>
<td><code>import &quot;$&quot;</code></td>
</tr>

<tr>
<td></td>
<td>ims</td>
<td><code>import (&quot;$&quot;)</code></td>
</tr>

<tr>
<td>常量变量</td>
<td>co</td>
<td><code>const name = value</code></td>
</tr>

<tr>
<td></td>
<td>cos</td>
<td><code>const (name = value)</code></td>
</tr>

<tr>
<td></td>
<td>var</td>
<td><code>var name type</code></td>
</tr>

<tr>
<td>type</td>
<td>tyf</td>
<td><code>type name func()</code></td>
</tr>

<tr>
<td></td>
<td>tyi</td>
<td><code>type name interface {}</code></td>
</tr>

<tr>
<td></td>
<td>tys</td>
<td><code>type name struct {}</code></td>
</tr>

<tr>
<td>流程控制</td>
<td>switch</td>
<td>switch 块</td>
</tr>

<tr>
<td></td>
<td>sel</td>
<td>select 块</td>
</tr>

<tr>
<td></td>
<td>for</td>
<td><code>for i := 0; i &lt; count; i++ {}</code></td>
</tr>

<tr>
<td></td>
<td>forr</td>
<td>for range 块</td>
</tr>

<tr>
<td></td>
<td>if</td>
<td>if 块</td>
</tr>

<tr>
<td></td>
<td>el</td>
<td>else 块</td>
</tr>

<tr>
<td></td>
<td>ie</td>
<td>if else 块</td>
</tr>

<tr>
<td></td>
<td>iferr</td>
<td><code>if err != nil {}</code></td>
</tr>

<tr>
<td>内置变量类型</td>
<td>ch</td>
<td><code>chan type</code></td>
</tr>

<tr>
<td></td>
<td>map</td>
<td><code>map[type]type</code></td>
</tr>

<tr>
<td></td>
<td>in</td>
<td><code>interface{}</code></td>
</tr>

<tr>
<td>打印</td>
<td>fp</td>
<td><code>fmt.Println(&quot;&quot;)</code></td>
</tr>

<tr>
<td></td>
<td>ff</td>
<td><code>fmt.Printf(&quot;&quot;, )</code></td>
</tr>

<tr>
<td></td>
<td>ff</td>
<td><code>fmt.Printf(&quot;&quot;, )</code></td>
</tr>

<tr>
<td></td>
<td>lp</td>
<td><code>log.Println(&quot;&quot;)</code></td>
</tr>

<tr>
<td></td>
<td>lf</td>
<td><code>log.Printf(&quot;&quot;, )</code></td>
</tr>

<tr>
<td></td>
<td>lv</td>
<td><code>log.Printf(&quot;var: %#+v\n&quot;, var)</code></td>
</tr>

<tr>
<td></td>
<td>tl</td>
<td><code>t.Log(&quot;&quot;)</code></td>
</tr>

<tr>
<td></td>
<td>tlf</td>
<td><code>t.Logf(&quot;&quot;, var)</code></td>
</tr>

<tr>
<td></td>
<td>tlv</td>
<td><code>t.Logf(&quot;var: %#+v\n&quot;, var)</code></td>
</tr>

<tr>
<td>构造</td>
<td>make</td>
<td><code>make(type, 0)</code></td>
</tr>

<tr>
<td></td>
<td>new</td>
<td><code>new(type)</code></td>
</tr>

<tr>
<td>panic</td>
<td>pn</td>
<td><code>panic(&quot;&quot;)</code></td>
</tr>

<tr>
<td>http</td>
<td>wr</td>
<td><code>w http.ResponseWriter, r *http.Request</code></td>
</tr>

<tr>
<td></td>
<td>hf</td>
<td><code>http.HandleFunc(&quot;/&quot;, handler)</code></td>
</tr>

<tr>
<td></td>
<td>hand</td>
<td><code>func (w http.ResponseWriter, r *http.Request) {}</code></td>
</tr>

<tr>
<td></td>
<td>rd</td>
<td><code>http.Redirect(w, r, &quot;/&quot;, http.StatusFound)</code></td>
</tr>

<tr>
<td></td>
<td>herr</td>
<td><code>http.Error(w, err.Error(), http.StatusInternalServerError)</code></td>
</tr>

<tr>
<td></td>
<td>las</td>
<td><code>http.ListenAndServe(&quot;:8080&quot;, nil)</code></td>
</tr>

<tr>
<td></td>
<td>sv</td>
<td><code>http.Serve(&quot;:8080&quot;, nil)</code></td>
</tr>

<tr>
<td></td>
<td>helloweb</td>
<td>一个简单的 http server</td>
</tr>

<tr>
<td>协程</td>
<td>go</td>
<td><code>go func() {}()</code></td>
</tr>

<tr>
<td></td>
<td>gf</td>
<td><code>go func()</code></td>
</tr>

<tr>
<td></td>
<td>df</td>
<td><code>defer func()</code></td>
</tr>

<tr>
<td>快速函数</td>
<td>func</td>
<td><code>func ()  {}</code></td>
</tr>

<tr>
<td></td>
<td>tf</td>
<td><code>func Test(t *testing.T) {}</code></td>
</tr>

<tr>
<td></td>
<td>tm</td>
<td><code>func TestMain(m *testing.M) {os.Exit(m.Run())}</code></td>
</tr>

<tr>
<td></td>
<td>bf</td>
<td><code>func Benchmark(b *testing.B) { for i := 0; i &lt; b.N; i++ {} }</code></td>
</tr>

<tr>
<td></td>
<td>tdt</td>
<td>生成表格驱动测试函数</td>
</tr>

<tr>
<td></td>
<td>finit</td>
<td><code>func init() {}</code></td>
</tr>

<tr>
<td></td>
<td>fmain</td>
<td><code>func main() {}</code></td>
</tr>

<tr>
<td></td>
<td>meth</td>
<td><code>func (receiver type) method()  {}</code></td>
</tr>

<tr>
<td>排序</td>
<td>sort</td>
<td>快速实现 Sort 接口</td>
</tr>
</tbody>
</table>

<h5 id="包导入">包导入</h5>

<ul>
<li>默认保存的时候会自动导入 go.mod 中声明的包。如果，包没有在 go.md 中声明，将光标放到 <code>import</code> 语句位置，按 <code>cmd+.</code> 选择 <code>go get xxx</code>，即可快速导入。</li>
<li>通过 <code>Go: Add Import</code> 命令，可以查找所有 GOPATH 和 Go module cache 中的包，并快速添加到 import 块中。</li>
</ul>

<p><img src="/image/vscode/golang/addimport.gif" alt="image" /></p>

<h5 id="格式化">格式化</h5>

<p>代码保存是将默认执行格式化。</p>

<h4 id="重构和代码生成">重构和代码生成</h4>

<h5 id="符号重命名">符号重命名</h5>

<p>Go 可以重命名工作空间的所有符号，将光标位于需要重命名的符号处，通过 Rename 命令，快捷键为 F2 触发。</p>

<p><img src="/image/vscode/golang/rename.gif" alt="image" /></p>

<h5 id="提取表达式">提取表达式</h5>

<p>选中一段代码，按 <code>cmd+.</code> 可能触发两种重构行为</p>

<ul>
<li>提取表达式到变量</li>
<li>提取代码块到函数</li>
</ul>

<p><img src="/image/vscode/golang/extract-variable.gif" alt="image" /></p>

<h5 id="添加和删除结构体-tags">添加和删除结构体 tags</h5>

<p>光标聚焦于结构体定义位置，执行命令 <code>Go: Add Tags to Struct Fields</code> 、<code>&gt;Go: Remove Tags From Struct Fields</code> 可以给结构体批量添加或删除 tags。</p>

<p><img src="/image/vscode/golang/addtagstostructfields.gif" alt="image" /></p>

<p>tags 的格式，通过如下配置项配置，配置项的含义，参见 <a href="https://pkg.go.dev/github.com/fatih/gomodifytags#section-readme">gomodifytags</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;go.addTags&#34;</span>: {
        <span style="color:#f92672">&#34;tags&#34;</span>: <span style="color:#e6db74">&#34;json&#34;</span>,
        <span style="color:#f92672">&#34;options&#34;</span>: <span style="color:#e6db74">&#34;json=omitempty&#34;</span>,
        <span style="color:#f92672">&#34;promptForTags&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;transform&#34;</span>: <span style="color:#e6db74">&#34;snakecase&#34;</span>,
        <span style="color:#f92672">&#34;template&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
    }
}</code></pre></div>
<h5 id="为结构体实现方法">为结构体实现方法</h5>

<p>通过 <code>&gt;Go: Generate Interface Stubs</code> 命令，并输入 <code>$接收者名 $结构体类型 $要实现的接口</code>，在光标处为该结构体生成相关接口的方法</p>

<p><img src="/image/vscode/golang/generateinterfaceimplementation.gif" alt="image" /></p>

<h5 id="生成测试">生成测试</h5>

<p>通过 <code>&gt;Go: Generate Unit Tests for</code> 可以快速生成单元测试文件和表格驱动的单元测试函数。该能力由 <a href="https://github.com/cweill/gotests/">gotests</a> 提供</p>

<p><img src="/image/vscode/golang/generateunittestsforfunction.gif" alt="image" /></p>

<h5 id="填充结构体类型实例化字段">填充结构体类型实例化字段</h5>

<p>光标位于结构体类型实例化语句内，通过 <code>cmd+.</code> 选择 <code>fill Xxx</code>，或者 <code>&gt;Go: Fill struct</code> 命令，即可快速填充结构体字段</p>

<p><img src="/image/vscode/golang/fillstructliterals.gif" alt="image" /></p>

<h4 id="构建-测试和运行调试">构建、测试和运行调试</h4>

<h5 id="调试或运行项目">调试或运行项目</h5>

<p>想调试、运行项目（main 函数），需要创建 VSCode 调试配置文件 <code>.vscode/launch.json</code> 文件并添加 Golang 配置。流程如下：</p>

<ul>
<li><code>cmd + p</code> 输入 <code>debug</code>，选择添加配置，选择 <code>Go: Launch Package</code> 回车将创建并打开 <code>.vscode/launch.json</code> 文件</li>
<li>启动调试，有如下几种方式

<ul>
<li><code>F5</code> 或者 <code>&gt;Debug: start debugging</code> 以上次选中的调试配置启动调试</li>
<li><code>cmd + p</code> 输入 <code>debug</code> 并选择调试配置，并启动调试</li>
<li>打开调试侧边栏，绿色三角号</li>
<li>菜单 &gt; Run &gt; Start debugging</li>
</ul></li>
<li>运行（不调试），有如下几种方式

<ul>
<li><code>ctrl+F5</code> 或者 <code>&gt;debug: start without debugging</code></li>
<li>菜单 &gt; Run &gt; start without debugging</li>
</ul></li>
</ul>

<p>关于调试运行配置，参见下文</p>

<h5 id="测试浏览器">测试浏览器</h5>

<p><img src="/image/vscode/golang/testexplorer.gif" alt="image" /></p>

<h5 id="测试-基准和覆盖度">测试、基准和覆盖度</h5>

<ul>
<li>测试启动可以通过如下方式运行或者调试

<ul>
<li>点击测试函数上方的 <a href="https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup">Code Lens</a></li>
<li>测试侧边栏，可以看到当前工作空间所有测试文件和函数鼠标点击</li>
<li>通过命令如下命令启动

<ul>
<li><code>&gt;Go: Test Function At Cursor</code></li>
<li><code>&gt;Go: Test File, Go: Test Package</code></li>
<li><code>&gt;Go: Test All Packages in Workspace</code></li>
</ul></li>
</ul></li>
<li>测试覆盖度

<ul>
<li>测试覆盖度运行后，编辑器会通过绿色背景色来表示哪些代码被覆盖了，红色背景色表示哪些代码没有被覆盖</li>
<li>通过如下命令触发

<ul>
<li><code>Go: Apply Cover Profile</code></li>
<li><code>Go: Toggle Test Coverage in Current Package</code></li>
</ul></li>
<li>一些配置如下

<ul>
<li><code>&quot;go.coverOnSave&quot;</code> 可以在保存文件时自动执行覆盖度计算（注意性能）</li>
<li><code>&quot;go.coverOnSingleTest&quot;</code> 可以在执行单测试函数时自动执行覆盖度计算</li>
<li><code>&quot;go.coverOnSingleTestFile&quot;</code> 可以在执行某个测试文件时自动执行覆盖度计算</li>
<li><code>&quot;go.coverShowCounts&quot;</code> 可以在覆盖度计算后再函数和条件语句后面显示命中次数</li>
</ul></li>
</ul></li>
<li><code>&gt;Go: Toggle Test File</code> 命令可以快速切换到当前文件的测试文件</li>
</ul>

<p><img src="/image/vscode/golang/toggletestfile.gif" alt="image" /></p>

<h4 id="问题诊断">问题诊断</h4>

<p>问题诊断主要有3种</p>

<ul>
<li>第一种为 编译级别错误。通过 <code>&quot;go.buildOnSave&quot;</code> 配置项配置保存时编译的范围，来控制检查的代码文件</li>
<li>第二种为 Vet 错误。通过 <code>&quot;go.vetOnSave&quot;</code> 配置项配置保存时执行 <code>go vet</code> 的范围，来控制检查的代码文件</li>
<li>第三种为 lint 工具提供的检查，可以通过 <code>&quot;go.lintTool&quot;</code> 配置项选择使用的 lint 工具，如果使用的是 staticcheck，可以通过 <code>&quot;gopls&quot;: { &quot;ui.diagnostic.staticcheck&quot;: true }</code> 来让其运行在 gopls 中</li>
</ul>

<h4 id="其他特性">其他特性</h4>

<h5 id="go-playground">Go Playground</h5>

<p><code>&gt;Go: Run On Go Playground</code> 命令可以把当前文件快速上传到 <code>https://play.studygolang.com/</code> （大陆地区无法使用）</p>

<h3 id="断点调试">断点调试</h3>

<blockquote>
<p>本部分介绍的是基于 dlv-dap 模式调试程序，参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/debugging.md">官方文档</a></p>
</blockquote>

<p><img src="/image/vscode/golang/dlvdap-install.gif" alt="image" /></p>

<p>通过 <code>.vscode/launch.json</code> 配置文件的 <code>configurations</code> 字段，来配置调试器</p>

<p><img src="/image/vscode/golang/create-launch-json.gif" alt="image" /></p>

<p>简单的例子：当前打开的工作空间是一个 main 包</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch Package&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;auto&#34;</span>,
    <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}&#34;</span>
}</code></pre></div>
<h4 id="配置字段">配置字段</h4>

<ul>
<li><code>name</code> 配置名称，可以随意设置</li>
<li><code>type</code> 调试器类型，必须是调试 go 程序，必须是 go</li>
<li><code>request</code> 可选项为 <code>launch</code> 或 <code>attach</code>，<code>launch</code> 为调试器会启动一个新的进程，<code>attach</code> 为调试器会连接到已经存在的进程或远端端口</li>
<li><code>mode</code> 调试模式

<ul>
<li>当 <code>request</code> 为 <code>launch</code> 可选项为：

<ul>
<li><code>debug</code> 构建并 debug 一个 main 包</li>
<li><code>test</code> 构建并 debug 一个 测试文件</li>
<li><code>exec</code> 调试编译好的的二进制文件。二进制文件需要使用 <code>-gcflags=all=&quot;-N -l&quot;</code> 标志来构建，以避免剥离调试信息</li>
<li><code>auto</code> 根据打开的文件自动选择 <code>debug</code> 或 <code>test</code> 模式</li>
<li><code>core</code> 调试核心转储文件</li>
<li><code>replay</code> 没找到相关文档</li>
</ul></li>
<li>当 <code>request</code> 为 <code>attach</code> 可选项为：

<ul>
<li><code>local</code> 连接到本机进程，对应执行 <code>dlv attach ...</code> 命令，需要填写 <code>processId</code> 字段</li>
<li><code>remote</code> 连接到远程进程，需要填写 <code>host</code> 和 <code>port</code> 字段</li>
</ul></li>
</ul></li>
<li><code>request</code> 为 <code>launch</code> 时配置属性

<ul>
<li><code>args</code> 命令行参数，默认为 <code>[]</code></li>
<li><code>backend</code> delve 使用的后端，传递到 dlv 的 <code>--backend</code> 标志，可选值为 <code>&quot;default&quot;</code>, <code>&quot;native&quot;</code>, <code>&quot;lldb&quot;</code>, <code>&quot;rr&quot;</code></li>
<li><code>buildFlags</code> 构建时使用的标志，传递到 dlv 的 <code>--build-flags</code> 标志，默认为 <code>[]</code></li>
<li><code>coreFilePath</code> 核心转储文件路径，传递到 dlv 仅当 <code>mode</code> 为 <code>core</code> 时有效</li>
<li><code>cwd</code> 默认为当前包所在路径</li>
<li><code>env</code> 传递给程序的环境变量</li>
<li><code>envFile</code> 包含环境变量定义的文件的绝对路径。可以通过提供一组绝对路径来指定多个文件，默认为 <code>${workspaceFolder}/.env</code></li>
<li><code>output</code> 被调试者的二进制文件的输出路径。默认为 <code>&quot;debug&quot;</code></li>
</ul></li>
<li><code>debugAdapter</code> 调试适配器模式 <code>&quot;legacy&quot;</code>, <code>&quot;dlv-dap&quot;</code> （默认），关于两种模式说明参见下文</li>
<li><code>dlvFlags</code> dlv 的额外标志。有关受支持的完整列表，请参阅 <code>dlv help</code>。 <code>--log-output</code>、<code>--log</code>、<code>--log-dest</code>、<code>--api-version</code>、<code>--output</code>、<code>--backend</code> 等标志在调试配置中已经有相应的属性，并且 <code>--listen和</code> 和 <code>-headless</code> 在内部使用。如果它们在 <code>dlvFlags</code> 中指定，它们可能会被忽略或导致错误。</li>
<li><code>hideSystemGoroutines</code> 是否在调用栈视图中隐层系统 goroutine，默认为 <code>false</code></li>
<li><code>host</code>

<ul>
<li>当 <code>debugAdapter</code> 为 <code>&quot;dlv-dap&quot;</code> (which does not yet support remote-attach)，表示 host 所在机器上，必须有 <code>&quot;dlv dap --listen=:&quot;</code> 监听的端口</li>
<li>当 <code>debugAdapter</code> 为 <code>&quot;legacy&quot;</code>，仅在 <code>request</code> 为 <code>attach</code> 且 <code>mode</code> 为 <code>remote</code> 时有效，表示该 host 所在机器上，必须有 <code>dlv ... --headless --listen=:</code> 监听的端口</li>
</ul></li>
<li><code>port</code> 远端端口， 和 <code>host</code> 类似</li>
<li><code>logDest</code> dlv 的 <code>--log-dest</code> 标志。有关详细信息，请参阅 <code>dlv log</code>。不允许使用数字参数。仅在  <code>debugAdapter</code> 为 <code>&quot;dlv-dap&quot;</code> 且系统为 Linux 和 Mac OS 上受支持。</li>
<li><code>logOutput</code> 映射到 dlv 的 <code>--log-output</code> 标志。请参阅 <code>dlv log</code>，允许值为 <code>&quot;debugger&quot;</code>, <code>&quot;gdbwire&quot;</code>, <code>&quot;lldbout&quot;</code>, <code>&quot;debuglineerr&quot;</code>, <code>&quot;rpc&quot;</code>, <code>&quot;dap&quot;</code>。(默认为 <code>&quot;debugger&quot;</code>)</li>
<li><code>remotePath</code> 废弃</li>
<li><code>showGlobalVariables</code> 变量视图是否展示全局变量</li>
<li><code>showLog</code> 映射到 dlv 的 <code>--log</code> 选项（默认为 <code>false</code>）</li>
<li><code>showRegisters</code> 是否在变量视图显示寄存器变量（默认为 <code>false</code>）</li>
<li><code>stackTraceDepth</code> dlv 收集的最大栈深度（默认为 50）</li>
<li><code>stopOnEntry</code> 启动后自动暂停程序（默认为 <code>false</code>）</li>
<li><code>substitutePath</code> 从本地路径（编辑器）到远程路径（调试器）的映射数组。在使用符号链接的文件系统中工作、运行远程调试或调试外部编译的可执行文件时，此设置很有用。调试适配器将在所有调用中用远程路径替换本地路径。

<ul>
<li><code>&quot;from&quot;</code>: 将路径传递给调试器时要替换的绝对本地路径（默认为 <code>&quot;&quot;</code>）</li>
<li><code>&quot;to&quot;</code>：将路径传回客户端时要替换的绝对远程路径（默认为 <code>&quot;&quot;</code>）</li>
</ul></li>
<li><code>processId</code> 仅 <code>request</code> 为 <code>attach</code> 且 <code>mode</code> 为 <code>local</code> 可用，可以填写如下内容

<ul>
<li>非零数字，直接attach到对应 pid 的进程</li>
<li>字符串，则认为是进程名，需要先找到对应进程，然后再 attach，如果存在多个则弹出选择框</li>
<li>0，列出所有进程，然后再选择一个进程（默认）</li>
</ul></li>
</ul>

<p><img src="/image/vscode/golang/attach.gif" alt="image" /></p>

<ul>
<li><code>trace</code> 调试控制台和 <code>&quot;Go Debug&quot;</code> 输出面变中，显示的各种级别的日志记录。当 <code>debugAdapter</code> 为 <code>&quot;legacy&quot;</code>，如果将日志设置为 <code>error</code> 以外的值，日志也将写入文件。可选值 <code>&quot;verbose&quot;</code>, <code>&quot;trace&quot;</code>, <code>&quot;log&quot;</code>, <code>&quot;info&quot;</code>, <code>&quot;warn&quot;</code>, <code>&quot;error&quot;</code>（默认值 <code>&quot;error&quot;</code>）</li>
<li><code>traceDirPath</code> Directory in which the record trace is located or to be created for a new output trace. For use on &lsquo;replay&rsquo; mode only (Default: &ldquo;&rdquo;)</li>
</ul>

<h4 id="调试行为">调试行为</h4>

<p><img src="/image/vscode/golang/debug-toolbar.png" alt="image" /></p>

<ul>
<li>继续/暂停 <code>F5</code></li>
<li>单步跳过 <code>F10</code></li>
<li>单步进入 <code>F11</code></li>
<li>单步跳出 <code>⇧F11</code></li>
<li>重启 <code>⇧⌘F5</code></li>
<li>停止 (当 <code>request</code> 为 <code>launch</code> 时) <code>⇧F5</code></li>
<li>断开连接 (当 <code>request</code> 为 <code>attach</code> 时) <code>⇧F5</code></li>
<li>Terminate (当 <code>request</code> 为 <code>attach</code> 时) <code>⌥⇧F5</code></li>
</ul>

<p>关于停止或者断开 debug 连接的行为为（大概的意思可能是：<code>local</code> 断开后，进程不会停止；<code>remote</code> 取决于远端 <code>dlv --headless</code> 的配置，原文如下）</p>

<ul>
<li>Disconnect: disconnect the client and

<ul>
<li><code>local</code>: leave the target process running (dlv terminates).</li>
<li><code>remote</code>: let dlv decide if it can continue running (<code>--accept-multiclient</code> mode only); if so, the target will stay in halted or running state it was in at disconnect.

<ul>
<li><code>dlv debug/test/exec</code>: terminate the target process if dlv terminates.</li>
<li><code>dlv attach</code>: leave the target process running even if dlv terminates.</li>
</ul></li>
</ul></li>
<li>Stop: stop the attached server and the target process.</li>
</ul>

<p><img src="/image/vscode/golang/attach-terminate.gif" alt="image" /></p>

<h4 id="断点">断点</h4>

<ul>
<li>普通断点，点击编辑器左侧边框，在指定行添加断点，或者在光标所在位置按 <code>F9</code></li>
</ul>

<p><img src="/image/vscode/golang/invalid-breakpoint.png" alt="image" /></p>

<ul>
<li>条件断点，右击编辑器左侧边框，选择条件断点

<ul>
<li>表达式类型，输入一个结果为 bool 的表达式</li>
<li>命中次数类型

<ul>
<li>支持一个确定的整数</li>
<li>支持比较运算符 (&gt;, &gt;=, &lt;, &lt;=, ==, !=) 加一个整数</li>
<li>支持 <code>% n</code> 表示每命中 n 次断点一次</li>
</ul></li>
</ul></li>
</ul>

<p><img src="/image/vscode/golang/conditional-breakpoint.gif" alt="image" /></p>

<ul>
<li>函数断点，在测试侧边栏中，最下方断点视图，点击加号，输入当前打开文件的函数名</li>
</ul>

<p><img src="/image/vscode/golang/function-breakpoint.gif" alt="image" /></p>

<ul>
<li>Logpoint，暂不支持</li>
</ul>

<h4 id="数据检查">数据检查</h4>

<p>观察方式</p>

<ul>
<li>通过将鼠标 hover 在要观察的变量上观察其变量值。</li>
</ul>

<p><img src="/image/vscode/golang/variable-hover.png" alt="image" /></p>

<ul>
<li>通过调试侧边栏，变量视图，观察当前调用函数内的局部变量值。注意 shadowed 的变量将通过 <code>(变量名)</code> 方式展示</li>
</ul>

<p><img src="/image/vscode/golang/shadowed-variables.png" alt="image" /></p>

<ul>
<li>通过调试侧边栏，监视视图，观察指定的局部变量和全局变量值。</li>
<li>通过 DEBUG CONSOLE，输入命令，详见 <a href="https://github.com/go-delve/delve/blob/master/Documentation/cli/expr.md">Delve expression</a>，调用函数的语法为 <code>call &lt;function_call_expression&gt;</code></li>
</ul>

<p>默认情况下，变量视图不会展示全局变量，如果需要展示，需在调试配置里面添加 <code>showGlobalVariables</code>，或者配置 <code>go.delveConfig</code></p>

<p>在变量和监视视图，变量右击可以做如下操作</p>

<ul>
<li>Set Value：只能更改简单的字符串、数字、指针值</li>
<li>Copy Value：将值复制到剪贴板</li>
<li>Copy as Expression：当您需要从 DEBUG CONSOLE 面板中的 REPL 进行查询时，这很有用</li>
</ul>

<p><img src="/image/vscode/golang/debug-console.png" alt="image" /></p>

<ul>
<li>Add to Watch：这将自动将表达式添加到 WATCH 部分</li>
</ul>

<h4 id="调用栈">调用栈</h4>

<p>可以在 在测试侧边栏，调用栈视图，观察所有 goroutines。并可以做如下操作</p>

<p><img src="/image/vscode/golang/callstack-section-annotated.gif" alt="image" /></p>

<ol>
<li>Goroutine 栈的函数名和内部 ID</li>
<li>当前的 goroutine 将使用 <code>*</code> 标识. 如果多个 goroutines 同时暂停（例如命中断点），Delve 将随机选择一个。也可能没有当前的 goroutine（例如，死锁、暂停或未运行 goroutine 的系统线程命中的内部断点）。</li>
<li>单击 goroutine 调用堆栈，则会选择该 goroutine。效果是可以看到当前运行到的位置并检查变量</li>
<li>可以选择所选 goroutine 的帧（某个函数调用层次）。 VARIABLE 和 WATCH 部分将相应更新，编辑器中的光标将移动到源代码中的相应位置。</li>
<li>不可以检查的堆栈帧将变灰或折叠</li>
<li>为调度的 goroutine 显示线程 ID</li>
<li>暂停理由。 goroutine 被暂停的原因可能有多种，但目前只给出了一个原因。</li>
<li>函数帧的的文件名和行号。</li>
<li>您可以使用选定的 goroutine 触发调试操作。注意：当前不支持仅恢复或停止单个 goroutine（Go Issue <a href="https://github.com/golang/go/issues/25578">25578</a>、<a href="https://github.com/golang/go/issues/31132">31132</a>），因此该操作将导致所有 goroutine 被激活或暂停。</li>
<li>帧的函数名称。</li>
</ol>

<p>当程序由于异常、panic 或错误访问错误而停止时，CALL STACK 会显示停止原因，并且编辑器会用更多详细信息突出显示源位置。</p>

<p><img src="/image/vscode/golang/panicinfo.png" alt="image" /></p>

<h4 id="legacy-和-dlv-dap-模式">legacy 和 dlv-dap 模式</h4>

<blockquote>
<p>扩展版本 0.29.0</p>
</blockquote>

<p>VSCode Go 的调试协议发展了两个版本：legacy 和 dlv-dap。目前默认已经使用 dlv-dap 了。两者的原理如下所示</p>

<p>dlv-dap 模式的原理为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">VSCode DAP Client ---(dap协议)---&gt; dlv-dap server ------&gt; debugee</pre></div>
<p>legacy 模式的原理为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">VSCode DAP Client ---(dap协议)---&gt; Golang 扩展 JS 实现的 Legacy DAP Server ---(dlv私有协议)---&gt; dlv server ------&gt; debugee</pre></div>
<p>官方对比如下图所示</p>

<p><img src="/image/vscode/golang/vscode-go-debug-arch.png" alt="image" /></p>

<p>可以看出 legacy 有一个中转层，而 dlv-dap 则更加直接。</p>

<p>但是注意，某些场景下目前仍需要使用 legacy 模式，如：Go 版本小于等于 Go 1.14</p>

<p>legacy 模式不再建议使用，如需了解，参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/debugging-legacy.md">官方文档</a></p>

<h4 id="dlv-全局配置">dlv 全局配置</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;go.delveConfig&#34;</span>: {
        <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;dlv-dap&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">dlv-dap,</span> <span style="color:#960050;background-color:#1e0010">可选值为</span> <span style="color:#960050;background-color:#1e0010">dlv-dap,</span> <span style="color:#960050;background-color:#1e0010">legacy</span>
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">仅列出两种模式均可用的配置项</span>
        <span style="color:#f92672">&#34;dlvFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">dlv</span> <span style="color:#960050;background-color:#1e0010">help，参见上文调试配置</span>
        <span style="color:#f92672">&#34;hideSystemGoroutines&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">false，参见上文调试配置</span>
        <span style="color:#f92672">&#34;logOutput&#34;</span>: <span style="color:#e6db74">&#34;debugger&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">debugger，参见上文调试配置</span>
        <span style="color:#f92672">&#34;showGlobalVariables&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">false，参见上文调试配置</span>
        <span style="color:#f92672">&#34;showLog&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">false，参见上文调试配置</span>
        <span style="color:#f92672">&#34;showRegisters&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">默认为</span> <span style="color:#960050;background-color:#1e0010">false，参见上文调试配置</span>
        <span style="color:#f92672">&#34;substitutePath&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">参见上文调试配置</span>
    }
}
<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">仅</span> <span style="color:#960050;background-color:#1e0010">legacy</span> <span style="color:#960050;background-color:#1e0010">模式可用配置</span>
{
    <span style="color:#f92672">&#34;go.delveConfig&#34;</span>: {
        <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;legacy&#34;</span>,
        <span style="color:#f92672">&#34;apiVersion&#34;</span>: <span style="color:#ae81ff">2</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">可选值为</span> <span style="color:#960050;background-color:#1e0010">1,</span> <span style="color:#960050;background-color:#1e0010">2</span> <span style="color:#960050;background-color:#1e0010">(默认为</span> <span style="color:#960050;background-color:#1e0010">2)</span>
        <span style="color:#f92672">&#34;dlvLoadConfig&#34;</span>: {
            <span style="color:#f92672">&#34;followPointers&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;maxVariableRecurse&#34;</span>: <span style="color:#ae81ff">1</span>,
            <span style="color:#f92672">&#34;maxStringLen&#34;</span>: <span style="color:#ae81ff">64</span>,
            <span style="color:#f92672">&#34;maxArrayValues&#34;</span>: <span style="color:#ae81ff">64</span>,
            <span style="color:#f92672">&#34;maxStructFields&#34;</span>: <span style="color:#ae81ff">-1</span>
        }
    }
}</code></pre></div>
<p>当 <code>debugAdapter</code> 为 <code>dlv-dap</code> 同时配置了 <code>dlvLoadConfig</code>，将会弹出如下提示。</p>

<p><img src="/image/vscode/golang/dlv-load-config-warning.png" alt="image" /></p>

<h4 id="远程-debug">远程 Debug</h4>

<h5 id="方式一-远程运行的是-headless-dlv">方式一：远程运行的是 headless dlv</h5>

<p>远端执行如下命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 使用 -gcflags=&#39;all=-N -l&#39; 编译出可执行文件</span>
dlv debug /path/to/program --headless --listen<span style="color:#f92672">=</span>:12345</code></pre></div>
<p>VSCode 调试配置如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Connect to external session&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
    <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;dlv-dap&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;attach&#34;</span>,
    <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;remote&#34;</span>,
    <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">12345</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端端口</span>
    <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;xxx.xxx.xxx.xxx&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端</span> <span style="color:#960050;background-color:#1e0010">ip</span> <span style="color:#960050;background-color:#1e0010">或者</span> <span style="color:#960050;background-color:#1e0010">host</span>
    <span style="color:#f92672">&#34;substitutePath&#34;</span>: [
      { <span style="color:#f92672">&#34;from&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}&#34;</span>, <span style="color:#f92672">&#34;to&#34;</span>: <span style="color:#e6db74">&#34;/path/to/remote/workspace&#34;</span> },
    <span style="color:#960050;background-color:#1e0010">//</span>   <span style="color:#960050;background-color:#1e0010">...</span>
  ]
}</code></pre></div>
<ul>
<li>注意：远端如果使用 <code>--accept-multiclient</code> 参数启动，则可以支持断开连接后，远端的 dlv 不会停止</li>
</ul>

<h5 id="方式二-远程运行的是-dlv-dap">方式二：远程运行的是 dlv-dap</h5>

<p>远端执行如下命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd /path/to/remote/workspace <span style="color:#75715e"># 目的是让 dlv-dap 可以自动编译程序</span>
dlv-dap dap --listen<span style="color:#f92672">=</span>:12345</code></pre></div>
<p>VSCode 调试配置如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Connect and launch&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
    <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;dlv-dap&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">12345</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端端口</span>
    <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;xxx.xxx.xxx.xxx&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端</span> <span style="color:#960050;background-color:#1e0010">ip</span> <span style="color:#960050;background-color:#1e0010">或者</span> <span style="color:#960050;background-color:#1e0010">host</span>
    <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">这种模式需要手动前往</span> <span style="color:#960050;background-color:#1e0010">使用</span> <span style="color:#960050;background-color:#1e0010">-gcflags=&#39;all=-N</span> <span style="color:#960050;background-color:#1e0010">-l&#39;</span> <span style="color:#960050;background-color:#1e0010">编译出可执行文件</span>
                    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">还支持</span> <span style="color:#960050;background-color:#1e0010">debug、test</span> <span style="color:#960050;background-color:#1e0010">模式，此时远端的</span> <span style="color:#960050;background-color:#1e0010">dlv-dap</span> <span style="color:#960050;background-color:#1e0010">将会先编译程序在启动调试</span>
    <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;/absolute/path/to/remote/workspace/program/executable&#34;</span>,
    <span style="color:#f92672">&#34;substitutePath&#34;</span>: [
        { <span style="color:#f92672">&#34;from&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}&#34;</span>, <span style="color:#f92672">&#34;to&#34;</span>: <span style="color:#e6db74">&#34;/path/to/remote/workspace&#34;</span> },
    ]
}</code></pre></div>
<p>注意：</p>

<ul>
<li>Delve DAP 不支持代码同步，因此 mode 为 <code>debug</code>、<code>test</code> 模式，编译的代码是远端的代码</li>
<li>Delve DAP 1.7.3：Delve DAP 不支持 <code>--accept-multiclient</code> 或 <code>--continue</code> 标志，这意味着在调试会话结束后，<code>dlv-dap</code> 进程将始终退出。<a href="https://github.com/go-delve/delve/blob/master/CHANGELOG.md#173-2021-11-16">1.7.3 之后已经支持</a></li>
</ul>

<h4 id="官方报告调试相关问题">官方报告调试相关问题</h4>

<p>参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/debugging.md#developing">官方文档</a></p>

<h3 id="gopls-配置">gopls 配置</h3>

<blockquote>
<p>参考：<a href="https://github.com/golang/tools/blob/master/gopls/doc/settings.md">官方文档</a></p>
</blockquote>

<p>参见下文：<a href="#全部配置项">全部配置项</a> 的 gopls 配置</p>

<h3 id="全部命令列表">全部命令列表</h3>

<blockquote>
<p>扩展版本 0.29.0，参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/commands.md">官方文档</a></p>
</blockquote>

<table>
<thead>
<tr>
<th>命令ID</th>
<th>命令名</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>go.gopath</td>
<td>Go: Current GOPATH</td>
<td>显示当前识别到的 GOPATH</td>
</tr>

<tr>
<td>go.locate.tools</td>
<td>Go: Locate Configured Go Tools</td>
<td>打印命令行工具的相关信息</td>
</tr>

<tr>
<td>go.test.cursor</td>
<td>Go: Test Function At Cursor</td>
<td>执行当前光标所在的测试函数</td>
</tr>

<tr>
<td>go.test.cursorOrPrevious</td>
<td>Go: Test Function At Cursor or Test Previous</td>
<td>执行当前光标所在的测试函数，或者执行上一个测试函数</td>
</tr>

<tr>
<td>go.subtest.cursor</td>
<td>Go: Subtest At Cursor</td>
<td>执行当前光标所在的子测试，需要输入 <code>t.Run</code> 指示的名字</td>
</tr>

<tr>
<td>go.benchmark.cursor</td>
<td>Go: Benchmark Function At Cursor</td>
<td>运行光标处的基准函数</td>
</tr>

<tr>
<td>go.debug.cursor</td>
<td>Go: Debug Test At Cursor</td>
<td>调试光标处的测试函数</td>
</tr>

<tr>
<td>go.test.file</td>
<td>Go: Test File</td>
<td></td>
</tr>

<tr>
<td>go.test.package</td>
<td>Go: Test Package</td>
<td>运行当前文件所在的包的所有测试</td>
</tr>

<tr>
<td>go.benchmark.package</td>
<td>Go: Benchmark Package</td>
<td>运行当前文件所在的包的所有基准测试</td>
</tr>

<tr>
<td>go.benchmark.file</td>
<td>Go: Benchmark File</td>
<td>运行当前文件的所有基准测试</td>
</tr>

<tr>
<td>go.test.workspace</td>
<td>Go: Test All Packages In Workspace</td>
<td>运行当前工作空间的所有测试</td>
</tr>

<tr>
<td>go.test.previous</td>
<td>Go: Test Previous</td>
<td>运行上一个测试</td>
</tr>

<tr>
<td>go.debug.previous</td>
<td>Go: Debug Previous</td>
<td>调试上一个测试</td>
</tr>

<tr>
<td>go.test.coverage</td>
<td>Go: Toggle Test Coverage In Current Package</td>
<td>运行当前包的测试并计算覆盖度</td>
</tr>

<tr>
<td>go.test.generate.package</td>
<td>Go: Generate Unit Tests For Package</td>
<td>为当前包生成单元测试</td>
</tr>

<tr>
<td>go.test.generate.file</td>
<td>Go: Generate Unit Tests For File</td>
<td>为当前文件生成单元测试</td>
</tr>

<tr>
<td>go.test.generate.function</td>
<td>Go: Generate Unit Tests For Function</td>
<td>为当前函数生成单元测试</td>
</tr>

<tr>
<td>go.impl.cursor</td>
<td>Go: Generate Interface Stubs</td>
<td>为当前光标处的类型生成接口代码</td>
</tr>

<tr>
<td>go.extractServerChannel</td>
<td>Go: Extract Language Server Logs To Editor</td>
<td>将语言服务器的日志提取到编辑器中</td>
</tr>

<tr>
<td>go.welcome</td>
<td>Go: Welcome</td>
<td>显示欢迎信息</td>
</tr>

<tr>
<td>go.toggle.gc_details</td>
<td>Go: Toggle gc details</td>
<td>切换编译器优化选项的显示，打开后，将直接会在问题面板里面显示每个函数的优化情况</td>
</tr>

<tr>
<td>go.import.add</td>
<td>Go: Add Import</td>
<td>添加导入</td>
</tr>

<tr>
<td>go.add.package.workspace</td>
<td>Go: Add Package to Workspace</td>
<td>添加包到工作空间，将光标放到 <code>import</code> 块，运行该命令会直接将当前包在 VSCode 中打开，在阅读源码的时候很有用</td>
</tr>

<tr>
<td>go.tools.install</td>
<td>Go: Install/Update Tools</td>
<td>安装或更新扩展依赖的命令行工具</td>
</tr>

<tr>
<td>go.toggle.test.file</td>
<td>Go: Toggle Test File</td>
<td>切换当前文件的测试文件</td>
</tr>

<tr>
<td>go.add.tags</td>
<td>Go: Add Tags To Struct Fields</td>
<td>为当前结构体的字段添加标签</td>
</tr>

<tr>
<td>go.remove.tags</td>
<td>Go: Remove Tags From Struct Fields</td>
<td>为当前结构体的字段移除标签</td>
</tr>

<tr>
<td>go.fill.struct</td>
<td>Go: Fill struct</td>
<td>为当前结构体的字段填充值</td>
</tr>

<tr>
<td>go.show.commands</td>
<td>Go: Show All Commands&hellip;</td>
<td>显示所有命令</td>
</tr>

<tr>
<td>go.browse.packages</td>
<td>Go: Browse Packages</td>
<td>浏览包</td>
</tr>

<tr>
<td>go.get.package</td>
<td>Go: Get Package</td>
<td>获取包，光标放到 <code>import</code> 块，运行该命令会执行 <code>go get</code></td>
</tr>

<tr>
<td>go.playground</td>
<td>Go: Run on Go Playground</td>
<td>在 Go Playground 上运行当前文件</td>
</tr>

<tr>
<td>go.lint.package</td>
<td>Go: Lint Current Package</td>
<td>对当前包执行 lint</td>
</tr>

<tr>
<td>go.lint.workspace</td>
<td>Go: Lint Workspace</td>
<td>对工作空间执行 lint</td>
</tr>

<tr>
<td>go.vet.package</td>
<td>Go: Vet Current Package</td>
<td>对当前包执行 vet</td>
</tr>

<tr>
<td>go.vet.workspace</td>
<td>Go: Vet Workspace</td>
<td>对工作空间执行 vet</td>
</tr>

<tr>
<td>go.build.package</td>
<td>Go: Build Current Package</td>
<td>构建当前包</td>
</tr>

<tr>
<td>go.build.workspace</td>
<td>Go: Build Workspace</td>
<td>构建工作空间</td>
</tr>

<tr>
<td>go.install.package</td>
<td>Go: Install Current Package</td>
<td>安装当前包，光标放到 <code>import</code> 块，运行该命令会执行 <code>go get</code></td>
</tr>

<tr>
<td>go.run.modinit</td>
<td>Go: Initialize go.mod</td>
<td>初始化 go.mod</td>
</tr>

<tr>
<td>go.test.cancel</td>
<td>Go: Cancel Running Tests</td>
<td>需要正在运行的测试</td>
</tr>

<tr>
<td>go.apply.coverprofile</td>
<td>Go: Apply Cover Profile</td>
<td>应用覆盖度文件</td>
</tr>

<tr>
<td>go.godoctor.extract</td>
<td>Go: Extract to function</td>
<td>提取函数</td>
</tr>

<tr>
<td>go.godoctor.var</td>
<td>Go: Extract to variable</td>
<td>提取变量</td>
</tr>

<tr>
<td>go.languageserver.restart</td>
<td>Go: Restart Language Server</td>
<td>重启语言服务器</td>
</tr>

<tr>
<td>go.environment.choose</td>
<td>Go: Choose Go Environment</td>
<td>选择 Go 环境</td>
</tr>

<tr>
<td>go.survey.showConfig</td>
<td>Go: Show Survey Configuration</td>
<td>配置 Go 扩展的问卷调查</td>
</tr>

<tr>
<td>go.survey.resetConfig</td>
<td>Go: Reset Survey Configuration</td>
<td>重置 Go 扩展的问卷调查</td>
</tr>

<tr>
<td>go.workspace.resetState</td>
<td>Go: Reset Workspace State</td>
<td>重置工作空间状态</td>
</tr>

<tr>
<td>go.global.resetState</td>
<td>Go: Reset Global State</td>
<td>重置全局状态</td>
</tr>
</tbody>
</table>

<h3 id="全部配置项">全部配置项</h3>

<blockquote>
<p>扩展版本 0.29.0，参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/settings.md">官方文档</a></p>
</blockquote>

<p>除非特殊说明，下文配置项的值为默认值。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;go.buildOnSave&#34;</span>: <span style="color:#e6db74">&#34;package&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">保存时自动构建的范围</span>
    <span style="color:#f92672">&#34;go.buildFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">如</span> <span style="color:#960050;background-color:#1e0010">[</span><span style="color:#f92672">&#34;-ldflags=&#39;-s&#39;&#34;</span><span style="color:#960050;background-color:#1e0010">]</span> <span style="color:#960050;background-color:#1e0010">在</span> <span style="color:#960050;background-color:#1e0010">build-on-save</span> <span style="color:#960050;background-color:#1e0010">或者运行测试时编译参数。如果</span> <span style="color:#960050;background-color:#1e0010">`gopls.build.buildFlags`</span> <span style="color:#960050;background-color:#1e0010">没指定，则使用该参数</span>
    <span style="color:#e6db74">&#34;go.buildTags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">支持</span> <span style="color:#960050;background-color:#1e0010">-tags</span> <span style="color:#960050;background-color:#1e0010">&#39;&#39;</span> <span style="color:#960050;background-color:#1e0010">（条件编译）</span> <span style="color:#960050;background-color:#1e0010">，运行测试时，如果</span> <span style="color:#960050;background-color:#1e0010">go.testTags</span> <span style="color:#960050;background-color:#1e0010">没指定，则使用该参数。如果</span> <span style="color:#960050;background-color:#1e0010">`gopls.build.buildFlags`</span> <span style="color:#960050;background-color:#1e0010">没指定，则使用该参数</span>
    <span style="color:#f92672">&#34;go.testTags&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">参见</span> <span style="color:#960050;background-color:#1e0010">go.buildTags</span>
    <span style="color:#f92672">&#34;go.disableConcurrentTests&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">使用禁用并发测试，如果该配置为</span> <span style="color:#960050;background-color:#1e0010">true，则新的测试运行时，旧的测试将被取消</span>
    <span style="color:#f92672">&#34;go.installDependenciesWhenBuilding&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">build</span> <span style="color:#960050;background-color:#1e0010">时传递</span> <span style="color:#960050;background-color:#1e0010">-i</span> <span style="color:#960050;background-color:#1e0010">参数，非</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">mod</span> <span style="color:#960050;background-color:#1e0010">模式可能才需要配置</span>
    <span style="color:#f92672">&#34;go.lintOnSave&#34;</span>: <span style="color:#e6db74">&#34;package&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">保存时自动</span> <span style="color:#960050;background-color:#1e0010">lint</span> <span style="color:#960050;background-color:#1e0010">的范围</span>
    <span style="color:#f92672">&#34;go.lintTool&#34;</span>: <span style="color:#e6db74">&#34;staticcheck&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">lint</span> <span style="color:#960050;background-color:#1e0010">工具</span>
    <span style="color:#f92672">&#34;go.lintFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">lint</span> <span style="color:#960050;background-color:#1e0010">工具</span> <span style="color:#960050;background-color:#1e0010">flag，如</span> <span style="color:#960050;background-color:#1e0010">[</span><span style="color:#f92672">&#34;-min_confidence=.8&#34;</span><span style="color:#960050;background-color:#1e0010">]</span>
    <span style="color:#e6db74">&#34;go.vetOnSave&#34;</span>: <span style="color:#e6db74">&#34;package&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">保存时自动</span> <span style="color:#960050;background-color:#1e0010">vet</span> <span style="color:#960050;background-color:#1e0010">的范围</span>
    <span style="color:#f92672">&#34;go.vetFlags&#34;</span>: [],  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">tool</span> <span style="color:#960050;background-color:#1e0010">vet</span> <span style="color:#960050;background-color:#1e0010">参数，如</span> <span style="color:#960050;background-color:#1e0010">[</span><span style="color:#f92672">&#34;-all&#34;</span>, <span style="color:#f92672">&#34;-shadow&#34;</span><span style="color:#960050;background-color:#1e0010">]</span>
    <span style="color:#e6db74">&#34;go.gopath&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">指定</span> <span style="color:#960050;background-color:#1e0010">GOPATH，go.inferGopath</span> <span style="color:#960050;background-color:#1e0010">为</span> <span style="color:#960050;background-color:#1e0010">true</span> <span style="color:#960050;background-color:#1e0010">推导出的</span> <span style="color:#960050;background-color:#1e0010">GOPATH</span> <span style="color:#960050;background-color:#1e0010">优先级高于该配置（如果该配置在</span> <span style="color:#960050;background-color:#1e0010">.vscode/settings.json</span> <span style="color:#960050;background-color:#1e0010">中配置，仅当</span> <span style="color:#f92672">&#34;Go: Toggle Workspace Trust Flag&#34;</span> <span style="color:#960050;background-color:#1e0010">为信任时才生效）</span>
    <span style="color:#e6db74">&#34;go.toolsGopath&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">扩展依赖的命令行工具编译安装时使用的</span> <span style="color:#960050;background-color:#1e0010">GOPATH，如果没有指定，则使用系统</span> <span style="color:#960050;background-color:#1e0010">GOPATH（如果该配置在</span> <span style="color:#960050;background-color:#1e0010">.vscode/settings.json</span> <span style="color:#960050;background-color:#1e0010">中配置，仅当</span> <span style="color:#f92672">&#34;Go: Toggle Workspace Trust Flag&#34;</span> <span style="color:#960050;background-color:#1e0010">为信任时才生效）</span>
    <span style="color:#e6db74">&#34;go.goroot&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">指定</span> <span style="color:#960050;background-color:#1e0010">GOROOT（如果该配置在</span> <span style="color:#960050;background-color:#1e0010">.vscode/settings.json</span> <span style="color:#960050;background-color:#1e0010">中配置，仅当</span> <span style="color:#f92672">&#34;Go: Toggle Workspace Trust Flag&#34;</span> <span style="color:#960050;background-color:#1e0010">为信任时才生效）</span>
    <span style="color:#e6db74">&#34;go.testOnSave&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span>	<span style="color:#960050;background-color:#1e0010">是否在保存时对当前包运行</span> <span style="color:#960050;background-color:#1e0010">&#39;go</span> <span style="color:#960050;background-color:#1e0010">test&#39;</span>
    <span style="color:#f92672">&#34;go.coverOnSave&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">是否在保存时对当前包运行</span> <span style="color:#960050;background-color:#1e0010">&#39;go</span> <span style="color:#960050;background-color:#1e0010">test</span> <span style="color:#960050;background-color:#1e0010">-coverprofile&#39;</span>
    <span style="color:#f92672">&#34;go.coverOnTestPackage&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在执行</span> <span style="color:#960050;background-color:#1e0010">Go:</span> <span style="color:#960050;background-color:#1e0010">Test</span> <span style="color:#960050;background-color:#1e0010">Package</span> <span style="color:#960050;background-color:#1e0010">是否计算覆盖度</span>
    <span style="color:#f92672">&#34;go.coverOnSingleTest&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在执行</span> <span style="color:#960050;background-color:#1e0010">Go:</span> <span style="color:#960050;background-color:#1e0010">Test</span> <span style="color:#960050;background-color:#1e0010">Function</span> <span style="color:#960050;background-color:#1e0010">at</span> <span style="color:#960050;background-color:#1e0010">cursor</span> <span style="color:#960050;background-color:#1e0010">时是否计算覆盖度</span>
    <span style="color:#f92672">&#34;go.coverOnSingleTestFile&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在执行</span> <span style="color:#960050;background-color:#1e0010">Go:</span> <span style="color:#960050;background-color:#1e0010">Test</span> <span style="color:#960050;background-color:#1e0010">Single</span> <span style="color:#960050;background-color:#1e0010">File</span> <span style="color:#960050;background-color:#1e0010">时是否计算覆盖度</span>
    <span style="color:#f92672">&#34;go.coverMode&#34;</span>:	<span style="color:#e6db74">&#34;default&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">覆盖度模式</span>
    <span style="color:#f92672">&#34;go.coverShowCounts&#34;</span>: <span style="color:#66d9ef">false</span> , <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">运行覆盖度时，是否在编辑器中显示函数和条件的命中次数如</span> <span style="color:#960050;background-color:#1e0010">--374--</span>
    <span style="color:#f92672">&#34;go.coverageOptions&#34;</span>: <span style="color:#e6db74">&#34;showBothCoveredAndUncoveredCode&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">覆盖度结果在编辑器中的显示样式</span>
    <span style="color:#f92672">&#34;go.coverageDecorator&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">覆盖度结果显示的样式</span>
        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;highlight&#34;</span>,
        <span style="color:#f92672">&#34;coveredHighlightColor&#34;</span>: <span style="color:#e6db74">&#34;rgba(64,128,128,0.5)&#34;</span>,
        <span style="color:#f92672">&#34;uncoveredHighlightColor&#34;</span>: <span style="color:#e6db74">&#34;rgba(128,64,64,0.25)&#34;</span>,
        <span style="color:#f92672">&#34;coveredBorderColor&#34;</span>: <span style="color:#e6db74">&#34;rgba(64,128,128,0.5)&#34;</span>,
        <span style="color:#f92672">&#34;uncoveredBorderColor&#34;</span>: <span style="color:#e6db74">&#34;rgba(128,64,64,0.25)&#34;</span>,
        <span style="color:#f92672">&#34;coveredGutterStyle&#34;</span>: <span style="color:#e6db74">&#34;blockblue&#34;</span>,
        <span style="color:#f92672">&#34;uncoveredGutterStyle&#34;</span>: <span style="color:#e6db74">&#34;slashyellow&#34;</span>
    },
    <span style="color:#f92672">&#34;go.testTimeout&#34;</span>: <span style="color:#e6db74">&#34;30s&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试超时时间</span>
    <span style="color:#f92672">&#34;go.testEnvVars&#34;</span>: {}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试运行时的环境变量</span>
    <span style="color:#f92672">&#34;go.testEnvFile&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试时环境变量的绝对路径</span>
    <span style="color:#f92672">&#34;go.testFlags&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">构建测试时的</span> <span style="color:#960050;background-color:#1e0010">flag，参见</span> <span style="color:#960050;background-color:#1e0010">go.buildFlags</span>
    <span style="color:#f92672">&#34;go.testExplorer.enable&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">是否启用测试浏览器</span>
    <span style="color:#f92672">&#34;go.testExplorer.packageDisplayMode&#34;</span>: <span style="color:#e6db74">&#34;flat&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Present</span> <span style="color:#960050;background-color:#1e0010">packages</span> <span style="color:#960050;background-color:#1e0010">in</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">test</span> <span style="color:#960050;background-color:#1e0010">explorer</span> <span style="color:#960050;background-color:#1e0010">flat</span> <span style="color:#960050;background-color:#1e0010">or</span> <span style="color:#960050;background-color:#1e0010">nested.</span>	<span style="color:#960050;background-color:#1e0010">flat</span>
    <span style="color:#f92672">&#34;go.testExplorer.alwaysRunBenchmarks&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">运行所有测试时，是否也运行基准测试</span>
    <span style="color:#f92672">&#34;go.testExplorer.concatenateMessages&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Concatenate</span> <span style="color:#960050;background-color:#1e0010">all</span> <span style="color:#960050;background-color:#1e0010">test</span> <span style="color:#960050;background-color:#1e0010">log</span> <span style="color:#960050;background-color:#1e0010">messages</span> <span style="color:#960050;background-color:#1e0010">for</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">given</span> <span style="color:#960050;background-color:#1e0010">location</span> <span style="color:#960050;background-color:#1e0010">into</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">single</span> <span style="color:#960050;background-color:#1e0010">message</span>
    <span style="color:#f92672">&#34;go.testExplorer.showDynamicSubtestsInEditor&#34;</span>: <span style="color:#66d9ef">false</span>,	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">动态发现的子测试是否显示到测试浏览器中</span>
    <span style="color:#f92672">&#34;go.testExplorer.showOutput&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">开始测试运行时打开测试输出终端</span> 
    <span style="color:#f92672">&#34;go.generateTestsFlags&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">生成测试的命令行参数，去</span> <span style="color:#960050;background-color:#1e0010">https://github.com/cweill/gotests</span> <span style="color:#960050;background-color:#1e0010">查看</span>
    <span style="color:#f92672">&#34;go.toolsEnvVars&#34;</span>: {}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">运行命令行工具时的环境变量</span>
    <span style="color:#f92672">&#34;go.useLanguageServer&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">启用</span> <span style="color:#960050;background-color:#1e0010">gopls</span>
    <span style="color:#f92672">&#34;go.languageServerFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">命令行参数</span> <span style="color:#960050;background-color:#1e0010">Flags</span> <span style="color:#960050;background-color:#1e0010">like</span> <span style="color:#960050;background-color:#1e0010">-rpc.trace</span> <span style="color:#960050;background-color:#1e0010">and</span> <span style="color:#960050;background-color:#1e0010">-logfile</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">be</span> <span style="color:#960050;background-color:#1e0010">used</span> <span style="color:#960050;background-color:#1e0010">while</span> <span style="color:#960050;background-color:#1e0010">running</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">language</span> <span style="color:#960050;background-color:#1e0010">server.</span>	
    <span style="color:#f92672">&#34;go.languageServerExperimentalFeatures&#34;</span>: { <span style="color:#f92672">&#34;diagnostics&#34;</span>: <span style="color:#66d9ef">true</span> }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">临时标志去</span> <span style="color:#960050;background-color:#1e0010">enable/disable</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">的</span> <span style="color:#960050;background-color:#1e0010">diagnostics</span> <span style="color:#960050;background-color:#1e0010">能力.很快要废弃了.</span> <span style="color:#960050;background-color:#1e0010">请看</span> <span style="color:#960050;background-color:#1e0010">Issue</span> <span style="color:#960050;background-color:#1e0010">50:</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/vscode-go/issues/50</span>
    <span style="color:#f92672">&#34;go.trace.server&#34;</span>: <span style="color:#e6db74">&#34;off&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">跟踪</span> <span style="color:#960050;background-color:#1e0010">VS</span> <span style="color:#960050;background-color:#1e0010">Code</span> <span style="color:#960050;background-color:#1e0010">和</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">语言服务器之间的通信。</span>
    <span style="color:#f92672">&#34;go.logging.level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">扩展日志登记</span>
    <span style="color:#f92672">&#34;go.toolsManagement.checkForUpdates&#34;</span>: <span style="color:#e6db74">&#34;proxy&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">指定是否提示新版本的</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">以及扩展依赖的</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">工具（目前只有</span> <span style="color:#960050;background-color:#1e0010">gopls）</span> 
    <span style="color:#f92672">&#34;go.toolsManagement.autoUpdate&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">自动更新扩展依赖的命令行工具，不提示用户</span> 
    <span style="color:#f92672">&#34;go.useGoProxyToCheckForToolUpdates&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">启用后，扩展程序会自动检查</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">代理是否有可用于</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">和它所依赖的</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">工具（目前只有</span> <span style="color:#960050;background-color:#1e0010">gopls）的更新，并相应地提示用户</span>
    <span style="color:#f92672">&#34;go.enableCodeLens&#34;</span>: {
        <span style="color:#f92672">&#34;references&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">函数上的引用次数</span>
        <span style="color:#f92672">&#34;runtest&#34;</span>: <span style="color:#66d9ef">true</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试上面的运行/debug</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">CodeLen</span>
    <span style="color:#f92672">&#34;go.addTags&#34;</span>: {
        <span style="color:#f92672">&#34;tags&#34;</span>: <span style="color:#e6db74">&#34;json&#34;</span>,
        <span style="color:#f92672">&#34;options&#34;</span>: <span style="color:#e6db74">&#34;json=omitempty&#34;</span>,
        <span style="color:#f92672">&#34;promptForTags&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;transform&#34;</span>: <span style="color:#e6db74">&#34;snakecase&#34;</span>,
        <span style="color:#f92672">&#34;template&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">添加标签命令将使用此处配置的标签和选项将标签添加到结构字段。如果</span> <span style="color:#960050;background-color:#1e0010">promptForTags</span> <span style="color:#960050;background-color:#1e0010">为</span> <span style="color:#960050;background-color:#1e0010">true，则将提示用户输入标签和选项。默认添加json标签。</span> 
    <span style="color:#f92672">&#34;go.removeTags&#34;</span>: {
        <span style="color:#f92672">&#34;tags&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
        <span style="color:#f92672">&#34;options&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
        <span style="color:#f92672">&#34;promptForTags&#34;</span>: <span style="color:#66d9ef">false</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">此处配置的标签和选项将被移除标签命令用于移除结构字段的标签。</span> <span style="color:#960050;background-color:#1e0010">如果</span> <span style="color:#960050;background-color:#1e0010">promptForTags</span> <span style="color:#960050;background-color:#1e0010">为</span> <span style="color:#960050;background-color:#1e0010">true，则将提示用户输入标签和选项。</span> <span style="color:#960050;background-color:#1e0010">默认情况下，所有标签和选项都将被删除。</span>
    <span style="color:#f92672">&#34;go.playground&#34;</span>: {
        <span style="color:#f92672">&#34;openbrowser&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;share&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;run&#34;</span>: <span style="color:#66d9ef">true</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">此处配置的标志将传递给命令</span> <span style="color:#960050;background-color:#1e0010">`goplay`</span>
    <span style="color:#f92672">&#34;go.survey.prompt&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">是否展示问卷调查</span>
    <span style="color:#f92672">&#34;go.editorContextMenuCommands&#34;</span>: {
        <span style="color:#f92672">&#34;toggleTestFile&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;addTags&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;removeTags&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;fillStruct&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;testAtCursor&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;testFile&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;testPackage&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;generateTestForFunction&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;generateTestForFile&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;generateTestForPackage&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;addImport&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;testCoverage&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;playground&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;debugTestAtCursor&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;benchmarkAtCursor&#34;</span>: <span style="color:#66d9ef">false</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">实验性功能：从编辑器的上下文菜单中启用/禁用条目。</span>
    <span style="color:#f92672">&#34;go.delveConfig&#34;</span>: {}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">dlv</span> <span style="color:#960050;background-color:#1e0010">配置</span>
    <span style="color:#f92672">&#34;go.alternateTools&#34;</span>: {}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">为每一个</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">扩展依赖的外部工具提供绝对路径，比如想包装一下你的工具，如</span> <span style="color:#f92672">&#34;gopls&#34;</span>: <span style="color:#e6db74">&#34;/path/to/gopls&#34;</span>
    <span style="color:#e6db74">&#34;gopls&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">gopls，默认值为</span> <span style="color:#960050;background-color:#1e0010">{</span>}<span style="color:#960050;background-color:#1e0010">，详见：https</span>:<span style="color:#960050;background-color:#1e0010">//github.com/golang/tools/blob/master/gopls/doc/settings.md</span>
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">构建</span>
        <span style="color:#e6db74">&#34;build.buildFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">构建标志</span>
        <span style="color:#f92672">&#34;build.env&#34;</span>: {}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">环境变量</span>
        <span style="color:#f92672">&#34;build.directoryFilters&#34;</span>: [<span style="color:#e6db74">&#34;-node_modules&#34;</span>], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">目录过滤器，配置方法参见：</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/settings.md#directoryfilters-string</span>
        <span style="color:#f92672">&#34;build.templateExtensions&#34;</span>: [<span style="color:#e6db74">&#34;tmpl&#34;</span>,<span style="color:#e6db74">&#34;gotmpl&#34;</span>], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">template</span> <span style="color:#960050;background-color:#1e0010">扩展名</span>
        <span style="color:#f92672">&#34;build.memoryMode&#34;</span>: <span style="color:#e6db74">&#34;Normal&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">DegradeClosed</span> <span style="color:#960050;background-color:#1e0010">，在</span> <span style="color:#960050;background-color:#1e0010">DegradeClosed</span> <span style="color:#960050;background-color:#1e0010">模式下，gopls</span> <span style="color:#960050;background-color:#1e0010">将收集关于没有打开文件的包的较少信息。因此，诸如</span> <span style="color:#960050;background-color:#1e0010">Find</span> <span style="color:#960050;background-color:#1e0010">References</span> <span style="color:#960050;background-color:#1e0010">和</span> <span style="color:#960050;background-color:#1e0010">Rename</span> <span style="color:#960050;background-color:#1e0010">之类的功能将错过此类包中的结果。</span>
        <span style="color:#f92672">&#34;build.expandWorkspaceToModule&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span>  <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">expandWorkspaceToModule</span> <span style="color:#960050;background-color:#1e0010">指示</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">调整工作区的范围以找到最佳的可用模块根。</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">首先在工作区文件夹的任何父目录中查找</span> <span style="color:#960050;background-color:#1e0010">go.mod</span> <span style="color:#960050;background-color:#1e0010">文件，如果存在，则将范围扩展到该目录。如果找不到可行的父目录，gopls</span> <span style="color:#960050;background-color:#1e0010">将检查是否只有一个包含</span> <span style="color:#960050;background-color:#1e0010">go.mod</span> <span style="color:#960050;background-color:#1e0010">文件的子目录，如果存在，则将范围缩小到该目录。</span>
        <span style="color:#f92672">&#34;build.experimentalWorkspaceModule&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span>  <span style="color:#960050;background-color:#1e0010">选择用户加入对多模块工作区的实验支持。</span>
        <span style="color:#f92672">&#34;build.experimentalPackageCacheKey&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">控制是否对包类型信息使用更粗略的缓存键以增加缓存命中。此设置从缓存键中删除用户的环境、构建标志和工作目录，这应该是一个安全的更改，因为类型检查传递的所有相关输入都已散列到键中。这是由实验暂时保护的，因为缓存行为是微妙的，难以全面测试。</span>
        <span style="color:#f92672">&#34;build.allowModfileModifications&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">禁用</span> <span style="color:#960050;background-color:#1e0010">-mod=readonly，允许从范围外模块导入。此选项最终将被删除。</span>
        <span style="color:#f92672">&#34;build.allowImplicitNetworkAccess&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">禁用</span> <span style="color:#960050;background-color:#1e0010">GOPROXY=off，允许隐式模块下载而不需要用户操作。此选项最终将被删除。</span>
        <span style="color:#f92672">&#34;build.experimentalUseInvalidMetadata&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">如果</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">命令由于某种原因（例如无效的</span> <span style="color:#960050;background-color:#1e0010">go.mod</span> <span style="color:#960050;background-color:#1e0010">文件）无法加载包，则</span> <span style="color:#960050;background-color:#1e0010">ExperimentUseInvalidMetadata</span> <span style="color:#960050;background-color:#1e0010">使</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">能够回退到过时的包元数据以提供编辑器功能。这最终将成为默认行为，并且此设置将被删除。</span>

        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">格式化</span>
        <span style="color:#f92672">&#34;formatting.local&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">local</span> <span style="color:#960050;background-color:#1e0010">相当于</span> <span style="color:#960050;background-color:#1e0010">goimports</span> <span style="color:#960050;background-color:#1e0010">-local</span> <span style="color:#960050;background-color:#1e0010">标志，它将以该字符串开头的导入放在第三方包之后。它应该是导入应该单独分组的导入路径的前缀。</span>
        <span style="color:#f92672">&#34;formatting.gofumpt&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">gofumpt</span> <span style="color:#960050;background-color:#1e0010">指示我们是否应该运行</span> <span style="color:#960050;background-color:#1e0010">gofumpt</span> <span style="color:#960050;background-color:#1e0010">格式化。gofumpt</span> <span style="color:#960050;background-color:#1e0010">是比</span> <span style="color:#960050;background-color:#1e0010">go</span> <span style="color:#960050;background-color:#1e0010">fmt</span> <span style="color:#960050;background-color:#1e0010">更严格的规范，https://github.com/mvdan/gofumpt</span>

        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">UI</span>
        <span style="color:#f92672">&#34;ui.codelenses&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">参见</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses</span>
            <span style="color:#f92672">&#34;gc_details&#34;</span>: <span style="color:#66d9ef">false</span>,
            <span style="color:#f92672">&#34;generate&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;regenerate_cgo&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;tidy&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;upgrade_dependency&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;vendor&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        <span style="color:#f92672">&#34;ui.semanticTokens&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">是否</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">把语义化令牌发到</span> <span style="color:#960050;background-color:#1e0010">UI</span> <span style="color:#960050;background-color:#1e0010">上，开启后，有更好的高亮效果</span>
        <span style="color:#f92672">&#34;ui.completion.usePlaceholders&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">函数调用的自动完成是否填充参数列表</span>
        <span style="color:#f92672">&#34;ui.completion.completionBudget&#34;</span>: <span style="color:#e6db74">&#34;100ms&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">此设置仅用于调试目的。完成请求的耗时预算。大多数请求在几毫秒内完成，但在某些情况下，深度完成可能需要更长的时间。当我们用完预算时，我们会动态缩小搜索范围，以确保及时返回结果。零意味着无限。</span>
        <span style="color:#f92672">&#34;ui.completion.matcher&#34;</span>: <span style="color:#e6db74">&#34;Fuzzy&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">匹配算法，可以是</span> <span style="color:#960050;background-color:#1e0010">Fuzzy、CaseSensitive、CaseInsensitive</span>
        <span style="color:#f92672">&#34;ui.completion.experimentalPostfixCompletions&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">启用后缀补全，例如</span> <span style="color:#f92672">&#34;someSlice.sort!&#34;</span>
        <span style="color:#e6db74">&#34;ui.diagnostic.analyses&#34;</span>: {
            <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">再次记录没有默认开启的分析器</span>
            <span style="color:#f92672">&#34;fieldalignment&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">结构体排序检查，那种排列内存占用更小，但是没有提供自动修复的功能，不建议开启</span>
            <span style="color:#f92672">&#34;nilness&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">对于</span> <span style="color:#960050;background-color:#1e0010">nil</span> <span style="color:#960050;background-color:#1e0010">的一些检查</span>
            <span style="color:#f92672">&#34;shadow&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">检查变量覆盖</span>
            <span style="color:#f92672">&#34;unusedparams&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">未使用的参数</span>
            <span style="color:#f92672">&#34;unusedwrite&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">未使用的写入</span>
        }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">分析器配置，参见</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md</span>
        <span style="color:#f92672">&#34;ui.diagnostic.staticcheck&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">启用</span> <span style="color:#960050;background-color:#1e0010">staticcheck</span> <span style="color:#960050;background-color:#1e0010">分析器，让</span> <span style="color:#960050;background-color:#1e0010">lint</span> <span style="color:#960050;background-color:#1e0010">在</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">中执行</span>
        <span style="color:#f92672">&#34;ui.diagnostic.annotations&#34;</span>: {<span style="color:#f92672">&#34;bounds&#34;</span>:<span style="color:#66d9ef">true</span>,<span style="color:#f92672">&#34;escape&#34;</span>:<span style="color:#66d9ef">true</span>,<span style="color:#f92672">&#34;inline&#34;</span>:<span style="color:#66d9ef">true</span>,<span style="color:#f92672">&#34;nil&#34;</span>:<span style="color:#66d9ef">true</span>}, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">注释指定应由</span> <span style="color:#960050;background-color:#1e0010">gc_details</span> <span style="color:#960050;background-color:#1e0010">命令报告的各种优化诊断。</span>
        <span style="color:#f92672">&#34;ui.diagnostic.diagnosticsDelay&#34;</span>: <span style="color:#e6db74">&#34;250ms&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">参见</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/settings.md#diagnosticsdelay-timeduration</span>
        <span style="color:#f92672">&#34;ui.diagnostic.experimentalWatchedFileDelay&#34;</span>: <span style="color:#e6db74">&#34;0s&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">参见</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/settings.md#experimentalwatchedfiledelay-timeduration</span>
        <span style="color:#f92672">&#34;ui.documentation.hoverKind&#34;</span>: <span style="color:#e6db74">&#34;FullDocumentation&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">hover</span> <span style="color:#960050;background-color:#1e0010">种类</span>
        <span style="color:#f92672">&#34;ui.documentation.linkTarget&#34;</span>: <span style="color:#e6db74">&#34;pkg.go.dev&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">link</span> <span style="color:#960050;background-color:#1e0010">目标</span>
        <span style="color:#f92672">&#34;ui.documentation.linksInHover&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在</span> <span style="color:#960050;background-color:#1e0010">hover</span> <span style="color:#960050;background-color:#1e0010">中显示链接</span>
        <span style="color:#f92672">&#34;ui.navigation.importShortcut&#34;</span>: <span style="color:#e6db74">&#34;Both&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">导入块执行调转到定义的方式，可以是</span> <span style="color:#960050;background-color:#1e0010">Both、Definition、Link</span>
        <span style="color:#f92672">&#34;ui.navigation.symbolMatcher&#34;</span>: <span style="color:#e6db74">&#34;Fuzzy&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">设置查找工作区符号时使用的算法，可以是</span> <span style="color:#960050;background-color:#1e0010">Fuzzy、FastFuzzy、CaseSensitive、CaseInsensitive</span>
        <span style="color:#f92672">&#34;ui.navigation.symbolStyle&#34;</span>: <span style="color:#e6db74">&#34;Dynamic&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">控制符号唯一表示非风格，可以是</span> <span style="color:#960050;background-color:#1e0010">Dynamic、Full、Package</span>
        <span style="color:#f92672">&#34;verboseOutput&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">输出</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">的详细信息</span>
    }<span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#960050;background-color:#1e0010">}</span>
<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">非</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">模式才生效的配置</span>
{
    <span style="color:#f92672">&#34;go.formatTool&#34;</span>: <span style="color:#e6db74">&#34;default&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span>  <span style="color:#960050;background-color:#1e0010">格式化工具</span>
    <span style="color:#f92672">&#34;go.formatFlags&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Flags</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">pass</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">format</span> <span style="color:#960050;background-color:#1e0010">tool</span> <span style="color:#960050;background-color:#1e0010">(e.g.</span> <span style="color:#960050;background-color:#1e0010">[</span><span style="color:#f92672">&#34;-s&#34;</span><span style="color:#960050;background-color:#1e0010">])</span>
    <span style="color:#e6db74">&#34;go.inferGopath&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">从工作空间推断</span> <span style="color:#960050;background-color:#1e0010">gopath，优先级高于</span> <span style="color:#960050;background-color:#1e0010">go.gopath（如果该配置在</span> <span style="color:#960050;background-color:#1e0010">.vscode/settings.json</span> <span style="color:#960050;background-color:#1e0010">中配置，仅当</span> <span style="color:#f92672">&#34;Go: Toggle Workspace Trust Flag&#34;</span> <span style="color:#960050;background-color:#1e0010">为信任时才生效）</span>
    <span style="color:#e6db74">&#34;go.gocodeFlags&#34;</span>: <span style="color:#66d9ef">null</span>, <span style="color:#960050;background-color:#1e0010">//</span>	<span style="color:#960050;background-color:#1e0010">gocode</span> <span style="color:#960050;background-color:#1e0010">命令行参数</span> <span style="color:#960050;background-color:#1e0010">如-builtin,-ignore-case,-unimported-packages</span>
    <span style="color:#f92672">&#34;go.gocodeAutoBuild&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span>	<span style="color:#960050;background-color:#1e0010">Enable</span> <span style="color:#960050;background-color:#1e0010">gocode&#39;s</span> <span style="color:#960050;background-color:#1e0010">autobuild</span> <span style="color:#960050;background-color:#1e0010">feature</span>
    <span style="color:#f92672">&#34;go.gocodePackageLookupMode&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Used</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">determine</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">package</span> <span style="color:#960050;background-color:#1e0010">lookup</span> <span style="color:#960050;background-color:#1e0010">rules</span> <span style="color:#960050;background-color:#1e0010">for</span> <span style="color:#960050;background-color:#1e0010">completions</span> <span style="color:#960050;background-color:#1e0010">by</span> <span style="color:#960050;background-color:#1e0010">gocode.</span> <span style="color:#960050;background-color:#1e0010">Latest</span> <span style="color:#960050;background-color:#1e0010">versions</span> <span style="color:#960050;background-color:#1e0010">of</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">extension</span> <span style="color:#960050;background-color:#1e0010">uses</span> <span style="color:#960050;background-color:#1e0010">mdempsky/gocode</span> <span style="color:#960050;background-color:#1e0010">by</span> <span style="color:#960050;background-color:#1e0010">default.</span>
    <span style="color:#f92672">&#34;go.useCodeSnippetsOnFunctionSuggest&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Complete</span> <span style="color:#960050;background-color:#1e0010">functions</span> <span style="color:#960050;background-color:#1e0010">with</span> <span style="color:#960050;background-color:#1e0010">their</span> <span style="color:#960050;background-color:#1e0010">parameter</span> <span style="color:#960050;background-color:#1e0010">signature,</span> <span style="color:#960050;background-color:#1e0010">including</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">variable</span> <span style="color:#960050;background-color:#1e0010">type.</span>
    <span style="color:#f92672">&#34;go.useCodeSnippetsOnFunctionSuggestWithoutType&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">完成函数的建议列表时，填充函数参数（不包括类型）</span> <span style="color:#960050;background-color:#1e0010">如果是</span> <span style="color:#960050;background-color:#1e0010">gopls，使用</span> <span style="color:#960050;background-color:#1e0010">`gopls.usePlaceholders`</span> <span style="color:#960050;background-color:#1e0010">可进行配置。</span>
    <span style="color:#f92672">&#34;go.autocompleteUnimportedPackages&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">在自动完成建议中包含未导入的包</span> 
    <span style="color:#f92672">&#34;go.docsTool&#34;</span>: <span style="color:#e6db74">&#34;godoc&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span>	<span style="color:#960050;background-color:#1e0010">选择</span> <span style="color:#960050;background-color:#1e0010">&#39;godoc&#39;</span> <span style="color:#960050;background-color:#1e0010">or</span> <span style="color:#960050;background-color:#1e0010">&#39;gogetdoc&#39;</span> <span style="color:#960050;background-color:#1e0010">获取文档</span>
    <span style="color:#f92672">&#34;go.gotoSymbol.includeImports&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">If</span> <span style="color:#960050;background-color:#1e0010">false,</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">import</span> <span style="color:#960050;background-color:#1e0010">statements</span> <span style="color:#960050;background-color:#1e0010">will</span> <span style="color:#960050;background-color:#1e0010">be</span> <span style="color:#960050;background-color:#1e0010">excluded</span> <span style="color:#960050;background-color:#1e0010">while</span> <span style="color:#960050;background-color:#1e0010">using</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">Symbol</span> <span style="color:#960050;background-color:#1e0010">in</span> <span style="color:#960050;background-color:#1e0010">File</span> <span style="color:#960050;background-color:#1e0010">feature.</span>
    <span style="color:#f92672">&#34;go.gotoSymbol.includeGoroot&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">If</span> <span style="color:#960050;background-color:#1e0010">false,</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">standard</span> <span style="color:#960050;background-color:#1e0010">library</span> <span style="color:#960050;background-color:#1e0010">located</span> <span style="color:#960050;background-color:#1e0010">at</span> <span style="color:#960050;background-color:#1e0010">$GOROOT</span> <span style="color:#960050;background-color:#1e0010">will</span> <span style="color:#960050;background-color:#1e0010">be</span> <span style="color:#960050;background-color:#1e0010">excluded</span> <span style="color:#960050;background-color:#1e0010">while</span> <span style="color:#960050;background-color:#1e0010">using</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">Symbol</span> <span style="color:#960050;background-color:#1e0010">in</span> <span style="color:#960050;background-color:#1e0010">File</span> <span style="color:#960050;background-color:#1e0010">feature.</span>
    <span style="color:#f92672">&#34;go.liveErrors&#34;</span>: {
        <span style="color:#f92672">&#34;enabled&#34;</span>: <span style="color:#66d9ef">false</span>,
        <span style="color:#f92672">&#34;delay&#34;</span>: <span style="color:#ae81ff">500</span>
    }, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Use</span> <span style="color:#960050;background-color:#1e0010">gotype</span> <span style="color:#960050;background-color:#1e0010">on</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">file</span> <span style="color:#960050;background-color:#1e0010">currently</span> <span style="color:#960050;background-color:#1e0010">being</span> <span style="color:#960050;background-color:#1e0010">edited</span> <span style="color:#960050;background-color:#1e0010">and</span> <span style="color:#960050;background-color:#1e0010">report</span> <span style="color:#960050;background-color:#1e0010">any</span> <span style="color:#960050;background-color:#1e0010">semantic</span> <span style="color:#960050;background-color:#1e0010">or</span> <span style="color:#960050;background-color:#1e0010">syntactic</span> <span style="color:#960050;background-color:#1e0010">errors</span> <span style="color:#960050;background-color:#1e0010">found</span> <span style="color:#960050;background-color:#1e0010">after</span> <span style="color:#960050;background-color:#1e0010">configured</span> <span style="color:#960050;background-color:#1e0010">delay.</span>
    <span style="color:#f92672">&#34;go.gotoSymbol.ignoreFolders&#34;</span>: [], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Folder</span> <span style="color:#960050;background-color:#1e0010">names</span> <span style="color:#960050;background-color:#1e0010">(not</span> <span style="color:#960050;background-color:#1e0010">paths)</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">ignore</span> <span style="color:#960050;background-color:#1e0010">while</span> <span style="color:#960050;background-color:#1e0010">using</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">Symbol</span> <span style="color:#960050;background-color:#1e0010">in</span> <span style="color:#960050;background-color:#1e0010">Workspace</span> <span style="color:#960050;background-color:#1e0010">feature.</span>
}</code></pre></div>
<h2 id="场景">场景</h2>

<h3 id="go-扩展最佳配置">Go 扩展最佳配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;go.disableConcurrentTests&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">禁用并发测试</span>
    <span style="color:#f92672">&#34;go.testFlags&#34;</span>: [
        <span style="color:#e6db74">&#34;-v&#34;</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试打印标准输出</span>
    ],
    <span style="color:#f92672">&#34;go.vetFlags&#34;</span>: [
        <span style="color:#e6db74">&#34;-all&#34;</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">vet</span> <span style="color:#960050;background-color:#1e0010">执行所有规则</span>
    ],
    <span style="color:#f92672">&#34;go.testTimeout&#34;</span>: <span style="color:#e6db74">&#34;30s&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">测试超时时间</span>
    <span style="color:#f92672">&#34;go.testExplorer.showDynamicSubtestsInEditor&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">动态发现的子测试是否显示到测试浏览器中</span>
    <span style="color:#f92672">&#34;go.toolsManagement.autoUpdate&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">自动更新扩展依赖的命令行工具，不提示用户</span> 
    <span style="color:#f92672">&#34;go.enableCodeLens&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">显示的</span> <span style="color:#960050;background-color:#1e0010">lens</span>
        <span style="color:#f92672">&#34;references&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;runtest&#34;</span>: <span style="color:#66d9ef">true</span>
    },
    <span style="color:#f92672">&#34;go.addTags&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">add</span> <span style="color:#960050;background-color:#1e0010">Tags</span> <span style="color:#960050;background-color:#1e0010">命令</span>
        <span style="color:#f92672">&#34;tags&#34;</span>: <span style="color:#e6db74">&#34;json,yaml&#34;</span>,
        <span style="color:#f92672">&#34;options&#34;</span>: <span style="color:#e6db74">&#34;json=omitempty&#34;</span>,
        <span style="color:#f92672">&#34;promptForTags&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">询问添加的</span> <span style="color:#960050;background-color:#1e0010">tag</span>
        <span style="color:#f92672">&#34;transform&#34;</span>: <span style="color:#e6db74">&#34;snakecase&#34;</span>,
        <span style="color:#f92672">&#34;template&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
    },
    <span style="color:#f92672">&#34;go.removeTags&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">配置</span> <span style="color:#960050;background-color:#1e0010">remove</span> <span style="color:#960050;background-color:#1e0010">Tags</span> <span style="color:#960050;background-color:#1e0010">命令</span>
        <span style="color:#f92672">&#34;tags&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
        <span style="color:#f92672">&#34;options&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
        <span style="color:#f92672">&#34;promptForTags&#34;</span>: <span style="color:#66d9ef">true</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">询问删除的</span> <span style="color:#960050;background-color:#1e0010">tag</span>
    },
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#f92672">&#34;go.delveConfig&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">dlv</span> <span style="color:#960050;background-color:#1e0010">配置，如果特殊场景需要看比较长的字符串时，可以打开该注释，启用该配置</span>
    <span style="color:#960050;background-color:#1e0010">//</span>     <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;legacy&#34;</span>,
    <span style="color:#960050;background-color:#1e0010">//</span>     <span style="color:#f92672">&#34;dlvLoadConfig&#34;</span>: {
    <span style="color:#960050;background-color:#1e0010">//</span>         <span style="color:#f92672">&#34;followPointers&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#960050;background-color:#1e0010">//</span>         <span style="color:#f92672">&#34;maxVariableRecurse&#34;</span>: <span style="color:#ae81ff">1</span>,
    <span style="color:#960050;background-color:#1e0010">//</span>         <span style="color:#f92672">&#34;maxStringLen&#34;</span>: <span style="color:#ae81ff">1000000</span>,
    <span style="color:#960050;background-color:#1e0010">//</span>         <span style="color:#f92672">&#34;maxArrayValues&#34;</span>: <span style="color:#ae81ff">64</span>,
    <span style="color:#960050;background-color:#1e0010">//</span>         <span style="color:#f92672">&#34;maxStructFields&#34;</span>: <span style="color:#ae81ff">-1</span>
    <span style="color:#960050;background-color:#1e0010">//</span>     }
    <span style="color:#960050;background-color:#1e0010">//</span> },
    <span style="color:#f92672">&#34;gopls&#34;</span>: {
        <span style="color:#f92672">&#34;build.experimentalWorkspaceModule&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">工作空间多模块支持（MonoRepo）</span>
        <span style="color:#f92672">&#34;build.directoryFilters&#34;</span>: [<span style="color:#e6db74">&#34;-node_modules&#34;</span>, <span style="color:#e6db74">&#34;-output&#34;</span>], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">构建排除的目录，合理配置可以节省资源</span>
        <span style="color:#f92672">&#34;formatting.gofumpt&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">使用</span> <span style="color:#960050;background-color:#1e0010">gofumpt</span> <span style="color:#960050;background-color:#1e0010">格式化代码</span>
        <span style="color:#f92672">&#34;ui.semanticTokens&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">是否</span> <span style="color:#960050;background-color:#1e0010">gopls</span> <span style="color:#960050;background-color:#1e0010">把语义化令牌发到</span> <span style="color:#960050;background-color:#1e0010">UI</span> <span style="color:#960050;background-color:#1e0010">上，开启后，有更好的高亮效果</span>
        <span style="color:#f92672">&#34;ui.completion.usePlaceholders&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">函数调用的自动完成是否填充参数列表</span>
        <span style="color:#f92672">&#34;ui.diagnostic.analyses&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">开启默认没有开启的且误报较少的分析器，参见</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md</span>
            <span style="color:#f92672">&#34;nilness&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;shadow&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;unusedparams&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;unusedwrite&#34;</span>: <span style="color:#66d9ef">true</span>,
        },
    },
}</code></pre></div>
<h3 id="大型项目资源占用优化">大型项目资源占用优化</h3>

<p>参考如下，酌情进行配置，这些配置可能影响稳定性以及功能</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;gopls&#34;</span>: {
        <span style="color:#f92672">&#34;build.directoryFilters&#34;</span>: [<span style="color:#e6db74">&#34;-node_modules&#34;</span>, <span style="color:#e6db74">&#34;-output&#34;</span>], <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">将工作空间不是</span> <span style="color:#960050;background-color:#1e0010">Go</span> <span style="color:#960050;background-color:#1e0010">代码的目录排除出去，配置方法参见：</span> <span style="color:#960050;background-color:#1e0010">https://github.com/golang/tools/blob/master/gopls/doc/settings.md#directoryfilters-string</span>
        <span style="color:#f92672">&#34;build.experimentalPackageCacheKey&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">控制是否对包类型信息使用更粗略的缓存键以增加缓存命中。此设置从缓存键中删除用户的环境、构建标志和工作目录，这应该是一个安全的更改，因为类型检查传递的所有相关输入都已散列到键中。这是由实验暂时保护的，因为缓存行为是微妙的，难以全面测试。</span>

        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">下方配置功能有损</span>
        <span style="color:#f92672">&#34;build.memoryMode&#34;</span>: <span style="color:#e6db74">&#34;DegradeClosed&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">&lt;实验性&gt;</span> <span style="color:#960050;background-color:#1e0010">DegradeClosed</span> <span style="color:#960050;background-color:#1e0010">，在</span> <span style="color:#960050;background-color:#1e0010">DegradeClosed</span> <span style="color:#960050;background-color:#1e0010">模式下，gopls</span> <span style="color:#960050;background-color:#1e0010">将收集关于没有打开文件的包的较少信息。因此，诸如</span> <span style="color:#960050;background-color:#1e0010">Find</span> <span style="color:#960050;background-color:#1e0010">References</span> <span style="color:#960050;background-color:#1e0010">和</span> <span style="color:#960050;background-color:#1e0010">Rename</span> <span style="color:#960050;background-color:#1e0010">之类的功能将错过此类包中的结果。</span>
    }
}</code></pre></div>
<h3 id="关于调试的一些场景和问题">关于调试的一些场景和问题</h3>

<h4 id="vscode-打开符号链接上的项目如何调试">VSCode 打开符号链接上的项目如何调试</h4>

<p>如果一个项目，真实路径为 <code>/path/to/actual/helloWorld</code>，同时可以通过一个符号链接访问 <code>/link/to/helloWorld</code>，此时调试配置如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch with symlinks&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;debug&#34;</span>,
    <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;/path/to/actual/helloWorld&#34;</span>,
    <span style="color:#f92672">&#34;substitutePath&#34;</span>: [
		{
			<span style="color:#f92672">&#34;from&#34;</span>: <span style="color:#e6db74">&#34;/link/to/helloWorld&#34;</span>,
			<span style="color:#f92672">&#34;to&#34;</span>: <span style="color:#e6db74">&#34;/path/to/actual/helloWorld&#34;</span>,
		},
	],
}</code></pre></div>
<h4 id="调试时字符串被截断">调试时字符串被截断</h4>

<p>目前，默认启用了 <code>dlv-dap</code> 模式，因此无法配置 dlv 的字符串长度。这种情况下</p>

<ul>
<li>通过变量视图，直接查看，最多只能看到 512 个字符</li>
<li>通过如下方式可以看到 4096 个字符

<ul>
<li>右击 Copy Value</li>
<li>右击 Copy as Expression，并在 DEBUG CONSOLE 输入</li>
<li>鼠标 Hover 在变量上</li>
</ul></li>
</ul>

<p>如果是更长的字符串，在 <code>dlv-dap</code> 模式下，只能通过 fmt.Println 打印出来查看。</p>

<p>另外不推荐的方法是，切换回 <code>legacy</code> 模式。配置 <code>maxStringLen</code> 属性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;go.delveConfig&#34;</span>: {
        <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;legacy&#34;</span>,
        <span style="color:#f92672">&#34;dlvLoadConfig&#34;</span>: {
            <span style="color:#f92672">&#34;followPointers&#34;</span>: <span style="color:#66d9ef">true</span>,
            <span style="color:#f92672">&#34;maxVariableRecurse&#34;</span>: <span style="color:#ae81ff">1</span>,
            <span style="color:#f92672">&#34;maxStringLen&#34;</span>: <span style="color:#ae81ff">1000000</span>,
            <span style="color:#f92672">&#34;maxArrayValues&#34;</span>: <span style="color:#ae81ff">64</span>,
            <span style="color:#f92672">&#34;maxStructFields&#34;</span>: <span style="color:#ae81ff">-1</span>
        }
    }
}</code></pre></div>
<h4 id="debug-过程中添加断点后-会暂停一次">debug 过程中添加断点后，会暂停一次</h4>

<p>已知问题，官方正在处理，可以关注相关 <a href="https://github.com/golang/vscode-go/issues/1676">issue</a></p>

<h4 id="调试编译十分复杂的项目">调试编译十分复杂的项目</h4>

<p>在编译配置文件添加 debug 模式的构建，添加 <code>-gcflags='all=-N -l'</code> 选项。</p>

<p>配置任务和调试</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">.vscode/launch.json</span>
{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch external builded binary&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
            <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;exec&#34;</span>,
            <span style="color:#f92672">&#34;preLaunchTask&#34;</span>: <span style="color:#e6db74">&#34;make debug&#34;</span>,
            <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceRoot}/path/to/debug-binary&#34;</span>,
        }
    ]
}
<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">.vscode/tasks.json</span>
{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.0.0&#34;</span>,
    <span style="color:#f92672">&#34;tasks&#34;</span>: [
        {
            <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;make debug&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
            <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;make debug&#34;</span>
        }
    ]
}</code></pre></div>
<h4 id="如何调试-go-1-14-及之前版本的项目">如何调试 go 1.14 及之前版本的项目</h4>

<p>最新版的 dlv 已经放弃了对 go 1.14 以及之前版本的支持。因此需要安装旧版 dlv，步骤如下所示</p>

<ul>
<li>执行 <code>&gt;Go: Locate Configured Go Tools</code> 命令，确认 dlv 安装位置</li>
<li>执行 <code>GOBIN=上面输出的dlv所在目录 GO111MODULE=on go get github.com/go-delve/delve/cmd/dlv@v1.4.1</code>，安装旧版本 dlv</li>

<li><p>确保关闭自动更新并启用 legacy 模式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;go.toolsManagement.autoUpdate&#34;</span>: <span style="color:#66d9ef">false</span>,
<span style="color:#f92672">&#34;go.delveConfig&#34;</span>: {
    <span style="color:#f92672">&#34;debugAdapter&#34;</span>: <span style="color:#e6db74">&#34;legacy&#34;</span>,
}
}</code></pre></div></li>
</ul>

<h4 id="如何调试-core-dump-文件">如何调试 core dump 文件</h4>

<p>core dump 是 *nix 类操作系统提供的进程 crash 时刻的进程状态快照。在该文件中，包含进程 crash 时刻的所有堆栈和寄存器信息。利用调试器，可以查看 crash 时刻的各个线程的栈帧，变量。（以下仅在 Linux 测试通过）</p>

<p>假设，编写一个 go 程序，该程序会 sleep 1 小时。在 sleep 的时候通过 <code>ctrl + \</code> 发送一个 <code>SIGQUIT</code> 信号，制造一个 go 的 coredump 文件。在实验之前确保确保操作系统不限制 core dump 大小：执行 <code>ulimit -a</code>，观察 <code>-c</code> 一行是否为 <code>unlimited</code>。如果不是执行 <code>ulimit -c unlimited</code> （恢复方式为 <code>ulimit -c 原始值</code>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">a</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">a</span>)

	<span style="color:#75715e">// ctrl + \
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Hour</span>)

	<span style="color:#a6e22e">b</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">2</span>

	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">b</span>)
}</code></pre></div>
<p>编译成带有符号信息的可执行文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go build -gcflags<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;all=-N -l&#39;</span> -o main ./</code></pre></div>
<p>执行（注意环境变量），键盘输入 <code>ctrl + \</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">GOTRACEBACK<span style="color:#f92672">=</span>crash ./main</code></pre></div>
<p>执行 <code>cat /proc/sys/kernel/core_pattern</code> 获取 core dump 路径。</p>

<p>配置 VSCode 调试器 <code>.vscode/launch.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch core dump&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
            <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;core&#34;</span>,
            <span style="color:#f92672">&#34;coreFilePath&#34;</span>: <span style="color:#e6db74">&#34;上一步获取到 core dump 文件路径&#34;</span>,
            <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}/main&#34;</span>
        }
    ]
}</code></pre></div>
<p>按 F5 即可启动调试，此时观察下，调试视图的调用栈视图，即可观察各个栈帧的变量情况。</p>

<h4 id="一键同时调试多个进程">一键同时调试多个进程</h4>

<p>假设某个项目是同时包含客户端和服务端。调试时，希望一键启动客户端和服务端。配置如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
  <span style="color:#f92672">&#34;configurations&#34;</span>: [
    {
      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Server&#34;</span>,
      <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
    },
    {
      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Client&#34;</span>,
      <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
    }
  ],
  <span style="color:#f92672">&#34;compounds&#34;</span>: [
    {
      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Server/Client&#34;</span>,
      <span style="color:#f92672">&#34;configurations&#34;</span>: [<span style="color:#e6db74">&#34;Server&#34;</span>, <span style="color:#e6db74">&#34;Client&#34;</span>],
      <span style="color:#f92672">&#34;preLaunchTask&#34;</span>: <span style="color:#e6db74">&#34;${defaultBuildTask}&#34;</span>
    }
  ]
}</code></pre></div>
<p>更多参见：<a href="https://code.visualstudio.com/docs/editor/debugging#_compound-launch-configurations">https://code.visualstudio.com/docs/editor/debugging#_compound-launch-configurations</a></p>

<h4 id="vscode-go-插件版本-v0-31-0-远程调试问题">VSCode Go 插件版本 v0.31.0 远程调试问题</h4>

<p>v0.31.0 后，默认 Remote 调试协议已经替换为 <code>dap</code>，因此需要使用，如果远端仍然使用旧的模式，需要在 <code>launch.json</code> 添加 <code>&quot;debugAdapter&quot;:&quot;legacy&quot;</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch: server&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;attach&#34;</span>,
            <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
            <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">2345</span>,
            <span style="color:#f92672">&#34;debugAdapter&#34;</span>:<span style="color:#e6db74">&#34;legacy&#34;</span>,
        }
    ]
}</code></pre></div>
<h3 id="如何高效进行项目源码阅读">如何高效进行项目源码阅读</h3>

<ul>
<li>参见本文的 <a href="#代码导航">代码导航</a> 章节</li>
<li>阅读依赖包的源码，通过 <code>&gt;Go: Add Package to Workspace</code>，将依赖包加入到工作空间</li>
</ul>

<h3 id="系统里装了多个版本的-go-sdk-如何管理">系统里装了多个版本的 Go SDK，如何管理</h3>

<p>可以尝试 <code>&gt;Go: Choose Go Environment</code> 命令，官方解决办法参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/advanced.md#choosing-a-different-version-of-go">官方文档</a></p>

<h3 id="多-module-项目-go-workspace">多 module 项目 （Go workspace）</h3>

<blockquote>
<p>参见：<a href="https://github.com/golang/tools/blob/master/gopls/doc/workspace.md#multiple-modules">官方文档</a></p>
</blockquote>

<p>形如：<a href="https://irilivibi.medium.com/golang-multimodule-monorepo-tutorial-3f5cf10e9b9a">Golang Multimodule Monorepos</a> 的项目。</p>

<h4 id="go-1-17-及之前版本">Go 1.17 及之前版本</h4>

<p>开启如下配置即可开启该特性（目前处于实验阶段）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;gopls&#34;</span>: {
        <span style="color:#f92672">&#34;experimentalWorkspaceModule&#34;</span>: <span style="color:#66d9ef">true</span>
    }
}</code></pre></div>
<p>注意，目前处于实验阶段，已知 issue 为：</p>

<ul>
<li>该 feature 与 go mod vender 存在重复，开启后 go mod vender 的项目会报 <code>Inconsistent vendoring detected. Please re-run &quot;go mod vendor&quot;. See https://github.com/golang/go/issues/39164 for more detail on this issue.</code> 错误。</li>
</ul>

<p>其他信息</p>

<ul>
<li><a href="https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md">设计文档</a></li>
<li><a href="https://github.com/golang/tools/blob/master/gopls/doc/settings.md#experimentalworkspacemodule-bool">gopls 配置文档</a></li>
<li><a href="https://github.com/golang/go/milestone/179">gopls/workspace-module milestone</a></li>
</ul>

<h4 id="go-1-18-及更新版本">Go 1.18 及更新版本</h4>

<p>无需任何配置，在根目录添加 <code>go.work</code> 文件，更多参见：<a href="/posts/go-1-18-features/#工作空间">Go 1.18 新特性</a></p>

<h3 id="如何使用-vscode-go-扩展开发-go-标准库">如何使用 VSCode Go 扩展开发 Go 标准库</h3>

<p>解决方法参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/advanced.md#working-on-the-go-standard-library-and-the-go-tools">官方文档</a></p>

<h3 id="存在多个格式化器时如何配置使用-go-扩展">存在多个格式化器时如何配置使用 Go 扩展</h3>

<p>解决方法参见：<a href="https://github.com/golang/vscode-go/blob/master/docs/advanced.md#formatting-code-and-organizing-imports">官方文档</a></p>

<h3 id="非-go-mod-项目">非 go mod 项目</h3>

<p>GOPATH 迁移到 Go modudle 非常容易，建议花少量时间迁移到 Go modudle 模式。如果仍要使用 GOPATH 开发模式，可以参考：<a href="https://github.com/golang/vscode-go/blob/master/docs/gopath.md">官方文档</a></p>

<h3 id="跨平台开发-如在-mac-开发-linux">跨平台开发，如在 Mac 开发 Linux</h3>

<p>添加如下 VSCode 配置，参考： <a href="https://github.com/golang/go/issues/29202#issuecomment-488469829">github issue</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;gopls&#34;</span>: {
        <span style="color:#f92672">&#34;build.env&#34;</span>: {
            <span style="color:#f92672">&#34;GOOS&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>,
            <span style="color:#f92672">&#34;GOARCH&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>
        },
    },
}</code></pre></div>]]></description></item><item><title>Python 语言</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/python-language/</link><pubDate>Wed, 15 Dec 2021 21:50:51 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/python-language/</guid><description type="html"><![CDATA[

<h2 id="导读">导读</h2>

<blockquote>
<p>VSCode Python 扩展版本 v2021.12.1559732655</p>
</blockquote>

<p>阅读本章节，可以了解到如何使用 VSCode 开发 Python 语言项目，其功能主要由如下几款 VSCode Python 扩展：</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a> - <a href="https://marketplace.visualstudio.com/items/ms-python.python/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a> - <a href="https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license">MIT 许可证</a> （仅交互式 Python 涉及部分）</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance</a> - 闭源 <a href="https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license">Microsoft proprietary license</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">Visual Studio IntelliCode</a> - 闭源 <a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">许可证</a></li>
</ul>

<h2 id="特性速览">特性速览</h2>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/languages/python">VSCode Docs - Language Python</a></p>
</blockquote>

<ul>
<li>环境管理</li>
<li>代码编辑（自动完成、快速修复、格式化、重构、代码生成、代码片段）</li>
<li>代码浏览（调转定义、查找引用、查找实现、调用层次、类型层次）</li>
<li>问题诊断（类型检查、Lint）</li>
<li>代码运行和调试</li>
<li>测试</li>
<li>数据分析（本文暂不涉及，后续有专门文章讲解描述）</li>
<li>交互式 Python</li>
<li>场景

<ul>
<li>Django 支持</li>
<li>Flask 支持</li>
<li>VSCode Web 支持</li>
</ul></li>
</ul>

<h2 id="快速开始">快速开始</h2>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/python/python-tutorial">VSCode Docs - Python Tutorial</a></p>
</blockquote>

<ul>
<li>安装 Python，参见 <a href="https://www.python.org/downloads/">Python 官方文档</a></li>
<li><a href="https://code.visualstudio.com/download">安装 VSCode</a></li>
<li>在 VSCode 中，安装如下几个扩展

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a> - <a href="https://marketplace.visualstudio.com/items/ms-python.python/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a> - <a href="https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance</a> - 闭源 <a href="https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license">Microsoft proprietary license</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">Visual Studio IntelliCode</a> - 闭源 <a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">许可证</a></li>
</ul></li>
<li>使用 VSCode 打开 Python 文件或者 Python 项目</li>
<li>Enjoy it!</li>
</ul>

<h2 id="使用指南">使用指南</h2>

<h3 id="环境管理">环境管理</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/python/environments">VSCode Docs - Python Environments</a></p>
</blockquote>

<p>一台设备可以装多个 Python 解析器，这些 Python 解释器会关联一些了 Python 包。因此某个和 Python 解释器和其关联的 Python 包的集合称为一个 Python 环境。更多关于 Python 环境，可以参见另一篇文章：<a href="/posts/understand-the-python-environment/">一文彻底理解 Python 环境</a></p>

<p>VSCode Python 支持管理各种的 Python 环境，可以通过状态栏观察当前工作空间绑定的 Python 环境。</p>

<p><img src="/image/vscode/python/python-status-bar.png" alt="image" /></p>

<p>点击上图状态栏，可以快速浏览当前系统所有可用的 Python 环境，如下图所示：</p>

<p><img src="/image/vscode/python/interpreters-list.png" alt="image" /></p>

<p>选择一个，即可快速和工作空间关联。通过 <code>&quot;python.defaultInterpreterPath&quot;</code> 可以给所有工作空间配置一个默认的 Python 环境。（该配置支持引用环境变量，语法为 <code>${env:环境变量名}</code>，例如 <code>&quot;python.defaultInterpreterPath&quot;: &quot;${env:PYTHON_INSTALL_LOC}&quot;</code>）</p>

<p>Python 环境列表除了通过状态唤起外，还支持通过 <code>&gt;python: select interpreter</code> 命令唤起，该列表默认按照如下逻辑去查找系统内的 Python 环境：</p>

<ul>
<li>操作系统 PATH 环境变量指向的路径（<code>echo $PATH</code> 查看，查找路径）</li>
<li>当前工作空间目录的虚拟环境目录（一般为 .venv）</li>
<li><code>python.venvPath</code> 配置指向的路径，该路径可以包含多个虚拟环境目录</li>
<li><a href="https://virtualenvwrapper.readthedocs.io/">virtualenvwrapper</a> 支持的 <code>~/.virtualenvs</code> 文件夹中的虚拟环境</li>
<li>由 <a href="https://github.com/pyenv/pyenv">pyenv</a>、<a href="https://pypi.org/project/pipenv/">Pipenv</a> 和 <a href="https://poetry.eustace.io/">Poetry</a> 安装的解释器</li>
<li>位于 <code>WORKON_HOME</code> 环境变量指向目录中的虚拟环境</li>
<li>包含 Python 解释器的 Conda 环境（不显示不包含解释器的 conda 环境）</li>
<li><a href="https://direnv.net/">direnv</a> 安装的 <code>.direnv</code> 目录</li>
</ul>

<p>此外</p>

<ul>
<li>还可以手动指定 Python 解释器，此外如果在工作空间目录中发现 <a href="https://pipenv.readthedocs.io/">pipenv</a> 环境，则不会显示其他的 Python 环境，因此 pipenv 会管理这一切。</li>
<li>Python 扩展还会加载由 <code>python.envFile</code> 配置指向的环境变量文件。默认值为 <code>${workspaceFolder}/.env</code></li>
</ul>

<p>VSCode 选择一个 Python 环境后，其作用范围如下所示：</p>

<ul>
<li>打开终端，会自动选中的 Python 环境，可用通过 <code>&quot;python.terminal.activateEnvironment&quot;: false</code> 配置关闭该特性。</li>
<li>智能感知、Lint 等特性都会使用该 Python 环境。</li>
<li>断点调试时，默认使用该 Python 环境启动调试器（可以通过调试器的 <code>python</code> 字段覆盖默认配置）</li>
</ul>

<p>默认情况下 Python 扩展会读取 <code>${workspaceFolder}/.env</code> 文件中定义的环境变量（可以通过 <code>python.envFile</code> 配置自定义）。该文件语法非常简单：</p>

<ul>
<li>通过 <code>environment_variable=value</code> 定义变量</li>
<li>使用 <code>#</code> 注释</li>
<li>支持 <code>${env:EXISTING_VARIABLE}</code> 进行变量替换，不支持嵌套</li>
</ul>

<p>在该文件中定义的环境变量将影响 Language Server 和调试器的行为（如 <code>PYTHONPATH</code>）</p>

<h3 id="代码编辑">代码编辑</h3>

<h4 id="智能感知">智能感知</h4>

<p>编写代码中会自动触发建议列表、参数及其位置提示，鼠标 Hover 到代码出会显示对应位置的符号的文档。</p>

<ul>
<li>建议列表 <code>editor.action.triggerSuggest</code>，手动唤起默认快捷键为 <code>cmd + i</code>。</li>
<li>显示参数和参数位置提示 <code>editor.action.triggerParameterHints</code>，手动唤起默认快捷键为 <code>cmd+shift+space</code>。</li>
<li>显示悬浮文档 <code>editor.action.showHover</code>，手动唤起默认快捷键为 <code>cmd+k cmd+i</code>。</li>
</ul>

<p><img src="/image/vscode/python/hello-world.gif" alt="image" /></p>

<p>注意：由于 Python 是弱类型语言，在没有启用类型注解的情况下，智能感知的能力是有限的。</p>

<p>Python 的智能感知能力由如下 Language Server 提供，通过 <code>python.languageServer</code> 进行配置</p>

<ul>
<li>Default （默认），当安装了 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance</a> 时，将使用 Pylance，否则使用 Jedi。</li>
<li>Jedi 使用 <a href="https://github.com/davidhalter/jedi">jedi</a></li>
<li>None 关闭智能感知</li>
</ul>

<p>通过 <code>&gt;Python: show language server output</code> 可以输出，关于 Pylance 和 Jedi 参见下文。</p>

<p>智能感知支持添加自定义 Python 包，通过 <code>python.autoComplete.extraPaths</code> 进行配置，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.autoComplete.extraPaths&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
    <span style="color:#e6db74">&#34;~/.local/lib/Google/google_appengine&#34;</span>,
    <span style="color:#e6db74">&#34;~/.local/lib/Google/google_appengine/lib/flask-0.12&#34;</span> ]</code></pre></div>
<h4 id="快速修复和重构">快速修复和重构</h4>

<ul>
<li>自动导入（需在当前环境中安装相关包）</li>
</ul>

<p><img src="/image/vscode/python/quickFix.gif" alt="image" /></p>

<ul>
<li>提取变量、提取函数函数</li>
</ul>

<p><img src="/image/vscode/python/refactorExtractVar.gif" alt="image" /></p>

<p><img src="/image/vscode/python/refactorExtractMethod.gif" alt="image" /></p>

<ul>
<li>符号重命名和模块重命名</li>
</ul>

<p><img src="/image/vscode/python/refactorRenameModule.gif" alt="image" /></p>

<ul>
<li>导入排序 (<code>&gt;Python Refactor: Sort Imports</code> 命令唤起)，可以通过 <code>&quot;python.sortImports.args&quot;: [&quot;-rc&quot;, &quot;--atomic&quot;],</code> 配置项进行配置。排序脚本参见 <a href="https://pycqa.github.io/isort/docs/configuration/config_files.html">isort</a></li>
</ul>

<p><img src="/image/vscode/python/sortImports.gif" alt="image" /></p>

<h4 id="格式化">格式化</h4>

<p>通过 <code>python.formatting.provider</code> 配置项可以配置 Python 格式化器，默认为 <code>&quot;autopep8&quot;</code>，选项为 <code>&quot;autopep8&quot;</code>、<code>&quot;yapf&quot;</code>或<code>&quot;black&quot;</code>。使用这些格式化器时，需要如下配置</p>

<ul>
<li><code>autopep8</code>，<code>pip install pep8 &amp; pip install --upgrade autopep8</code>，特殊的配置项 <code>python.formatting.autopep8Args</code>，<code>python.formatting.autopep8Path</code></li>
<li><code>black</code>，<code>pip install black</code>，特殊的配置项 <code>python.formatting.blackArgs</code>，<code>python.formatting.blackPath</code> （只支持运行在 Python 3 环境）</li>
<li><code>yapf</code>，<code>pip install yapf</code>，特殊的配置项 <code>python.formatting.yapfArgs</code>，<code>python.formatting.yapfPath</code></li>
</ul>

<p>下面有一些配置例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.formatting.autopep8Args&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--max-line-length&#34;</span>, <span style="color:#e6db74">&#34;120&#34;</span>, <span style="color:#e6db74">&#34;--experimental&#34;</span>]<span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;python.formatting.yapfArgs&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--style&#34;</span>, <span style="color:#e6db74">&#34;{based_on_style: chromium, indent_width: 20}&#34;</span>]<span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;python.formatting.blackArgs&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--line-length&#34;</span>, <span style="color:#e6db74">&#34;100&#34;</span>]</code></pre></div>
<h3 id="代码浏览">代码浏览</h3>

<p>代码编辑器鼠标右击，打开上下文菜单，可以调转定义、查找引用、查找实现、调用层次</p>

<ul>
<li>调转定义 <code>editor.action.revealDefinition</code> ，快捷键 <code>F12</code></li>
<li>查找引用 <code>editor.action.goToReferences</code>，快捷键 <code>shift+F12</code></li>
<li>查找实现 <code>editor.action.goToImplementation</code>，快捷键 <code>cmd+F12</code>，光标在接口上</li>
<li>查找工作空间所有符号</li>
<li>调用层次 <code>references-view.showCallHierarchy</code>，快捷键 <code>shift+option+H</code>，命令名 <code>&gt;Calls: Show Call Hierarchy</code></li>
<li>Explorer （资源管理器）的 Outline （大纲），可以看到当前编辑器打开文件的符号列表</li>
</ul>

<h3 id="问题诊断">问题诊断</h3>

<p>默认情况下，针对语法问题，VSCode Python 默认会通过 Language Server 对项目进行语法检查。但在 Python 这种弱类型的编程语言的项目路中，如果想在工程中使用，光靠语法检查是不够的，静态代码检查（如代码风格）是必不可少的。因此 VSCode Python 支持与多种 Python 业界主流的 Lint 工具集成。</p>

<ul>
<li>启用或禁用 Lint

<ul>
<li>方式 1：通过 <code>&gt;Python: Select Linter</code> 命令启用或者禁用 Lint 工具（默认启用 pylint）</li>
<li>方式 2：通过 <code>&quot;python.linting.&lt;linter&gt;Enabled&quot;: true</code> 配置启用或禁用 Lint 工具</li>
</ul></li>
<li>触发 Lint 运行

<ul>
<li>检查单文件：保存文件时自动触发</li>
<li>检查整个项目：通过 <code>&gt;Python: Run Linting</code> 命令手动触发</li>
</ul></li>
<li>Lint 输出，在编辑器会以波浪线方式展示，并问题面板可以浏览所有问题</li>
</ul>

<p><img src="/image/vscode/python/lint-messages.png" alt="image" /></p>

<ul>
<li><p>Lint 相关配置</p>

<ul>
<li><code>python.linting.enabled</code> 是否启用 Lint，默认 true</li>
<li><code>python.linting.lintOnSave</code> 在保存文件时执行 Lint，默认 true</li>
<li><code>python.linting.maxNumberOfProblems</code> 最大问题数量，默认 100</li>
<li><code>python.linting.ignorePatterns</code> 忽略检查的目录或文件，默认为 <code>[&quot;.vscode/*.py&quot;, &quot;**/site-packages/**/*.py&quot;]</code></li>
</ul></li>

<li><p>不同 Lint 工具集成有不同的配置，需要安装不同的依赖和配置项，所有支持的 Lint 参见下表</p></li>
</ul>

<table>
<thead>
<tr>
<th>Linter</th>
<th>包名 (<code>pip install</code>)</th>
<th>启用配置  (python.linting.)</th>
<th>参数设置  (python.linting.)</th>
<th>自定义路径  (python.linting.)</th>
</tr>
</thead>

<tbody>
<tr>
<td>Pylint</td>
<td><a href="https://pypi.org/project/pylint/">pylint</a></td>
<td>pylintEnabled</td>
<td>pylintArgs</td>
<td>pylintPath</td>
</tr>

<tr>
<td>Flake8</td>
<td><a href="https://pypi.org/project/flake8/">flake8</a></td>
<td>flake8Enabled</td>
<td>flake8Args</td>
<td>flake8Path</td>
</tr>

<tr>
<td>mypy</td>
<td><a href="https://pypi.org/project/mypy/">mypy</a></td>
<td>mypyEnabled</td>
<td>mypyArgs</td>
<td>mypyPath</td>
</tr>

<tr>
<td>pycodestyle (pep8)</td>
<td><a href="https://pypi.org/project/pycodestyle/">pycodestyle</a></td>
<td>pycodestyleEnabled</td>
<td>pycodestyleArgs</td>
<td>pycodestylePath</td>
</tr>

<tr>
<td>prospector</td>
<td><a href="https://pypi.org/project/prospector/">prospector</a></td>
<td>prospectorEnabled</td>
<td>prospectorArgs</td>
<td>prospectorPath</td>
</tr>

<tr>
<td>pylama</td>
<td><a href="https://pypi.org/project/pylama/">pylama</a></td>
<td>pylamaEnabled</td>
<td>pylamaArgs</td>
<td>pylamaPath</td>
</tr>

<tr>
<td>bandit</td>
<td><a href="https://pypi.org/project/bandit/">bandit</a></td>
<td>banditEnabled</td>
<td>banditArgs</td>
<td>banditPath</td>
</tr>
</tbody>
</table>

<ul>
<li><p>命令行参数例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.linting.pylintArgs&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--reports&#34;</span>, <span style="color:#e6db74">&#34;12&#34;</span>, <span style="color:#e6db74">&#34;--disable&#34;</span>, <span style="color:#e6db74">&#34;I0011&#34;</span>]<span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;python.linting.flake8Args&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--ignore=E24,W504&#34;</span>, <span style="color:#e6db74">&#34;--verbose&#34;</span>]
<span style="color:#e6db74">&#34;python.linting.pydocstyleArgs&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#e6db74">&#34;--ignore=D400&#34;</span>, <span style="color:#e6db74">&#34;--ignore=D4&#34;</span>]</code></pre></div></li>

<li><p>不同 Lint 工具配置参见下方链接</p>

<ul>
<li><a href="https://code.visualstudio.com/docs/python/linting#_pylint">Pylint</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting#_pydocstyle">pydocstyle</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting#_pycodestyle-pep8">pycodestyle (pep8)</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting#_prospector">prospector</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting#_flake8">Flake8</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting#_mypy">mypy</a></li>
<li><a href="https://pypi.org/project/pylama/">pylama</a></li>
<li><a href="https://pypi.org/project/bandit/">bandit</a></li>
</ul></li>
</ul>

<h3 id="代码运行和调试">代码运行和调试</h3>

<h4 id="交互式运行">交互式运行</h4>

<ul>
<li><code>&gt;Python: Run Selection/Line in Python Terminal</code> 通过该命令，可以快速启动一个 Python REPL，并将选中代码发送到 该 REPL 中运行。</li>
<li><code>&gt;Python: Start REPL</code> 快速启动一个 Python 解释器。</li>
<li><code>&gt;Python: run selection/line in Django shell</code> 针对 Django 项目，可以选择在 Django Shell 中运行。</li>
<li>还可以田健 <code>#%%</code> 注释，快速在交互式窗口运行，参见下文：交互式 Python</li>
</ul>

<h4 id="使用默认配置快速运行或调试">使用默认配置快速运行或调试</h4>

<p>VSCode Python 支持免配置的快速启动调试</p>

<ul>
<li>按 <code>F5</code> 快速调试当前文件，按 <code>Ctrl + F5</code> 快速调试当前文件</li>
<li>点击调试视图的【运行和调试】按钮</li>
<li>点击编辑器标题栏右上角图标快速运行调试当前 Python 文件

<ul>
<li>点击三角号运行当前文件</li>
<li>点击右侧展开按钮可以调试当前文件</li>
</ul></li>
</ul>

<p><img src="/image/vscode/python/quick-run-or-debug-pythonfile.png" alt="image" /></p>

<h4 id="创建或添加调试配置">创建或添加调试配置</h4>

<p>打开调试视图</p>

<ul>
<li>在没有调试配置时，点击 create a launch.json file

<ul>
<li>根据情况选择调试的调试配置模板</li>
<li>将创建并打开 <code>.vscode/launch.json</code> 文件</li>
</ul></li>
<li>在有调试配置时，按可通过如下方式添加一个新的调试配置</li>
</ul>

<p><img src="/image/vscode/python/add-configuration.png" alt="image" /></p>

<p>调试 Python 文件默认生成的文件配置的例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Python: 当前文件&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;python&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
            <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${file}&#34;</span>,
            <span style="color:#f92672">&#34;console&#34;</span>: <span style="color:#e6db74">&#34;integratedTerminal&#34;</span>
        }
    ]
}</code></pre></div>
<h4 id="状态栏调试信息">状态栏调试信息</h4>

<p>当存在调试配置时，可以通过状态栏观察当前选中的调试配置，点击可快速选择并启动调试</p>

<p><img src="/image/vscode/python/debug-status-bar.png" alt="image" /></p>

<h4 id="调试能力详解">调试能力详解</h4>

<p>在此仅介绍断点能力</p>

<ul>
<li>普通断点，点击编辑器左侧边框，在指定行添加断点，或者在光标所在位置按 <code>F9</code></li>
<li>条件断点，右击编辑器左侧边框，选择条件断点

<ul>
<li>表达式类型，输入一个结果为 bool 的表达式</li>
<li>命中次数类型

<ul>
<li>支持一个确定的整数</li>
<li>支持比较运算符 (&gt;, &gt;=, &lt;, &lt;=, ==, !=) 加一个整数</li>
<li>支持 <code>% n</code> 表示每命中 n 次断点一次</li>
</ul></li>
<li>Logpoint，命中断点后再调试控制台输出日志</li>
</ul></li>
<li>函数断点，在测试侧边栏中，最下方断点视图，点击加号，输入当前打开文件的函数名</li>
<li>通过代码添加断点

<ul>
<li><code>import debugpy</code></li>
<li><code>debugpy.breakpoint()</code></li>
</ul></li>
</ul>

<p>其他调试 VSCode Debug 视图和通用能力，参见：<a href="https://code.visualstudio.com/docs/editor/debugging">VSCode Docs - User Guide Debugging</a></p>

<h4 id="选择一个配置启动调试">选择一个配置启动调试</h4>

<ul>
<li><code>cmd + p</code> 输入 <code>debug</code>，选择添加配置，选择 <code>Go: Launch Package</code> 回车将创建并打开 <code>.vscode/launch.json</code> 文件</li>
<li>启动调试，有如下几种方式

<ul>
<li><code>F5</code> 或者 <code>&gt;Debug: start debugging</code> 以上次选中的调试配置启动调试</li>
<li><code>cmd + p</code> 输入 <code>debug</code> 并选择调试配置，并启动调试</li>
<li>菜单 &gt; Run &gt; Start debugging</li>
<li>打开调试侧边栏，绿色三角号</li>
</ul></li>
</ul>

<p><img src="/image/vscode/python/debug-start-button.png" alt="image" /></p>

<ul>
<li>运行（不调试），有如下几种方式

<ul>
<li><code>ctrl+F5</code> 或者 <code>&gt;debug: start without debugging</code></li>
<li>菜单 &gt; Run &gt; start without debugging</li>
</ul></li>
</ul>

<h4 id="调试配置详解">调试配置详解</h4>

<p>本部分描述 <code>.vscode/launch.json</code> Python 调试配置支持的全部字段</p>

<ul>
<li><code>name</code> 调试配置名，可以自定义</li>
<li><code>type</code> 调试类型，写死 <code>python</code></li>
<li><code>request</code> 可选值为 <code>launch</code> 或者 <code>attach</code></li>
<li><code>request</code> 为 <code>launch</code> 相关配置，配置 debug 的程序如何启动

<ul>
<li><code>pragram</code> 启动的 Python 文件的绝对路径，可以使用魔法变量如（<code>${file}</code>、<code>${workspaceFolder}</code>，更多参见：<a href="https://code.visualstudio.com/docs/editor/variables-reference">变量手册</a>）</li>
<li><code>module</code> 提供指定要调试的模块名称的功能，类似于在命令行运行时的 -m 参数。有关更多信息，请参阅 <a href="https://docs.python.org/3/using/cmdline.html#cmdoption-m">Python.org</a></li>
<li><code>python</code> Python 解释器路径。如果未指定，则此设置默认为为您的工作区选择的解释器，相当于使用 <code>${command:python.interpreterPath}</code></li>
<li><code>pythonArgs</code> 传递给 Python 解释器的命令行参数</li>
<li><code>console</code> 启动用户程序的终端

<ul>
<li><code>integratedTerminal</code> 集成终端，即在 VSCode 终端中启动（可以处理标准输入）（推荐）（默认）</li>
<li><code>internalConsole</code> 调试控制台，即在 VSCode 调试控制台启动（不能向进程里面输入标准输入）</li>
<li><code>externalTerminal</code> 外部终端，不建议（调用操作系统中的终端程序）</li>
</ul></li>
<li><code>internalConsoleOptions</code> 控制是否如何打开调试控制台

<ul>
<li><code>neverOpen</code> 从不打开</li>
<li><code>openOnFirstSessionStart&quot;</code> 仅第一次调试时打来</li>
<li><code>openOnSessionStart</code> 每次调试都打开</li>
</ul></li>
<li><code>env</code> 环境变量</li>
<li><code>envFile</code> 环境变量文件</li>
<li><code>args</code> 命令行参数</li>
<li><code>stopOnEntry</code> 当设置为 true 时，在被调试程序的第一行中断调试器。如果省略（默认）或设置为 false，调试器将程序运行到第一个断点。</li>
<li><code>autoReload</code> 允许在调试器执行达到断点后对代码进行更改时自动重新加载调试器。要启用此功能，请设置 <code>{&quot;enable&quot;: true}</code> （注意：当调试器执行重新加载时，导入时运行的代码可能会再次执行。为避免这种情况，请尝试仅在模块中使用导入、常量和定义，将所有代码放入函数中。或者，您也可以使用 <code>if __name__==&quot;__main__&quot;</code> 检查。）</li>
<li><code>purpose</code> 该配置覆盖一些按钮的默认行为

<ul>
<li><code>debug-test</code>，该配置用于调试测试</li>
<li><code>debug-in-terminal</code>，该配置应用与于 <code>Debug Python File in Terminal</code></li>
</ul></li>
<li><code>subProcess</code> 调试子进程，默认为 false，更多参见：<a href="https://code.visualstudio.com/docs/editor/debugging#_multitarget-debugging">multi-target debugging</a></li>
<li><code>cwd</code> 调试进程的 WorkDir，默认为 <code>${workspaceFolder}</code></li>
<li><code>redirectOutput</code> 当设置为 true（internalConsole 的默认值）时，会导致调试器将程序的所有输出打印到 VSCode 调试控制台窗口中。如果设置为 false（integratedTerminal 和 externalTerminal 的默认值），则程序输出不会显示在调试器输出窗口中。</li>
<li><code>justMyCode</code> 默认为 true，将调试仅限于用户编写的代码。设置为 false 还可以启用标准库函数的调试。（建议修改为 <code>false</code>）</li>
<li><code>django</code> 默认为 false，当设置为 true 时，将启用 django 特性，支持 django 模板的调试</li>
<li><code>sudo</code> 默认为 false，当设置为 true 时，会在启动程序是通过 sudo 启动，可以输入密码</li>
<li><code>pyramid</code> 默认为 false，当设置为 true 时，将启用 pyramid 特性</li>
<li><code>gevent</code> 启用 gevent，参见 <a href="https://www.gevent.org/intro.html">https://www.gevent.org/intro.html</a></li>
<li><code>jinja</code> 默认为 false，当设置为 true 时，会启用 Jinja templating engine 的支持</li>
</ul></li>
<li><code>request</code> 为 <code>attach</code> 相关配置，配置 debug 如何连接到远端

<ul>
<li><code>connect</code> 配置远端 host 和 port</li>
<li><code>pathMappings</code> 远端代码和本地代码的映射</li>
</ul></li>
<li><code>logToFile</code> Enable logging of debugger events to a log file.</li>
<li><code>showReturnValue</code> 默认为 true，是否展示返回值</li>
<li>平台特性配置

<ul>
<li><code>windows</code></li>
<li><code>linux</code></li>
<li><code>osx</code></li>
</ul></li>
</ul>

<h3 id="测试">测试</h3>

<p>VSCode Python 支持 <a href="https://docs.python.org/3/library/unittest.html">unittest</a> 和 <a href="https://docs.pytest.org/">pytest</a></p>

<h4 id="相关代码">相关代码</h4>

<p>假设我们待测是模块为 <code>inc_dec.py</code>，代码如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-py" data-lang="py"><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increment</span>(x):
    <span style="color:#66d9ef">return</span> x <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>

<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">decrement</span>(x):
    <span style="color:#66d9ef">return</span> x <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span></code></pre></div>
<p>添加 pytest 测试代码 <code>test_pytest.py</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-py" data-lang="py"><span style="color:#f92672">import</span> inc_dec    <span style="color:#75715e"># The code to test</span>


<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_increment</span>():
    <span style="color:#66d9ef">assert</span> inc_dec<span style="color:#f92672">.</span>increment(<span style="color:#ae81ff">3</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">4</span>


<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_decrement</span>():
    <span style="color:#66d9ef">assert</span> inc_dec<span style="color:#f92672">.</span>decrement(<span style="color:#ae81ff">3</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">4</span></code></pre></div>
<p>添加 unittest 测试代码 <code>test_unittest.py</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-py" data-lang="py"><span style="color:#f92672">import</span> inc_dec    <span style="color:#75715e"># The code to test</span>
<span style="color:#f92672">import</span> unittest   <span style="color:#75715e"># The test framework</span>


<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Test_TestIncrementDecrement</span>(unittest<span style="color:#f92672">.</span>TestCase):
    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_increment</span>(self):
        self<span style="color:#f92672">.</span>assertEqual(inc_dec<span style="color:#f92672">.</span>increment(<span style="color:#ae81ff">3</span>), <span style="color:#ae81ff">4</span>)

    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_decrement</span>(self):
        self<span style="color:#f92672">.</span>assertEqual(inc_dec<span style="color:#f92672">.</span>decrement(<span style="color:#ae81ff">3</span>), <span style="color:#ae81ff">4</span>)


<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;__main__&#39;</span>:
    unittest<span style="color:#f92672">.</span>main()</code></pre></div>
<h4 id="快速启用测试特性">快速启用测试特性</h4>

<ul>
<li>打开测试浏览器，点击 <code>Configuare Python Tests</code>，可以快速配置测试。或者通过 <code>&gt;Python: Configure Tests</code> 命令，也可以配置测试</li>
</ul>

<p><img src="/image/vscode/python/test-explorer-no-tests.png" alt="image" /></p>

<ul>
<li>以上选择，都会应用到 <code>python.testing.unittestEnabled</code> 和 <code>python.testing.pytestEnabled</code> 配置项，如果两个框架都启用，那么 Python 扩展将只运行 pytest。当启用测试框架时，如果当前激活的环境中尚不存在框架包，VS Code 会提示您安装该框架包：</li>
</ul>

<p><img src="/image/vscode/python/install-framework.png" alt="image" /></p>

<h4 id="运行或调试测试">运行或调试测试</h4>

<p>有很多中运行测试的方法</p>

<ul>
<li>打开测试文件后，选择测试定义行旁边的装订线中显示的绿色运行图标。点击可以直接运行，右击可以选择调试</li>
</ul>

<p><img src="/image/vscode/python/run-tests-gutter.png" alt="image" /></p>

<ul>
<li>通过如下命令运行测试

<ul>
<li><code>&gt;Test: Run All Tests</code> 运行所有测试</li>
<li><code>&gt;Test: Run Tests in Current File</code> 运行当前文件的所有测试</li>
<li><code>&gt;Test: Run Test at Cursor</code> 运行光标位置处的测试</li>
</ul></li>
<li>通过如下命令调试测试

<ul>
<li><code>&gt;Test: Debug All Tests</code> 调试所有测试</li>
<li><code>&gt;Test: Debug Tests in Current File</code> 调试当前文件的所有测试</li>
<li><code>&gt;Test: Debug Test at Cursor</code> 调试    光标位置处的测试</li>
</ul></li>
<li>从测试浏览器启动运行

<ul>
<li>要运行所有发现的测试，请选择测试资源管理器顶部的播放按钮：</li>
<li>要运行一组特定的测试或单个测试，请选择文件、类或测试，然后选择该项目右侧的播放按钮</li>
<li>还可以通过测试资源管理器运行一系列测试。为此，请在要运行的测试上按 Ctrl+单击（或在 macOS 上为 Cmd+单击），右键单击其中一个，然后选择运行测试。</li>
</ul></li>
</ul>

<p><img src="/image/vscode/python/test-explorer-run-all-tests.png" alt="image" />
<img src="/image/vscode/python/test-explorer-run-scoped-tests.png" alt="image" /></p>

<ul>
<li>从测试浏览器启动调试

<ul>
<li>要调试所有发现的测试，请选择测试资源管理器顶部的小虫子按钮：</li>
<li>要运行一组特定的测试或单个测试，请选择文件、类或测试，然后选择该项目右侧的小虫子按钮</li>
<li>您还可以通过测试资源管理器运行一系列测试。为此，请在要运行的测试上按 Ctrl+单击（或在 macOS 上为 Cmd+单击），右键单击其中一个，然后选择运行测试。</li>
</ul></li>
</ul>

<p><img src="/image/vscode/python/debug-test-in-explorer.png" alt="image" /></p>

<p>测试运行后，VS Code 将结果直接在编辑器中显示为装订线装饰。失败的测试也将在编辑器中突出显示，带有显示测试运行错误消息和所有测试运行历史记录的查看视图。您可以按 Escape 关闭视图，可通过 <code>testing.automaticallyOpenPeekView</code> 配置项设置。</p>

<p><img src="/image/vscode/python/test-results.png" alt="image" /></p>

<p>另外，关于测试运行的输出，可以在 Python Test Log 输出面板找到。</p>

<p><img src="/image/vscode/python/python-test-log-output.png" alt="image" /></p>

<p>通过 <code>pytest-xdist</code> 可以支持并发测试，更多参见：<a href="https://code.visualstudio.com/docs/python/testing#_run-tests">官方文档</a></p>

<p>通过 <code>purpose</code> 字段设置为 <code>&quot;debug-test&quot;</code> 可以为测试的调试添加调试配置，例如</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Python: Current File&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;python&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${file}&#34;</span>,
    <span style="color:#f92672">&#34;purpose&#34;</span>: [<span style="color:#e6db74">&#34;debug-test&#34;</span>],
    <span style="color:#f92672">&#34;console&#34;</span>: <span style="color:#e6db74">&#34;integratedTerminal&#34;</span>,
    <span style="color:#f92672">&#34;justMyCode&#34;</span>: <span style="color:#66d9ef">false</span>
}</code></pre></div>
<h4 id="发现项目的测试">发现项目的测试</h4>

<p>VSCode Python 默认开启了自动测试发现特性（可通过 <code>python.testing.autoTestDiscoverOnSaveEnabled</code> 配置关闭）。</p>

<p>也可以通过测试浏览器的刷新按钮手动刷刷新测试。</p>

<p><img src="/image/vscode/python/test-explorer.png" alt="image" /></p>

<p>如果测试找不到，可能是测试目录没有 <code>__init__.py</code>，没有被识别成 Python 包。</p>

<p>如果有发现失败，可以通过 Python 输出面板查看具体原因</p>

<p><img src="/image/vscode/python/test-discovery-error.png" alt="image" /></p>

<h4 id="全部测试命令">全部测试命令</h4>

<table>
<thead>
<tr>
<th>命令名</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td><strong>Python: Configure Tests</strong></td>
<td>配置要与 Python 扩展一起使用的测试框架</td>
</tr>

<tr>
<td><strong>Test: Clear All Results</strong></td>
<td>清除所有测试状态，因为 UI 会在会话之间保留测试结果。</td>
</tr>

<tr>
<td><strong>Test: Debug Failed Tests</strong></td>
<td>调试在最近一次测试运行中失败的测试。</td>
</tr>

<tr>
<td><strong>Test: Debug Last Run</strong></td>
<td>调试在最近测试运行中执行的测试。</td>
</tr>

<tr>
<td><strong>Test: Debug Test at Cursor</strong></td>
<td>调试将光标集中在编辑器上的测试方法。类似于 <strong>Python：调试测试方法&hellip;</strong>在 2021.9 之前的版本上。</td>
</tr>

<tr>
<td><strong>Test: Debug Tests in Current File</strong></td>
<td>当前以编辑器为焦点的文件中的调试测试。</td>
</tr>

<tr>
<td><strong>Test: Go to Next Test Failure</strong></td>
<td>如果错误速览视图已打开，请打开并移动到资源管理器中失败的下一个测试的扫视视图。</td>
</tr>

<tr>
<td><strong>Test: Go to Previous Test Failure</strong></td>
<td>如果错误速览视图处于打开状态，请打开并移动到资源管理器中失败时上一个测试的扫视视图。</td>
</tr>

<tr>
<td><strong>Test: Peek Output</strong></td>
<td>打开失败的测试方法的错误速览视图。</td>
</tr>

<tr>
<td><strong>Test: Refresh Tests</strong></td>
<td>执行测试发现并更新测试资源管理器以反映任何测试更改、添加或删除。类似于<strong>Python：在2021.9之前的版本上发现测试</strong>。</td>
</tr>

<tr>
<td><strong>Test: Rerun Failed Tests</strong></td>
<td>运行在最近一次测试运行中失败的测试。类似于<strong>Python：在2021.9之前的版本上运行失败的测试</strong>。</td>
</tr>

<tr>
<td><strong>Test: Rerun Last Run</strong></td>
<td>调试在最近测试运行中执行的测试。</td>
</tr>

<tr>
<td><strong>Test: Run All Tests</strong></td>
<td>运行所有发现的测试。等效于 <strong>Python：在 2021.9 之前的版本上运行所有测试</strong>。</td>
</tr>

<tr>
<td><strong>Test: Run Test at Cursor</strong></td>
<td>运行测试方法，将光标聚焦在编辑器上。类似于<strong>Python：在2021.9之前的版本上运行测试方法&hellip;</strong>。</td>
</tr>

<tr>
<td><strong>Test: Run Test in Current File</strong></td>
<td>在当前以编辑器为焦点的文件中运行测试。等效于 <strong>Python：在 2021.9 之前的版本上运行当前测试文件</strong>。</td>
</tr>

<tr>
<td><strong>Test: Show Output</strong></td>
<td>打开包含所有测试运行详细信息的输出。类似于 <strong>Python：在 2021.9 之前的版本上显示测试输出</strong>。</td>
</tr>

<tr>
<td><strong>Testing: Focus on Test Explorer View</strong></td>
<td>打开&rdquo;测试资源管理器&rdquo;视图。类似于 2021.9 之前版本的<strong>测试：专注于 Python View</strong>。</td>
</tr>

<tr>
<td><strong>Test: Stop Refreshing Tests</strong></td>
<td>取消测试发现。</td>
</tr>
</tbody>
</table>

<h4 id="全部测试配置">全部测试配置</h4>

<p>VSCode 测试通用配置，按 <code>cmd + ,</code>，搜索 <code>Testing</code></p>

<p>Python 扩展通用配置</p>

<table>
<thead>
<tr>
<th>Setting  (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>autoTestDiscoverOnSaveEnabled</td>
<td><code>true</code></td>
<td>指定在保存测试文件时是启用还是禁用自动运行测试发现。更改此设置后，可能需要重新加载窗口才能应用该窗口。</td>
</tr>

<tr>
<td>cwd</td>
<td>null</td>
<td>指定测试的可选工作目录。</td>
</tr>

<tr>
<td>debugPort</td>
<td><code>3000</code></td>
<td>用于调试单元测试的端口号。</td>
</tr>

<tr>
<td>promptToConfigure</td>
<td><code>true</code></td>
<td>指定在发现潜在测试时 VS Code 是否提示配置测试框架。</td>
</tr>
</tbody>
</table>

<p>unittest 配置</p>

<table>
<thead>
<tr>
<th>Setting (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>unittestEnabled</td>
<td><code>false</code></td>
<td>指定是否启用单元测试作为测试框架。应禁用 pytest 的等效设置。</td>
</tr>

<tr>
<td>unittestArgs</td>
<td><code>[&quot;-v&quot;, &quot;-s&quot;, &quot;.&quot;, &quot;-p&quot;, &quot;*test*.py&quot;]</code></td>
<td>传递给 unittest 的参数，其中由空格分隔的每个元素都是列表中的单独项。有关默认值的说明，请参阅下文。</td>
</tr>
</tbody>
</table>

<p>unittest 的默认参数如下：</p>

<ul>
<li><code>-v</code> 设置 verbosity 级别的输出。删除此参数以获得更简单的输出。</li>
<li><code>-s .</code> 指定用于发现测试的起始目录。如果 <code>&quot;test&quot;</code> 文件夹中有测试，请将参数更改为 <code>-s test</code>（在参数数组中表示<code>&quot;-s&quot;, &quot;test&quot;</code>）。</li>
<li><code>&quot;-p *test*.py&quot;</code> 是用于查找测试的发现模式。在本例中，它是包含单词 <code>test</code> 的任何 <code>.py</code> 文件。如果以不同的方式命名测试文件，例如将 <code>_test</code> 附加到每个文件名，请在数组的相应参数中使用类似 <code>&quot;*_test.py&quot;</code> 的模式。</li>
</ul>

<p>若要在第一次失败时停止测试运行，请将快速失败选项 <code>&quot;-f&quot;</code> 添加到参数数组中。</p>

<p>关于 unittestArgs 更多参见 <a href="https://docs.python.org/3/library/unittest.html#command-line-interface">unittest command-line interface</a></p>

<p>pytest 配置</p>

<table>
<thead>
<tr>
<th>Setting  (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pytestEnabled</td>
<td><code>false</code></td>
<td>指定是否将 pytest 启用为测试框架。应禁用单元测试的等效设置。</td>
</tr>

<tr>
<td>pytestPath</td>
<td><code>&quot;pytest&quot;</code></td>
<td>通往 pytest 的路径。如果 pytest 位于当前环境之外，请使用完整路径。</td>
</tr>

<tr>
<td>pytestArgs</td>
<td><code>[]</code></td>
<td>传递给 pytest 的参数，其中由空格分隔的每个元素都是列表中的单独项。请参阅 <a href="https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags">pytest 命令行选项</a>。</td>
</tr>
</tbody>
</table>

<p>可以通过 <code>pytest.ini</code> 文件配置，更多参见 <a href="https://docs.pytest.org/en/latest/reference/customize.html">pytest Configuration</a>.</p>

<p>注意：如果安装了 <code>pytest-cov</code> 覆盖模块，则 VSCode 在调试时不会在断点处停止，因为 pytest-cov 使用相同的技术来访问正在运行的源代码。为防止此行为，请在调试测试时在 pytestArgs 中包含 <code>--no-cov</code>，或者通过将 <code>&quot;env&quot;: {&quot;PYTEST_ADDOPTS&quot;: &quot;--no-cov&quot;}</code> 添加到您的调试配置中。 （有关更多信息，请参阅 pytest-cov 文档中的<a href="https://pytest-cov.readthedocs.io/en/latest/debuggers.html">调试器和 PyCharm</a>）</p>

<h3 id="交互式-python">交互式 Python</h3>

<p>VSCode 支持一个普通的 Python 文件提供 Jupyter Cell 一样的交互式能力。（本文关注的是与 Python 源代码文件有关的特性，更多关于  <a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a> 详细说明，后续会在数据分析专题中讲述）</p>

<h4 id="python-交互式文件">Python 交互式文件</h4>

<ul>
<li>在后缀为 <code>py</code> 的文件，通过特殊注释进行单元格区分 <code># %%</code>，那么这个文件就会被识别为 Python 交互式文件。</li>
</ul>

<p><img src="/image/vscode/python/code-cells-01.png" alt="image" /></p>

<ul>
<li>点击 Run Cell 会创建一个 Python 交互式窗口</li>
</ul>

<p><img src="/image/vscode/python/code-cells-02.png" alt="image" /></p>

<ul>
<li><p>识别为 Python 交互式文件可以支持如下快捷键，常见的为</p>

<ul>
<li><code>Ctrl+Enter</code> Run Cell</li>
<li><code>Shift+Enter</code> Run Cell 并聚焦到下一个单元格，如果不存在将创建一个</li>
</ul></li>

<li><p>更多快捷键，参见下表</p></li>
</ul>

<table>
<thead>
<tr>
<th>命令</th>
<th>快捷键</th>
</tr>
</thead>

<tbody>
<tr>
<td>Python: Go to Next Cell</td>
<td>Ctrl+Alt+]</td>
</tr>

<tr>
<td>Python: Go to Previous Cell</td>
<td>Ctrl+Alt+[</td>
</tr>

<tr>
<td>Python: Extend Selection by Cell Above</td>
<td>Ctrl+Shift+Alt+[</td>
</tr>

<tr>
<td>Python: Extend Selection by Cell Below</td>
<td>Ctrl+Shift+Alt+]</td>
</tr>

<tr>
<td>Python: Move Selected Cells Up</td>
<td>Ctrl+; U</td>
</tr>

<tr>
<td>Python: Move Selected Cells Down</td>
<td>Ctrl+; D</td>
</tr>

<tr>
<td>Python: Insert Cell Above</td>
<td>Ctrl+; A</td>
</tr>

<tr>
<td>Python: Insert Cell Below</td>
<td>Ctrl+; B</td>
</tr>

<tr>
<td>Python: Insert Cell Below Position</td>
<td>Ctrl+; S</td>
</tr>

<tr>
<td>Python: Delete Selected Cells</td>
<td>Ctrl+; X</td>
</tr>

<tr>
<td>Python: Change Cell to Code</td>
<td>Ctrl+; C</td>
</tr>

<tr>
<td>Python: Change Cell to Markdown</td>
<td>Ctrl+; M</td>
</tr>
</tbody>
</table>

<h4 id="python-交互式窗">Python 交互式窗</h4>

<p>除了通过上文的方式创建交互是窗口，还可以通过 <code>&gt;Jupyter: Create Interactive Window</code> 命令创建一个交互式窗口。该窗口支持能力</p>

<ul>
<li>自动完成</li>
</ul>

<p><img src="/image/vscode/python/interactive-window-intellisense.gif" alt="image" /></p>

<ul>
<li>Plot Viewer</li>
</ul>

<p><img src="/image/vscode/python/plot-viewer.gif" alt="image" /></p>

<h4 id="变量和数据检查">变量和数据检查</h4>

<ul>
<li>变量浏览器</li>
</ul>

<p><img src="/image/vscode/python/jupyter-variable-explorer.png" alt="image" /></p>

<ul>
<li>变量数据视图</li>
</ul>

<p><img src="/image/vscode/python/jupyter-data-viewer.png" alt="image" /></p>

<h4 id="其他-feature">其他 feature</h4>

<ul>
<li>连接到远端 Jupyter，通过 <code>Jupyter: Specify local or remote Jupyter server for connections</code> 命令选择，参见：<a href="https://code.visualstudio.com/docs/python/jupyter-support-py#_connect-to-a-remote-jupyter-server">官方文档</a></li>
<li>Jupyter notebooks 和 Python 文件相互转换，参见：<a href="https://code.visualstudio.com/docs/python/jupyter-support-py#_connect-to-a-remote-jupyter-server">官方文档</a></li>
<li><a href="https://code.visualstudio.com/docs/python/jupyter-support-py#_debug-a-jupyter-notebook">Debug Jupyter notebook</a></li>
<li><a href="https://code.visualstudio.com/docs/python/jupyter-support-py#_export-a-jupyter-notebook">Export a Jupyter notebook</a></li>
</ul>

<h3 id="全部命令">全部命令</h3>

<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>python.analysis.restartLanguageServer</code></td>
<td>重启 Language Server</td>
</tr>

<tr>
<td><code>python.clearPersistentStorage</code></td>
<td>清空扩展内部缓存</td>
</tr>

<tr>
<td><code>python.clearWorkspaceInterpreter</code></td>
<td>清空工作区解释器配置</td>
</tr>

<tr>
<td><code>python.configureTests</code></td>
<td>开始配置测试</td>
</tr>

<tr>
<td><code>python.createTerminal</code></td>
<td>创建一个终端</td>
</tr>

<tr>
<td><code>python.enableLinting</code></td>
<td>启用 Lint</td>
</tr>

<tr>
<td><code>python.enableSourceMapSupport</code></td>
<td>Enable Source Map Support For Extension Debugging</td>
</tr>

<tr>
<td><code>python.execInTerminal</code></td>
<td>在终端中运行 Python 文件</td>
</tr>

<tr>
<td><code>python.debugInTerminal</code></td>
<td>调试 Python 文件</td>
</tr>

<tr>
<td><code>python.execSelectionInDjangoShell</code></td>
<td>在 Django Shell 中执行选中内容</td>
</tr>

<tr>
<td><code>python.execSelectionInTerminal</code></td>
<td>在 Python 中运行 Select/Line</td>
</tr>

<tr>
<td><code>python.goToPythonObject</code></td>
<td>转到 Python 对象</td>
</tr>

<tr>
<td><code>python.launchTensorBoard</code></td>
<td>启动 TensorBoard</td>
</tr>

<tr>
<td><code>python.refreshTensorBoard</code></td>
<td>刷新 TensorBoard</td>
</tr>

<tr>
<td><code>python.refreshTests</code></td>
<td>Refresh Tests</td>
</tr>

<tr>
<td><code>python.refreshingTests</code></td>
<td>刷新测试</td>
</tr>

<tr>
<td><code>python.stopRefreshingTests</code></td>
<td>停止刷新测试</td>
</tr>

<tr>
<td><code>python.reportIssue</code></td>
<td>Report Issue&hellip;</td>
</tr>

<tr>
<td><code>testing.reRunFailTests</code></td>
<td>重新运行失败的测试</td>
</tr>

<tr>
<td><code>python.runLinting</code></td>
<td>Run Linting</td>
</tr>

<tr>
<td><code>python.setInterpreter</code></td>
<td>设置 Python 解释器</td>
</tr>

<tr>
<td><code>python.setLinter</code></td>
<td>选择 Lint</td>
</tr>

<tr>
<td><code>python.sortImports</code></td>
<td>对导入排序</td>
</tr>

<tr>
<td><code>python.startREPL</code></td>
<td>开始 REPL</td>
</tr>

<tr>
<td><code>python.switchOffInsidersChannel</code></td>
<td>关闭 Insider</td>
</tr>

<tr>
<td><code>python.switchToDailyChannel</code></td>
<td>切换到预览体验成员每日频道</td>
</tr>

<tr>
<td><code>python.switchToWeeklyChannel</code></td>
<td>切换到预览体验成员每周频道</td>
</tr>

<tr>
<td><code>python.viewLanguageServerOutput</code></td>
<td>Show Language Server Output</td>
</tr>

<tr>
<td><code>python.viewOutput</code></td>
<td>显示输出</td>
</tr>
</tbody>
</table>

<h3 id="全部配置">全部配置</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/python/settings-reference">VSCode Docs - Python settings reference</a></p>
</blockquote>

<h4 id="通用配置">通用配置</h4>

<table>
<thead>
<tr>
<th>Setting  (python.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>condaPath</td>
<td><code>&quot;conda&quot;</code></td>
<td><code>conda</code> 可执行文件的路径。</td>
</tr>

<tr>
<td>defaultInterpreterPath</td>
<td><code>&quot;python&quot;</code></td>
<td>Python 扩展首次为工作区加载时要使用的默认 Python 解释器的路径，或者包含 Python 解释器的文件夹的路径。可以使用 <code>&quot;${workspaceFolder}&quot;</code> 和 <code>&quot;${workspaceFolder}/.venv&quot;</code> 等变量。使用文件夹的路径允许使用项目的任何人在 <code>&quot;.venv&quot;</code> 文件夹中创建适合其操作系统的环境，而不必指定与平台完全相关的路径。然后，<code>&quot;settings.json&quot;</code> 文件可以包含在源代码存储库中。<strong>注意</strong>：Python 扩展将不会应用或考虑为工作区选择解释器后对此设置所做的更改。同样，Python扩展不会自动添加或更改此设置。</td>
</tr>

<tr>
<td>pipenvPath</td>
<td><code>&quot;pipenv&quot;</code></td>
<td>用于激活的 pipenv 可执行文件的路径。</td>
</tr>

<tr>
<td>disableInstallationCheck</td>
<td><code>false</code></td>
<td>如果设置为 <code>&quot;true&quot;</code>，则在未安装 Python 解释器时禁用来自扩展的警告。在 macOS 上，还会禁用一条警告，如果您使用的是操作系统安装的 Python 解释器，则会显示该警告。通常建议在 macOS 上安装单独的解释器。</td>
</tr>

<tr>
<td>venvFolders</td>
<td><code>[]</code></td>
<td>创建的虚拟环境文件夹的路径。根据所使用的虚拟化工具，它可以是项目本身：<code>&quot;${workspaceFolder}&quot;</code>，也可以是并排放置的所有虚拟环境的单独文件夹：<code>&quot;./envs&quot;</code>、<code>&quot;~/.virtualenvs&quot;</code> 等。</td>
</tr>

<tr>
<td>envFile</td>
<td><code>&quot;${workspaceFolder}/.env&quot;</code></td>
<td>包含环境变量定义的文件的绝对路径。</td>
</tr>

<tr>
<td>globalModuleInstallation</td>
<td><code>false</code></td>
<td>false 时使用 <code>&quot;--user&quot;</code> 命令行参数来安装包，true 是为全局环境中的所有用户安装包。使用虚拟环境时忽略。有关 &ndash;user 参数的详细信息，请参阅 <a href="https://pip.pypa.io/en/stable/user_guide/#user-installs">pip - 用户安装</a>。</td>
</tr>

<tr>
<td>poetryPath</td>
<td><code>&quot;poetry&quot;</code></td>
<td>指定 <a href="https://poetry.eustace.io/">Poetry 依赖项管理器</a> 可执行文件（如果已安装）的位置。默认假定可执行文件位于当前路径中。Python 扩展使用此设置在 Poetry 可用且工作区文件夹中有一个 <code>&quot;poetry.lock&quot;</code> 文件时安装包。</td>
</tr>

<tr>
<td>terminal.launchArgs</td>
<td><code>[]</code></td>
<td>执行 <code>&gt;Python: Run Python File in Terminal</code> 等命令运行文件时提供给 Python 解释器的参数。注意，VSCode 在调试时会忽略此设置，调试是通过 <code>&quot;launch.json&quot;</code> 中的 <code>purpose</code> 为 <code>debug-in-terminal</code> 的配置的</td>
</tr>

<tr>
<td>terminal.executeInFileDir</td>
<td><code>false</code></td>
<td>执行 <code>&gt;Python: Run Python File in Terminal</code> 是 WorkDir 是否在文件所在目录，默认 false 为在工作空间目录</td>
</tr>

<tr>
<td>terminal.activateEnvironment</td>
<td><code>true</code></td>
<td>创建新的终端时是否自动执行 <code>source xxx/bin/activate</code></td>
</tr>

<tr>
<td>terminal.activateEnvInCurrentTerminal</td>
<td><code>false</code></td>
<td>指定在激活 Python 扩展时，在已经创建的终端中执行 <code>source xxx/bin/activate</code></td>
</tr>

<tr>
<td>logging.level</td>
<td><code>error</code></td>
<td>指定扩展要执行的日志记录级别。在提供的信息级别不断增加的情况下，日志记录的可能级别为 off, error, warn, info, and debug</td>
</tr>

<tr>
<td>insidersChannel</td>
<td><code>off</code></td>
<td>是否安装 insider 版本</td>
</tr>
</tbody>
</table>

<h4 id="代码分析设置">代码分析设置</h4>

<p>Language Server 提供者设置</p>

<table>
<thead>
<tr>
<th>Setting  (python.)</th>
<th>默认</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>languageServer</td>
<td>Default</td>
<td>Default （默认），当安装了 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance</a> 时，将使用 Pylance，否则使用 Jedi；Jedi 使用 <a href="https://github.com/davidhalter/jedi">jedi</a>；None 关闭智能感知</td>
</tr>
</tbody>
</table>

<h4 id="pylance-具体设置">Pylance 具体设置</h4>

<table>
<thead>
<tr>
<th>Setting  (python.analysis.)</th>
<th>默认</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>typeCheckingMode</td>
<td>off</td>
<td>指定要执行的类型检查分析的级别。可用值包括 off、basic 和 strict。当设置为 off 时，不进行类型检查分析；产生未解决的导入/变量诊断。当设置为基本非类型检查相关规则时（所有规则都关闭），以及使用基本类型检查规则。当设置为严格时，将使用最高错误严重性的所有类型检查规则（包括关闭和基本类别中的所有规则）。如果项目强制使类型<a href="https://docs.python.org/zh-cn/3/library/typing.html">标注</a>，建议使用强 strict，否则使用 basic</td>
</tr>

<tr>
<td>diagnosticMode</td>
<td>openFilesOnly</td>
<td>指定语言服务器针对问题分析哪些代码文件。可用的值为workspace 和openFilesOnly。</td>
</tr>

<tr>
<td>stubPath</td>
<td>./typings</td>
<td>指定包含自定义类型 stub 的目录的路径。每个包的类型 stub 文件都应位于其自己的子目录中。</td>
</tr>

<tr>
<td>autoSearchPaths</td>
<td>true</td>
<td>指示是否根据一些预定义的名称（如 src）自动添加搜索路径。可用值为 true 和 false。</td>
</tr>

<tr>
<td>extraPaths</td>
<td>[]</td>
<td>为导入解析指定额外的搜索路径。路径应指定为字符串，当有多个路径时必须用逗号分隔。 <code>[&quot;path 1&quot;,&quot;path 2&quot;]</code></td>
</tr>

<tr>
<td>completeFunctionParens</td>
<td>false</td>
<td>为函数调用自动完成添加括号。</td>
</tr>

<tr>
<td>useLibraryCodeForTypes</td>
<td>true</td>
<td>当未找到类型 stub 时，解析包的源代码类型。</td>
</tr>

<tr>
<td>autoImportCompletions</td>
<td>true</td>
<td>控制在完成中提供自动导入。</td>
</tr>

<tr>
<td>diagnosticSeverityOverrides</td>
<td>{}</td>
<td>允许用户覆盖单个诊断的严重性级别。对于每个规则，可用的严重性级别为错误（红色波浪线）、警告（黄色波浪线）、信息（蓝色波浪线）和无（规则禁用）。有关用于诊断严重性规则的键的信息，请参阅下面的诊断严重性规则部分。</td>
</tr>
</tbody>
</table>

<blockquote>
<p><strong>Note:</strong> 对于 Pylance 内测版，可以使用 <code>pylance.insidersChannel</code> 配置项进行体验</p>
</blockquote>

<p><strong>诊断严重性规则</strong></p>

<p>本节详细介绍了可以使用 <code>&quot;python.analysis.diagnosticSeverityOverrides&quot;</code> 设置自定义的所有可用规则，如以下示例所示。</p>

<p>一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
  &#34;python.analysis.diagnosticSeverityOverrides&#34;: {
    &#34;reportUnboundVariable&#34;: &#34;information&#34;,
    &#34;reportImplicitStringConcatenation&#34;: &#34;warning&#34;
  }
}</pre></div>
<table>
<thead>
<tr>
<th>诊断规则</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>reportGeneralTypeIssues</td>
<td>对一般类型不一致、不受支持的操作、参数/参数不匹配等的诊断。这涵盖了其他规则未涵盖的所有基本类型检查规则。它不包括语法错误。</td>
</tr>

<tr>
<td>reportPropertyTypeMismatch</td>
<td>对传递给 setter 的值的类型和 getter 返回的值的类型进行诊断。这种不匹配违反了属性的预期用途，这些属性的作用类似于变量。</td>
</tr>

<tr>
<td>reportFunctionMemberAccess</td>
<td>成员访问函数的诊断。</td>
</tr>

<tr>
<td>reportMissingImports</td>
<td>对没有相应导入的 python 文件或类型 stub 文件的导入进行诊断。</td>
</tr>

<tr>
<td>reportMissingModuleSource</td>
<td>没有相应源文件的导入的诊断。当找到类型 stub ，但找不到模块源文件时，会发生这种情况，这表明使用此执行环境时代码可能会在运行时失败。类型检查将使用类型 stub 完成。</td>
</tr>

<tr>
<td>reportMissingTypeStubs</td>
<td>对没有相应类型 stub 文件（类型化文件或自定义类型存根）的导入进行诊断。类型检查器需要类型存根以在分析中发挥最佳作用。</td>
</tr>

<tr>
<td>reportImportCycles</td>
<td>循环导入。这不是 Python 中的错误，但它们确实会减慢类型分析的速度，并且经常暗示架构分层问题。通常，应避免使用它们。</td>
</tr>

<tr>
<td>reportUnusedImport</td>
<td>未使用的导入。</td>
</tr>

<tr>
<td>reportUnusedClass</td>
<td>未使用的类。</td>
</tr>

<tr>
<td>reportUnusedFunction</td>
<td>未使用的函数。</td>
</tr>

<tr>
<td>reportUnusedVariable</td>
<td>未使用的变量。</td>
</tr>

<tr>
<td>reportDuplicateImport</td>
<td>重复导入。</td>
</tr>

<tr>
<td>reportWildcardImportFromLibrary</td>
<td>从外部库导入使用了通配符的诊断。</td>
</tr>

<tr>
<td>reportOptionalSubscript</td>
<td>尝试使用可选类型下标（索引）变量的诊断。</td>
</tr>

<tr>
<td>reportOptionalMemberAccess</td>
<td>尝试访问具有可选类型的变量成员的诊断。</td>
</tr>

<tr>
<td>reportOptionalCall</td>
<td>尝试调用具有可选类型的变量的诊断。</td>
</tr>

<tr>
<td>reportOptionalIterable</td>
<td>尝试使用可选类型作为可迭代值（例如，在 for 语句中）的诊断。</td>
</tr>

<tr>
<td>reportOptionalContextManager</td>
<td>尝试将 Optional 类型用作上下文管理器（作为 with 语句的参数）的诊断。</td>
</tr>

<tr>
<td>reportOptionalOperand</td>
<td>尝试使用可选类型作为二进制或一元运算符（如&rdquo;+&ldquo;、&rdquo;==&ldquo;、&rdquo;or&rdquo;、&rdquo;not&rdquo;）的操作数的诊断。</td>
</tr>

<tr>
<td>reportUntypedFunctionDecorator</td>
<td>诊断没有类型批注的函数修饰器。这些功能模糊了函数类型，破坏了许多类型分析特征。</td>
</tr>

<tr>
<td>reportUntypedClassDecorator</td>
<td>没有类型批注的类修饰器的诊断。这些会模糊类类型，破坏许多类型分析功能。</td>
</tr>

<tr>
<td>reportUntypedBaseClass</td>
<td>无法静态确定其类型的基类的诊断。这些会模糊类类型，破坏许多类型分析功能。</td>
</tr>

<tr>
<td>reportUntypedNamedTuple</td>
<td>使用&rdquo;namedtuple&rdquo;而不是&rdquo;NamedTuple&rdquo;时的诊断。前者不包含类型信息，而后者则包含类型信息。</td>
</tr>

<tr>
<td>reportPrivateUsage</td>
<td>诊断是否不正确地使用了私有或受保护的变量或函数。受保护的类成员以单个下划线&rdquo;_&ldquo;开头，只能由子类访问。私有类成员以双下划线开头，但不以双下划线结尾，并且只能在声明类中访问。如果在类外部声明的变量和函数的名称以单下划线或双下划线开头，并且无法在声明模块外部访问它们，则将其视为私有变量和函数。</td>
</tr>

<tr>
<td>reportConstantRedefinition</td>
<td>尝试重新定义名称为全大写且带有下划线和数字的变量的诊断。</td>
</tr>

<tr>
<td>reportIncompatibleMethodOverride</td>
<td>以不兼容的方式（参数数错误、参数类型不兼容或不兼容的返回类型）重写基类中同名方法的方法的诊断。</td>
</tr>

<tr>
<td>reportIncompatibleVariableOverride</td>
<td>对类变量声明的诊断，这些声明覆盖了与基类符号类型不兼容的基类中同名符号。</td>
</tr>

<tr>
<td>reportInvalidStringEscapeSequence</td>
<td>对字符串文本中使用的无效转义序列的诊断。Python 规范指示此类序列将在将来的版本中生成语法错误。</td>
</tr>

<tr>
<td>reportUnknownParameterType</td>
<td>对具有未知类型的函数或方法的输入或返回参数的诊断。</td>
</tr>

<tr>
<td>reportUnknownArgumentType</td>
<td>具有未知类型的函数或方法的调用参数的诊断。</td>
</tr>

<tr>
<td>reportUnknownLambdaType</td>
<td>对具有未知类型的 lambda 的输入或返回参数进行诊断。</td>
</tr>

<tr>
<td>reportUnknownVariableType</td>
<td>具有未知类型的变量的诊断。</td>
</tr>

<tr>
<td>reportUnknownMemberType</td>
<td>具有未知类型的类或实例变量的诊断。</td>
</tr>

<tr>
<td>reportMissingTypeArgument</td>
<td>诊断何时使用泛型类而不提供显式或隐式类型参数。</td>
</tr>

<tr>
<td>reportInvalidTypeVarUse</td>
<td>诊断函数签名中未正确使用类型变量。</td>
</tr>

<tr>
<td>reportCallInDefaultInitializer</td>
<td>对默认值初始化表达式中的函数调用进行诊断。此类调用可能会掩盖在模块初始化时执行的代价高昂的操作。</td>
</tr>

<tr>
<td>reportUnnecessaryIsInstance</td>
<td>诊断没有必要的 &ldquo;isinstance&rdquo; 或 &ldquo;issubclass&rdquo; 调用，此类调用通常表示编程错误。</td>
</tr>

<tr>
<td>reportUnnecessaryCast</td>
<td>诊断没有必要的类型转换 cast ，此类调用有时表示编程错误。</td>
</tr>

<tr>
<td>reportAssertAlwaysTrue</td>
<td>诊断 assert 始终为 true 语句。这可能表示编程错误。</td>
</tr>

<tr>
<td>reportSelfClsParameterName</td>
<td>诊断实例方法中缺少或命名错误的&rdquo;self&rdquo;参数和类方法中的&rdquo;cls&rdquo;参数。允许元类（从&rdquo;类型&rdquo;派生的类）中的实例方法对实例方法使用&rdquo;cls&rdquo;。</td>
</tr>

<tr>
<td>reportImplicitStringConcatenation</td>
<td>Diagnostics for two or more string literals that follow each other, indicating an implicit concatenation. This is considered a bad practice and often masks bugs such as missing commas.</td>
</tr>

<tr>
<td>reportUndefinedVariable</td>
<td>未定义变量的诊断。</td>
</tr>

<tr>
<td>reportUnboundVariable</td>
<td>未绑定和可能未绑定变量的诊断。</td>
</tr>

<tr>
<td>reportInvalidStubStatement</td>
<td>诊断不应出现在 stub 文件中的语句。</td>
</tr>

<tr>
<td>reportUnusedCallResult</td>
<td>其结果未被使用的调用表达式的诊断。</td>
</tr>

<tr>
<td>reportUnsupportedDunderAll</td>
<td>对 <code>__all__</code> 上执行的不受支持的操作的诊断。</td>
</tr>

<tr>
<td>reportUnusedCoroutine</td>
<td>对返回协程且其结果未被使用的调用表达式的诊断。</td>
</tr>
</tbody>
</table>

<h4 id="自动完成配置">自动完成配置</h4>

<table>
<thead>
<tr>
<th>Setting  (python.autoComplete.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>extraPaths</td>
<td><code>[]</code></td>
<td>指定要为其加载自动完成数据的附加包的位置。</td>
</tr>
</tbody>
</table>

<h4 id="格式化设置">格式化设置</h4>

<table>
<thead>
<tr>
<th>Setting (python.formatting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>provider</td>
<td><code>&quot;autopep8&quot;</code></td>
<td>指定要使用的格式化程序，<code>&quot;autopep8&quot;</code>、<code>&quot;black&quot;</code> 或 <code>&quot;yapf&quot;</code>。</td>
</tr>

<tr>
<td>autopep8Path</td>
<td><code>&quot;autopep8&quot;</code></td>
<td>autopep8 路径</td>
</tr>

<tr>
<td>autopep8Args</td>
<td><code>[]</code></td>
<td>autopep8 参数</td>
</tr>

<tr>
<td>blackPath</td>
<td><code>&quot;black&quot;</code></td>
<td>blackPath 路径</td>
</tr>

<tr>
<td>blackArgs</td>
<td><code>[]</code></td>
<td>blackArgs 参数</td>
</tr>

<tr>
<td>yapfPath</td>
<td><code>&quot;yapf&quot;</code></td>
<td>yapf 路径</td>
</tr>

<tr>
<td>yapfArgs</td>
<td><code>[]</code></td>
<td>yapf 参数</td>
</tr>
</tbody>
</table>

<h4 id="重构之导入排序">重构之导入排序</h4>

<table>
<thead>
<tr>
<th>Setting (python.sortImports.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>path</td>
<td><code>&quot;&quot;</code></td>
<td>isort script 路径</td>
</tr>

<tr>
<td>args</td>
<td><code>[]</code></td>
<td>isort 参数</td>
</tr>
</tbody>
</table>

<h4 id="linting-设置">Linting 设置</h4>

<p>通用</p>

<table>
<thead>
<tr>
<th>Setting (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>enabled</td>
<td><code>true</code></td>
<td>是否启用 Lint</td>
</tr>

<tr>
<td>lintOnSave</td>
<td><code>true</code></td>
<td>是否保存文件时执行 Lint.</td>
</tr>

<tr>
<td>maxNumberOfProblems</td>
<td><code>100</code></td>
<td>问题数限制</td>
</tr>

<tr>
<td>ignorePatterns</td>
<td><code>[&quot;.vscode/*.py&quot;, &quot;**/site-packages/**/*.py&quot;]</code></td>
<td>执行 Lint 时忽略的路径模式</td>
</tr>
</tbody>
</table>

<p>Pylint</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pylintEnabled</td>
<td><code>true</code></td>
<td>启用 Pylint</td>
</tr>

<tr>
<td>pylintArgs</td>
<td><code>[]</code></td>
<td>Pylint 命令行参数</td>
</tr>

<tr>
<td>pylintPath</td>
<td><code>&quot;pylint&quot;</code></td>
<td>Pylint 路径</td>
</tr>

<tr>
<td>pylintCategorySeverity.convention</td>
<td><code>&quot;Information&quot;</code></td>
<td>Pylint convention 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>pylintCategorySeverity.refactor</td>
<td><code>&quot;Hint&quot;</code></td>
<td>Pylint refactor 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>pylintCategorySeverity.warning</td>
<td><code>&quot;Warning&quot;</code></td>
<td>Pylint warning 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>pylintCategorySeverity.error</td>
<td><code>&quot;Error&quot;</code></td>
<td>Pylint error 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>pylintCategorySeverity.fatal</td>
<td><code>&quot;Error&quot;</code></td>
<td>Pylint fatal 类型消息映射到 VSCode 问题的类型</td>
</tr>
</tbody>
</table>

<p>pycodestyle (pep8)</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pycodestyleEnabled</td>
<td><code>false</code></td>
<td>启用 pycodestyle</td>
</tr>

<tr>
<td>pycodestyleArgs</td>
<td><code>[]</code></td>
<td>pycodestyle 命令行参数</td>
</tr>

<tr>
<td>pycodestylePath</td>
<td><code>&quot;pycodestyle&quot;</code></td>
<td>pycodestyle 路径</td>
</tr>

<tr>
<td>pycodestyleCategorySeverity.W</td>
<td><code>&quot;Warning&quot;</code></td>
<td>pycodestyle W 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>pycodestyleCategorySeverity.E</td>
<td><code>&quot;Error&quot;</code></td>
<td>pycodestyle E 类型消息映射到 VSCode 问题的类型</td>
</tr>
</tbody>
</table>

<p>Flake8</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>flake8Enabled</td>
<td><code>false</code></td>
<td>启用 flake8</td>
</tr>

<tr>
<td>flake8Args</td>
<td><code>[]</code></td>
<td>flake8 命令行参数</td>
</tr>

<tr>
<td>flake8Path</td>
<td><code>&quot;flake8&quot;</code></td>
<td>flake8 路径</td>
</tr>

<tr>
<td>flake8CategorySeverity.F</td>
<td><code>&quot;Error&quot;</code></td>
<td>flake8 F 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>flake8CategorySeverity.E</td>
<td><code>&quot;Error&quot;</code></td>
<td>flake8 E 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>flake8CategorySeverity.W</td>
<td><code>&quot;Warning&quot;</code></td>
<td>flake8 W 类型消息映射到 VSCode 问题的类型</td>
</tr>
</tbody>
</table>

<p>mypy</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>mypyEnabled</td>
<td><code>false</code></td>
<td>启用 mypy</td>
</tr>

<tr>
<td>mypyArgs</td>
<td><code>[&quot;--ignore-missing-imports&quot;, &quot;--follow-imports=silent&quot;]</code></td>
<td>mypy 命令行参数</td>
</tr>

<tr>
<td>mypyPath</td>
<td><code>&quot;mypy&quot;</code></td>
<td>mypy 路径</td>
</tr>

<tr>
<td>mypyCategorySeverity.error</td>
<td><code>&quot;Error&quot;</code></td>
<td>mypy error 类型消息映射到 VSCode 问题的类型</td>
</tr>

<tr>
<td>mypyCategorySeverity.note</td>
<td><code>&quot;Information&quot;</code></td>
<td>mypy note 类型消息映射到 VSCode 问题的类型</td>
</tr>
</tbody>
</table>

<p>pydocstyle</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pydocstyleEnabled</td>
<td><code>false</code></td>
<td>启用 pydocstyle</td>
</tr>

<tr>
<td>pydocstyleArgs</td>
<td><code>[]</code></td>
<td>pydocstyle 命令行参数</td>
</tr>

<tr>
<td>pydocstylePath</td>
<td><code>&quot;pydocstyle&quot;</code></td>
<td>pydocstyle 路径</td>
</tr>
</tbody>
</table>

<p>prospector</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>prospectorEnabled</td>
<td><code>false</code></td>
<td>启用 prospector</td>
</tr>

<tr>
<td>prospectorArgs</td>
<td><code>[]</code></td>
<td>prospector 命令行参数</td>
</tr>

<tr>
<td>prospectorPath</td>
<td><code>&quot;prospector&quot;</code></td>
<td>prospector 路径</td>
</tr>
</tbody>
</table>

<p>pylama</p>

<table>
<thead>
<tr>
<th>Setting  (python.linting.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pylamaEnabled</td>
<td><code>false</code></td>
<td>启用 pylama</td>
</tr>

<tr>
<td>pylamaArgs</td>
<td><code>[]</code></td>
<td>pylama 命令行参数</td>
</tr>

<tr>
<td>pylamaPath</td>
<td><code>&quot;pylama&quot;</code></td>
<td>pylama 路径</td>
</tr>
</tbody>
</table>

<h4 id="测试设置">测试设置</h4>

<p>通用测试</p>

<table>
<thead>
<tr>
<th>Setting  (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>cwd</td>
<td>null</td>
<td>指定测试的可选工作目录。</td>
</tr>

<tr>
<td>promptToConfigure</td>
<td><code>true</code></td>
<td>指定在发现潜在测试时 VS Code 是否提示配置测试框架。</td>
</tr>

<tr>
<td>debugPort</td>
<td><code>3000</code></td>
<td>用于调试单元测试的端口号。</td>
</tr>

<tr>
<td>autoTestDiscoverOnSaveEnabled</td>
<td><code>true</code></td>
<td>指定在保存测试文件时是启用还是禁用自动运行测试发现。</td>
</tr>
</tbody>
</table>

<p>unittest 配置</p>

<table>
<thead>
<tr>
<th>Setting  (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>unittestEnabled</td>
<td><code>false</code></td>
<td>是否启用 unittest</td>
</tr>

<tr>
<td>unittestArgs</td>
<td><code>[&quot;-v&quot;, &quot;-s&quot;, &quot;.&quot;, &quot;-p&quot;, &quot;*test*.py&quot;]</code></td>
<td>命令行参数</td>
</tr>
</tbody>
</table>

<p>pytest 配置</p>

<table>
<thead>
<tr>
<th>Setting  (python.testing.)</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>pytestEnabled</td>
<td><code>false</code></td>
<td>是否启用 pytest</td>
</tr>

<tr>
<td>pytestPath</td>
<td><code>&quot;pytest&quot;</code></td>
<td>pytest 路径</td>
</tr>

<tr>
<td>pytestArgs</td>
<td><code>[]</code></td>
<td>pytest 命令行参数</td>
</tr>
</tbody>
</table>

<h4 id="预定义变量">预定义变量</h4>

<p>Python 扩展设置支持预定义的变量。与常规 VS Code 设置类似，变量使用 <code>${变量名称}</code> 语法。具体而言，该扩展支持以下变量：</p>

<ul>
<li><strong><code>${cwd}</code></strong> - 启动时任务运行者的当前工作目录</li>
<li><strong><code>${workspaceFolder}</code></strong> - 在 VS 代码中打开的文件夹的路径</li>
<li><strong><code>${workspaceRootFolderName}</code></strong> - 在 VSCode 中打开的文件夹的名称，不带任何斜杠 （/）</li>
<li><strong><code>${workspaceFolderBasename}</code></strong> - 在 VSCode 中打开的文件夹的名称，不带任何斜杠 （/）</li>
<li><strong><code>${file}</code></strong> - 当前打开的文件</li>
<li><strong><code>${relativeFile}</code></strong> - 当前打开的文件相对于 <code>&quot;${workspaceFolder}&quot;</code> 的相对路径</li>
<li><strong><code>${relativeFileDirname}</code></strong> - 当前打开的文件所在目录相对于 <code>&quot;${workspaceFolder}&quot;</code> 的相对路径</li>
<li><strong><code>${fileBasename}</code></strong> - the current opened file&rsquo;s basename</li>
<li><strong><code>${fileBasenameNoExtension}</code></strong> - 当前打开的文件去掉文件扩展名后的名字</li>
<li><strong><code>${fileDirname}</code></strong> - 当前打开的文件的目录名称</li>
<li><strong><code>${fileExtname}</code></strong> - 当前打开的文件扩展名</li>
<li><strong><code>${lineNumber}</code></strong> - 活动文件中当前选定的行号</li>
<li><strong><code>${selectedText}</code></strong> - 活动文件中当前选定的文本</li>
<li><strong><code>${execPath}</code></strong> - the path to the running VSCode executable</li>
</ul>

<p>有关预定义变量和示例用法的其他信息，请参阅 <a href="/docs/editor/variables-reference">VSCode 官方文档</a>。</p>

<h3 id="pylance">Pylance</h3>

<p>具体参见： <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance</a></p>

<h3 id="jedi">Jedi</h3>

<p><a href="https://code.visualstudio.com/docs/python/environments#_limited-support-for-python-27">不支持 Python 2.7</a>，更多参见：<a href="https://github.com/davidhalter/jedi">Github</a></p>

<h2 id="场景和说明">场景和说明</h2>

<h3 id="vscode-python-最佳配置">VSCode Python 最佳配置</h3>

<p>VSCode Python 多数情况下使用默认配置即可，如下两个配置可以酌情选择</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">Python</span> <span style="color:#960050;background-color:#1e0010">配置</span>
    <span style="color:#f92672">&#34;python.analysis.typeCheckingMode&#34;</span>: <span style="color:#e6db74">&#34;basic&#34;</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">指定要执行的类型检查分析的级别。未强制要求类型标注的项目使用</span> <span style="color:#960050;background-color:#1e0010">basic</span> <span style="color:#960050;background-color:#1e0010">；对项目代码质量有要求的使用</span> <span style="color:#960050;background-color:#1e0010">strict</span>
    <span style="color:#f92672">&#34;python.analysis.completeFunctionParens&#34;</span>: <span style="color:#66d9ef">true</span>,  <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">为函数调用自动完成添加括号</span>
}</code></pre></div>
<h3 id="开发-django-项目">开发 Django 项目</h3>

<p>开发 Django 项目和普通 Python 主要区别在于调试配置，<code>&quot;django&quot;: true</code>，对 HTML 模板调试的支持。</p>

<p><code>.vscode/launch.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Python: Django&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;python&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}/manage.py&#34;</span>,
    <span style="color:#f92672">&#34;args&#34;</span>: [
        <span style="color:#e6db74">&#34;runserver&#34;</span>,
    ],
    <span style="color:#f92672">&#34;django&#34;</span>: <span style="color:#66d9ef">true</span>
}<span style="color:#960050;background-color:#1e0010">,</span></code></pre></div>
<p>此外，VSCode Python 还提供了 <code>python.execSelectionInDjangoShell</code> 命令，直接在 Django Shell 中执行选中内容</p>

<p>更多关于 Django 开发手册，参见：<a href="https://code.visualstudio.com/docs/python/tutorial-django">VSCode 官方文档</a></p>

<h3 id="开发-flask-项目">开发 Flask 项目</h3>

<p>开发 Flask 项目和普通 Python 主要区别在于调试配置，<code>&quot;jinja&quot;: true</code>，对 HTML 模板调试的支持，以及使用 flask module 启动项目。</p>

<p><code>.vscode/launch.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Python: Flask&#34;</span>,
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;python&#34;</span>,
    <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
    <span style="color:#f92672">&#34;module&#34;</span>: <span style="color:#e6db74">&#34;flask&#34;</span>,
    <span style="color:#f92672">&#34;env&#34;</span>: {
        <span style="color:#f92672">&#34;FLASK_APP&#34;</span>: <span style="color:#e6db74">&#34;app.py&#34;</span>,
        <span style="color:#f92672">&#34;FLASK_ENV&#34;</span>: <span style="color:#e6db74">&#34;development&#34;</span>,
        <span style="color:#f92672">&#34;FLASK_DEBUG&#34;</span>: <span style="color:#e6db74">&#34;0&#34;</span>
    },
    <span style="color:#f92672">&#34;args&#34;</span>: [
        <span style="color:#e6db74">&#34;run&#34;</span>,
        <span style="color:#e6db74">&#34;--no-debugger&#34;</span>,
        <span style="color:#e6db74">&#34;--no-reload&#34;</span>
    ],
    <span style="color:#f92672">&#34;jinja&#34;</span>: <span style="color:#66d9ef">true</span>
}<span style="color:#960050;background-color:#1e0010">,</span></code></pre></div>
<p>更多关于  Flask 开发手册，参见：<a href="https://code.visualstudio.com/docs/python/tutorial-flask">VSCode 官方文档</a></p>

<h3 id="远程调试">远程调试</h3>

<p>远端，通过 <code>python -m debugpy --listen 5678 ./myscript.py</code> 命令启动调试器</p>

<p>VSCode 添加如下配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Python: Attach&#34;</span>,
  <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;python&#34;</span>,
  <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;attach&#34;</span>,
  <span style="color:#f92672">&#34;connect&#34;</span>: { <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">根据情况替换</span>
    <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;localhost&#34;</span>,
    <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">5678</span>
  }
}</code></pre></div>
<p>详见：</p>

<ul>
<li><a href="https://code.visualstudio.com/docs/python/debugging#_command-line-debugging">VSCode Docs - Python Debugging - Command Line Debugging</a></li>
<li><a href="https://code.visualstudio.com/docs/python/debugging#_command-line-debugging">VSCode Docs - Python Debugging - Debugging by attaching over a network connection</a></li>
</ul>

<h3 id="调试时依赖库断点不命中">调试时依赖库断点不命中</h3>

<p>调试配置添加： <code>&quot;justMyCode&quot;: false</code></p>

<h3 id="pyright-pylance-python三个vscode扩展关系">Pyright、Pylance、Python三个VSCode扩展关系</h3>

<p>在 VSCode 中，默认情况下为：</p>

<ul>
<li>Python 的 Language Server 的实现依赖 Pylance</li>
<li>Pylance 自身有自己的特性，并依赖 Pyright 库（注意不是 Pyright 扩展）</li>
</ul>

<h3 id="为快速运行调试打开的文件以及测试添浏览器添加自定义配置">为快速运行调试打开的文件以及测试添浏览器添加自定义配置</h3>

<p>针对如下情况也可以添加自定义配置</p>

<ul>
<li>1、通过如下方式快速运行调试打开的文件</li>
</ul>

<p><img src="/image/vscode/python/quick-run-or-debug-pythonfile.png" alt="image" /></p>

<ul>
<li>2、通过 <code>python.execInTerminal</code> 以及 <code>python.debugInTerminal</code> 运行或调试当前文件</li>
<li>3、通过测试浏览器运行或调试测试</li>
</ul>

<p><img src="/image/vscode/python/test-explorer-run-all-tests.png" alt="image" /></p>

<p>这几种情况，可以通过如下方式进行自定义配置</p>

<ul>
<li>1 和 2 的运行情况，通过 <code>python.terminal.executeInFileDir</code> 以及 <code>python.terminal.launchArgs</code> 指定 <code>cwd</code> 以及 <code>args</code>；通过 <code>terminal.integrated.env.linux</code> 配置环境变量（如 <code>&quot;PYTHONPATH&quot;: &quot;.&quot;</code>）</li>
<li>1 和 2 的调试情况，通过 <code>.vscode/launch.json</code> 中添加调试配置，并通过 <code>purpose</code> 字段添加 <code>debug-in-terminal</code> 将该配置项应用到调试中</li>
<li>3 的情况，通过 <code>.vscode/launch.json</code> 中添加调试配置，并通过 <code>purpose</code> 字段添加 <code>debug-test</code> 将该配置项应用到调试中</li>
</ul>

<h3 id="jedi-和-pyright-能力对比">Jedi 和 Pyright 能力对比</h3>

<p>暂时未找到相关内容</p>

<h3 id="如何开发-python-2-7-项目">如何开发 Python 2.7 项目</h3>

<p>Jedi 完全不支持 Python 2.7。Pylance 和 Pyright 可以支持 Python 2.7 （但是只能识别 Python3 的语法和智能提示），参见：<a href="https://code.visualstudio.com/docs/python/environments#_limited-support-for-python-27">Python 2.7 的限制</a></p>

<h3 id="如何让-ide-更了解你的项目">如何让 IDE 更了解你的项目</h3>

<p>在项目中强制使用标注（<a href="https://docs.python.org/zh-cn/3/library/typing.html">typing</a>），同时启用强类型检查：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;python.analysis.typeCheckingMode&#34;</span>: <span style="color:#e6db74">&#34;strict&#34;</span>,
}</code></pre></div>
<p>更多参见</p>

<ul>
<li><a href="https://github.com/python/typing">https://github.com/python/typing</a></li>
<li><a href="https://github.com/python/typeshed">https://github.com/python/typeshed</a></li>
</ul>

<h3 id="如何解决-importerror">如何解决 ImportError</h3>

<p>参见：<a href="/posts/understand-the-python-environment/">一文彻底理解 Python 环境</a>。</p>

<h3 id="完全开源的-vscode-python-开发方案">完全开源的 VSCode Python 开发方案</h3>

<p>目前 VSCode 使用了 Pylance 作为其默认 Language Server，但是 Pylance 是闭源的。因此，无法在非官方 VSCode 中使用 Pylance。</p>

<h4 id="只开发-python3-项目">只开发 Python3 项目</h4>

<p>安装扩展</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a> - <a href="https://marketplace.visualstudio.com/items/ms-python.python/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a> - <a href="https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license">MIT 许可证</a></li>
</ul>

<p>VSCode 配置添加配置，兼用默认（如果安装了 Pylance，需禁用之）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.languageServer&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;Jedi&#34;</span></code></pre></div>
<h4 id="同时支持-python2-和-python3-项目-推荐">同时支持 Python2 和 Python3 项目（推荐）</h4>

<blockquote>
<p>该方案使用 Pyright 替换 Pylance，可以获得 Pylance 类似的体验</p>
</blockquote>

<p>安装扩展</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a> - <a href="https://marketplace.visualstudio.com/items/ms-python.python/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a> - <a href="https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license">MIT 许可证</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-pyright.pyright">Pyright</a> - <a href="https://github.com/microsoft/pyright/blob/main/LICENSE.txt">MIT 许可证</a></li>
</ul>

<p>VSCode 配置添加配置，兼用默认（如果安装了 Pylance，需禁用之）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.languageServer&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;None&#34;</span></code></pre></div>
<h2 id="reference">Reference</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/languages/python">VSCode Docs - Language Python</a></li>
<li><a href="https://code.visualstudio.com/docs/python/python-tutorial">VSCode Docs - Python Tutorial</a></li>
<li><a href="https://code.visualstudio.com/docs/python/editing">VSCode Docs - Python Editing Code</a></li>
<li><a href="https://code.visualstudio.com/docs/python/linting">VSCode Docs - Python Linting</a></li>
<li><a href="https://code.visualstudio.com/docs/python/debugging">VSCode Docs - Python Debugging</a></li>
<li><a href="https://code.visualstudio.com/docs/python/environments">VSCode Docs - Python Environments</a></li>
<li><a href="https://code.visualstudio.com/docs/python/testing">VSCode Docs - Python Testing</a></li>
<li><a href="https://code.visualstudio.com/docs/python/jupyter-support-py">VSCode Docs - Python Interactive</a></li>
<li><a href="https://code.visualstudio.com/docs/python/tutorial-django">VSCode Docs - Python Django Tutorial</a></li>
<li><a href="https://code.visualstudio.com/docs/python/tutorial-flask">VSCode Docs - Python Flask Tutorial</a></li>
<li><a href="https://code.visualstudio.com/docs/python/settings-reference">VSCode Docs - Python Settings reference</a></li>
</ul>
]]></description></item><item><title>Java 语言</title><link>https://www.rectcircle.cn/series/vscode/good-extensions/java-language/</link><pubDate>Wed, 29 Dec 2021 19:40:51 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/good-extensions/java-language/</guid><description type="html"><![CDATA[

<h2 id="导读">导读</h2>

<p>VSCode 对 Java 的支持主要由 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> 提供，该扩展包包含如下扩展：</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a> - <a href="https://marketplace.visualstudio.com/items/redhat.java/license">Eclipse Public License - v 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug">Debugger for Java</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-java-debug/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test">Test Runner for Java</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-java-test/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-maven">Maven for Java</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-maven/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-dependency">Project Manager for Java</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-java-dependency/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">Visual Studio IntelliCode</a> - 闭源 <a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">许可证</a></li>
</ul>

<p>社区还提供了如下常用的 Java 扩展：</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Pivotal.vscode-boot-dev-pack">Spring Boot Extension Pack</a> - <a href="https://github.com/spring-projects/sts4/blob/main/license.txt">Eclipse Public License - v 1.0</a>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Pivotal.vscode-spring-boot">Spring Boot Tools</a> - <a href="https://marketplace.visualstudio.com/items/Pivotal.vscode-spring-boot/license">Eclipse Public License - v 1.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-initializr">Spring Initializr Java Support</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-spring-initializr/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-boot-dashboard">Spring Boot Dashboard</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-spring-boot-dashboard/license">MIT License</a></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle">Gradle for Java</a> - <a href="https://marketplace.visualstudio.com/items/vscjava.vscode-gradle/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=BazelBuild.vscode-bazel">Bazel</a> - <a href="https://marketplace.visualstudio.com/items/BazelBuild.vscode-bazel/license">Apache License 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-community-server-connector">Community Server Connectors</a> (for Apache Felix, Karaf, Tomcat, Jetty, etc) - <a href="https://marketplace.visualstudio.com/items/redhat.vscode-community-server-connector/license">Eclipse Public License - v 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-server-connector">Server Connector</a> (for Red Hat Servers, e.g. Wildfly) - <a href="https://marketplace.visualstudio.com/items/redhat.vscode-server-connector/license">Eclipse Public License - v 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.vscode-microprofile-pack">Extension Pack for MicroProfile</a> - <a href="https://marketplace.visualstudio.com/items/MicroProfile-Community.vscode-microprofile-pack/license">Apache License 2.0</a>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-microprofile">Tools for MicroProfile</a> - <a href="https://marketplace.visualstudio.com/items/redhat.vscode-microprofile/license">Apache License 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.mp-starter-vscode-ext">MicroProfile Starter</a> - <a href="https://marketplace.visualstudio.com/items/MicroProfile-Community.mp-starter-vscode-ext/license">Apache License 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.mp-rest-client-generator-vscode-ext">Generator for MicroProfile Rest Client</a> - <a href="https://marketplace.visualstudio.com/items/MicroProfile-Community.mp-rest-client-generator-vscode-ext/license">Eclipse Public License - v 2.0</a></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus">Quarkus</a> - <a href="https://marketplace.visualstudio.com/items/redhat.vscode-quarkus/license">Apache License 2.0</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=shengchen.vscode-checkstyle">CheckStyle</a> - <a href="https://marketplace.visualstudio.com/items/shengchen.vscode-checkstyle/license">LGPL-3.0 License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode">SonarLint</a> - <a href="https://marketplace.visualstudio.com/items/SonarSource.sonarlint-vscode/license">LGPL-3.0 License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GabrielBB.vscode-lombok">Lombok Annotations Support for VS Code</a> - <a href="https://marketplace.visualstudio.com/items/GabrielBB.vscode-lombok/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Rectcircle.vscode-p3c">Java P3C Checker</a> - <a href="https://marketplace.visualstudio.com/items/Rectcircle.vscode-p3c/license">MIT License</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dgileadi.java-decompiler">Java Decompiler</a> - 无开源许可证</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ithildir.java-properties">Java Properties</a> - <a href="https://marketplace.visualstudio.com/items/ithildir.java-properties/license">MIT License</a></li>
</ul>

<p>本文将介绍在使用 VSCode 开发 Java 项目时，如上扩展提供提供了那些能力、以及提供了那些配置项。</p>

<h2 id="特性速览">特性速览</h2>

<ul>
<li>帮助中心</li>
<li>JDK 配置</li>
<li>项目管理</li>
<li>构建工具</li>
<li>代码浏览和编辑（自动完成、代码片段、符号搜索、调转定义、查找引用、查找实现、调用层次、类型层次）</li>
<li>重构、代码生成和快速修复</li>
<li>格式化和 Lint</li>
<li>运行和调试</li>
<li>测试</li>
<li>框架支持

<ul>
<li>Java EE Server</li>
<li>Spring Boot</li>
<li>Quarkus 和 MicroProfile</li>
<li>Lombok</li>
</ul></li>
</ul>

<h2 id="快速开始">快速开始</h2>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/languages/java#_install-visual-studio-code-for-java">VSCode Docs - Language Java</a></p>
</blockquote>

<ul>
<li>未安装过 VSCode / JDK 的设备可以点击下方链接一键安装，VSCode、JDK 和 Java VSCode 扩展包

<ul>
<li><a href="https://aka.ms/vscode-java-installer-win">Install the Coding Pack for Java - Windows</a></li>
<li><a href="https://aka.ms/vscode-java-installer-mac">Install the Coding Pack for Java - macOS</a></li>
</ul></li>
<li>手动安装

<ul>
<li>安装 JDK 11 +，参见 <a href="https://jdk.java.net/">jdk.java.net</a></li>
<li><a href="https://code.visualstudio.com/download">安装 VSCode</a></li>
<li>打开 VSCode，安装 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> 扩展包</li>
</ul></li>
<li>使用 VSCode 打开一个 Maven / Gradle 项目</li>
<li>Enjoy it!</li>
</ul>

<h2 id="使用指南">使用指南</h2>

<h3 id="帮助中心">帮助中心</h3>

<blockquote>
<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> （v0.20.0）</p>
</blockquote>

<p>VSCode Java 提供了一个好用的可视化帮助中心，通过 <code>&gt;Java: help center</code> 命令可以进入。该页面提供：</p>

<ul>
<li>创建或打开一个项目</li>
<li>打开 Java Tour</li>
<li>General 标签页，提供一些常用的环境行管的配置</li>
<li>Spring 标签页，提供一些关于 Spring 的一些配置</li>
<li>Student 标签页，提供一些教学资料</li>
</ul>

<h3 id="jdk-配置">JDK 配置</h3>

<p>VSCode Java 支持 Java 1.5 到 Java 17 版本的 JDK。</p>

<p>在 VSCode Java 中， JDK 路径的查找规则为：</p>

<ul>
<li><code>java.jdt.ls.java.home</code> 配置项</li>
<li><code>java.home</code> (deprecated, use java.jdt.ls.java.home instead)</li>
<li><code>JDK_HOME</code> 环境变量</li>
<li><code>JAVA_HOME</code> 环境变量</li>
<li><code>PATH</code> 环境变量（路径应以包含 bin 文件夹的父文件夹结尾。示例路径：如果 <code>/usr/lib/jvm/java-11/bin</code> 中存在 bin，则使用 <code>/usr/lib/jvm/java-11</code>）</li>
</ul>

<p>此 JDK 将用于启动 Java Language Server。默认情况下，将用于编译项目。</p>

<p>如果需要针对不同的 JDK 版本编译项目，建议您在用户设置中配置 <code>java.configuration.runtimes</code> 属性，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;java.configuration.runtimes&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-1.8&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/path/to/jdk-8&#34;</span>,
        },
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-11&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/path/to/jdk-11&#34;</span>,
        },
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-14&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/path/to/jdk-14&#34;</span>,
            <span style="color:#f92672">&#34;default&#34;</span>: <span style="color:#66d9ef">true</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">打开独立</span> <span style="color:#960050;background-color:#1e0010">Java</span> <span style="color:#960050;background-color:#1e0010">文件时将使用默认运行时。</span>
        },
    ]
}</code></pre></div>
<p>⚠ 对于通用版本，仅仅在 <code>java.configuration.runtimes</code> 中定义 JavaSE-11 是不足以让 vscode-java 启动的，java.jdt.ls.java.home（或者它的任何替代环境变量）仍然需要指向一个有效的 JDK 11 位置。</p>

<p>更多参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/JDK-Requirements">Wiki</a></p>

<h3 id="项目管理">项目管理</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-project">VSCode Docs - Java - Project Manager</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-dependency">Project Manager for Java</a> （v0.18.9） 和 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> （v0.20.0）</p>
</blockquote>

<h4 id="支持的项目类型">支持的项目类型</h4>

<p>VSCode Java 支持如下几种类型的 Java 项目</p>

<ul>
<li>无构建工具的项目</li>
<li>Eclipse 项目</li>
<li>Maven 项目</li>
<li>Gradle 项目</li>
</ul>

<h4 id="project-view">Project View</h4>

<p>打开一个 Java 项目后，在 Explorer 视图，可以看到 Java Projects 视图。</p>

<ul>
<li>在该视图，是一个树结构，树的顶层是打开的工作空间包含的项目，每个项目包含如下内容

<ul>
<li>项目源码</li>
<li>JDK 源码</li>
<li>依赖的 Jar 包及其源码</li>
</ul></li>
</ul>

<p><img src="/image/vscode/java/projectmanager-overview.png" alt="image" /></p>

<p>在右上角的工具按钮栏提供了一些关于项目快捷操作。</p>

<ul>
<li><code>+</code> 快速创建一个 Java 项目（命令为：<code>&gt;Java: Create Java Project...</code>）</li>
</ul>

<p><img src="/image/vscode/java/projectmanager-createproject.png" alt="image" /></p>

<ul>
<li><code>|-&gt;</code> 导出到 Jar 文件，快速创建一个 Jar 包 （命令为：<code>&gt;java: export jar...</code>）</li>
</ul>

<p><img src="/image/vscode/java/exportjar.gif" alt="image" /></p>

<ul>
<li><code>🔄</code> 刷新项目视图 （命令为：<code>&gt;java: refresh</code>）</li>
<li><code>折叠</code> 折叠树</li>
<li><code>...</code> 溢出菜单

<ul>
<li>切换层级展示/平行展示</li>
<li>打开/关闭编辑器关联</li>
<li>构建工作空间</li>
<li>清理工作空间 （命令为：<code>&gt;java: clean java language server workspace</code>）</li>
<li>Configure Java Runtime （命令为：<code>&gt;Java: Configure Java Runtime</code>）</li>
<li>Configure Classpath （命令为：<code>&gt;Java: Configure classpath</code>）</li>
</ul></li>
</ul>

<p><img src="/image/vscode/java/overflow-button.png" alt="image" /></p>

<p>右键项目/包/类型，可以呼出上下文菜单。</p>

<p><img src="/image/vscode/java/context-menu.png" alt="image" /></p>

<h4 id="配置项目运行时">配置项目运行时</h4>

<p>通过 <code>java.configuration.runtimes</code> 配置项可以配置 VSCode Java 的运行时。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;java.configuration.runtimes&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-1.8&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/jdk1.8.0_201&#34;</span>
        },
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-11&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/jdk-11.0.3&#34;</span>,
            <span style="color:#f92672">&#34;sources&#34;</span> : <span style="color:#e6db74">&#34;/usr/local/jdk-11.0.3/lib/src.zip&#34;</span>,
            <span style="color:#f92672">&#34;javadoc&#34;</span> : <span style="color:#e6db74">&#34;https://docs.oracle.com/en/java/javase/11/docs/api&#34;</span>,
            <span style="color:#f92672">&#34;default&#34;</span>:  <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-12&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/jdk-12.0.2&#34;</span>
        },
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;JavaSE-13&#34;</span>,
            <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/jdk-13&#34;</span>
        }
    ]
}</code></pre></div>
<p>其中 <code>&quot;default&quot;: true</code> 表示该 JDK 将作为无构建工具项目的默认运行时。</p>

<p>另外，可以通过 <code>&gt;Java: Configure Java Runtime</code> 命令查看和配置当前项目运行时。</p>

<p><img src="/image/vscode/java/configure-project-runtime.png" alt="image" /></p>

<p>通过 <code>&gt;Java: Install New JDK</code> 可以快速下载一个 JDK 的安装包。</p>

<p><img src="/image/vscode/java/download-jdk.png" alt="image" /></p>

<p>通过 <code>&gt;Java: Configure Classpath</code> 可以快速配置无构建工具的项目的 Classpath，如果是 Maven 或者 Gradle 的项目，则是只读的。</p>

<p><img src="/image/vscode/java/configure-classpath.png" alt="image" /></p>

<p>当 VSCode Java 出现问题时，可以通过 <code>&gt;Java: Clean Java Language Server Workspace</code> 重新初始化工作空间。</p>

<h4 id="依赖管理">依赖管理</h4>

<ul>
<li>针对 Maven 项目，可以点击下图 <code>+</code> 快速搜索依赖，并添加到 pom.xml 文件中</li>
</ul>

<p><img src="/image/vscode/java/add-maven-dependency.png" alt="image" /></p>

<ul>
<li>针对 无构建工具项目，可以点击下图 <code>+</code> / <code>-</code> 快速添加删除本地磁盘的 jar 包到 classpath 中（本质上是修改 <code>java.project.referencedLibraries</code> 配置项）</li>
</ul>

<p><img src="/image/vscode/java/manage-referenced-libraries.png" alt="image" /></p>

<ul>
<li><p>关于 <code>java.project.referencedLibraries</code> 配置项的一些例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">简单添加</span>
<span style="color:#e6db74">&#34;java.project.referencedLibraries&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
<span style="color:#e6db74">&#34;library/**/*.jar&#34;</span>,
<span style="color:#e6db74">&#34;/home/username/lib/foo.jar&#34;</span>
]
<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">包括和排除以及源代码路径</span>
<span style="color:#e6db74">&#34;java.project.referencedLibraries&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
<span style="color:#f92672">&#34;include&#34;</span>: [
    <span style="color:#e6db74">&#34;library/**/*.jar&#34;</span>,
    <span style="color:#e6db74">&#34;/home/username/lib/foo.jar&#34;</span>
],
<span style="color:#f92672">&#34;exclude&#34;</span>: [
    <span style="color:#e6db74">&#34;library/sources/**&#34;</span>
],
<span style="color:#f92672">&#34;sources&#34;</span>: {
    <span style="color:#f92672">&#34;library/bar.jar&#34;</span>: <span style="color:#e6db74">&#34;library/sources/bar-src.jar&#34;</span>
}
}</code></pre></div></li>
</ul>

<h4 id="语言服务器模式">语言服务器模式</h4>

<p>通过 <code>java.server.launchMode</code> 可以配置 VSCode Java 的语言服务器的模式</p>

<ul>
<li><code>Hybrid</code> 混合模式 （默认）先以轻量级模式打开工作区。如果您的工作区包含未解析的 Java 项目，系统将询问您是否切换到标准模式。如果选择 “稍后”，它将保持轻量模式。您可以单击状态栏上的服务器模式图标手动切换到标准模式。</li>
<li><code>Standard</code> 标准模式</li>
<li><code>LightWeight</code> 轻量级模式（消耗资源小，但是能力有限）</li>
</ul>

<h4 id="状态栏">状态栏</h4>

<p>状态栏使用不同的图标指示当前工作区处于哪种模式。</p>

<ul>
<li>🚀 - 轻量级模式</li>
<li>🔄 - 正在以标准模式打开的工作区</li>
<li>👍 - 标准模式</li>
</ul>

<p>切换到标准模式方法如下：</p>

<p><img src="/image/vscode/java/switch-to-standard.gif" alt="image" /></p>

<h3 id="构建工具">构建工具</h3>

<h4 id="无构建工具项目">无构建工具项目</h4>

<p>VSCode Java 支持无构建工具的 Java 项目，下方有一个例子（通过 <code>&gt;Java: Create Java Project...</code> 命令）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── .vscode
│   └── settings.json
├── README.md
├── bin
│   └── App.class
├── lib
└── src
    └── App.java</pre></div>
<p>其中 <code>.vscode/settings.json</code> 内容为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;java.project.sourcePaths&#34;</span>: [<span style="color:#e6db74">&#34;src&#34;</span>],
    <span style="color:#f92672">&#34;java.project.outputPath&#34;</span>: <span style="color:#e6db74">&#34;bin&#34;</span>
}</code></pre></div>
<h4 id="maven">Maven</h4>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-build#_maven">VSCode Docs - Java - Build Tools</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-maven">Maven for Java</a> （v0.34.2）</p>
</blockquote>

<ul>
<li>使用 VSCode 直接打开包含 pom.xml 的目录，即可导入 Maven 项目，并激活 Maven 扩展</li>
<li>Maven 项目中可以键入未在 POM 中声明的依赖的符号时，Hover 在符号上，点击 Resolve unknown type 可以快速搜索并将依赖添加到 POM 中</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-resolve-unknown-type.mp4" type="video/mp4">
</video>

<ul>
<li>提供对 <code>pom.xml</code> 的智能提示和检查</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-pom-editing.mp4" type="video/mp4">
</video>

<ul>
<li>Effective POM （合并父 POM，含义参见：<a href="https://stackoverflow.com/questions/26114768/what-are-the-difference-between-pom-xml-and-effective-pom-in-apache-maven/26114868">stackoverflow</a>）</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-effective-pom.mp4" type="video/mp4">
</video>

<ul>
<li>通过 <code>&gt;Maven: Add a Dependency</code> 命令，交互式的搜索添加依赖（同样可以同通过 Java Projects 视图，Maven Dependencies 子树 <code>+</code> 号按钮添加依赖）</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-add-dependency.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-add-dependency-2.mp4" type="video/mp4">
</video>

<ul>
<li>通过资源管理器侧边栏 -&gt; Maven 区域右击项目，可以快速展示依赖树视图</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-dependency-tree.mp4" type="video/mp4">
</video>

<ul>
<li>执行 Maven 命令和目标：资源管理器侧边栏 -&gt; Maven 区域，右击项目，可以运行目标和命令</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-run.mp4" type="video/mp4">
</video>

<ul>
<li>可以通过如下方式查看 maven 命令的执行历史

<ul>
<li><code>&gt;Maven: History</code> 命令</li>
<li>资源管理器侧边栏 -&gt; Maven 区域，右击项目，选择 History 菜单项</li>
</ul></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-history.mp4" type="video/mp4">
</video>

<ul>
<li>通过 <code>maven.terminal.favorites</code> 配置项可以定义一些常用 maven 目标和命令的快捷方式。然后在，资源管理器侧边栏 -&gt; Maven 区域，右击项目，选择 Favorites&hellip; 菜单项</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-favorite-command.mp4" type="video/mp4">
</video>

<ul>
<li>资源管理器侧边栏 -&gt; Maven 区域，展开项目，选择 Plugin，即可浏览所有 Plugin 提供的目标</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-plugin-goal.mp4" type="video/mp4">
</video>

<ul>
<li>调试项目中 Maven Plugin，向工作空间同时加入普通项目和 Maven Plugin 项目，在 Maven Plugin 项目中添加断点，资源管理器侧边栏 -&gt; Maven 区域，展开项目，选择 Plugin，右击选择 Debug，即可快速 Debug Maven Plugin 项目</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/debug-maven-plugin-goals.mp4" type="video/mp4">
</video>

<ul>
<li>根据 Maven Archetype 创建一个 Java 项目，入口有如下几个

<ul>
<li>资源管理器侧边栏 -&gt; Maven 区域，标题区域的 <code>+</code> 号</li>
<li>通过 <code>&gt;Java: Create Java Project</code> 命名</li>
<li>资源管理器在一个目录上右击，选择： <code>Create Maven Project</code> 菜单项</li>
</ul></li>
</ul>

<p><img src="/image/vscode/java/create-maven-project.png" alt="image" /></p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-archetype-command.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/maven-archetype-folder.mp4" type="video/mp4">
</video>

<h4 id="gradle">Gradle</h4>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-build#_gradle">VSCode Docs - Java - Build Tools</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle">Gradle for Java</a>（v3.9.0）</p>
</blockquote>

<p>暂不包括 Android。该扩展为 Gradle 构建提供了一个可视化界面（侧边栏），可以使用此界面查看 Gradle 任务和项目依赖项，或者将 Gradle 任务作为 VSCode Task 运行。该扩展还提供了更好的 Gradle 构建配置文件编写体验，包括语法突出显示、错误报告和自动完成。</p>

<ul>
<li>使用 VSCode 直接打开包含 Gradle 配置文件的目录，即可导入 Gradle 项目，并激活 Gradle 扩展，然后即可点击 Activity Bar 的 Gradle 图标打开 Gradle 界面</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/gradle-tasks.mp4" type="video/mp4">
</video>

<ul>
<li>Gradle 界面提供了 Pinned Task 视图。通过在 Gradle 界面浏览视图，右击一个任务，选择 Pin Task 菜单项即可 Pin Task 到 Pinned Task 视图中</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/gradle-pinned-recent-tasks.mp4" type="video/mp4">
</video>

<ul>
<li>Gradle 页面通过浏览视图可以查看依赖项</li>
</ul>

<p><img src="/image/vscode/java/gradle-dependencies.png" alt="image" /></p>

<ul>
<li>Gradle 页面 还可以管理 Gradle Deamon</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/gradle-daemons.mp4" type="video/mp4">
</video>

<ul>
<li>Gradle 扩展提供了 gradle 配置文件的语法高亮、大纲、错误报告、智能提示（包括依赖和版本列表的提示）的能力</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/gradle-auto-completion.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/gradle-dependency-completion.mp4" type="video/mp4">
</video>

<p>更多参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/Gradle-Support">Wiki</a></p>

<h4 id="bazel">Bazel</h4>

<p>参见：<a href="https://marketplace.visualstudio.com/items?itemName=BazelBuild.vscode-bazel">Bazel</a></p>

<h3 id="代码浏览和编辑">代码浏览和编辑</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-editing">VSCode Docs - Java - Navigate and Edit</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a> （v1.2.0） 和 <a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">Visual Studio IntelliCode</a> （v1.2.15）</p>
</blockquote>

<ul>
<li>搜索<strong>工作空间</strong>类/函数/变量等符号： <code>⌘T</code> 或 <code>⌘P</code> 输入 <code>#</code></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/search-in-workspace.mp4" type="video/mp4">
</video>

<ul>
<li>搜索<strong>当前文件</strong>类/函数/变量等符号： <code>⌘P</code> 输入 <code>@</code></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/search-in-file.mp4" type="video/mp4">
</video>

<ul>
<li>Peek 定义： <code>⌥F12</code> 或 光标处右键点选 <code>Peek Definition</code></li>
<li>跳转到定义： <code>F12</code> 或 光标处右键点选 <code>Go to Definition</code>，</li>
<li>快速跳转到函数的父类实现： 鼠标 hover 在函数上，点击 Go to Super Implementation</li>
</ul>

<p><img src="/image/vscode/java/goto-super.png" alt="image" /></p>

<ul>
<li>Peek 调用层次： 函数上右击点选 <code>Peek &gt; Peek Call Hierarchy</code></li>
</ul>

<p><img src="/image/vscode/java/call-hierarchy.png" alt="image" /></p>

<ul>
<li>展示调用层次： 函数上右击点选 <code>Show Call Hierarchy</code></li>
</ul>

<p><img src="/image/vscode/java/call-hierarchy.gif" alt="image" /></p>

<ul>
<li>类型层次： 类和接口上右击点选 <code>Show Type Hierarchy</code></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/type-hierarchy.mp4" type="video/mp4">
</video>

<ul>
<li><p>通过命令查看类型关系信息</p>

<ul>
<li><code>&gt;Types: Show supertypes</code> 查找当前类型的父类和实现的接口</li>
<li><code>&gt;Types: Show subtypes</code> 查找当前类型的的子类或者实现</li>
<li><code>&gt;Types: Show type hierarchy</code> 查看类型层次</li>
</ul></li>

<li><p>折叠</p></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/folding-range.mp4" type="video/mp4">
</video>

<ul>
<li>智能选择

<ul>
<li>扩大选择范围： <code>⌃⇧⌘→</code></li>
<li>缩小选择范围： <code>⌃⇧⌘←</code></li>
</ul></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/smart-selection.mp4" type="video/mp4">
</video>

<ul>
<li>语义化高亮（根据语义信息更好的对代码单词进行高亮），更多参见 <a href="https://github.com/redhat-developer/vscode-java/wiki/Semantic-Highlighting">Wiki</a></li>
</ul>

<p><img src="/image/vscode/java/semantic-highlighting.png" alt="image" /></p>

<ul>
<li>搜索 Spring boot 相关符号： <code>⌘P</code> 输入 （更多参见 <a href="#Spring">Spring</a>）

<ul>
<li><code>@/</code> 展示所有已定义的请求映射（映射路径、请求方法、源代码位置）</li>
<li><code>@+</code> 展示所有定义的 bean （bean 名, bean 类型, 源代码位置）</li>
<li><code>@&gt;</code> shows all functions (prototype implementation)</li>
<li><code>@</code> 展示代码中的所有 Spring 注解</li>
</ul></li>
</ul>

<p><img src="/image/vscode/java/spring-navigation.png" alt="image" /></p>

<ul>
<li>自动完成</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/code-editing.mp4" type="video/mp4">
</video>

<ul>
<li>快速修复</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/quick-fix.mp4" type="video/mp4">
</video>

<ul>
<li>智能感知（能提供 Eclipse 同样级别的智能提示，配合 <a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode">Visual Studio IntelliCode</a>，可以提供基于 AI 的对智能提示进行排序的能力，优先展示可能性更大的选项）</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/intellicode.mp4" type="video/mp4">
</video>

<ul>
<li>创建文件</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/create-new-file.mp4" type="video/mp4">
</video>

<ul>
<li>代码片段</li>
</ul>

<p><img src="/image/vscode/java/code-snippet.png" alt="image" /></p>

<h3 id="重构-代码生成和快速修复">重构、代码生成和快速修复</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-refactoring">VSCode Docs - Java - Refactoring</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a> （v1.2.0）</p>
</blockquote>

<h4 id="重构">重构</h4>

<p>编辑器右键，选择 Refactor&hellip; （重构）</p>

<ul>
<li><p>赋值给变量</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span>Arrays.<span style="color:#a6e22e">asList</span>(<span style="color:#e6db74">&#34;apple&#34;</span>, <span style="color:#e6db74">&#34;lemon&#34;</span>, <span style="color:#e6db74">&#34;banana&#34;</span>);
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span>List<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">fruits</span> <span style="color:#f92672">=</span> Arrays.<span style="color:#a6e22e">asList</span>(<span style="color:#e6db74">&#34;apple&#34;</span>, <span style="color:#e6db74">&#34;lemon&#34;</span>, <span style="color:#e6db74">&#34;banana&#34;</span>);</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/assign-to-field.mp4" type="video/mp4">
</video>

<ul>
<li><p>匿名类转换为内部类</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Clazz {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Interface</span> method() {
<span style="color:#66d9ef">final</span> <span style="color:#a6e22e">boolean</span> isValid <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Interface() {
  <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> isValid() {
    <span style="color:#66d9ef">return</span> isValid;
  }
};
}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Clazz {
<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyInterface</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Interface</span> {
<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isValid</span>;

<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">MyInterface</span>(<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isValid</span>) {
  <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">isValid</span> <span style="color:#f92672">=</span> isValid;
}

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> isValid() {
  <span style="color:#66d9ef">return</span> isValid;
}
}

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Interface</span> method() {
<span style="color:#66d9ef">final</span> <span style="color:#a6e22e">boolean</span> isValid <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> MyInterface(isValid);
}
}</code></pre></div></li>

<li><p>转换为匿名类创建</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
Runnable <span style="color:#a6e22e">runnable</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">-&gt;</span> {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>};
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
Runnable <span style="color:#a6e22e">runnable</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Runnable() {
<span style="color:#a6e22e">@Override</span>
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> run() {
  <span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
};
}</code></pre></div></li>

<li><p>转换为增强的 for 循环</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> order(String[] <span style="color:#a6e22e">books</span>) {
<span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> 0; i <span style="color:#f92672">&lt;</span> books.<span style="color:#a6e22e">length</span>; i<span style="color:#f92672">++</span>) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> order(String[] <span style="color:#a6e22e">books</span>) {
<span style="color:#66d9ef">for</span> (String <span style="color:#a6e22e">book</span> <span style="color:#f92672">:</span> books) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/convert-for-loop.mp4" type="video/mp4">
</video>

<ul>
<li><p>转换为 lambda 表达式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
Runnable <span style="color:#a6e22e">runnable</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Runnable(){
<span style="color:#a6e22e">@Override</span>
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> run() {
  <span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
};
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
Runnable <span style="color:#a6e22e">runnable</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">-&gt;</span> {
  <span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>};
}</code></pre></div></li>

<li><p>转换为静态导入</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">junit</span>.<span style="color:#a6e22e">Assert</span>;
<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> test() {
Assert.<span style="color:#a6e22e">assertEquals</span>(expected, actual);
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#f92672">import</span> <span style="color:#a6e22e">static</span> org.<span style="color:#a6e22e">junit</span>.<span style="color:#a6e22e">Assert</span>.<span style="color:#a6e22e">assertEquals</span>;
<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> test() {
assertEquals(expected, actual);
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/convert-static-imports.mp4" type="video/mp4">
</video>

<ul>
<li><p>提取到常数</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">double</span> getArea(<span style="color:#66d9ef">double</span> <span style="color:#a6e22e">r</span>) {
<span style="color:#66d9ef">return</span> 3.<span style="color:#a6e22e">14</span> <span style="color:#f92672">*</span> r <span style="color:#f92672">*</span> r;
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">double</span> PI <span style="color:#f92672">=</span> 3.<span style="color:#a6e22e">14</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">double</span> getArea(<span style="color:#66d9ef">double</span> <span style="color:#a6e22e">r</span>) {
<span style="color:#66d9ef">return</span> PI <span style="color:#f92672">*</span> r <span style="color:#f92672">*</span> r;
}</code></pre></div></li>

<li><p>提取到字段</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Square</span> {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> calculateArea() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">area</span> <span style="color:#f92672">=</span> height <span style="color:#f92672">*</span> width;
}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Square</span> {
<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">int</span> area;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> calculateArea() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
area <span style="color:#f92672">=</span> height <span style="color:#f92672">*</span> width;
}
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/extract-field.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/convert-field.mp4" type="video/mp4">
</video>

<ul>
<li><p>提取到方法</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">area</span> <span style="color:#f92672">=</span> height <span style="color:#f92672">*</span> width;
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">area</span> <span style="color:#f92672">=</span> getArea(height, width);
}

<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">int</span> getArea(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span>, <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span>) {
<span style="color:#66d9ef">return</span> height <span style="color:#f92672">*</span> width;
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/refactor.mp4" type="video/mp4">
</video>

<ul>
<li><p>提取到函数局部变量</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">if</span> (platform.<span style="color:#a6e22e">equalsIgnoreCase</span>(<span style="color:#e6db74">&#34;MAC&#34;</span>)) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isMac</span> <span style="color:#f92672">=</span> platform.<span style="color:#a6e22e">equalsIgnoreCase</span>(<span style="color:#e6db74">&#34;MAC&#34;</span>);
<span style="color:#66d9ef">if</span> (isMac) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/extract-local-variable.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/extract-rename.mp4" type="video/mp4">
</video>

<ul>
<li><p>内联常量</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">double</span> PI <span style="color:#f92672">=</span> 3.<span style="color:#a6e22e">14</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">double</span> getArea(<span style="color:#66d9ef">double</span> <span style="color:#a6e22e">r</span>) {
<span style="color:#66d9ef">return</span> PI <span style="color:#f92672">*</span> r <span style="color:#f92672">*</span> r;
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">double</span> PI <span style="color:#f92672">=</span> 3.<span style="color:#a6e22e">14</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">double</span> getArea(<span style="color:#66d9ef">double</span> <span style="color:#a6e22e">r</span>) {
<span style="color:#66d9ef">return</span> 3.<span style="color:#a6e22e">14</span> <span style="color:#f92672">*</span> r <span style="color:#f92672">*</span> r;
}</code></pre></div></li>

<li><p>内联局部变量</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isMac</span> <span style="color:#f92672">=</span> platform.<span style="color:#a6e22e">equalsIgnoreCase</span>(<span style="color:#e6db74">&#34;MAC&#34;</span>);
<span style="color:#66d9ef">if</span> (isMac) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">if</span> (platform.<span style="color:#a6e22e">equalsIgnoreCase</span>(<span style="color:#e6db74">&#34;MAC&#34;</span>)) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}</code></pre></div></li>

<li><p>内联方法</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">area</span> <span style="color:#f92672">=</span> getArea(height, width);
}

<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">int</span> getArea(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span>, <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span>) {
<span style="color:#66d9ef">return</span> height <span style="color:#f92672">*</span> width;
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method() {
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">height</span> <span style="color:#f92672">=</span> 1;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">width</span> <span style="color:#f92672">=</span> 2;
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">area</span> <span style="color:#f92672">=</span> height <span style="color:#f92672">*</span> width;
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/inline.mp4" type="video/mp4">
</video>

<ul>
<li><p>条件翻转</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">value</span>) {
<span style="color:#66d9ef">if</span> (value <span style="color:#f92672">&gt;</span> 5 <span style="color:#f92672">&amp;&amp;</span> value <span style="color:#f92672">&lt;</span> 15) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">value</span>) {
<span style="color:#66d9ef">if</span> (value <span style="color:#f92672">&lt;=</span> 5 <span style="color:#f92672">||</span> value <span style="color:#f92672">&gt;=</span> 15) {
<span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}</code></pre></div></li>

<li><p>反转 bool 表达式局部变量</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">value</span>) {
<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">valid</span> <span style="color:#f92672">=</span> value <span style="color:#f92672">&gt;</span> 5 <span style="color:#f92672">&amp;&amp;</span> value <span style="color:#f92672">&lt;</span> 15;
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">value</span>) {
<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">notValid</span> <span style="color:#f92672">=</span> value <span style="color:#f92672">&lt;=</span> 5 <span style="color:#f92672">||</span> value <span style="color:#f92672">&gt;=</span> 15;
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/invert-variable.mp4" type="video/mp4">
</video>

<ul>
<li><p>移动</p>

<ul>
<li>将类移动到另一个包</li>
<li>将静态或实例方法移动到另一个类</li>

<li><p>将内部类移动到新文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Office {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">main</span>(String[] <span style="color:#a6e22e">args</span>) {
print();
}

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print</span>() {
System.<span style="color:#a6e22e">out</span>.<span style="color:#a6e22e">println</span>(<span style="color:#e6db74">&#34;This is printer&#34;</span>);
}

<span style="color:#66d9ef">static</span> <span style="color:#a6e22e">class</span> Printer { }
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Office {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">main</span>(String[] <span style="color:#a6e22e">args</span>) {
Printer.<span style="color:#a6e22e">print</span>();
}

<span style="color:#66d9ef">static</span> <span style="color:#a6e22e">class</span> Printer {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print</span>() {
System.<span style="color:#a6e22e">out</span>.<span style="color:#a6e22e">println</span>(<span style="color:#e6db74">&#34;This is printer&#34;</span>);
}
}
}</code></pre></div></li>
</ul></li>
</ul>

<p>如果静态方法在另一个类中比在自己的类中使用得更多，则在静态方法上移动重构。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/move-static-method.mp4" type="video/mp4">
</video>

<p>将一个类移动到另一个包。目前，文件资源管理器不支持移动重构。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/move-class.mp4" type="video/mp4">
</video>

<p>将内部类移动到新文件。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/move-inner-type.mp4" type="video/mp4">
</video>

<ul>
<li><p>重命名 <code>F2</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Foo {
<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> myMethod() {
Foo <span style="color:#a6e22e">myClass</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Foo();
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Bar {
<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> myMethod() {
Bar <span style="color:#a6e22e">myClass</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Bar();
}</code></pre></div></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/rename.mp4" type="video/mp4">
</video>

<p>文件夹和文件的文件资源管理器也支持重命名重构。请求更改后，将提供受影响文件的预览，您可以决定如何应用这些更改。</p>

<p><img src="/image/vscode/java/rename-explorer.gif" alt="image" /></p>

<ul>
<li><p>将具体的类型更改为 var 类型</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span>String <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span>var <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;</code></pre></div></li>

<li><p>将 var 类型更改为具体类型</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span>var <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span>String <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;</code></pre></div></li>
</ul>

<h4 id="source-actions">Source Actions</h4>

<ul>
<li>生成构造函数</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-constructor.mp4" type="video/mp4">
</video>

<ul>
<li>生成委托方法（生成对环境变量方法的包装方法）</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-delegate-methods.mp4" type="video/mp4">
</video>

<ul>
<li>覆盖/实现方法</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/override-implement-methods.mp4" type="video/mp4">
</video>

<ul>
<li>组织导入</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/resolve-ambiguous-imports.mp4" type="video/mp4">
</video>

<ul>
<li>生成 Getters 和 Setters 方法</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/advancedgettersetter.mp4" type="video/mp4">
</video>

<ul>
<li>生成 <code>hashCode()</code> 和 <code>equals()</code> 方法

<ul>
<li>如果使用的 Java 7+，可以将 <code>java.codeGeneration.hashCodeEquals.useJava7Objects</code> 设置为 <code>true</code> 以生成调用 Objects.hash 和 Objects.equals 的较短代码。</li>
<li>还可以将 <code>java.codeGeneration.hashCodeEquals.useInstanceof</code> 设置为 <code>true</code> 以使用 <code>instanceOf</code> 运算符来检查对象类型，而不是调用 <code>Object.getClass()</code>。</li>
</ul></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-hashcode-equals.mp4" type="video/mp4">
</video>

<ul>
<li>生成 <code>toString()</code> 方法</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-tostring.mp4" type="video/mp4">
</video>

<ul>
<li><p>尽可能将修饰符更改为 final</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Before
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Clazz {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">value</span>) {
<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">notValid</span> <span style="color:#f92672">=</span> value <span style="color:#f92672">&gt;</span> 5;
<span style="color:#66d9ef">if</span> (notValid) {
  <span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
}
<span style="color:#75715e">// After
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> Clazz {
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> method(<span style="color:#66d9ef">final</span> <span style="color:#a6e22e">int</span> value) {
<span style="color:#66d9ef">final</span> <span style="color:#a6e22e">boolean</span> notValid <span style="color:#f92672">=</span> value <span style="color:#f92672">&gt;</span> 5;
<span style="color:#66d9ef">if</span> (notValid) {
  <span style="color:#75715e">// do something
</span><span style="color:#75715e"></span>}
}
}</code></pre></div></li>
</ul>

<h4 id="其他">其他</h4>

<ul>
<li>修复不可访问的引用</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/fix-non-access-reference.mp4" type="video/mp4">
</video>

<ul>
<li>创建不存在的包</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/create-non-exist-package.mp4" type="video/mp4">
</video>

<ul>
<li>更多

<ul>
<li>创建无法解决的的类型</li>
<li>删除 final 修饰符</li>
<li>删除不必要的强制转换</li>
<li>删除冗余接口</li>
<li>在 switch 语句中添加缺少的 case 标签</li>
<li>Jump to definition on break/continue</li>
<li>Correct access to static elements</li>
</ul></li>
</ul>

<h4 id="代码生成模板配置">代码生成模板配置</h4>

<p><code>cmd +,</code> 打开设置，输入 <code>java.template</code> 过滤配置，即可看到相关配置。</p>

<p>模板配置支持的变量列表参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/Predefined-Variables-for-Java-Template-Snippets">Wiki</a></p>

<h3 id="格式化和-lint">格式化和 Lint</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-linting">VSCode Docs - Java - Linting</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a> （v1.2.0）、<a href="https://marketplace.visualstudio.com/items?itemName=shengchen.vscode-checkstyle">Checkstyle for Java</a> （v1.4.1）、<a href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode">SonarLint</a> （v3.1.0）、<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> （v0.20.0）</p>
</blockquote>

<h4 id="格式化">格式化</h4>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/formatting.mp4" type="video/mp4">
</video>

<ul>
<li><p>通过 <code>&quot;java.format.settings.url&quot;</code> 配置项，配置 format profile (Eclipse Schema)</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;java.format.settings.url&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml&#34;</span><span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;java.format.settings.profile&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;GoogleStyle&#34;</span><span style="color:#960050;background-color:#1e0010">,</span></code></pre></div></li>

<li><p>通过 <code>&gt;Java: Open Java Formatter Settings with Preview</code> 命令可以拉起 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a>  扩展提供了可视化的 VSCode 格式化配置页面。（将创建配置文件位于 <code>.vscode/java-formatter.xml</code> 文件），可以实时预览配置效果。支持 <code>cmd + z</code> 撤销上一步的更改，通过 <code>cmd + s</code> 保存配置到文件中。</p></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/formatting-editing.mp4" type="video/mp4">
</video>

<p>更多参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/Formatter-settings">Wiki</a></p>

<h4 id="lint">Lint</h4>

<p>Java 比较主流的 Lint 工具在 VSCode 上有</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=shengchen.vscode-checkstyle">Checkstyle for Java</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode">SonarLint</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Rectcircle.vscode-p3c">Java P3C Checker</a></li>
</ul>

<p>细节参见：</p>

<ul>
<li><a href="https://code.visualstudio.com/docs/java/java-linting#_sonarlint">VSCode Docs - Java - Linting</a></li>
<li><a href="/posts/java-code-style-check-implement">Java 代码风格样式检查落地</a></li>
</ul>

<h3 id="运行和调试">运行和调试</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-debugging">VSCode Docs - Java - Debugging</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug">Debugger for Java</a> （v0.38.0）</p>
</blockquote>

<h4 id="配置文件">配置文件</h4>

<p>默认情况下，调试器将通过自动查找主类并在内存中生成默认启动配置来启动您的应用程序，开箱即用地运行。如果想自定义和保留的启动配置，您可以在运行和调试视图中选择创建 launch.json 文件链接。</p>

<p>launch.json 文件位于工作区（项目根文件夹）中的 .vscode 文件夹中。</p>

<h4 id="启动调试和或运行">启动调试和或运行</h4>

<p>可以通过如下位置启动运行和调试。</p>

<ul>
<li>代码文件 main 函数上方的 <code>Run | Debug</code> <a href="https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup">CodeLens</a></li>
</ul>

<p><img src="/image/vscode/java/java-codelens.png" alt="image" /></p>

<ul>
<li>从编辑器菜单运行，从编辑器顶部标题栏中选择 Run Java 或 Debug Java 菜单。</li>
</ul>

<p><img src="/image/vscode/java/run-menu.png" alt="image" /></p>

<ul>
<li>按 F5 快捷键启动，更多参见：<a href="https://code.visualstudio.com/docs/editor/debugging">Debugging in VS Code</a></li>
</ul>

<h4 id="调试和运行单文件">调试和运行单文件</h4>

<p>除了支持调试由构建工具管理的 Java 项目外，VS Code 还支持在没有任何项目的情况下调试单个 Java 文件。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/single-file-debugging.mp4" type="video/mp4">
</video>

<h4 id="标准输入">标准输入</h4>

<p>默认 Java 调试不支持读取标准输入，如果需要读取标准输入，可以通过 <code>java.debug.settings.console</code> 配置项或者 <code>launch.json</code> 的 <code>console</code> 字段来设置。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/launch-in-terminal.mp4" type="video/mp4">
</video>

<h4 id="断点">断点</h4>

<p>VSCode Java 支持如下断点</p>

<ul>
<li>常规行断点，通过 F9 或者点击源代码编辑器左侧的装订栏添加</li>
<li>条件断点，右击源代码编辑器左侧的装订栏，选择条件断点</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/conditional-bp.mp4" type="video/mp4">
</video>

<ul>
<li>数据改变断点，当变量更改其值时，您可以让调试器中断。数据断点只能在调试会话中设置。这意味着您需要启动应用程序并首先在常规断点处中断。然后，您可以在 VARIABLES 视图中选择一个字段并设置一个数据断点。</li>
</ul>

<p><img src="/image/vscode/java/data-breakpoint.png" alt="image" /></p>

<ul>
<li>Logpoints，Java 调试器也支持日志点。日志点允许在不编辑代码的情况下将输出发送到调试控制台。它们与断点不同，因为它们不会停止应用程序的执行流程。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/logpoints.mp4" type="video/mp4">
</video>

<h4 id="表达式执行">表达式执行</h4>

<p>在 Watch 视图可以观察单个表达式 或者 Debug Console 视图可以执行一些函数。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/expression-evaluation.mp4" type="video/mp4">
</video>

<h4 id="代码热替换">代码热替换</h4>

<p>调试器支持的另一个高级功能是“热代码”替换。热代码替换 (HCR) 是一种调试技术，其中 Java 调试器通过调试通道将类更改传输到另一个 Java 虚拟机 (JVM)。 HCR 促进实验开发并促进迭代试错编码。使用这个新功能，您可以在开发环境中启动调试会话并更改 Java 文件，并且调试器将替换正在运行的 JVM 中的代码。不需要重新启动，这就是它被称为“热”的原因。下面是一个说明如何在 VS Code 中将 HCR 与 Debugger for Java 一起使用。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/hcr.mp4" type="video/mp4">
</video>

<p>您可以使用调试设置 <code>java.debug.settings.hotCodeReplace</code> 来控制如何触发热代码替换。可能的设置值为：</p>

<ul>
<li><code>manual</code> - 单击工具栏以应用更改（默认）。</li>
<li><code>auto</code> - 编译后自动应用更改。</li>
<li><code>never</code> - 禁用热代码替换。</li>
</ul>

<h4 id="步进过滤">步进过滤</h4>

<p>扩展支持分步过滤器，以过滤掉您在调试时不希望看到或单步执行的类型。使用此功能，您可以将包配置为在您的 launch.json 中进行过滤（<code>stepFilters</code> 字段），以便在您逐步执行时跳过它们。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/step-filter.mp4" type="video/mp4">
</video>

<h4 id="调试会话配置">调试会话配置</h4>

<p>有许多选项和设置可用于配置调试器。例如，使用启动选项可以轻松配置 JVM 参数和环境变量。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/launch-configuration.mp4" type="video/mp4">
</video>

<p>配置的详细说明可以参见：<a href="https://github.com/microsoft/vscode-java-debug/blob/main/Configuration.md">Github 文档</a></p>

<h5 id="launch">Launch</h5>

<ul>
<li><code>mainClass</code> (required) - 类全限定名 (for example <code>[java module name/]com.xyz.MainApp</code>) 或者 Java Main 文件路径。</li>
<li><code>args</code> - 传递给进程的命令行参数。可以使用 <code>&quot;${command:SpecifyProgramArgs}&quot;</code> 调用一个外部命令，并将其返回值作为参数传递给进程。</li>
<li><code>sourcePaths</code> - 程序的额外源目录。默认情况下，调试器从项目设置中查找源代码。此选项允许调试器在额外的目录中查找源代码。</li>
<li><code>modulePaths</code> - 用于启动 JVM 的模块路径。如果未指定，调试器将自动从当前项目解析。</li>
<li><code>classPaths</code> - 用于启动 JVM 的类路径。如果未指定，调试器将自动从当前项目解析。</li>
<li><code>encoding</code> - JVM 的 <code>file.encoding</code>设置。如果未指定，将使用 &ldquo;UTF-8&rdquo;。可能的值可以在 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">Supported Encodings</a> 中找到。</li>
<li><code>vmArgs</code> - JVM 的额外选项和系统属性（例如 <code>-Xms&lt;size&gt;</code> <code>-Xmx&lt;size&gt;</code> <code>-D&lt;name&gt;=&lt;value&gt;</code>），它接受字符串或字符串数组。</li>
<li><code>projectName</code> - 调试器在其中搜索类的首选项目。不同的项目中可能存在重复的类名。当调试器在启动程序时查找指定的主类时，此设置也有效。当工作区具有多个 Java 项目时，这是必需的，否则表达式计算和条件断点可能不起作用。</li>
<li><code>cwd</code> - 程序的工作目录。默认为 <code>${workspaceFolder}</code>。</li>
<li><code>env</code> - 程序的额外环境变量。</li>
<li><code>envFile</code> - 包含环境变量定义的文件的绝对路径。</li>
<li><code>stopOnEntry</code> - 启动后自动暂停程序。</li>
<li><code>console</code> - 用于启动程序的指定控制台。如果未指定，请使用由 <code>java.debug.settings.console</code> 用户设置指定的控制台。

<ul>
<li><code>internalConsole</code> - VS 代码调试控制台（不支持输入流）。</li>
<li><code>integratedTerminal</code> - VS 代码集成终端。</li>
<li><code>externalTerminal</code> - 可在用户设置中配置的外部终端。</li>
</ul></li>
<li><code>shortenCommandLine</code> - 当项目具有较长的类路径或较大的 VM 参数时，用于启动程序的命令行可能会超过操作系统允许的最大命令行字符串限制。此配置项目提供了多种缩短命令行的方法。默认为 <code>auto</code>。

<ul>
<li><code>none</code> - 使用标准命令行 <code>java [options] classname [args]</code> 启动程序。.</li>
<li><code>jarmanifest</code> - 将类路径参数生成到临时类路径.jar文件，并使用命令行 <code>java -cp classpath.jar类名 [args]</code> 启动程序。</li>
<li><code>argfile</code> - 生成临时参数文件的类路径参数，并使用命令行 <code>java @argfile [args]</code> 启动程序。此值仅适用于 Java 9 及更高版本。</li>
<li><code>auto</code> - 自动检测命令行长度并确定是否通过适当的方法缩短命令行。</li>
</ul></li>
<li><code>stepFilters</code> - 单步执行时跳过指定的类或方法。

<ul>
<li><code>classNameFilters</code> - [<strong>Deprecated</strong> - replaced by <code>skipClasses</code>] 单步执行时跳过指定的类。类名应是完全限定的。支持通配符。</li>
<li><code>skipClasses</code> - 单步执行时跳过指定的类。您可以使用内置变量（如 <code>$JDK</code> 和 <code>$Libraries</code>）跳过一组类，或添加特定的类名表达式，例如 <code>java.*</code>, <code>*.Foo</code>.</li>
<li><code>skipSynthetics</code> - 步进时跳过合成方法（编译器生成）。</li>
<li><code>skipStaticInitializers</code> - 单步执行时跳过静态初始值设定项方法。</li>
<li><code>skipConstructors</code> - 单步执行时跳过构造函数方法。</li>
</ul></li>
</ul>

<h5 id="attach">Attach</h5>

<ul>
<li><code>hostName</code> (required) - 远程调试器的主机名或 IP 地址。</li>
<li><code>port</code> (required) - 远程调试器的调试端口。</li>
<li><code>processId</code> - 使用进程选取器选择要附加的进程，或使用进程 ID 作为整数。

<ul>
<li><code>${command:PickJavaProcess}</code> - 使用进程选取器选择要附加的进程（默认）。</li>
<li>An integer PID - 附加到指定的本地进程。</li>
</ul></li>
<li><code>timeout</code> - 重新连接前的超时值（以毫秒为单位）（默认值为 30000 毫秒）。</li>
<li><code>sourcePaths</code> - 程序的额外源目录。默认情况下，调试器从项目设置中查找源代码。此选项允许调试器在额外的目录中查找源代码。</li>
<li><code>projectName</code> - 调试器在其中搜索类的首选项目。不同的项目中可能存在重复的类名。当调试器在启动程序时查找指定的主类时，此设置也有效。当工作区具有多个 Java 项目时，这是必需的，否则表达式计算和条件断点可能不起作用。</li>
<li><code>stepFilters</code> - 单步执行时跳过指定的类或方法。

<ul>
<li><code>classNameFilters</code> - [<strong>Deprecated</strong> - replaced by <code>skipClasses</code>] 单步执行时跳过指定的类。类名应是完全限定的。支持通配符。</li>
<li><code>skipClasses</code> - 单步执行时跳过指定的类。您可以使用内置变量（如 <code>$JDK</code> 和 <code>$Libraries</code>）跳过一组类，或添加特定的类名表达式，例如 <code>java.*</code>, <code>*.Foo</code>.</li>
<li><code>skipSynthetics</code> - 步进时跳过合成方法（编译器生成）。</li>
<li><code>skipStaticInitializers</code> - 单步执行时跳过静态初始值设定项方法。</li>
<li><code>skipConstructors</code> - 单步执行时跳过构造函数方法。</li>
</ul></li>
</ul>

<h4 id="调试相关用户设置">调试相关用户设置</h4>

<ul>
<li><code>java.debug.logLevel</code>: 发送到 VS Code 的调试器日志的最低级别默认为 <code>warn</code>.</li>
<li><code>java.debug.settings.showHex</code>: 在 <strong>Variables</strong> 视图中以十六进制格式显示数字，默认为 <code>false</code>。</li>
<li><code>java.debug.settings.showStaticVariables</code>: 在 <strong>Variables</strong> 视图中显示静态变量，默认为 <code>false</code>。</li>
<li><code>java.debug.settings.showQualifiedNames</code>: 在 <strong>Variables</strong> 视图中显示完全限定的类名，默认为  <code>false</code>。</li>
<li><code>java.debug.settings.showLogicalStructure</code>: 在 <strong>Variables</strong> 视图中显示集合和映射类的逻辑结构，默认为 <code>true</code>。</li>
<li><code>java.debug.settings.showToString</code>: 在 <strong>Variables</strong> 视图中显示所有的覆盖了 <code>toString</code> 方法的实例的 <code>toString()</code> 的值，默认为 <code>true</code>。</li>
<li><code>java.debug.settings.maxStringLength</code>: <strong>Variables</strong> 或 <strong>Debug Console</strong> 视图中显示的字符串的最大长度。长度超过此限制的字符串将被修剪。默认值为 <code>0</code>，表示不执行任何修剪。</li>
<li><code>java.debug.settings.hotCodeReplace</code>: 在调试期间重新加载更改的 Java 类，默认为 <code>manual</code>。 确保 <a href="https://github.com/redhat-developer/vscode-java">Java Language Support extension</a> 扩展的 <code>java.autobuild.enabled</code> 配置没有禁用。更多参见《代码热替换》章节。

<ul>
<li>manual - 单击工具栏以应用更改。</li>
<li>auto - 编译后自动应用更改。</li>
<li>never - 禁用。</li>
</ul></li>
<li><code>java.debug.settings.enableHotCodeReplace</code>: 是否启用代码热替换，默认为 true。确保 <a href="https://github.com/redhat-developer/vscode-java">Java Language Support extension</a> 扩展的 <code>java.autobuild.enabled</code> 配置没有禁用。更多参见《代码热替换》章节。</li>
<li><code>java.debug.settings.enableRunDebugCodeLens</code>: 为主入口点上的运行和调试按钮启用 CodeLens 提供程序，默认为 <code>true</code>.</li>
<li><code>java.debug.settings.forceBuildBeforeLaunch</code>: 在启动 java 程序之前强制构建工作区，默认为 <code>true</code>。</li>
<li><code>java.debug.settings.console</code>: 用于启动 Java 程序的指定控制台，默认为 <code>integratedTerminal</code>。如果要为特定调试会话自定义控制台，请修改 <code>launch.json</code> 中的 <code>console</code> 配置。

<ul>
<li><code>internalConsole</code> - VS 代码调试控制台（不支持输入流）。</li>
<li><code>integratedTerminal</code> - VS 代码集成终端。</li>
<li><code>externalTerminal</code> - 可在用户设置中配置的外部终端。</li>
</ul></li>
<li><code>java.debug.settings.exceptionBreakpoint.skipClasses</code>: 出现异常时跳过指定的类。您可以使用内置变量（如 <code>$JDK</code> 和 <code>$Libraries</code>）跳过一组类，或添加特定的类名表达式，例如 <code>java.*</code>、<code>*.Foo</code>。</li>
<li><code>java.debug.settings.stepping.skipClasses</code>: 单步执行时跳过指定的类。您可以使用内置变量（如 <code>$JDK</code> 和 <code>$Libraries</code>）跳过一组类，或添加特定的类名表达式，例如 <code>java.*</code>、<code>*.Foo</code>。</li>
<li><code>java.debug.settings.stepping.skipSynthetics</code>: 步进时跳过合成方法（编译器生成的方法）。</li>
<li><code>java.debug.settings.stepping.skipStaticInitializers</code>: 单步执行时跳过静态初始值设定项方法。</li>
<li><code>java.debug.settings.stepping.skipConstructors</code>: 单步执行时跳过构造函数方法。</li>
<li><code>java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest</code>: 一个 JDWP 请求中可以请求的最大变量或字段数。该值越高，在展开变量视图时请求调试对象的频率就越低。此外，较大的数字也可能导致 JDWP 请求超时。默认值为 100。</li>
<li><code>java.debug.settings.jdwp.requestTimeout</code>: 调试器与目标 JVM 通信时 JDWP 请求的超时 （ms）。默认值为 3000。</li>
<li><code>java.debug.settings.vmArgs</code>: 用于启动 Java 程序的缺省 VM 参数。例如，使用 <code>-Xmx1G -ea</code> 将堆大小增加到 1 GB 并启用断言。如果要自定义特定调试会话的 VM 参数，可以在 <code>launch.json</code> 中修改 <code>vmArgs</code> 配置。</li>
<li><code>java.silentNotification</code>: 控制是否可以使用通知弹窗来报告进度。如果为 true，则改为状态栏报告进度。默认为 <code>false</code>。</li>
</ul>

<h4 id="调试相关-troubleshooting">调试相关 Troubleshooting</h4>

<p>参见 <a href="https://github.com/microsoft/vscode-java-debug/blob/main/Troubleshooting.md">Troubleshooting</a>，常见的问题为：</p>

<ul>
<li>Java Language Support extension fails to start.</li>
<li>Build failed, do you want to continue?</li>
<li>isn&rsquo;t on the classpath. Only syntax errors will be reported.</li>
<li>Program Error: Could not find or load main class X.</li>
<li>Program throws ClassNotFoundException.</li>
<li>Failed to complete Hot Code Replace.</li>
<li>Please specify the host name and the port of the remote debuggee in the launch.json.</li>
<li>Failed to evaluate. Reason: Cannot evaluate because the thread is resumed.</li>
<li>Cannot find a class with the main method.</li>
<li>No delegateCommandHandler for vscode.java.startDebugSession when starting Debugger.</li>
<li>Failed to resolve classpath.</li>
<li>Request type &ldquo;X&rdquo; is not supported. Only &ldquo;launch&rdquo; and &ldquo;attach&rdquo; are supported.</li>
</ul>

<h3 id="测试">测试</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-testing">VSCode Docs - Java - Testing</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test">Test Runner for Java</a> （v0.34.0）</p>
</blockquote>

<p>VSCode Java 支持如下几种测试框架</p>

<ul>
<li><a href="https://junit.org/junit4/">JUnit 4</a> (v4.8.0+)</li>
<li><a href="https://junit.org/junit5/">JUnit 5</a> (v5.1.0+)</li>
<li><a href="https://testng.org/doc/">TestNG</a> (v6.8.0+)</li>
</ul>

<p><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test">Test Runner for Java</a> 与 Red Hat 的 <a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™</a> 和 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug">Debugger for Java</a> 扩展一起使用，可提供以下功能：</p>

<ul>
<li>运行/调试测试用例</li>
<li>自定义测试配置</li>
<li>查看测试报告</li>
<li>在测试资源管理器中查看测试</li>
</ul>

<h4 id="项目配置">项目配置</h4>

<p>在项目中添加单侧依赖如下：</p>

<ul>
<li>JUnit4 <code>junit</code> <code>junit</code></li>
<li>JUnit5 参见：<a href="https://github.com/junit-team/junit5-samples">junit5-sample repository</a></li>
<li>TestNG <code>org.testng</code> <code>testng</code></li>
</ul>

<h4 id="特性">特性</h4>

<ul>
<li>Run/Debug 测试用例，通过编辑器左侧装订栏的绿色三角号可以快速启动单侧。</li>
</ul>

<p><img src="/image/vscode/java/editor-decoration.png" alt="image" /></p>

<ul>
<li>Test Explorer 测试资源管理器，是一个树形视图，用于显示工作区中的所有测试用例。您可以单击 VSCode 左侧活动栏上的烧杯按钮将其打开。您还可以运行/调试您的测试用例并从那里查看它们的测试结果。</li>
</ul>

<p><img src="/image/vscode/java/test_explorer.png" alt="image" /></p>

<ul>
<li>自定义测试配置，可以通过 <code>java.test.config</code> 以及 <code>java.test.defaultConfig</code> 配置项，配置测试的运行配置，更多细节参见：<a href="https://github.com/Microsoft/vscode-java-test/wiki/Run-with-Configuration">vscode-java-test Wiki</a>。<code>java.test.config</code> 所有字段如下：

<ul>
<li><strong>args</strong>: 命令行参数。</li>
<li><strong>classPaths</strong>: 此设置中定义的类路径将附加到已解析的类路径中。</li>
<li><strong>env</strong>: 通过键值对象运行测试时指定额外的环境变量。</li>
<li><strong>envFile</strong>: 指定包含环境变量定义的文件的绝对路径。</li>
<li><strong>modulePaths</strong>: 此设置中定义的模块路径将附加到已解析的模块路径中。</li>
<li><strong>name</strong>: 指定配置项的名称。可以通过配置项 <code>java.test.defaultConfig</code> 来设置默认配置名称。</li>
<li><strong>preLaunchTask</strong>: 指定 tasks.json 中指定的任务的标签（在工作区的 .vscode 文件夹中）。该任务将在测试开始之前启动。</li>
<li><strong>sourcePaths</strong>: 调试测试时指定额外的源路径。</li>
<li><strong>vmArgs</strong>: 指定 JVM 的额外选项和系统属性。</li>
<li><strong>workingDirectory</strong>: 运行测试时指定工作目录。</li>
</ul></li>
</ul>

<p><img src="/image/vscode/java/configuration.png" alt="image" /></p>

<ul>
<li>查看测试结果，运行/调试测试用例后，相关测试项的状态将在编辑器装饰和测试资源管理器中更新。还可以通过命令 <code>&gt;Test: Peek Output</code> 来查看结果视图。可以选择堆栈跟踪中的链接以导航到源位置。</li>
</ul>

<p><img src="/image/vscode/java/test_report.png" alt="image" /></p>

<ul>
<li>生成测试，在编辑器右击，选 <code>Source Action...</code> -&gt; <code>Generate Tests....</code>，分为如下两种场景

<ul>
<li>从主要源代码触发生成测试</li>
<li>从测试源代码触发生成测试</li>
</ul></li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-tests-from-main.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/generate-tests-from-test.mp4" type="video/mp4">
</video>

<ul>
<li>测试跳转，VSCode Java 提供了在测试和测试主题之间快速跳转的功能。如果源代码包含在 <code>src/main/java</code> 或 <code>src/test/java</code> 中，您可以在编辑器上下文菜单中找到名为 <code>Go to Test</code> 或 <code>Go to Test Subject</code> 的条目。另外还可以通过 <code>&gt;Java: Go to Test</code> 命令跳转。</li>
</ul>

<p><img src="/image/vscode/java/goto-test.png" alt="image" /></p>

<ul>
<li>测试命令，通过 <code>&gt;Test:</code> 可以看到所有的 Java 测试相关的命令</li>
</ul>

<p><img src="/image/vscode/java/command_palette.png" alt="image" /></p>

<ul>
<li>VSCode 测试功能的通用配置，通过 <code>⌘,</code> 搜索 testing，查看。</li>
</ul>

<p><img src="/image/vscode/java/settings.png" alt="image" /></p>

<ul>
<li>运行调试测试命令，<code>Java: Run Tests</code> 运行测试，<code>Java: Debug Tests</code> 调试测试</li>
</ul>

<h4 id="更多">更多</h4>

<ul>
<li><a href="https://github.com/microsoft/vscode-java-test/wiki">Wiki</a><br /></li>
<li><a href="https://github.com/microsoft/vscode-java-test/wiki/FAQ">FAQ</a></li>
<li><a href="https://github.com/microsoft/vscode-java-test/issues">issue List</a></li>
</ul>

<h2 id="框架支持">框架支持</h2>

<h3 id="java-ee-server">Java EE Server</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-spring-boot">VSCode Docs - Java - Application Servers</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-community-server-connector">Community Server Connectors</a> （v0.25.2） 和 <a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-rsp-ui">Remote Server Protocol UIP</a> （v0.23.11）</p>
</blockquote>

<p><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-community-server-connector">Community Server Connectors</a> 提供了连接到常见的开源的 Servlet 容器的能力：</p>

<ul>
<li>Apache Tomcat ( 5.5 | 6.0 | 7.0 | 8.0 | 8.5 | 9.0 )</li>
<li>Apache Karaf ( 4.8 )</li>
<li>Apache Felix ( 3.2 | 4.6 | 5.6 | 6.0 )</li>
<li>Jetty ( 9.x )</li>
<li>Glassfish ( 5.x )</li>
<li>Websphere Liberty ( 21.x )</li>
</ul>

<p>通过 <code>&gt;Servers:</code> 可以查看所有的命令。更多参见：<a href="https://github.com/redhat-developer/vscode-rsp-ui">github</a></p>

<p>其他 Java EE Server 支持自行在 <a href="https://marketplace.visualstudio.com/">VSCode 扩展市场</a> 搜索</p>

<h3 id="spring-boot">Spring Boot</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-spring-boot">VSCode Docs - Java - Spring Boot</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=Pivotal.vscode-spring-boot">Spring Boot Tools</a> （v1.29.0）和 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-initializr">Spring Initializr</a> （ v0.8.0） 和 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-boot-dashboard">Spring Boot Dashboard</a> （v0.2.0） （通过 <a href="https://marketplace.visualstudio.com/items?itemName=pivotal.vscode-boot-dev-pack">Spring Boot Extension Pack</a> 一键安装）</p>
</blockquote>

<h4 id="创建项目">创建项目</h4>

<p>通过 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-initializr">Spring Initializr</a> 项目，可以快速创建 Spring 项目骨架。通过 <code>&gt;Spring Initializr: create</code> 命令触发。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/spring-initializr.mp4" type="video/mp4">
</video>

<h4 id="编辑项目">编辑项目</h4>

<p>通过 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-initializr">Spring Initializr</a> 项目，可以为已存在的 Spring Boot 项目添加新的 starter。通过 <code>&gt;Spring Initializr: add</code> 或者 <code>pom.xml</code> 文件编辑器（右击）上下文菜单，选择 <code>Add starters...</code> 添加。</p>

<h4 id="开发应用">开发应用</h4>

<p>通过 <a href="https://marketplace.visualstudio.com/items?itemName=Pivotal.vscode-spring-boot">Spring Boot Tools</a> 扩展，提供了 Spring Boot 相关的特性，细节参见：<a href="https://github.com/spring-projects/sts4/tree/main/vscode-extensions/vscode-spring-boot#usage">detailed usage guide</a>。包括：</p>

<ul>
<li>快速导航到工作区中的 Spring 元素</li>
<li>Spring 的 application 的 <code>.yml</code> 和 <code>.properties</code> 提供智能代码完成</li>
<li>快速访问正在运行的 Spring 应用程序</li>
<li>实时应用信息</li>
<li>代码模板</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/spring-live-info.mp4" type="video/mp4">
</video>

<h4 id="运行应用">运行应用</h4>

<p>除了使用 F5 来运行应用程序之外，还有 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-spring-boot-dashboard">Spring Boot Dashboard</a> 扩展，它允许您查看和管理工作区中所有可用的 Spring Boot 项目，以及快速启动、停止或调试您的项目。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/spring-dashboard.mp4" type="video/mp4">
</video>

<h3 id="quarkus-和-microprofile">Quarkus 和 MicroProfile</h3>

<blockquote>
<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.vscode-microprofile-pack">Extension Pack for MicroProfile</a>
 和 <a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus">Quarkus</a>
Quarkus 可以理解为 MicroProfile 标准的一个实现，并原生支持 k8s 的全栈 Java 后端框架。</p>
</blockquote>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.vscode-microprofile-pack">Extension Pack for MicroProfile</a>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.mp-starter-vscode-ext">MicroProfile Starter</a> - 提供快速生成 MicroProfile 的 Maven 项目</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=MicroProfile-Community.mp-rest-client-generator-vscode-ext">Generator for MicroProfile Rest Client</a> - 从 OpenAPI 文档快速生成 MicroProfile Rest Client 接口模板。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-microprofile">Tools for MicroProfile</a> - 通过 <a href="https://github.com/eclipse/lsp4mp">Language Server for Eclipse MicroProfile</a> 对 MicroProfile API 和 properties 配置文件提供语言支持</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus">Quarkus</a>，对 Quarkus 提供创建项目、交互式添加 Extension、调试、构建可执行文件、部署到 OpenShift、对 properties/yml 配置文件提供语言支持、对相关 Java 文件提供支持。</li>
</ul>

<h3 id="lombok">Lombok</h3>

<blockquote>
<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=GabrielBB.vscode-lombok">Lombok Annotations Support for VS Code</a></p>
</blockquote>

<p>参见：<a href="https://marketplace.visualstudio.com/items?itemName=GabrielBB.vscode-lombok">README</a></p>

<h3 id="gui-应用">GUI 应用</h3>

<blockquote>
<p><a href="https://code.visualstudio.com/docs/java/java-gui">VSCode Docs - Java - GUI Applications</a></p>

<p>特性提供扩展：<a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a> （v0.21.0）</p>
</blockquote>

<h4 id="javafx">JavaFX</h4>

<p><strong>创建</strong></p>

<ul>
<li>步骤 1: 安装 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a></li>
<li>步骤 2: <code>⇧⌘P</code> 输入 <code>&gt;Java: Create Java Project</code></li>
<li>步骤 3: 选择 JavaFX</li>
</ul>

<p><img src="/image/vscode/java/create-javafx.png" alt="image" /></p>

<p><strong>运行</strong></p>

<blockquote>
<p>注意：以下指南仅适用于 Maven 管理的项目。</p>
</blockquote>

<p>要运行 JavaFX 应用程序，您可以打开 Maven Explorer，展开 hellofx &gt; Plugins &gt; javafx 并运行 Maven 目标：javafx:run。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="">
  <source src="/image/vscode/java/run-javafx.mp4" type="video/mp4">
</video>

<p>更多 JavaFX 例子，参见：<a href="https://github.com/openjfx/samples/tree/master/IDE/VSCode">openjfx samples repository</a></p>

<h4 id="awt">AWT</h4>

<p>开发 AWT 需要开启该特性： <code>&gt;Java: Help Center</code> 打开 <code>Student</code> 选择 <code>Enable AWT Development</code> （没找到配置的地方，好像已经默认支持了）。</p>

<h4 id="swing">Swing</h4>

<p>默认支持 Swing 应用程序开发。您无需任何设置即可直接编写 Swing 应用程序代码。更多参见： <a href="https://docs.oracle.com/javase/tutorial/uiswing/examples/components/index.html">Oracle Swing documentation</a></p>

<h2 id="扩展其他贡献">扩展其他贡献</h2>

<h3 id="project-manager-for-java-https-marketplace-visualstudio-com-items-itemname-vscjava-vscode-java-dependency"><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-dependency">Project Manager for Java</a></h3>

<h4 id="配置">配置</h4>

<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
<th>默认值</th>
</tr>
</thead>

<tbody>
<tr>
<td>java.dependency.showMembers</td>
<td>在资源管理器中显示成员</td>
<td>false</td>
</tr>

<tr>
<td>java.dependency.syncWithFolderExplorer</td>
<td>将 Java 项目资源管理器选择与文件夹资源管理器同步</td>
<td>true</td>
</tr>

<tr>
<td>java.dependency.autoRefresh</td>
<td>将 Java 项目资源管理器与更改同步</td>
<td>true</td>
</tr>

<tr>
<td>java.dependency.refreshDelay</td>
<td>检测到更改时调用自动刷新的延迟时间 (ms)</td>
<td>2000</td>
</tr>

<tr>
<td>java.dependency.packagePresentation</td>
<td>包装呈现方式：flat 或 hierarchical</td>
<td>flat</td>
</tr>

<tr>
<td>java.project.exportJar.targetPath</td>
<td>指定导出 jar 的输出路径。 默认值为 <code>${workspaceFolder}/${workspaceFolderBasename}.jar</code>。 要在每次导出 jar 文件时手动选择输出位置，只需将其留空或将其设置为 askUser。</td>
<td><code>${workspaceFolder}/${workspaceFolderBasename}.jar</code></td>
</tr>
</tbody>
</table>

<h4 id="命令">命令</h4>

<blockquote>
<p>仅展示可以只能通过 命令面板 （Cmd + Shift + P） 调用的命令</p>
</blockquote>

<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>java.project.refreshLibraries</td>
<td>刷新</td>
</tr>
</tbody>
</table>

<h3 id="language-support-for-java-by-red-hat-https-marketplace-visualstudio-com-items-itemname-redhat-java"><a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a></h3>

<h4 id="代码片段">代码片段</h4>

<table>
<thead>
<tr>
<th>前缀</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>main</td>
<td>生成 main 函数</td>
</tr>

<tr>
<td>ctor</td>
<td>生成公有构造函数</td>
</tr>

<tr>
<td>try_catch</td>
<td>生成 try catch 块</td>
</tr>

<tr>
<td>try_resources</td>
<td>生成 try resources 块</td>
</tr>

<tr>
<td>private_method</td>
<td>生成私有方法</td>
</tr>

<tr>
<td>public_method</td>
<td>生成公有方法</td>
</tr>

<tr>
<td>private_static_method</td>
<td>生成私有静态方法</td>
</tr>

<tr>
<td>public_static_method</td>
<td>生成公有静态方法</td>
</tr>

<tr>
<td>protected_method</td>
<td>生成受保护方法</td>
</tr>

<tr>
<td>switch</td>
<td>生成 switch 块</td>
</tr>

<tr>
<td>new</td>
<td>生成 new 块</td>
</tr>

<tr>
<td>prf</td>
<td>生成私有字段声明</td>
</tr>

<tr>
<td>sysout</td>
<td>生成 System.out.println</td>
</tr>

<tr>
<td>syserr</td>
<td>生成 System.err.println</td>
</tr>

<tr>
<td>fori</td>
<td>生成 for int &hellip;</td>
</tr>

<tr>
<td>foreach</td>
<td>生成 <code>for (type var : iterable)</code> 语句</td>
</tr>

<tr>
<td>if</td>
<td>生成 if 语句</td>
</tr>

<tr>
<td>ifelse</td>
<td>生成 if else 语句</td>
</tr>

<tr>
<td>ifnull</td>
<td>生成 if (condition == null)</td>
</tr>

<tr>
<td>ifnotnull</td>
<td>生成 if (condition != null)</td>
</tr>

<tr>
<td>while</td>
<td>生成 while 块</td>
</tr>

<tr>
<td>dowhile</td>
<td>生成 do while 块</td>
</tr>
</tbody>
</table>

<h4 id="配置-1">配置</h4>

<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
<th>默认值</th>
</tr>
</thead>

<tbody>
<tr>
<td>java.home</td>
<td>废弃，使用 <code>java.jdt.ls.java.home</code> 替代，用来启动后 Java Language Server 的 JDK 的绝对路径，需要 VSCode 重启生效</td>
<td></td>
</tr>

<tr>
<td>java.jdt.ls.vmargs</td>
<td>运行 Java Language Server 的命令行参数，需要 VSCode 重启生效</td>
<td></td>
</tr>

<tr>
<td>java.errors.incompleteClasspath.severity</td>
<td>指定 Java 文件的类路径不完整时消息的严重性。 支持的值是忽略、信息、警告、错误。</td>
<td>warn</td>
</tr>

<tr>
<td>java.trace.server</td>
<td>跟踪 VS Code 和 Java 语言服务器之间的通信。</td>
<td>off</td>
</tr>

<tr>
<td>java.configuration.updateBuildConfiguration</td>
<td>指定对构建文件的修改如何更新 Java 类路径/配置。 支持的值被禁用（没有任何反应）、交互（询问每次修改时的更新）、自动（自动触发更新）。</td>
<td>interactive</td>
</tr>

<tr>
<td>java.configuration.maven.userSettings</td>
<td>Maven 用户 settings.xml 的路径。</td>
<td></td>
</tr>

<tr>
<td>java.configuration.checkProjectSettingsExclusions</td>
<td>控制是否从文件资源管理器中排除扩展生成的项目设置文件（.project、.classpath、.factorypath、.settings/）</td>
<td>true</td>
</tr>

<tr>
<td>java.referencesCodeLens.enabled</td>
<td>Enable/disable 代码引用 code lenses.</td>
<td>false</td>
</tr>

<tr>
<td>java.implementationsCodeLens.enabled</td>
<td>Enable/disable 实现的 code lenses</td>
<td>false</td>
</tr>

<tr>
<td>java.signatureHelp.enabled</td>
<td>Enable/disable 函数签名智能提示（通过 <code>()</code> 触发）</td>
<td>false</td>
</tr>

<tr>
<td>java.contentProvider.preferred</td>
<td>反编译器 ID (see 3rd party decompilers available in <a href="https://marketplace.visualstudio.com/items?itemName=dgileadi.java-decompiler">vscode-java-decompiler</a>).</td>
<td>fernflower</td>
</tr>

<tr>
<td>java.import.exclusions</td>
<td>通过 glob 模式从导入中排除文件夹。 利用 ! 否定模式以允许子文件夹导入。 您必须包含父目录。 顺序很重要。</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.enabled</td>
<td>Enable/disable Gradle 导入</td>
<td>true</td>
</tr>

<tr>
<td>java.import.gradle.wrapper.enabled</td>
<td>使用 <code>gradle-wrapper.properties</code> 文件中的 Gradle</td>
<td>true</td>
</tr>

<tr>
<td>java.import.gradle.version</td>
<td>如果 Gradle Wrapper 丢失或禁用，使用特定版本的 Gradle。</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.home</td>
<td>如果 Gradle 包装器丢失或禁用且未指定 <code>java.import.gradle.version</code>，则使用指定本地安装目录中的 Gradle 或 GRADLE_HOME。</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.arguments</td>
<td>Gradle 命令行参数</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.jvmArguments</td>
<td>Gradle JVM 命令行参数</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.user.home</td>
<td>setting for GRADLE_USER_HOME</td>
<td></td>
</tr>

<tr>
<td>java.import.gradle.offline.enabled</td>
<td>Enable/disable Gradle offline 模式</td>
<td>false</td>
</tr>

<tr>
<td>java.import.maven.enabled</td>
<td>Enable/disable Maven 导入</td>
<td>true</td>
</tr>

<tr>
<td>java.autobuild.enabled</td>
<td>Enable/disable &lsquo;auto build&rsquo;</td>
<td>true</td>
</tr>

<tr>
<td>java.maxConcurrentBuilds</td>
<td>设置最大构建并发数</td>
<td>1</td>
</tr>

<tr>
<td>java.completion.enabled</td>
<td>Enable/disable 代码完成支持</td>
<td>true</td>
</tr>

<tr>
<td>java.completion.overwrite</td>
<td>设置为 true 时，代码完成将 overwrite 当前文本。 当设置为 false 时，只是简单地添加代码</td>
<td>true</td>
</tr>

<tr>
<td>java.completion.guessMethodArguments</td>
<td>当设置为 true 时，当从代码提示列表中选择方法时，会猜测方法参数。</td>
<td>false</td>
</tr>

<tr>
<td>java.completion.filteredTypes</td>
<td>定义类型过滤器。 在内容辅助或快速修复建议以及组织导入时，将忽略其完全限定名称与所选过滤器字符串匹配的所有类型。 例如，&rsquo;java.awt.*&rsquo; 将隐藏 awt 包中的所有类型。</td>
<td></td>
</tr>

<tr>
<td>java.completion.favoriteStaticMembers</td>
<td>定义静态成员列表或具有静态成员的类型。</td>
<td></td>
</tr>

<tr>
<td>java.completion.importOrder</td>
<td>定义导入语句的排序顺序。</td>
<td></td>
</tr>

<tr>
<td>java.progressReports.enabled</td>
<td>实验性 Enable/disable 进度报告</td>
<td>false</td>
</tr>

<tr>
<td>java.format.enabled</td>
<td>Enable/disable Java 默认格式化器</td>
<td>true</td>
</tr>

<tr>
<td>java.format.settings.url</td>
<td>格式化详细配置 URL</td>
<td></td>
</tr>

<tr>
<td>java.format.settings.profile</td>
<td>来自 Eclipse 格式化程序设置的可选格式化程序配置文件名称</td>
<td></td>
</tr>

<tr>
<td>java.format.comments.enabled</td>
<td>注释格式化</td>
<td>true</td>
</tr>

<tr>
<td>java.format.onType.enabled</td>
<td>Enable/disable on-type 格式化 (触发字符 <code>;,</code> 或 <code>&lt;return&gt;</code> 或 <code>}</code>).</td>
<td></td>
</tr>

<tr>
<td>java.foldingRange.enabled</td>
<td>Enable/disable 智能的折叠范围。如果禁用，它将使用 VS Code 提供的默认基于缩进的折叠范围</td>
<td>true</td>
</tr>

<tr>
<td>java.maven.downloadSources</td>
<td>作为导入 Maven 项目的一部分，启用/禁用 Maven 源代码的下载</td>
<td>false</td>
</tr>

<tr>
<td>java.maven.updateSnapshots</td>
<td>强制更新 Snapshots/Releases</td>
<td>false.</td>
</tr>

<tr>
<td>java.codeGeneration.hashCodeEquals.useInstanceof</td>
<td>生成 hashCode 和 equals 方法时使用 <code>instanceof</code> 比较类型</td>
<td>false.</td>
</tr>

<tr>
<td>java.codeGeneration.hashCodeEquals.useJava7Objects</td>
<td>在生成 hashCode 和 equals 方法时使用 Objects.hash 和 Objects.equals。 此设置仅适用于 Java 7 及更高版本。</td>
<td>false</td>
</tr>

<tr>
<td>java.codeGeneration.useBlocks</td>
<td>生成方法时在 if 语句中使用块。</td>
<td>false</td>
</tr>

<tr>
<td>java.codeGeneration.generateComments</td>
<td>生成方法时生成方法注释</td>
<td>false</td>
</tr>

<tr>
<td>java.codeGeneration.toString.template</td>
<td>用于生成 toString 方法的模板。</td>
<td><code>${object.className} [${member.name()}=${member.value}, ${otherMembers}]</code></td>
</tr>

<tr>
<td>java.codeGeneration.toString.codeStyle</td>
<td>用于生成 toString 方法的代码样式。</td>
<td>STRING_CONCATENATION。</td>
</tr>

<tr>
<td>java.codeGeneration.toString.skipNullValues</td>
<td>生成 toString 方法时跳过空值。</td>
<td>false</td>
</tr>

<tr>
<td>java.codeGeneration.toString.listArrayContents</td>
<td>列出数组的内容，而不是使用本机 toString()。</td>
<td>true</td>
</tr>

<tr>
<td>java.codeGeneration.toString.limitElements</td>
<td>限制要列出的 arrays/collections/maps 中的项目数，如果为 0，则列出所有。</td>
<td>0</td>
</tr>

<tr>
<td>java.selectionRange.enabled</td>
<td>Enable/disable Java 的智能选择支持。 禁用此选项不会影响 VSCode 内置的基于单词和基于括号的智能选择。</td>
<td>true</td>
</tr>

<tr>
<td>java.showBuildStatusOnStart.enabled</td>
<td>启动时自动显示构建状态，默认为通知，枚举值为 (notification、terminal、off)</td>
<td>notification</td>
</tr>

<tr>
<td>java.project.outputPath</td>
<td>存储编译输出的工作区的相对路径。 仅在 WORKSPACE 范围内有效。 该设置不会影响 Maven 或 Gradle 项目。</td>
<td></td>
</tr>

<tr>
<td>java.project.referencedLibraries</td>
<td>配置 glob 模式以将本地库引用到 Java 项目。</td>
<td></td>
</tr>

<tr>
<td>java.completion.maxResults</td>
<td>完成结果的最大数量（不包括片段）。 0（默认值）禁用限制，返回所有结果。 如果出现性能问题，请考虑设置一个合理的限制。</td>
<td>0</td>
</tr>

<tr>
<td>java.configuration.runtimes</td>
<td>将 Java 执行环境映射到本地 JDK</td>
<td></td>
</tr>

<tr>
<td>java.server.launchMode</td>
<td>Language Server 启动模式 Standard、LightWeight、Hybrid</td>
<td>Hybrid</td>
</tr>

<tr>
<td>java.sources.organizeImports.starThreshold</td>
<td>指定在使用星形导入声明之前添加的导入数量</td>
<td>99</td>
</tr>

<tr>
<td>java.sources.organizeImports.staticStarThreshold</td>
<td>指定在使用星形导入声明之前添加的静态导入数量</td>
<td>99</td>
</tr>

<tr>
<td>java.imports.gradle.wrapper.checksums</td>
<td>定义 Gradle Wrappers 的允许/禁止 SHA-256 校验和</td>
<td></td>
</tr>

<tr>
<td>java.project.importOnFirstTimeStartup</td>
<td>指定首次以混合模式打开文件夹时是否导入 Java 项目。 支持的值是 disabled（从不导入）、interactive（询问是否导入）、automatic（始终导入）</td>
<td>automatic</td>
</tr>

<tr>
<td>java.project.importHint</td>
<td>Enable/disable 服务器模式切换信息，当 Java 项目导入在启动时被跳过。</td>
<td>true.</td>
</tr>

<tr>
<td>java.import.gradle.java.home</td>
<td>指定用于运行 Gradle 守护程序的 JVM 的位置</td>
<td></td>
</tr>

<tr>
<td>java.project.resourceFilters</td>
<td>将文件和文件夹排除在 Java 语言服务器刷新之外，这可以提高整体性能。 例如，<code>[&quot;node_modules&quot;,&quot;.git&quot;]</code> 将排除所有名为 <code>'node_modules'</code> 或 <code>'.git'</code> 的路径</td>
<td><code>[&quot;node_modules&quot;,&quot;.git&quot;]</code></td>
</tr>

<tr>
<td>java.templates.fileHeader</td>
<td>指定新 Java 文件的文件头注释。 支持使用字符串数组配置多行注释，并使用 <code>${variable}</code> 引用预定义的变量。</td>
<td></td>
</tr>

<tr>
<td>java.templates.typeComment</td>
<td>指定新 Java 类型的类型注释。 支持使用字符串数组配置多行注释，并使用 <code>${variable}</code> 引用预定义的变量</td>
<td></td>
</tr>

<tr>
<td>java.references.includeAccessors</td>
<td>查找引用时包括 getter、setter 和 builder/constructor。</td>
<td>false</td>
</tr>

<tr>
<td>java.configuration.maven.globalSettings</td>
<td>Maven 全局 settings.xml 的路径。</td>
<td></td>
</tr>

<tr>
<td>java.eclipse.downloadSources</td>
<td>Enable/disable Eclipse 项目的 Maven 源工件的下载。</td>
<td></td>
</tr>

<tr>
<td>java.recommendations.dependency.analytics.show</td>
<td>显示推荐的依赖分析扩展</td>
<td>true</td>
</tr>

<tr>
<td>java.references.includeDecompiledSources</td>
<td>查找参考时包括反编译的源。</td>
<td>true</td>
</tr>

<tr>
<td>java.project.sourcePaths</td>
<td>配置源文件的工作空间的相对路径。 仅在 WORKSPACE 范围内有效。 该设置不会影响 Maven 或 Gradle 项目。</td>
<td></td>
</tr>

<tr>
<td>java.typeHierarchy.lazyLoad</td>
<td>Enable/disable 延迟加载类型层次结构中的内容。 延迟加载可以节省大量加载时间，但每种类型都应手动扩展以加载其内容。</td>
<td>false</td>
</tr>

<tr>
<td>java.codeGeneration.insertionLocation</td>
<td>指定源操作生成的代码的插入位置。</td>
<td>afterCursor。</td>
</tr>

<tr>
<td>java.settings.url</td>
<td>指定工作区 Java 设置的 url 或文件路径。 请参阅：<a href="https://github.com/redhat-developer/vscode-java/wiki/Settings-Global-Preferences">设置全局首选项</a></td>
<td></td>
</tr>

<tr>
<td>java.quickfix.showAt</td>
<td>在问题或行级别显示快速修复。</td>
<td>line</td>
</tr>

<tr>
<td>java.configuration.workspaceCacheLimit</td>
<td>保留未使用的工作区缓存数据的天数（如果启用）。 超出此限制，缓存的工作区数据可能会被删除。</td>
<td></td>
</tr>

<tr>
<td>java.import.generatesMetadataFilesAtProjectRoot</td>
<td>指定是否将在项目根目录生成项目元数据文件（.project、.classpath、.factorypath、.settings/）。</td>
<td>false</td>
</tr>

<tr>
<td>java.jdt.ls.java.home</td>
<td>用于启动 Java 语言服务器的 JDK 主文件夹的绝对路径。 此设置将替换 Java 扩展的嵌入式 JRE 以启动 Java 语言服务器。 需要重启 VS Code (1.3.0 新增)</td>
<td></td>
</tr>
</tbody>
</table>

<p>Eclipse 配置，参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/Settings-Global-Preferences">Wiki</a></p>

<h4 id="命令-1">命令</h4>

<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>Switch to Standard Mode</td>
<td>切换到标准模式</td>
</tr>

<tr>
<td>Java: Update Project (Shift+Alt+U)</td>
<td>当编辑器专注于 Maven pom.xml 或 Gradle 文件时可用。 它根据项目构建描述符强制项目配置/类路径更新（例如依赖项更改或 Java 编译级别）。</td>
</tr>

<tr>
<td>Java: Import Java Projects into Workspace</td>
<td>检测所有 Java 项目并将其导入 Java 语言服务器工作区。</td>
</tr>

<tr>
<td>Java: Open Java Language Server Log File</td>
<td>打开 Java 语言服务器日志文件，这对于解决问题很有用。</td>
</tr>

<tr>
<td>Java: Open Java Extension Log File</td>
<td>打开 Java 扩展日志文件，这对于解决问题很有用。</td>
</tr>

<tr>
<td>Java: Open All Log Files</td>
<td>打开 Java 语言服务器日志文件和 Java 扩展日志文件。</td>
</tr>

<tr>
<td>Java: Force Java Compilation (Shift+Alt+B)</td>
<td>强制触发工作区的编译。</td>
</tr>

<tr>
<td>Java: Open Java Formatter Settings</td>
<td>打开 Eclipse 格式化程序设置。 如果不存在，则创建一个新的设置文件。</td>
</tr>

<tr>
<td>Java: Clean Java Language Server Workspace</td>
<td>清理 Java 语言服务器工作区。</td>
</tr>

<tr>
<td>Java: Attach Source: attaches a jar/zip source to the currently opened binary class file</td>
<td>此命令仅在编辑器上下文菜单中可用。</td>
</tr>

<tr>
<td>Java: Add Folder to Java Source Path</td>
<td>将所选文件夹添加到其项目源代码路径。 此命令仅在文件资源管理器上下文菜单中可用，并且仅适用于非托管文件夹。</td>
</tr>

<tr>
<td>Java: Remove Folder from Java Source Path</td>
<td>从其项目源路径中删除选定的文件夹。 此命令仅在文件资源管理器上下文菜单中可用，并且仅适用于非托管文件夹。</td>
</tr>

<tr>
<td>Java: List All Java Source Paths</td>
<td>列出 Java 语言服务器工作区识别的所有 Java 源路径。</td>
</tr>

<tr>
<td>Java: Show Build Job Status</td>
<td>在 Visual Studio Code 终端中显示 Java 语言服务器作业状态。</td>
</tr>

<tr>
<td>Java: Go to Super Implementation</td>
<td>跳转到当前选中符号的超类实现</td>
</tr>
</tbody>
</table>

<h4 id="wiki">Wiki</h4>

<p><a href="https://github.com/redhat-developer/vscode-java/wiki">https://github.com/redhat-developer/vscode-java/wiki</a></p>

<h3 id="maven-for-java-https-marketplace-visualstudio-com-items-itemname-vscjava-vscode-maven"><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-maven">Maven for Java</a></h3>

<h4 id="配置-2">配置</h4>

<table>
<thead>
<tr>
<th>配置</th>
<th>描述</th>
<th>Default Value</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>maven.excludedFolders</code></td>
<td>指定搜索 Maven 项目时要排除的文件夹的文件路径模式。指定搜索 Maven 项目时要排除的文件夹的文件路径模式。</td>
<td><code>[ &quot;**/.*&quot;, &quot;**/node_modules&quot;, &quot;**/target&quot;, &quot;**/bin&quot;, &quot;**/archetype-resources&quot; ]</code></td>
</tr>

<tr>
<td><code>maven.executable.preferMavenWrapper</code></td>
<td>指定您是否更喜欢使用 Maven 包装器。 如果为真，它会尝试通过遍历父文件夹来使用 <code>mvnw</code>。 如果为 false 或未找到 <code>mvnw</code>，则改为在 PATH 中尝试 <code>mvn</code>。</td>
<td><code>true</code></td>
</tr>

<tr>
<td><code>maven.executable.path</code></td>
<td>指定 <code>mvn</code> 可执行文件的绝对路径。 当此值为空时，它会根据 <code>maven.executable.preferMavenWrapper</code> 的值尝试使用 <code>mvn</code> 或 <code>mvnw</code>。 例如。 <code>/usr/local/apache-maven-3.6.0/bin/mvn</code></td>
<td></td>
</tr>

<tr>
<td><code>maven.executable.options</code></td>
<td>指定所有 mvn 命令的默认选项。 例如 <code>-o -DskipTests</code></td>
<td></td>
</tr>

<tr>
<td><code>maven.projectOpenBehavior</code></td>
<td>新建项目的默认打开方式</td>
<td><code>&quot;Interactive&quot;</code></td>
</tr>

<tr>
<td><code>maven.pomfile.autoUpdateEffectivePOM</code></td>
<td>指定是否在检测到更改时自动更新有效 pom。</td>
<td><code>false</code></td>
</tr>

<tr>
<td><code>maven.pomfile.globPattern</code></td>
<td>指定用于查找 pom.xml 文件的 glob 模式。</td>
<td><code>**/pom.xml</code></td>
</tr>

<tr>
<td><code>maven.terminal.useJavaHome</code></td>
<td>如果此值为 true，并且设置 java.home 有值，则在创建新终端窗口时，环境变量 JAVA_HOME 将设置为 java.home 的值。</td>
<td><code>false</code></td>
</tr>

<tr>
<td><code>maven.terminal.customEnv</code></td>
<td>指定环境变量名称和值的数组。 这些环境变量值将在执行 Maven 之前添加。<code>environmentVariable</code>：要设置的环境变量的名称。<code>value</code>：要设置的环境变量的值。</td>
<td><code>[]</code></td>
</tr>

<tr>
<td><code>maven.terminal.favorites</code></td>
<td>指定要执行的预定义收藏命令。<code>alias</code>：命令的简称。<code>command</code>: 收藏命令的内容。</td>
<td><code>[]</code></td>
</tr>

<tr>
<td><code>maven.view</code></td>
<td>指定查看 Maven 项目的方式。 可能的值：<code>flat</code>、<code>hierarchical</code>。</td>
<td><code>flat</code></td>
</tr>

<tr>
<td><code>maven.settingsFile</code></td>
<td>指定 Maven <code>settings.xml</code> 文件的绝对路径。 如果未指定，则使用 <code>~/.m2/settings.xml</code>。</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>

<h2 id="场景和小技巧">场景和小技巧</h2>

<h3 id="快速创建项目">快速创建项目</h3>

<p>通过命令 <code>&gt;Java: Create Java Project</code> 可以快速创建 Maven、Gradle、Spring Boot、MicroProfile、Quarkus、JavaFX 项目。</p>

<h3 id="环境和依赖">环境和依赖</h3>

<h4 id="jdk-相关">JDK 相关</h4>

<p>参见：<a href="#JDK 配置">帮助中心</a> 和 <a href="#jdk-配置">JDK 配置</a></p>

<h4 id="快速添加依赖">快速添加依赖</h4>

<ul>
<li>搜索添加 Maven 依赖：执行命令 <code>&gt;Maven: Add a Dependency</code></li>
<li>搜索添加 Gradle 依赖，直接编写 <code>build.gradle</code> 等配置文件的 dependencies 时会自动提示</li>
<li>搜索添加 Spring Boot Starter 依赖：执行命令 <code>&gt;spring initializr: add starters</code></li>
</ul>

<h4 id="查看项目依赖及依赖图">查看项目依赖及依赖图</h4>

<ul>
<li>Maven 通过资源管理器侧边栏 -&gt; Maven 视图 -&gt; 右击项目 -&gt; 显示所有依赖，可以快速展示依赖树视图</li>
<li>Gradle 暂不支持，通过命令查看： <code>gradle dependencies</code></li>
</ul>

<h4 id="彻底清理生成项目配置和缓存">彻底清理生成项目配置和缓存</h4>

<p>Java Language Server 存在异常时，可以通过如下步骤从上到下尝试使用：</p>

<ul>
<li>先尝试执行 <code>&gt;java: clean java language server workspace</code> 命令</li>

<li><p>如果上述步骤无效，可以尝试彻底关闭 VSCode，清理 Language Server 生成的一些配置文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># cd 项目根目录</span>
rm -rf .project、.classpath、.factorypath、.settings</code></pre></div></li>
</ul>

<h3 id="maven-gradle-wrapper-加速">Maven/Gradle Wrapper 加速</h3>

<p>相关 Jar 包下载配置 <code>.mvn/wrapper/maven-wrapper.properties</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-properties" data-lang="properties">distributionUrl=https://mirrors.huaweicloud.com/repository/maven/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
wrapperUrl=https://mirrors.huaweicloud.com/repository/maven/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar</code></pre></div>
<p>项目级别 maven settings <code>.mvn/wrapper/settings.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
<span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;http://maven.apache.org/SETTINGS/1.0.0&#34;</span>
          <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span>
          <span style="color:#a6e22e">xsi:schemaLocation=</span><span style="color:#e6db74">&#34;http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd&#34;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;mirrors&gt;</span>
    <span style="color:#f92672">&lt;mirror&gt;</span>
      <span style="color:#f92672">&lt;id&gt;</span>alimaven<span style="color:#f92672">&lt;/id&gt;</span>
      <span style="color:#f92672">&lt;name&gt;</span>aliyun maven<span style="color:#f92672">&lt;/name&gt;</span>
      <span style="color:#f92672">&lt;url&gt;</span>http://maven.aliyun.com/nexus/content/groups/public/<span style="color:#f92672">&lt;/url&gt;</span>
      <span style="color:#f92672">&lt;mirrorOf&gt;</span>central<span style="color:#f92672">&lt;/mirrorOf&gt;</span>
    <span style="color:#f92672">&lt;/mirror&gt;</span>
  <span style="color:#f92672">&lt;/mirrors&gt;</span>
  <span style="color:#f92672">&lt;profiles&gt;</span>
  <span style="color:#f92672">&lt;/profiles&gt;</span>
<span style="color:#f92672">&lt;/settings&gt;</span></code></pre></div>
<h3 id="如何开发-java-8-以及以下版本的项目">如何开发 Java 8 以及以下版本的项目</h3>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.java">Language Support for Java™ by Red Hat</a> 1.3.0 版本以上的平台特定版本免配置，即可自动支持（限制在 VSCode 中通过 微软官方扩展市场支持特定平台的版本扩展市场）（Open VSX 不支持）</li>

<li><p>旧版或者通用版本 Java 11 以上版本的 JRE 来启动 Language Server</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#f92672">&#34;java.home&#34;</span>:<span style="color:#e6db74">&#34;/path/to/jdk11&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">旧版本</span> <span style="color:#960050;background-color:#1e0010">Java</span> <span style="color:#960050;background-color:#1e0010">Languange</span> <span style="color:#960050;background-color:#1e0010">Server</span>
<span style="color:#f92672">&#34;java.jdt.ls.java.home&#34;</span>: <span style="color:#e6db74">&#34;/path/to/jdk11&#34;</span>
}</code></pre></div></li>
</ul>

<h3 id="如何支持-maven-项目动态生成的代码">如何支持 Maven 项目动态生成的代码</h3>

<blockquote>
<p>参见：<a href="https://github.com/redhat-developer/vscode-java/wiki/Annotation-Processing-support-for-Maven-projects">Wiki</a></p>
</blockquote>

<p>pom.xml 中添加 <code>build-helper-maven-plugin</code>，让 Java Language Server 识别生成的代码。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">  <span style="color:#f92672">&lt;properties&gt;</span>
    <span style="color:#f92672">&lt;generatedSources&gt;</span>${project.build.directory}/generated-sources/java<span style="color:#f92672">&lt;/generatedSources&gt;</span>
  <span style="color:#f92672">&lt;/properties&gt;</span>
  <span style="color:#f92672">&lt;build&gt;</span>
    <span style="color:#f92672">&lt;plugins&gt;</span>
      <span style="color:#f92672">&lt;plugin&gt;</span>
        <span style="color:#f92672">&lt;groupId&gt;</span>org.codehaus.mojo<span style="color:#f92672">&lt;/groupId&gt;</span>
        <span style="color:#f92672">&lt;artifactId&gt;</span>build-helper-maven-plugin<span style="color:#f92672">&lt;/artifactId&gt;</span>
        <span style="color:#f92672">&lt;version&gt;</span>3.0.0<span style="color:#f92672">&lt;/version&gt;</span>
        <span style="color:#f92672">&lt;executions&gt;</span>
          <span style="color:#f92672">&lt;execution&gt;</span>
            <span style="color:#75715e">&lt;!-- Need to ensure the generated source folder is added to the project classpath, in jdt.ls --&gt;</span>
            <span style="color:#f92672">&lt;id&gt;</span>add-source<span style="color:#f92672">&lt;/id&gt;</span>
            <span style="color:#f92672">&lt;phase&gt;</span>generate-sources<span style="color:#f92672">&lt;/phase&gt;</span>
            <span style="color:#f92672">&lt;goals&gt;</span>
              <span style="color:#f92672">&lt;goal&gt;</span>add-source<span style="color:#f92672">&lt;/goal&gt;</span>
            <span style="color:#f92672">&lt;/goals&gt;</span>
            <span style="color:#f92672">&lt;configuration&gt;</span>
              <span style="color:#f92672">&lt;sources&gt;</span>
                <span style="color:#f92672">&lt;source&gt;</span>${generatedSources}<span style="color:#f92672">&lt;/source&gt;</span>
              <span style="color:#f92672">&lt;/sources&gt;</span>
            <span style="color:#f92672">&lt;/configuration&gt;</span>
          <span style="color:#f92672">&lt;/execution&gt;</span>
        <span style="color:#f92672">&lt;/executions&gt;</span>
      <span style="color:#f92672">&lt;/plugin&gt;</span>
    <span style="color:#f92672">&lt;/plugins&gt;</span>
  <span style="color:#f92672">&lt;/build&gt;</span></code></pre></div>
<h3 id="vscode-spring-boot-yaml-和-properties-识别自定义配置类">VSCode Spring Boot Yaml 和 Properties 识别自定义配置类</h3>

<p>添加如下依赖，生成项目的 Configuration Metadata，更多参见：<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata.annotation-processor">Spring 官方文档</a> | <a href="https://www.baeldung.com/spring-boot-configuration-metadata">博客</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;dependency&gt;</span>
    <span style="color:#f92672">&lt;groupId&gt;</span>org.springframework.boot<span style="color:#f92672">&lt;/groupId&gt;</span>
    <span style="color:#f92672">&lt;artifactId&gt;</span>spring-boot-configuration-processor<span style="color:#f92672">&lt;/artifactId&gt;</span>
    <span style="color:#f92672">&lt;optional&gt;</span>true<span style="color:#f92672">&lt;/optional&gt;</span>
<span style="color:#f92672">&lt;/dependency&gt;</span></code></pre></div>
<h3 id="避免断点到-spring-切面代码中">避免断点到 Spring 切面代码中</h3>

<p>将相关不想断点进入的包路径，配置到 launch.json 的 Java 调试配置的 <code>stepFilters</code> 的 <code>skipClasses</code> 中。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;java&#34;</span>,
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch DemoApplication&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
            <span style="color:#f92672">&#34;mainClass&#34;</span>: <span style="color:#e6db74">&#34;com.example.demo.DemoApplication&#34;</span>,
            <span style="color:#f92672">&#34;projectName&#34;</span>: <span style="color:#e6db74">&#34;demo&#34;</span>,
            <span style="color:#f92672">&#34;stepFilters&#34;</span>: {
                <span style="color:#f92672">&#34;skipClasses&#34;</span>: [
                    <span style="color:#e6db74">&#34;$JDK&#34;</span>,
                    <span style="color:#e6db74">&#34;$Libraries&#34;</span>,
                ],
                <span style="color:#f92672">&#34;skipSynthetics&#34;</span>: <span style="color:#66d9ef">true</span>,
                <span style="color:#f92672">&#34;skipStaticInitializers&#34;</span>: <span style="color:#66d9ef">false</span>,
                <span style="color:#f92672">&#34;skipConstructors&#34;</span>: <span style="color:#66d9ef">false</span>
            }
        }
    ]
}</code></pre></div>
<p>注意：上例中的配置将会禁止调试时进入 <code>JDK</code> 和 所有外部依赖库，可能会影响阅读和调试库代码。可以尝试使用：<code>xxx.xxx.ClassXxx</code> 或 <code>xxx.xxx.*</code>的方式进行细粒度的禁用（相对比较繁琐）。</p>

<h2 id="故障排除-troubleshooting">故障排除 Troubleshooting</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/java/java-faq">FAQ</a></li>
<li>确认提供该 Feature 的扩展</li>
<li>查看各个扩展 Troubleshooting 手册

<ul>
<li>调试相关 <a href="https://github.com/microsoft/vscode-java-debug/blob/main/Troubleshooting.md">Troubleshooting</a></li>
<li>Language Server <a href="https://github.com/redhat-developer/vscode-java/wiki/Troubleshooting">Troubleshooting</a></li>
<li>Language Server <a href="https://github.com/redhat-developer/vscode-java/wiki/Use-proper-cacerts-to-import-Java-projects">证书问题</a></li>
<li>Language Server <a href="https://github.com/redhat-developer/vscode-java/wiki/Using-a-Proxy">Proxy 问题</a></li>
</ul></li>
</ul>

<h2 id="reference">Reference</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/languages/java#_install-visual-studio-code-for-java">VSCode Docs - Language Java</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-project">VSCode Docs - Java - Project Manager</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-editing">VSCode Docs - Java - Navigate and Edit</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-build#_maven">VSCode Docs - Java - Build Tools</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-editing">VSCode Docs - Java - Navigate and Edit</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-refactoring">VSCode Docs - Java - Refactoring</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-linting">VSCode Docs - Java - Linting</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-debugging">VSCode Docs - Java - Debugging</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-testing">VSCode Docs - Java - Testing</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-spring-boot">VSCode Docs - Java - Application Servers</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-spring-boot">VSCode Docs - Java - Spring Boot</a></li>
<li><a href="https://code.visualstudio.com/docs/java/java-gui">VSCode Docs - Java - GUI Applications</a></li>
</ul>
]]></description></item><item><title>VSCode 1.45 (2020-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_45_2020-04/</link><pubDate>Sat, 16 May 2020 11:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_45_2020-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_45">https://code.visualstudio.com/updates/v1_45</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li><code>F6</code> / <code>Shift  + F6</code> 焦点切换</li>
<li>使用鼠标滚轮快速切换标签 <code>shift + 滚轮</code></li>
<li>默认 开启 关闭文件再次打开后，Undo/Redo 仍有数据</li>
<li>通过命令窗口开启调试 <code>&gt;debug select and start debugging</code></li>
<li>内置 Github 凭据管理器，可以免输密码</li>
<li>内联 Diff 可编辑</li>
</ul>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<ul>
<li>焦点切换

<ul>
<li>快捷键 <code>F6</code> / <code>Shift  + F</code></li>
<li>命令名 <code>workbench.action.focusNextPart</code></li>
<li>描述：将焦点在侧边栏/编辑器组/面板快速移动</li>
</ul></li>
<li>屏幕阅读器可以读取状态栏内容</li>
<li>在工作台的每个列表和树小部件中引入了适当的ARIA标签，例如“打开编辑器”，“面包屑”，“问题”视图等。

<ul>
<li>关于 <a href="https://developer.mozilla.org/zh-CN/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute">aria-label</a></li>
<li>这个功能主要为屏幕阅读器使用，用来告诉用户当前内容是什么</li>
</ul></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>使用鼠标滚轮快速切换标签

<ul>
<li>配置 <code>workbench.editor.scrollToSwitchTabs</code></li>
<li>上述配置没有打开： 鼠标放置与标签栏中 <code>shift + 滚轮</code> 即可快速切换编辑器</li>
<li>上述配置打开： 鼠标放置与标签栏中 <code>滚轮</code> 即可快速切换编辑器</li>
</ul></li>
<li>自定义窗口标题分隔符

<ul>
<li>配置 <code>window.titleSeparator</code></li>
</ul></li>
<li>更新了侧栏部分标题的默认主题

<ul>
<li>我们已经为默认的深色和浅色主题更新了侧边栏部分标题的样式。现在，我们使用透明背景，并为每个标题显示一个边框。</li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>语法高亮提速</li>
<li>语义令牌样式，指的是通过更深入的语义分析，来进行高亮，支持 JavaScript 和 TypeScript，Java 和 C++正在开发</li>
<li>默认 主题中区分 常量 和 变量，两者颜色不同</li>
<li>默认 开启 关闭文件再次打开后，Undo/Redo 仍有数据。配置项为 <code>files.restoreUndoStack</code></li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>删除 一系列 终端命令

<ul>
<li><code>workbench.action.terminal.deleteWordLeft</code></li>
<li><code>workbench.action.terminal.deleteWordRight</code></li>
<li><code>workbench.action.terminal.deleteToLineStart</code></li>
<li><code>workbench.action.terminal.moveToLineStart</code></li>
<li><code>workbench.action.terminal.moveToLineEnd</code></li>
</ul></li>
<li>以上命令通过 <code>workbench.action.terminal.sendSequence</code> 命令实现</li>
<li>Support for pasting of multi-line text in PowerShell 略</li>

<li><p>控制双击选中范围配置，默认为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;terminal.integrated.wordSeparators&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34; ()[]{}&#39;,\&#34;`─&#34;</span></code></pre></div></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>自动调试配置，通过命令窗口快速开始调试（扩展可以内置一些模板）

<ul>
<li><code>&gt;debug select and start debugging</code></li>
<li>未来会添加 <code>launch.json</code> 的 UI</li>
</ul></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<p>略</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<p>略</p>

<h2 id="源码版本控制-source-control">源码版本控制 (Source Control)</h2>

<ul>
<li>集成 Github 身份认证，可以在不配置系统凭据管理器的情况下免于输入密码，并与终端集成

<ul>
<li>可以通过 <code>git.githubAuthentication</code> 配置</li>
</ul></li>
<li>配置是否显示 commit 消息输入提示 <code>git.showCommitInput</code></li>
<li>内联 Diff 可编辑</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>设置同步（类似 <a href="https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync">Settings Sync</a>），同样类似于 Chrome 的账户配置功能</li>
<li>新 JavaScript 调试器</li>
<li>产品 图标 主题（类似于文件图标主题，比如活动栏图标）</li>
<li>在所有打开的项目中搜索TypeScript / JavaScript符号</li>
<li>终端链接识别和操作改进</li>
<li>动态视图图标和标题</li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li>Remote Development 略</li>
<li>GitHub Pull Requests and Issues 略</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-github-issue-notebooks">GitHub Issue Notebook</a></li>
</ul>

<h2 id="新文档">新文档</h2>

<ul>
<li>Java 主题，添加 了 关于 <a href="https://code.visualstudio.com/docs/java/java-refactoring">重构</a> 和 <a href="https://code.visualstudio.com/docs/java/java-linting">Lint</a> 的文章</li>
<li>添加 <a href="https://code.visualstudio.com/docs/editor/github">Working with GitHub</a></li>
</ul>

<h2 id="其他">其他</h2>

<p>参见官方文档</p>
]]></description></item><item><title>VSCode 1.46 (2020-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_46_2020-05/</link><pubDate>Sat, 18 Jul 2020 17:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_46_2020-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_46">https://code.visualstudio.com/updates/v1_46</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>支持将活动栏窗口拖动到面板窗口，反之亦然</li>
<li>固定住一个窗口，右键编辑器标签，选择 <code>Pin</code> （固定菜单项）</li>
<li>列表和树的平滑滚动 配置 <code>workbench.list.smoothScrolling</code>，系列平滑滚动配置搜索 <code>smooth</code></li>
</ul>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<ul>
<li>状态栏现在支持键盘导航。通过焦点轮转（F6）将焦点放在状态栏中时，使用左右方向键可以在状态栏项目中移动焦点。</li>
<li>锚点光标选中快捷键

<ul>
<li><code>⌘K ⌘B</code> 设置选择定位点</li>
<li><code>⌘K ⌘K</code> 选择从定位点到光标</li>
<li><code>Escape</code> 取消</li>
</ul></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>灵活布局，支持将活动栏窗口拖动到面板窗口，反之亦然。

<ul>
<li>重置命令为：

<ul>
<li><code>View: Reset Focused View Location</code> 重置当前视图位置</li>
<li><code>View: Reset View Locations</code> 重置所有视图位置</li>
</ul></li>
</ul></li>
<li>固定住一个窗口：右键编辑器标签，选择 <code>Pin</code> （固定菜单项）</li>
<li>搜索编辑器，添加了配置选项，略</li>
<li>资源管理器

<ul>
<li>资源管理器添加自动显示焦点而不会滚动到焦点位置的配置 <code>explorer.autoReveal</code></li>
</ul></li>
<li>列表和树的平滑滚动，配置项 <code>workbench.list.smoothScrolling</code></li>
<li>窗口边缘用于调节宽度的触发区域的宽度，默认4像素 <code>workbench.sash.size</code></li>
<li>配置 <code>Screencast</code> 模式将的字体大小 <code>screencastMode.fontSize</code></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<p>略</p>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>提升 对 Link 的支持</li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<p>略</p>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>代码仓库右键支持在终端打开</li>
<li>Add remote from GitHub</li>
<li>Generate .gitignore when publishing to GitHub</li>
<li>Input field font family</li>
<li>Abort in progress rebase</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>自动调试配置改进</li>
<li>Step Into Targets</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<p>略</p>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.47 (2020-06) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_47_2020-06/</link><pubDate>Sat, 18 Jul 2020 18:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_47_2020-06/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_47">https://code.visualstudio.com/updates/v1_47</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>settings 的 UI 支持单层 JSON Object 配置</li>
<li>支持通过将 <code>VSIX</code> 文件拖放到 Extensions 视图来安装 扩展</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>settings 的 UI 支持单层 JSON Object 配置</li>
<li>支持通过将 <code>VSIX</code> 文件拖放到 Extensions 视图来安装 扩展</li>
<li>切换 <code>workbench.list.horizontalScrolling</code> 列表和树水平滚动，不需要重新加载窗口</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>源代码视图变更，合并成一个</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>自动调试配置改进</li>
<li>Step Into Targets</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<p>略</p>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.48 (2020-07) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_48_2020-07/</link><pubDate>Sat, 19 Sep 2020 19:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_48_2020-07/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_48">https://code.visualstudio.com/updates/v1_48</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>官方设置同步已经在稳定版中进行预览 <a href="https://code.visualstudio.com/docs/editor/settings-sync">https://code.visualstudio.com/docs/editor/settings-sync</a> ，使用体验优于 <a href="https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync">Settings Async</a> 扩展</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>添加 <code>&gt;Open Search Editor</code> 打开搜索编辑器的命令</li>
<li>扩展侧边栏 添加 过滤器菜单、更多按钮重构</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>有一个新的设置，即 <code>scm.alwaysShowRepositories</code> ，它使Source Control视图总是显示版本库行，即使只有一个版本库打开。</li>
<li>键盘操作时，可以使用 空格打开视图</li>
<li>Git 更多菜单重构</li>
<li>在GitHub上发布版本库时，现在你可以选择将版本库公开，而不是之前的默认私有。</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li><code>debug.openDebug</code> 设置的默认值已改为 <code>openOnFirstSessionStart</code>。因此，只有在启动第一个调试会话时，才会自动打开Debug视图。</li>
<li>更新了状态栏中的调试图标，使其与我们在活动栏中使用的图标更加一致。这个新的图标应该更清楚地表示，当程序以这种方式启动时，断点将被尊重。</li>
<li>添加 <code>Debug: Open Link</code> 命令</li>
</ul>

<h2 id="浏览器支持-browser-support">浏览器支持 (Browser support)</h2>

<ul>
<li>在浏览器中运行时，VS Code桌面版的所有文本文件编码现在也支持。因此，现在可以为web和工作配置设置files.encoding和files.autoGuessEncoding，方式与桌面版相同。</li>
</ul>

<h2 id="预览特性">预览特性</h2>

<ul>
<li>官方设置同步已经在稳定版中进行预览，使用细节参见 <a href="https://code.visualstudio.com/docs/editor/settings-sync">https://code.visualstudio.com/docs/editor/settings-sync</a></li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor">Hex Editor</a> 二进制文件编辑器，使用 <code>&gt;reopen with</code> 命令打开，或者右键 打开方式，打开二进制文件</li>
<li>提供本地的 Notebook 支持</li>
</ul>
]]></description></item><item><title>VSCode 1.49 (2020-08) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_49_2020-08/</link><pubDate>Sat, 19 Sep 2020 19:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_49_2020-08/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_49">https://code.visualstudio.com/updates/v1_49</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>保存时格式化，配置仅格式化改变的行 <code>editor.formatOnSaveMode = modifications</code></li>
<li>控制在搜索键入时光标是否跳转以查找匹配项 <code>editor.find.cursorMoveOnType</code> 建议关闭，手动按回车再跳转比较好</li>
<li>源代码储存库视图可以通过配置再次显示出来：右击源代码版本控制视图标题</li>
<li>调试控制台支持 过滤器</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>保存时格式化，配置仅格式化改变的行 <code>editor.formatOnSaveMode = modifications</code>，该功能生效的前提

<ul>
<li>git 提供哪些行改变了的信息</li>
<li>格式化器提供部分格式化的能力</li>
</ul></li>
<li>添加命令 <code>&gt;Format Selection</code> 格式化选定内容</li>
<li>控制在搜索键入时光标是否跳转以查找匹配项 <code>editor.find.cursorMoveOnType</code></li>
<li>只显示行位空格配置 <code>editor.renderWhitespace = trailing</code></li>
<li>字体配置支持1-1000数字 <code>editor.fontWeight = 350</code></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>添加命令 支持输出快捷键诊断 <code>Developer: Toggle Keyboard Shortcuts Troubleshooting</code></li>
<li>录屏模式添加一些自定义配置（Screencast mode）

<ul>
<li><code>screencastMode.keyboardOverlayTimeout</code> - Change the timeout (milliseconds) for the keyboard shortcut overlay.</li>
<li><code>screencastMode.mouseIndicatorColor</code> - Set the mouse indicator color (hex #RGB, #RGBA, #RRGGBB or #RRGGBBAA).</li>
<li><code>screencastMode.mouseIndicatorSize</code> - Control the mouse indicator size (pixels).</li>
</ul></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>源代码储存库视图可以通过配置再次显示出来：右击源代码版本控制视图标题</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>调试控制台支持 过滤器</li>
<li>UX 改进：略</li>
</ul>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.50 (2020-09) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_50_2020-09/</link><pubDate>Sat, 17 Oct 2020 19:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_50_2020-09/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_50">https://code.visualstudio.com/updates/v1_50</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>设置编辑器，键盘可访问性增强</li>
</ul>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<ul>
<li>设置编辑器，键盘可访问性增强，支持上下键切换设置，enter键聚焦到当前设置，然后上下键可以增减数字</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>固定标签的改进

<ul>
<li>支持配置 <code>workbench.editor.pinnedTabSizing</code> 配置固定标签大小

<ul>
<li>normal: 固定的标签会继承其他标签的外观（新的默认设置）</li>
<li>shrink: 固定的标签页缩小到固定大小，显示出部分编辑器标签</li>
<li>compact: 固定的标签仅显示为图标或编辑器标签的首字母</li>
</ul></li>
<li>现在，即使禁用了选项卡，也可以固定编辑器。</li>
<li>Cmd + W（Ctrl + W）不再关闭固定的编辑器，而是选择下一个非固定的编辑器。</li>
<li>可以分配新命令 <code>workbench.action.closeActivePinnedEditor</code> 来关闭固定的编辑器。</li>
<li>可以指定新的 <code>tab.lastPinnedBorder</code> 颜色，以在最后固定的选项卡的右侧绘制边框。</li>
</ul></li>
<li>重命名了部分上下文 key （略）</li>
<li>避免扩展推荐过度打扰</li>
<li>面板布局提升</li>
<li>Linux ARM 支持</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<p>多年来，Ctrl + Space一直是触发IntelliSense的主要键绑定。 但是，在macOS和Windows上，使用相同的键绑定在键盘布局之间切换。 为了最大程度地减少混乱，我们添加了另一个键绑定来触发IntelliSense：在Windows和Linux上为Ctrl + I，在macOS上为Cmd + I。</p>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>在调试时，显示 语言 Hover （调试时默认 Hover 显示调试上下文信息），通过按住 <code>Alt</code> 触发。</li>
<li>自动调整调试 Hover 大小</li>
<li>调试控制台现在支持过滤，使用户可以更轻松地查找所需的输出或隐藏无关的日志输出。</li>
<li>JavaScript 调试器提升

<ul>
<li>Consolidated auto attach flows</li>
<li>实时性能视图</li>
<li>左重火焰图视图</li>
<li>逐步查找丢失的代码</li>
</ul></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>Git 添加: Commit &ndash;no-verify 系列命令</li>
<li>可以使用相同的 <code>git.path</code> 设置，使用字符串数组来指定要查找git可执行文件的位置列表</li>
<li>源代码版本控制视图将保存用户输入，当reload后用户输入自动恢复</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>通过在终端中选择文本来搜索工作空间</li>
</ul>
]]></description></item><item><title>VSCode 1.51 (2020-10) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_51_2020-10/</link><pubDate>Wed, 21 Oct 2020 14:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_51_2020-10/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_51">https://code.visualstudio.com/updates/v1_51</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>集成终端：支持 Local Echo ，在连接 server，或者通过 remote 开发时，用户输入的字符立即得到回显，发送到远端完成后，字符颜色从灰色变白，极大提升流畅度，和体验。</li>
<li>IntelliSense

<ul>
<li>建议列表（智能提示列表），支持拖动边角以调整控件的大小</li>
<li>建议列表下方添加状态栏，用于显示常用快捷键和操作指南</li>
<li>左右移动光标，将触发建议列表的更新</li>
</ul></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>pin tab 支持是否保存小圆点展示</li>
<li>扩展树使用自定义悬停</li>
<li>安装扩展支持不进行同步</li>
<li>支持直接从资源管理器的 <code>.vsix</code> 文件安装扩展</li>
<li>通过 <code>workbench.action.blur</code> 移除焦点，可以用来绑定快捷键</li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>支持 Local Echo ，在连接 server，或者通过 remote 开发时，用户输入的字符立即得到回显，发送到远端完成后，字符颜色从灰色变白，极大提升流畅度，和体验。两个配置如下：

<ul>
<li><code>terminal.integrated.localEchoLatencyThreshold</code> 配置检测到的延迟阈值（以毫秒为单位），在该阈值处将激活本地回波。 可以将其设置为0以始终打开该功能，或将其设置为-1以禁用它。 默认为30。</li>
<li><code>terminal.integrated.localEchoStyle</code> 配置本地字符的样式或颜色，默认为暗淡。</li>
</ul></li>
</ul>

<h2 id="智能感知-intellisense">智能感知 (IntelliSense)</h2>

<ul>
<li>建议列表（智能提示列表），支持拖动边角以调整控件的大小。建议列表的大小将在各个会话中保存和恢复。详细信息窗格的大小仅在每个会话中保存，因为该大小倾向于可变得多。同样，<code>editor.suggest.maxVisibleSuggestions</code> 设置也已过时。</li>
<li>建议列表下方添加状态栏，用于显示常用快捷键和操作指南，可使用 <code>editor.suggest.showStatusBar</code> 配置开关</li>
<li>左右移动光标，将触发建议列表的更新</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>源代码控制输入框保存提交消息历史记录，支持上下键切换到历史提交记录上</li>
<li>Git 子菜单（点点点）支持创建 Tag</li>
<li>Git 添加 Rebase 命令</li>
<li>Git 支持递归克隆命令（递归克隆 git 的 submodule）</li>
<li>时间线支持渲染 emoji 短码，比如 <code>:smile:</code></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown 支持智能选择

<ul>
<li>扩大选择 Expand: <code>⌃⇧⌘→</code></li>
<li>缩小选择 Shrink: <code>⌃⇧⌘←</code></li>
</ul></li>
<li>JavaScript 和 TypeScript 空函数格式化支持配置是否删除空格</li>
</ul>

<h2 id="浏览器支持-browser-support">浏览器支持 (Browser support)</h2>

<ul>
<li>下载一个目录 (Edge, Chrome)</li>
<li>当一个目录包含 <code>.code-workspace</code> 目录将收到通知</li>
<li>防止浏览器以为关闭，添加如下 <code>window.confirmBeforeClose</code> 配置

<ul>
<li><code>keyboardOnly</code> keyboardOnly仅当您使用键盘绑定关闭时（例如⌘W），才会显示确认。 （默认）</li>
<li><code>always</code> 即使您用鼠标手势关闭，也将始终显示确认对话框。</li>
<li><code>never</code> 永远不会显示该确认。</li>
</ul></li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>添加 Learn 站点 <a href="https://code.visualstudio.com/learn">code.visualstudio.com/learn</a></li>
</ul>

<h2 id="新命令-new-commands">新命令 (New commands)</h2>

<p>添加一系列 force 命令</p>
]]></description></item><item><title>VSCode 1.52 (2020-11) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_52_2020-11/</link><pubDate>Sun, 27 Dec 2020 18:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_52_2020-11/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_52">https://code.visualstudio.com/updates/v1_52</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>资源管理器支持 Undo 操作（比如撤销移动文件），焦点需要在资源管理器</li>
<li>打开编辑器页面，排序支持配置 <code>explorer.openEditors.sortOrder</code>，推荐使用 字母排序 <code>alphabetical</code></li>
<li>改善 整体 滚动条体验，比如侧边栏树过多时，将可以整体滚动</li>
<li>支持禁用扩展和其依赖项</li>
<li>扩展问题快速排查 <code>Help: Start Extension Bisect</code> （开始扩展二等分） 命令，<a href="https://code.visualstudio.com/updates/v1_52#_troubleshooting-extension-bisect">参见</a></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>预览编辑器模式（标题为斜体的模式）

<ul>
<li>添加了 <code>workbench.editor.enablePreviewFromQuickOpen</code> 配置</li>
<li>代码导航改善（比如 跳转到定义 F12）

<ul>
<li><code>A -&gt; B</code> 时，B 为 预览模式</li>
<li><code>B -&gt; C</code> 时，B 变为 非预览模式，C 为预览模式</li>
</ul></li>
<li>编辑器溢出菜单（右上角&hellip;），添加“使编辑器保持打开”菜单</li>
</ul></li>
<li>重新打开VSCode的恢复行为，<code>window.restoreWindows</code> 添加新的选项

<ul>
<li><code>preserve</code>，当 通过 命令行 <code>code</code> 命令打开一个目录时，将恢复之前未关闭的窗口</li>
</ul></li>
<li>添加配置 <code>workbench.editor.splitOnDragAndDrop</code> 控制拖动编辑器标签的行为（<code>shift</code> 键可以切换其行为）</li>
<li>资源管理器支持 Undo 操作（比如撤销移动文件），焦点需要在资源管理器</li>
<li>资源管理器长时间操作，进度条提示，并支持 undo 按钮</li>
<li>打开编辑器页面，排序支持配置 <code>explorer.openEditors.sortOrder</code>，不影响标签顺序

<ul>
<li><code>editorOrder</code> 默认，和打开顺序一致</li>
<li><code>alphabetical</code> 字母排序</li>
</ul></li>
<li>提升终端环境处理（异步解析 shell 环境变量）</li>
<li>改善 整体 滚动条体验，比如侧边栏树过多时，将可以整体滚动</li>
<li>可以通过 <code>sash.hoverBorder</code> 自定义 hover 边框颜色</li>
<li>树展开模式配置 <code>workbench.tree.expandMode</code></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>重启后 源代码控制 视图将仍然会保持之前的折叠状态</li>
<li>添加了几个命令

<ul>
<li>Git: Cherry Pick&hellip; - Cherry pick a specific commit to your branch.</li>
<li>Git: Rename - Perform a git rename of the active file.</li>
<li>Git: Push Tags - Push all local tags to the remote.</li>
<li>Git: Checkout to (Detached)&hellip; - Perform a checkout in detached mode.</li>
</ul></li>
<li>添加了几个新设置

<ul>
<li>git.pruneOnFetch - Make VS Code run git fetch &ndash;prune when fetching remote refs.</li>
<li>git.ignoreSubmodules - You can now make sure VS Code ignores changes in submodule repositories, which is useful in large monorepos.</li>
<li>git.openAfterClone - Control whether and how to open a folder after you cloned a git repository: on the current window, on a new window, when no folder is opened and by prompting the user.</li>
<li>git.useCommitInputAsStashMessage - Enable VS Code to use the commit message in the source control input box as a stash message, when running Git: Stash.</li>
<li>git.followTagsWhenSync - Follow tags when running Git: Sync.</li>
<li>git.checkoutType - Control what refs are shown, and in what order, when you run the Git: Checkout&hellip; command.</li>
</ul></li>
<li>其他略</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>调试侧边栏，断点视图，支持添加断点条件（VSCode 层面支持，但是还没有扩展支持）</li>
<li>UI 改进</li>
<li>launch.json 和 tasks.json 变量

<ul>
<li><code>${fileWorkspaceFolder}</code> 文件所在工具空间区目录</li>
<li><code>${fileDirnameBasename}</code> 文件所在目录</li>
<li><code>${pathSeparator}</code> 路径分隔符</li>
</ul></li>
<li>其他 略</li>
</ul>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li>扩展侧边栏和扩展详情页联动</li>
<li>扩展详情页支持更多操作</li>
<li>支持禁用扩展和其依赖项</li>
<li>扩展问题快速排查 <code>Help: Start Extension Bisect</code> （开始扩展二等分） 命令，<a href="https://code.visualstudio.com/updates/v1_52#_troubleshooting-extension-bisect">参见</a></li>
</ul>

<h2 id="快捷键编辑器-keyboard-shortcuts-editor">快捷键编辑器 (Keyboard Shortcuts editor)</h2>

<ul>
<li><code>cmd + shift + p</code> 命令窗口，支持快捷键快速配置，鼠标 hover 上去，快速跳转到快捷键编辑器</li>
<li>搜索框支持如下 filter 命令

<ul>
<li>@command:commandId - Filters by command ID. For example,</li>
<li>@command:workbench.actions.showCommands.</li>
<li>@keybinding:keybinding - Filters by keybinding. For example, @keybinding:f1.</li>
<li>@source:user|default|extension - Filters by source.</li>
</ul></li>
</ul>

<h2 id="智能感知-intellisense">智能感知 (IntelliSense)</h2>

<ul>
<li>在没有语言服务器的况下，VSCode 将提示本文档中的单词，现在通过 <code>editor.wordBasedSuggestionsMode</code> 支持提示全部文档中的单词</li>
<li>配置 <code>editor.suggest.showInlineDetails</code></li>
<li>TypeScript 自动导入，支持显示路径</li>
<li>自定义 <code>Customize CodeLens</code> 字体字号

<ul>
<li><code>&quot;editor.codeLensFontFamily&quot;: &quot;Comic Sans MS&quot;,</code></li>
<li><code>&quot;editor.codeLensFontSize&quot;: 12,</code></li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><code>editor.stickyTabStops</code> 空格选择类似于 Tab 的行为</li>
<li>添加命令，删除单词 <code>deleteInsideWord</code>，删除光标处的整个单词，和 <code>alt + 退格</code> 不同（为 <code>deleteWordLeft</code>）</li>
<li>差异编辑器，自动换行和编辑器行为一致</li>
<li><code>Insert Snippet</code> 命令将显示所有与的代码片段，添加隐藏按钮，以隐藏该代码片段</li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>集成终端配置快速进入，通过终端下拉框可以选择</li>
<li><a href="https://code.visualstudio.com/updates/v1_52#_keybindings-management">Keybindings management</a></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown 内联智能选择

<ul>
<li>Expand: <code>⌃⇧⌘→</code></li>
<li>Shrink: <code>⌃⇧⌘←</code></li>
</ul></li>
<li>其他略</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li><a href="https://docs.microsoft.com/zh-cn/learn/modules/use-docker-container-dev-env-vs-code/">借助 Visual Studio Code 将 Docker 容器用作开发环境</a></li>
<li><a href="https://docs.microsoft.com/zh-cn/learn/modules/introduction-to-github-visual-studio-code/">Visual Studio Code 中的 GitHub 简介</a></li>
<li><a href="https://www.youtube.com/watch?v=-Olo7N9xwV8">How we make VS Code in the open</a></li>
<li><a href="https://code.visualstudio.com/blogs/2020/12/03/chromebook-get-started">Learning with VS Code on Chromebooks</a></li>
</ul>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.53 (2021-01) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_53_2021-01/</link><pubDate>Sat, 06 Feb 2021 18:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_53_2021-01/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_53">https://code.visualstudio.com/updates/v1_53</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<ul>
<li>编辑器标签栏，支持 Wrap 标签特性，可以实现类似 IDEA 的多行标签栏特性。通过 <code>workbench.editor.wrapTabs</code> 配置，建议开启该特性</li>
<li>标签栏支持开启 Git 状态 的颜色和标签的展示，通过 <code>workbench.editor.decorations.colors</code> 和 <code>workbench.editor.decorations.badges</code> 配置 ，建议开启该特性</li>
<li>添加将变量命名转换为蛇形风格 的 命名 <code>&gt; Transform to Snake Case</code></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>编辑器标签栏，支持 Wrap 标签特性，可以实现类似 IDEA 的多行标签栏特性。通过 <code>workbench.editor.wrapTabs</code> 配置</li>
<li>标签栏支持开启 Git 状态 的颜色和标签的展示，通过 <code>workbench.editor.decorations.colors</code> 和 <code>workbench.editor.decorations.badges</code> 打开</li>
<li>新用户默认的资源管理器，将默认隐藏展示打开的编辑器</li>
<li>存在未保存的文件而退出 VSCode 时，将提示一个确认框</li>
<li>代码跳转时，新的编辑器默认更改为“打开模式”，之前为“预览模式”，可以通过 <code>workbench.editor.enablePreviewFromCodeNavigation</code> 配置</li>
<li>重构支持创建、移动、重命名、删除文件行为。并给用户 预览 和提示</li>
<li>使用 <code>&gt; Reopen Editor With</code> 命令重新打开文件时，支持更好的 keyboard nice

<ul>
<li>直接选中，替换掉当前编辑器</li>
<li>按住 ctrl 选中，则会以以分栏的形式打开新的编辑器</li>
<li>按右方向键，可以预览效果</li>
</ul></li>
<li>搜索模式支持配置（<code>command + shift + f</code>），通过 <code>search.mode</code> 配置

<ul>
<li><code>view</code> 默认模式，打开传统的搜索侧边栏</li>
<li><code>reuseEditor</code> 将打开 搜索编辑器，如果存在搜索编辑器，将打开</li>
<li><code>newEditor</code> 始终打开一个新的搜索编辑器</li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>代码片段，支持随机生成 UUID 能力</li>
<li>添加将变量命名转换为蛇形风格 的 命名 <code>&gt; Transform to Snake Case</code></li>
</ul>

<h2 id="调试器-debugger">调试器 (Debugger)</h2>

<ul>
<li>同一份调试配置可以启动多次</li>
<li>调试前提示用户保存文件</li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<p>略</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown 预览会监听本地图片变化而自动刷新</li>
</ul>

<h2 id="扩展创作-extension-authoring">扩展创作 (Extension authoring)</h2>

<ul>
<li>添加 <a href="https://code.visualstudio.com/api/references/extension-guidelines">VSCode 扩展开发准则</a> 文档</li>
<li><a href="https://code.visualstudio.com/api/extension-guides/product-icon-theme">产品图标主题</a>已稳定</li>
<li>状态栏支持背景颜色API</li>
<li>Secrets API</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/editor/workspaces">什么是 VSCode 的 workspace</a></li>
</ul>
]]></description></item><item><title>VSCode 1.54 (2021-02) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_54_2021-02/</link><pubDate>Sat, 13 Mar 2021 14:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_54_2021-02/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_54">https://code.visualstudio.com/updates/v1_54</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>在窗口重载后，保留集成终端状态。默认开启，使用 <code>terminal.integrated.enablePersistentSessions</code> 配置关闭</li>
<li>自动重新启动需要更改环境的终端。当终端环境发生变化（一般是 bashrc 等配置文件变更），终端将会显示一个 ⚠️  图标提示用户重启终端，现在，在没有操作过的终端中，可以将自动重启，以使流程更加连贯</li>
<li>首个产品图标主题已上线 <a href="https://marketplace.visualstudio.com/items?itemName=miguelsolorio.fluent-icons">Fluent Icons</a></li>
<li>支持 双击 shift 类的快捷键操作，可以自主的通过更改 <code>keybindings.json</code> 配置文件实现配置，例如 <code>{ &quot;key&quot;: &quot;shift shift&quot;, &quot;command&quot;: &quot;workbench.action.quickOpen&quot; }</code></li>
<li>时间线视图，支持比较两个分支的变化（通过右键上下文菜单，Select for Compare 和 Compare with Selected 两个选项）</li>
<li>断点视图改进

<ul>
<li>断点列表项，添加 X 图标用于删除该断点</li>
<li>Caught 和 Uncaught Exceptions 支持编辑断点条件</li>
</ul></li>
<li>新的配置项和其他改进

<ul>
<li><code>debug.console.collapseIdenticalLines</code> 用于折叠相同的行（类似 chrome 开发者工具的行为），默认开启</li>
<li><code>debug.saveBeforeStart</code> 在启动前保存行为的配置，建议更改为 <code>nonUntitledEditorsInActiveGroup</code></li>
</ul></li>
<li>Remote Development，更多参见 <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_54.md">remote-release-notes</a>

<ul>
<li>端口视图使用 表格布局

<ul>
<li>支持在编辑器中预览网页</li>
<li>支持显示进程 名 和 id</li>
</ul></li>
<li>Remote - SSH: X11 Forwarding</li>
<li><code>remote.autoForwardPortsSource</code> 添加 output 选项</li>
</ul></li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>列表/树 UI 更新，焦点元素以轮廓色方式显示（之前是背景色）</li>
<li>新增表格 UI 组件</li>
<li>Authentication Provider API</li>
</ul>

<h2 id="apple-silicon">Apple Silicon</h2>

<p>发布了 支持 Apple M1 平台的软件包</p>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<p>略</p>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>在窗口重载后，保留集成终端状态。默认开启，使用 <code>terminal.integrated.enablePersistentSessions</code> 配置关闭</li>
<li>Windows 平台，性能提升</li>
<li>自动重新启动需要更改环境的终端。当终端环境发生变化（一般是 bashrc 等配置文件变更），终端将会显示一个 ⚠️ 图标提示用户重启终端，现在，在没有操作过的终端中，可以将自动重启，以使流程更加连贯</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>Emment 添加 <code>emmet.extensionsPath</code> 配置</li>
<li>首个产品图标主题已上线 <a href="https://marketplace.visualstudio.com/items?itemName=miguelsolorio.fluent-icons">Fluent Icons</a></li>
<li><code>keybindings.json</code> when 字段的智能提示支持</li>
<li>打开的编辑器，添加新建无标题文件的 icon</li>
<li>菜单栏更新</li>
<li>webview 缓存提升</li>
<li>工作空间搜索行为改变</li>
<li>支持 双击 shift 类的快捷键操作，可以自主的通过更改 <code>keybindings.json</code> 配置文件实现配置，例如 <code>{ &quot;key&quot;: &quot;shift shift&quot;, &quot;command&quot;: &quot;workbench.action.quickOpen&quot; }</code></li>
<li>列表/树 UI 更新，焦点元素以轮廓色方式显示（之前是背景色）</li>
<li>新增表格 UI 组件</li>
<li>get started 编辑器 （未发布到稳定版）</li>
<li>时间线视图，支持比较两个分支的变化（通过右键上下文菜单，Select for Compare 和 Compare with Selected 两个选项）</li>
<li>提供默认边框悬停颜色，可通过在主题插件中可以通过 <code>sash.hoverBorder</code> 参数自定义</li>
</ul>

<h2 id="调试器-debugging">调试器 (Debugging)</h2>

<ul>
<li>断点视图改进

<ul>
<li>断点列表项，添加 X 图标用于删除该断点</li>
<li>Caught 和 Uncaught Exceptions 支持编辑断点条件</li>
</ul></li>
<li>新的配置项和其他改进

<ul>
<li><code>debug.console.collapseIdenticalLines</code> 用于折叠相同的行（类似 chrome 开发者工具的行为），默认开启</li>
<li><code>debug.saveBeforeStart</code> 在启动前保存行为的配置，建议更改为 <code>nonUntitledEditorsInActiveGroup</code></li>
<li>改进重新启动帧</li>
</ul></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/">TypeScript 4.2</a></li>
</ul>

<h2 id="notebooks">Notebooks</h2>

<p>略</p>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>Extensible Markdown renderers for notebooks （支持 公式 等能力）</li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li>添加 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.brackets-pack">Brackets 扩展包</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a> more see <a href="https://github.com/microsoft/vscode-pull-request-github/blob/master/CHANGELOG.md#0240">changelog 0.24.0</a></li>
<li>Remote Development，更多参见 <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_54.md">remote-release-notes</a>

<ul>
<li>端口视图使用 表格布局

<ul>
<li>支持在编辑器中预览网页</li>
<li>支持显示进程 名 和 id</li>
</ul></li>
<li>Remote - SSH: X11 Forwarding</li>
<li><code>remote.autoForwardPortsSource</code> 添加 output 选项</li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Authentication Provider API</li>
<li>其他 略</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>VS Code and Python in the classroom, more see <a href="https://code.visualstudio.com/learn/educators/python">learn/educators/python</a></li>
<li><a href="https://code.visualstudio.com/blogs/2021/02/16/extension-bisect">Troubleshooting extensions blog post</a></li>
</ul>
]]></description></item><item><title>VSCode 1.55 (2021-03) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_55_2021-03/</link><pubDate>Tue, 11 May 2021 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_55_2021-03/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_55">https://code.visualstudio.com/updates/v1_55</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>只打开的编辑器中搜索，在搜索侧边栏，通过：<code>... -&gt; 需要包含的文件输入框最右侧的书本图标</code> 配置</li>
<li>断点命中规则新增

<ul>
<li>命中次数类型</li>
<li>变量发生变更</li>
</ul></li>
<li>调试启动前清除终端输出配置 <code>debug.terminal.clearBeforeReusing</code></li>
<li>Remote Development

<ul>
<li>双击以命名端口</li>
</ul></li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>package.json 添加 <code>extensionPack</code> 属性，不用归类到扩展包，也可以在扩展详情页的扩展包标签展示依赖的扩展</li>
<li><a href="https://code.visualstudio.com/api/references/vscode-api#InlineValuesProvider">调试器内联值 API</a> 已经完成，例子<a href="https://code.visualstudio.com/updates/v1_54#_inline-value-provider-api">参见</a>，可以实现 IDEA 编辑器内直接显示变量值的效果</li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<ul>
<li>屏幕阅读器支持多光标</li>
<li>屏幕阅读器限制提升到 1000 行</li>
</ul>

<h2 id="工作区-workbench">工作区 (Workbench)</h2>

<ul>
<li>Windows 加密更新</li>
<li>更新 macOS Big Sur 的 icon 为规则圆角方形</li>
<li>添加 <code>workbench.sash.hoverDelay</code> 配置可拖动区域悬停反馈延迟，<code>workbench.sash.size</code> 对触摸设备进行优化</li>
<li>改进的列表/树导航，键盘导航 和 shift + 单击 更加自然</li>
<li>默认情况下，选项卡装饰开启，相关配置参见 <code>workbench.editor.decorations.color</code>  和<code>workbench.editor.decorations.badges</code></li>
<li>在键盘快捷键编辑器中调整列的大小，键盘快捷键通过表格组件重新实现</li>
<li>扩展管理器提升

<ul>
<li>VS Code现在可以通过VS Code CLI检测安装/卸载的扩展。Extensions can be activated/de-activated in the active window (instance) and will be correctly displayed in the Extensions view.</li>
<li>扩展开发中，package.json 添加 <code>extensionPack</code> 属性，不用归类到扩展包，也可以在扩展详情页的扩展包标签展示依赖的扩展</li>
</ul></li>
<li>问题视图中支持反向文本过滤 （<code>!</code> 字符开头）</li>
<li>添加<a href="https://github.com/microsoft/vsmarketplace">扩展市场 Issue Github 仓库</a></li>
<li>现在可以用 <code>workbench.hover.delay</code> 来配置 tree 视图延迟 悬停延迟</li>
<li><code>emmet.extensionsPath</code> 只支持字符串数组</li>
<li>只打开的编辑器中搜索，在搜索侧边栏，通过：<code>... -&gt; 需要包含的文件输入框最右侧的书本图标</code> 配置</li>
<li>配置源代码输入框字体大小 <code>scm.inputFontSize</code></li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>终端下拉框，添加本机常见的 Terminal 选项</li>
<li>添加 <code>terminal.integrated.profiles.&lt;platform&gt;</code> 配置项</li>
<li>WebGL 渲染成为终端渲染的默认选项</li>
<li>无缝终端重启：上个版本，我们引入了当扩展要改变环境时自动重新启动终端的功能。这个版本现在可以防止之前在重启发生时引起的闪烁现象。如果新终端的输出与上一个终端相同，就不会有重新启动引起的反馈或分心。还有一个新的设置，可以全部禁用这种自动重新启动的功能 <code>terminal.integrated.environmentChangesRelaunch</code> （<a href="https://code.visualstudio.com/api/references/vscode-api#EnvironmentVariableCollection">API 文档</a>）</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>括号自动闭合，只有当右括号是 VSCode 自动输入时，光标在括号中间，按退格时才删除前后两个括号</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>断点命中规则新增

<ul>
<li>命中次数类型</li>
<li>变量发生变更</li>
</ul></li>
<li>调试启动前清除终端输出配置 <code>debug.terminal.clearBeforeReusing</code></li>
<li>JavaScript debugging 略</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>多单元格选择：我们现在支持使用鼠标（Shift+Click）或键盘快捷键（Shift+Arrow）在笔记本中选择多个单元格。一旦选择了多个单元格，你就可以复制/剪切/粘贴/移动/复制选定的单元格。</li>
<li>在差异编辑器中显示/隐藏输出和元数据差异（位于溢出菜单），可通过 <code>notebook.diff.ignoreMetadata</code> 和 <code>notebook.diff.ignoreOutputs</code> 配置</li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li>Remote Development

<ul>
<li>双击以命名端口</li>
<li>可配置的默认端口检测行为</li>
<li>更多：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_55.md">参加</a></li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><a href="https://code.visualstudio.com/api/references/vscode-api#CustomDocumentOpenContext">untitledDocumentData</a></li>
<li><a href="https://code.visualstudio.com/api/references/vscode-api#InlineValuesProvider">调试器内联值 API</a> 已经完成，例子<a href="https://code.visualstudio.com/updates/v1_54#_inline-value-provider-api">参见</a>，可以实现 IDEA 编辑器内直接显示变量值的效果</li>
<li>Copy as 子菜单 <code>menuBar/edit/copy</code>，<code>editor/context/copy</code></li>
<li><code>ExtensionContext</code> 添加新属性，<a href="https://code.visualstudio.com/api/references/vscode-api#Extension%3CT%3E">参见</a></li>
<li>添加 <code>dockercompose</code> 语言</li>
<li>遥测启用API（追踪）：<code>isTelemetryEnabled</code> 和 <code>onDidChangeTelemetryEnabled</code></li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更快的Snap启动</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>树莓派：<a href="https://code.visualstudio.com/docs/setup/raspberry-pi">安装教程</a></li>
</ul>
]]></description></item><item><title>VSCode 1.56 (2021-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_56_2021-04/</link><pubDate>Sat, 03 Jul 2021 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_56_2021-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_56">https://code.visualstudio.com/updates/v1_56</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>Untitled 编辑器 hint，并添加可点击快速操作，可以选择该编辑器的语言</li>
<li>终端新快捷键

<ul>
<li>切换到上一个终端 - <code>Ctrl+PageUp</code> (macOS <code>Cmd+Shift+]</code>)</li>
<li>切换到下一个终端 - <code>Ctrl+PageDown</code> (macOS <code>Cmd+shift+[</code>)</li>
<li>聚焦到终端标签栏 - <code>Ctrl+Shift+\</code> (macOS <code>Cmd+Shift+\</code>)</li>
</ul></li>
<li>UI

<ul>
<li><code>&quot;window.dialogStyle&quot;: &quot;custom&quot;</code> 使用 非 操作系统Native 弹窗优化</li>
<li><code>workbench.experimental.useCustomHover</code>  开启和 VSCode UI 更加一致的 Hover 样式，替代操作系统原生的 Hover</li>
<li><code>&quot;terminal.integrated.tabs.enabled&quot;: true</code> 启用终端 tabs 栏</li>
</ul></li>
<li>调试强制打开调试扩展的内联变量 <code>debug.inlineValues</code> 配置（不一定显示正确）</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li><code>&quot;workbench.welcomePage.experimental.extensionContributions&quot;: true,</code> 可以开启展示扩展的演练场，开发者可以参考 <a href="https://marketplace.visualstudio.com/items?itemName=Tyriar.luna-paint">Luna Paint</a>，实现自己扩展的教学</li>
<li>远程仓库（一个内置扩展）(RemoteHub)，正在内测，引入虚拟工作空间相关概念和 API，参见 <a href="https://github.com/microsoft/vscode/wiki/Virtual-Workspaces">手册</a></li>
<li>扩展可以添加命令到 remote indicator menu（VSCode 左下角按钮），更多参考：<a href="https://code.visualstudio.com/updates/v1_56#_remote-indicator-menu">原文</a></li>
<li>工作空间受信相关 API，参见：<a href="https://code.visualstudio.com/updates/v1_56#_workspace-trust-extension-api">原文</a>，讨论参见：<a href="https://github.com/microsoft/vscode/issues/120251">Issue</a></li>
</ul>

<h2 id="工作区-workbench">工作区 (Workbench)</h2>

<ul>
<li>提升 Hover 反馈，更加明确</li>
<li>Untitled 编辑器 hint，并添加可点击快速操作，可以选择该编辑器的语言</li>
<li>默认自定义编辑器，如果您有两个编辑器都声明它们应该是资源的默认编辑器（例如，图像查看器和图像编辑器），您将收到解决冲突的提示。</li>
<li>更新自定义对话框样式，通过 <code>&quot;window.dialogStyle&quot;: &quot;custom&quot;</code> 配置项体验</li>
<li>支持配置仅自动更新启用的扩展，<code>&quot;extensions.autoUpdate&quot;: &quot;onlyEnabledExtensions&quot;</code></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>Profile 改善，相关介绍：<a href="https://code.visualstudio.com/updates/v1_55#_terminal-profiles">v1_55</a>

<ul>
<li>支持环境变量和 icon 配置</li>
<li><code>terminal.integrated.shell</code> 和 <code>terminal.integrated.shellArgs</code> 在未来将被启用</li>
</ul></li>
<li>新的快捷键

<ul>
<li>切换到上一个终端 - <code>Ctrl+PageUp</code> (macOS <code>Cmd+Shift+]</code>)</li>
<li>切换到下一个终端 - <code>Ctrl+PageDown</code> (<code>macOS Cmd+shift+[</code>)</li>
<li>聚焦到终端标签栏 - <code>Ctrl+Shift+\</code> (macOS <code>Cmd+Shift+\</code>)</li>
</ul></li>
<li>Linux selection paste command：<code>workbench.action.terminal.pasteSelection</code></li>
<li>终端工作区 shell 权限变更的配置：<code>&quot;terminal.integrated.allowWorkspaceConfiguration&quot;: true</code></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<p>略</p>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>断点列表视图提升

<ul>
<li>添加字段读写访问类型断点的展示</li>
<li>更好的异常断点状态/错误报告</li>
</ul></li>
<li>UI 提升：略</li>
<li>一些语言（Java）已经开启支持内联值展示（编辑器右侧支持显示此行的变量值），如果想强制打开使用 <code>debug.inlineValues</code> 配置（不一定显示正确）</li>
<li><code>debug.openDebug</code> 默认值为 <code>openOnDebugBreak</code></li>
</ul>

<h2 id="notebook">NoteBook</h2>

<ul>
<li>Cell 内联行号</li>
<li><code>notebook.cellToolbarLocation</code> 位置</li>
<li>Markdown cells 支持 Math 公式，通过 <code>$...$</code> 和 <code>$$...$$</code> 语法</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><code>markdown.preview.typographer</code> （默认禁用）配置 支持 特殊符号替换，比如 <code>(c)</code> 展示为 <code>©</code>。参见 <a href="https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.js">github</a>。</li>
<li><code>.xsession</code> 和 <code>.xprofile</code> 被识别为 <code>shellscript</code> 类型</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li><code>&quot;terminal.integrated.tabs.enabled&quot;: true</code> 终端 tab 栏被启用

<ul>
<li>UI

<ul>
<li>初始化时 Panel 标题栏右侧有四个可操作性区域分别为：图标、name、新建、下拉菜单</li>
<li>当大于等于两个 Shell 时，主窗口右侧将出现标签栏，hover 将出现删除和 split 图标</li>
</ul></li>
<li>我们努力使新标签的行为方式与资源管理器的工作方式保持一致。下面是一些其他的行为：

<ul>
<li>双击空白处将创建一个新终端。</li>
<li>双击 <code>sash</code> （就是分割线），将窗口宽度变化为能显示所有文字的最小宽度（资源管理器也可以，👍）</li>
<li><code>terminal.integrated.tabs.location</code> 配置可以移动到左侧</li>
<li><code>terminal.integrated.tabs</code> 设置区块下提供了更多的选项</li>
</ul></li>
</ul></li>
<li>终端状态，支持如下状态

<ul>
<li>Relaunch needed 需要重启：扩展想改变终端的环境变量</li>
<li>Disconnected 断开连接：当终端与其进程失去连接时，使用插头图标状态。</li>
<li>Bell 响铃：通过 <code>terminal.integrated.enableBell</code> 配置</li>
<li>未来将支持更多的 task 状态</li>
</ul></li>
<li>欢迎页演练场

<ul>
<li><code>&quot;workbench.welcomePage.experimental.extensionContributions&quot;: true,</code> 配置后，可以展示扩展的演练场，一个例子 <a href="https://marketplace.visualstudio.com/items?itemName=Tyriar.luna-paint">Luna Paint</a></li>
</ul></li>
<li><code>workbench.experimental.useCustomHover</code>  开启和 VSCode UI 更加一致的 Hover 样式，替代操作系统原生的 Hover</li>
<li>远程仓库（一个内置扩展）(RemoteHub)：直接从 VS Code 中即时浏览、搜索、编辑和提交到任何 GitHub 存储库，而无需克隆或拥有本地存储库。它目前仅在 VS Code 的 Insiders 版本中可用。

<ul>
<li>Feature

<ul>
<li>快速打开任意一个 Github 仓库而不需要 Clone 到 本地</li>
<li>轻松编辑和贡献任何 GitHub 存储库 - 直接将更改提交到 GitHub，或打开拉取请求。</li>
<li><code>Continue on...</code>，快速 Clone 到本地，或者容器中</li>
<li>提供类似本地目录的能力

<ul>
<li>资源管理器 Explorer</li>
<li>搜索</li>
<li>Source Control</li>
<li>Timeline 时间线</li>
<li>Quick Open</li>
<li>Remote Indicator</li>
</ul></li>
<li>同时在不同的分支上工作 - 每个远程分支都被视为一个单独的工作树（用 Git 的说法），这意味着您所做的任何更改都与该分支隔离。你不需要为了切换到一个新的分支来存储你的更改，以便签出一个 PR 或开始一个新的工作项。当您返回上一个分支时，您的更改仍然存在。</li>
<li>安装 GitHub 拉取请求和问题扩展，并快速查看、探索和签出拉取请求，查看并开始处理问题。</li>
</ul></li>
<li>限制

<ul>
<li>有限的语言智能 - 许多语言服务器还不了解这种虚拟化环境。 TypeScript 支持远程存储库的单文件智能。</li>
<li>有限的扩展支持 - 与语言服务器一样，许多扩展不适用于远程存储库。扩展可以选择退出，并且不会为虚拟工作区激活。有关更多详细信息，请参阅下面的扩展创作部分。</li>
<li>搜索 - 全文搜索需要预先构建的索引来进行精确的文本匹配，否则它将回退到 GitHub 的模糊默认分支仅本机搜索。</li>
<li>终端 - 不支持。任何打开的终端都将位于您的本地文件系统上。</li>
<li>Debugging - 不支持</li>
<li>Tasks - 不支持</li>
</ul></li>
</ul></li>
<li>TypeScript 略</li>
<li>Workspace Trust 工作区是否信任

<ul>
<li><code>security.workspace.trust.enabled</code></li>
<li><code>security.workspace.trust.startupPrompt</code></li>
</ul></li>
</ul>

<h2 id="贡献扩展-contributions-to-extensions">贡献扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_56.md">Remote</a></li>
<li><a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0260">GitHub Pull Requests and Issues</a></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>定义您的扩展是否支持虚拟工作区 <code>package.json</code> 添加 <code>capabilities.virtualWorkspaces</code> 声明，关于虚拟工作区，参见：<a href="https://github.com/microsoft/vscode/wiki/Virtual-Workspaces">手册</a>，支持虚拟工作区意味着：不能使用本地文件系统 API，需要使用 VSCode 提供的 fs API</li>
<li>扩展可以添加命令到 remote indicator menu（VSCode 左下角按钮），更多参考：<a href="https://code.visualstudio.com/updates/v1_56#_remote-indicator-menu">原文</a></li>
<li>使用 iframe 替代 webview，以提升浏览器端的一致性，通过 <code>Developer: Open Webview Developer Tools</code> 命令检查，参见：<a href="https://code.visualstudio.com/updates/v1_56#_easier-inspecting-of-webviews">原文</a></li>
<li>工作空间受信相关 API，参见：<a href="https://code.visualstudio.com/updates/v1_56#_workspace-trust-extension-api">原文</a>，讨论参见：<a href="https://github.com/microsoft/vscode/issues/120251">Issue</a></li>
</ul>

<h2 id="文档">文档</h2>

<ul>
<li>视频：<a href="https://code.visualstudio.com/updates/v1_56#_updated-introductory-videos">更新</a></li>
</ul>

<h2 id="合作伙伴">合作伙伴</h2>

<ul>
<li><a href="https://code.visualstudio.com/updates/v1_56#_azure-machine-learning">Azure 机器学习 扩展</a></li>
</ul>
]]></description></item><item><title>VSCode 1.57 (2021-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_57_2021-05/</link><pubDate>Mon, 05 Jul 2021 00:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_57_2021-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_57">https://code.visualstudio.com/updates/v1_57</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>支持跨窗口 tab 页标签拖拽复制到另外的窗口中（类似 Chrome）</li>
<li>取消了 <code>Cmd+W</code> / <code>Ctrl+W</code> 关闭窗口的特性，防止误操作</li>
<li>智能提示预览，通过 <code>editor.suggest.preview</code> 配置可以打开（建议开启，似乎只有部分语言支持）</li>
<li>新版终端 tabs（不喜欢可以通过 <code>&quot;terminal.integrated.tabs.enabled&quot;: false</code> 关闭该特性），使用指南

<ul>
<li>task 类型终端，支持显示正确异常 icon</li>
<li>支持配置颜色，以支持快速识别</li>
<li>将终端组中的选项卡拖入空白区域会将其从组中删除（取消拆分终端，也可通过上下文菜单使用）。</li>
<li>支持将终端标签拖拽到主窗口加入组中</li>
<li>现在，当单击选项卡、+ 按钮或单个选项卡时，Alt 会拆分终端。中键单击杀死终端也是如此。</li>
<li>alt + 单机将拆分终端，鼠标滚轮键将关闭终端</li>
<li>The inline actions won&rsquo;t be shown unless the tabs list is sufficiently large to avoid accidentally splitting/killing terminals.</li>
<li>鼠标删除到最后一个终端时，鼠标移出才会隐藏 tabs</li>
</ul></li>
<li>断点视图中高亮显示命中断点</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>在新终端的头部打印消息 <code>window.createTerminal</code></li>
<li>Tree hovers support command URIs：<code>[this is a link](command:workbench.action.quickOpenView)</code></li>
<li>为自己的扩展开发 get start 演练场：开发者可以参考 <a href="https://marketplace.visualstudio.com/items?itemName=Tyriar.luna-paint">Luna Paint</a>，实现自己扩展的教学</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>工作空间授信：Visual Studio Code 非常重视安全性，并希望帮助您安全地浏览和编辑代码，无论其来源或作者如何。 <a href="https://code.visualstudio.com/docs/editor/workspace-trust">Workspace Trust</a> 功能让您可以决定您的项目文件夹是允许还是限制自动代码执行。

<ul>
<li>命令：<code>Workspaces: Manage Workspace Trust</code></li>
<li>配置

<ul>
<li><code>security.workspace.trust.enabled</code> 通过可以关闭该特性</li>
<li><code>extensions.supportUntrustedWorkspaces</code> 在受限模式强制启用的扩展配置</li>
<li><code>security.workspace.trust.startupPrompt</code> 是否在启动时展示弹窗</li>
<li><code>security.workspace.trust.emptyWindow</code> 是否信任空窗口</li>
<li><code>security.workspace.trust.untrustedFiles</code></li>
</ul></li>
</ul></li>
<li>新的 Getting Started 演练启动页，已经默认启用

<ul>
<li>扩展可以自定义自己的 GetStart 演练页，实现可以参考 <a href="https://marketplace.visualstudio.com/items?itemName=Tyriar.luna-paint">Luna Paint</a></li>
<li><code>workbench.startupEditor</code> 可以配置启用 get start</li>
<li><code>workbench.welcomePage.walkthroughs.openOnInstall</code> 是否在安装时自动打开扩展提供的演练。</li>
</ul></li>
<li>Remote Repositories，该扩展决定不在内置到 VSCode 中，可以自行下载使用（微软小心思开源社区不同意吧），可以点此：<a href="https://marketplace.visualstudio.com/items?itemName=github.remotehub">安装</a></li>
<li>支持跨窗口 tab 页标签拖拽复制到另外的窗口中（类似 Chrome）</li>
<li>取消了 <code>Cmd+W</code> / <code>Ctrl+W</code> 关闭窗口的特性，防止误操作</li>
<li>Notebook 布局自定义

<ul>
<li>全局工具栏 <code>notebook.globalToolbar</code></li>
<li>输出工具栏收到 <code>...</code> 中，通过 <code>notebook.consolidatedOutputButton</code> 配置</li>
<li>将单元格突出显示在装订线上 <code>otebook.cellFocusIndicator</code></li>
<li>在鼠标悬停时显示折叠图标 <code>notebook.showFoldingControls</code></li>
<li>更多参见 <code>notebook.</code> 配置</li>
</ul></li>
<li>更新快速选择和建议小部件颜色</li>
<li>更新 macOS Touch Bar 图标风格</li>
<li>支持默认的 webview 上下文菜单</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>智能提示预览，通过 <code>editor.suggest.preview</code> 配置可以打开</li>
<li>是否展示废弃的方法字段 <code>editor.suggest.showDeprecated</code></li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>终端 tabs（不喜欢可以通过 <code>&quot;terminal.integrated.tabs.enabled&quot;: false</code> 关闭该特性）

<ul>
<li>task 类型终端，支持显示正确异常 icon</li>
<li>支持配置颜色，以支持快速识别</li>
<li>将终端组中的选项卡拖入空白区域会将其从组中删除（取消拆分终端，也可通过上下文菜单使用）。</li>
<li>支持将终端标签拖拽到主窗口加入组中</li>
<li>现在，当单击选项卡、+ 按钮或单个选项卡时，Alt 会拆分终端。中键单击杀死终端也是如此。</li>
<li>alt + 单机将拆分终端，鼠标滚轮键将关闭终端</li>
<li>The inline actions won&rsquo;t be shown unless the tabs list is sufficiently large to avoid accidentally splitting/killing terminals.</li>
<li>鼠标删除到最后一个终端时，鼠标移出才会隐藏 tabs</li>
</ul></li>
<li>Terminal profile improvements 略</li>
<li><code>terminal.integrated.titleMode</code> 配置，默认情况设置为 <code>executable</code></li>
<li>其他略</li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>Tasks 任务状态将显示到 Terminal Tabs 中</li>
<li>支持执行完成后自动关闭 Terminal，通过 <code>tasks.json</code> 的 <code>presentation.close</code> 配置</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>JavaScript debugging，给 Edge Developer Tools integration 的广告位，可以通过工具栏快速打开 前端开发者 工具</li>
<li>断点视图中高亮显示命中断点</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<p>略</p>

<h2 id="贡献扩展-contributions-to-extensions">贡献扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_57.md">Remote</a></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>在新终端的头部打印消息 <code>window.createTerminal</code></li>
<li>Tree hovers support command URIs：<code>[this is a link](command:workbench.action.quickOpenView)</code></li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li><a href="https://code.visualstudio.com/blogs/2021/06/02/build-2021">VS Code at Build 2021</a></li>
<li><a href="https://code.visualstudio.com/docs/nodejs/browser-debugging">浏览器debug</a></li>
<li><a href="https://code.visualstudio.com/docs/datascience/pytorch-support">PyTorch</a></li>
</ul>
]]></description></item><item><title>VSCode 1.58 (2021-06) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_58_2021-06/</link><pubDate>Tue, 13 Jul 2021 23:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_58_2021-06/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_58">https://code.visualstudio.com/updates/v1_58</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>集成终端展示在编辑器区域</li>
<li>Markdown 原生预览，支持 <a href="https://katex.org/">KaTeX</a> 数学公式 （<code>$</code> 和 <code>$$</code>）</li>
<li>新的官方维护的插件：<a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server">Live Preview</a>，直接在 VSCode 打开一个简易的浏览器</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<p>无</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>工作空间 Trust

<ul>
<li>设置编辑器添加 Tag： <code>@tag:workspaceTrust</code></li>
<li>配置项 <code>security.workspace.trust.banner</code></li>
</ul></li>
<li>设置编辑器

<ul>
<li>对于 一个配置对象，其 value 类型都是 bool 的场景，添加了 UI，参考 <code>gitlens.advanced.messages</code></li>
<li>支持枚举类型数组的下拉支持，参考 <code>git.checkoutType</code></li>
</ul></li>
<li>添加 <code>Transient workspaces</code> 类型，通过 <code>.code-workspace</code> 文件的 <code>transient</code> 字段配置，行为如下：

<ul>
<li>重新启动不会自动打开</li>
<li>不会出现在最近打开的目录中</li>
</ul></li>
<li>设置同步：添加 Troubleshot 视图</li>
<li>添加 <code>search.maxResults</code> 配置最大搜索结果</li>
</ul>

<h2 id="集成终端-integrated-terminal">集成终端 (Integrated Terminal)</h2>

<ul>
<li>集成终端展示在编辑器区域，通过如下方式

<ul>
<li>使用 <code>Create Terminal in Editor Area</code> 命令</li>
<li>拖拽标签栏到编辑器区域</li>
<li>焦点在终端的时候，运行 <code>Move Terminal into Editor Area</code> 命令</li>
<li>终端标签栏上下文菜单， 选择 <code>Move into Editor Area</code></li>
</ul></li>
<li>配置项 <code>terminal.integrated.defaultLocation</code> 配置默认新建终端的位置</li>
<li>配置项 <code>terminal.integrated.gpuAcceleration</code> 配置终渲染端添加 <code>canvas</code></li>
<li>配置项 <code>terminal.integrated.showLinkHover</code> 支持禁用终端 hover</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>滚动条支持自定义配置

<ul>
<li>可见性配置 <code>editor.scrollbar.horizontal</code> and <code>editor.scrollbar.vertical</code></li>
<li>粗细 <code>editor.scrollbar.horizontalScrollbarSize</code> and <code>editor.scrollbar.verticalScrollbarSize</code></li>
<li>单击行为是翻页还是跳转到相应位置 <code>editor.scrollbar.scrollByPage</code></li>
</ul></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown 原生预览，支持 <a href="https://katex.org/">KaTeX</a> 数学公式 （<code>$</code> 和 <code>$$</code>），可通过 <code>&quot;markdown.math.enabled&quot;: false</code> 配置关闭</li>
<li>支持数学公式高亮</li>
<li>其他略</li>
</ul>

<p>$$
\displaystyle
\left( \sum_{k=1}^n a_k b<em>k \right)^2
\leq
\left( \sum</em>{k=1}^n a<em>k^2 \right)
\left( \sum</em>{k=1}^n b_k^2 \right)
$$</p>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>无 <code>launch.json</code> 场景，可以记住每个文件的调试器</li>
<li>Debug Console 不在支持 Enter 键入提示，通过 tab 键入</li>
</ul>

<h2 id="贡献扩展-contributions-to-extensions">贡献扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>: <a href="https://code.visualstudio.com/updates/v1_58#_jupyter-interactive-window">原文</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.remotehub">Remote Repositories</a>: <a href="https://code.visualstudio.com/updates/v1_58#_remote-repositories">原文</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>: <a href="https://code.visualstudio.com/updates/v1_58#_github-pull-requests-and-issues">原文</a></li>
<li>Remote Development: <a href="https://code.visualstudio.com/updates/v1_58#_remote-development">原文</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server">Live Preview</a>: 新的插件，在编辑器内打开浏览器，<a href="https://code.visualstudio.com/updates/v1_58#_live-preview">原文</a></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<p>参考：<a href="https://code.visualstudio.com/updates/v1_58#_live-preview">原文</a></p>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>添加：<code>vscode.CompletionItemLabel</code> API ，<a href="https://code.visualstudio.com/updates/v1_58#_detailed-completion-item-labels">原文</a></li>
<li>模态弹窗支持 detail 信息</li>
<li>扩展可以贡献 terminal profiles，将展示在终端创建下拉框里面，<a href="https://code.visualstudio.com/updates/v1_58#_contribute-terminal-profiles">原文</a></li>
<li>终端名称相关，参见<a href="https://code.visualstudio.com/updates/v1_58#_change-extensionterminaloptionsbased-terminal-names">原文</a></li>
<li><code>window.createTerminal</code> 支持配置图标</li>
<li><code>Memento</code> （<code>globalState</code> 和 <code>workspaceState</code>），添加 <code>keys()</code> 函数，查询 keys</li>
</ul>

<h2 id="文档-document">文档 (Document)</h2>

<ul>
<li>新增数据科学专题提升到一级目录：<a href="https://code.visualstudio.com/docs/datascience/overview">https://code.visualstudio.com/docs/datascience/overview</a></li>
</ul>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.59 (2021-07) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_59_2021-07/</link><pubDate>Sat, 07 Aug 2021 11:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_59_2021-07/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_59">https://code.visualstudio.com/updates/v1_59</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>自动折叠代码的 import 块配置 <code>editor.foldingImportsByDefault</code></li>
<li>配置 <code>editor.find.seedSearchStringFromSelection</code> 是否将选中内容填充到搜索框中</li>
<li>【预览】 无标题文件，基于 AI 的语言自动检测技术 可通过 <code>&quot;workbench.editor.untitled.experimentalLanguageDetection&quot;: true</code> 启用，下一版本将默认启用（体验下来不准）</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>Testing APIs，参考：<a href="https://code.visualstudio.com/api/extension-guides/testing">文档</a>，目前实现比较完善的扩展只有 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test">Java Test Runner</a> （另外使用上，除了 Java 仍然建议使用 <a href="https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer">vscode-test-explorer</a> 解决方案）</li>
<li>支持状态栏悬浮 Markdown 内容配置</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>扩展侧边栏

<ul>
<li>宽度变小时，任然显示评分等信息</li>
<li>Hover Item 将显示一些有用 描述、激活时间、推荐原因等信息等信息</li>
</ul></li>
<li>已安装的扩展详情页，将展示该扩展的激活时间、是否有错误或者警告</li>
<li>扩展详情页右侧，将显示分类、资源信息、更多信息（发布、更新时间、扩展 id）</li>
<li>设置编辑器

<ul>
<li>设置编辑器现在支持 object 进行验证。验证会检查直接编辑 JSON 文件时可能引入的类型错误。</li>
<li>数组类型配置支持拖动，针对枚举类型，当 <code>uniqueItems</code> 时，用户下拉框将不显示已经加入的</li>
<li>支持多行文本输入</li>
</ul></li>
<li>扩展主题自定义语法，支持应用到多个主题，key 为 <code>&quot;[Abyss][Red]&quot;</code> 或者 <code>&quot;[Monokai*]&quot;</code>，如下配置项支持同时配置多个主题项

<ul>
<li><code>workbench.colorCustomizations</code></li>
<li><code>editor.tokenColorCustomizations</code></li>
<li><code>editor.semanticTokenColorCustomizations</code></li>
</ul></li>
<li><code>Jupyter Notebooks</code> 添加到内置扩展中，但是如果需要复杂渲染类型，则需要安装 Jupyter 扩展</li>
<li>Notebook 布局<a href="https://code.visualstudio.com/updates/v1_59#_notebook-layout-improvements">提升</a></li>
<li>复制相对路径菜单，支持分割符配置项 <code>explorer.copyRelativePathSeparator</code></li>
<li>关闭的编辑器如果在新的编辑器组中，可以通过配置 <code>workbench.editor.sharedViewState</code> 重新打开该编辑器组</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>添加折叠区域导航命令

<ul>
<li>Go to Next Fold (<code>editor.gotoNextFold</code>)</li>
<li>Go to Previous Fold (<code>editor.gotoPreviousFold</code>)</li>
<li>Go to Parent Fold (<code>editor.gotoParentFold</code>)</li>
</ul></li>
<li>自动折叠代码的 import 块配置 <code>editor.foldingImportsByDefault</code></li>
<li>配置 <code>editor.find.seedSearchStringFromSelection</code> 是否将选中内容填充到搜索框中</li>
<li>内联建议 和 内嵌提示 <a href="https://code.visualstudio.com/updates/v1_59#_inline-suggestions-improvements">提升</a></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>Terminal 支持跨窗口拖动</li>
<li>是否当 Terminal 存在子进程是是否提示，配置 <code>terminal.integrated.confirmOnKill</code></li>
<li>扩展配置的 Terminal Profile 可以设置为默认 terminal</li>
<li>集成终端支持删除线</li>
<li>新命令支持在编辑器区域的一侧，创建一个 Terminal <code>workbench.action.createTerminalEditorSide</code></li>
<li>主题配置项添加： <code>terminal.tab.activeBorder</code></li>
<li>配置项，终端标签图标的动画 <code>terminal.integrated.tabs.enableAnimation</code></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>编辑器标题栏按钮，运行调试下拉框行为改进，运行还是调试为上一次选择的内容</li>
</ul>

<h2 id="贡献扩展-contributions-to-extensions">贡献扩展 (Contributions to extensions)</h2>

<p><a href="https://code.visualstudio.com/updates/v1_59#_contributions-to-extensions">参见</a></p>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>无标题文件，基于 AI 的编程语言自动检测技术 可通过 <code>&quot;workbench.editor.untitled.experimentalLanguageDetection&quot;: true</code> 启用，下一版本将启用（体验下来不准）</li>
<li>其他：<a href="https://code.visualstudio.com/updates/v1_59#_typescript-44">略</a></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Testing APIs，参考：<a href="https://code.visualstudio.com/api/extension-guides/testing">文档</a>，目前实现比较完善的扩展只有 <a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test">Java Test Runner</a> （另外使用上，除了 Java 仍然建议使用 <a href="https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer">vscode-test-explorer</a> 解决方案）</li>
<li>支持 <code>file/newFile</code> 新的菜单贡献点，可以定制用户创建新文件</li>
<li>支持状态栏悬浮 Markdown 内容配置</li>
<li>状态栏警告前景/背景颜色支持：<code>statusBarItem.warningBackground</code> and <code>statusBarItem.warningForeground</code></li>
<li>扩展配置 Schema

<ul>
<li><code>additionalProperties</code> 设置为 <code>false</code> 将支持 配置 UI 进行配置</li>
<li>多行字符串配置 <code>&quot;editPresentation&quot;: &quot;multilineText&quot;</code></li>
</ul></li>
<li><code>workspace.onDidChangeTextDocument</code> 被触发，event 对象添加 <code>reason</code> 属性</li>
</ul>

<h2 id="其他">其他</h2>

<p>略</p>
]]></description></item><item><title>VSCode 1.60 (2021-08) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_60_2021-08/</link><pubDate>Fri, 10 Sep 2021 15:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_60_2021-08/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_60">https://code.visualstudio.com/updates/v1_60</a></p>
</blockquote>

<h2 id="本次更新推荐功能">本次更新推荐功能</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>无标题文件，基于 AI 的语言自动检测技术</li>
<li>支持原生的多色括号，通过 <code>editor.bracketPairColorization.enabled</code> 配置启用，性能更优</li>
<li>终端 - 绘图（表格）字符和块元素字符自定义渲染支持。在 GPU 渲染情况下（<code>terminal.integrated.gpuAcceleration</code>），效果是绘图字符将不会有空隙。可以使用 <a href="https://github.com/bvaisvil/zenith">Zenith</a> 观察效果</li>
<li>调试 - 调试运行中时，编辑器断点装订线上下文菜单，添加 运行到此行 的菜单项</li>
<li>JS/TS - 嵌入 JavaScript 和 TypeScript 提示， <code>command + ,</code> 搜索 <code>inlayHints</code> 开启</li>
<li>预览特性 - 编辑器组锁定 （参见下文）</li>
<li>预览特性 - 文件快速跳转支持通过 <code>&quot;&quot;</code> 实现精确包含匹配</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>设置编辑器的描述支持 markdown 代码块的高亮提示</li>
<li>Web 扩展，VSCode 已经支持完全在浏览器中运行，这种情况下扩展主机将在浏览器环境中运行，因此无法使用 Node 相关 API（如本地文件系统，进程相关 NodeAPI），但是仍可以使用 VSCode 所有 API，运行环境为 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">Browser WebWorker</a>。如果扩展不需要使用操作系统API，则可以在 <code>package.json</code> 声明扩展的 <code>&quot;browser&quot;</code> 入口点以支持 Web 模式。一旦一个扩展支持 Web 模式，则可以在 <code>github.dev</code> 中使用（或 <code>github1s.com</code> ）。体验纯 Web 版 VSCode，可以在 github 仓库页面按 <code>.</code> 键。关于 Web 扩展 的开发，参见官方扩展： <a href="https://code.visualstudio.com/api/extension-guides/web-extensions">Web Extensions</a>。</li>
<li>API 添加 <code>vscode.env.appHost</code> 属性，返回当前环境是在 桌面、Codespaces 还是 <code>github.dev</code></li>
<li>平台特定扩展，<a href="https://github.com/microsoft/vscode/issues/23251">issue #23251</a> ，这将允许扩展作者为不同平台（Windows、macOS、Linux）创建单独的扩展版本。方案已确定，将在九月的里程碑落地。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>无标题文件，基于 AI 的语言自动检测技术，默认已开启，可以通过 <code>&gt;change language mode</code> 的 自动检测 (Auto detect) 配置手动触发</li>
<li>终端编辑器如果正在运行，弹出的二次确认对话框，将显示 取消 和 终止</li>
<li>当编辑器重新打开后，记忆的文件如果无法加载时，仍然显示编辑器，而不是关闭，这样可以保留编辑器布局</li>
<li>设置编辑器的描述支持 markdown 代码块的高亮提示</li>

<li><p>打开设置编辑器的快捷键配置，支持在侧边栏的方式打开</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;cmd+,&#34;</span>,
<span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;workbench.action.openSettings&#34;</span>,
<span style="color:#f92672">&#34;args&#34;</span>: {
<span style="color:#f92672">&#34;openToSide&#34;</span>: <span style="color:#66d9ef">true</span>
}
}</code></pre></div></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>支持原生的多色括号，通过 <code>editor.bracketPairColorization.enabled</code> 配置启用，性能更优</li>
<li>自动完成的内联完成，参见：<a href="https://code.visualstudio.com/updates/v1_60#_inline-suggestions-in-autocomplete">文档</a></li>
<li>Peek 视图更新</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>绘图（表格）字符和块元素字符自定义渲染支持。在 GPU 渲染情况下（<code>terminal.integrated.gpuAcceleration</code>），效果是绘图字符将不会有空隙。可以使用 <a href="https://github.com/bvaisvil/zenith">Zenith</a> 观察效果</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_60/terminal-glyph-before.png" alt="terminal-glyph-before" />
<img src="https://code.visualstudio.com/assets/updates/1_60/terminal-glyph-after.png" alt="terminal-glyph-after" /></p>

<ul>
<li>改进了某些字体的下划线的渲染方法</li>
<li>重新加载窗口后更快的重新连接</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>在 Watch 视图设置值</li>
<li>UI 小更新

<ul>
<li>针对 URL 类型的变量字符串，可以快速调起浏览器</li>
<li>同一个调试配置第二次启动时，将提示用户防止误启动</li>
<li>Clicking on a disabled breakpoint in the editor&rsquo;s breakpoint gutter now enables the breakpoint instead of removing it.</li>
<li>调试运行中时，编辑器断点装订线上下文菜单，添加 运行到此行 的菜单项</li>
</ul></li>
<li>新设置

<ul>
<li><code>debug.confirmOnExit</code> 当窗口存在 Debug 会话时，关闭窗口将弹出二次确认窗</li>
<li><code>debug.console.acceptSuggestionOnEnter</code> 控制调试控制台是否接收建议</li>
</ul></li>
<li><a href="https://code.visualstudio.com/updates/v1_60#_javascript-debugging">JavaScript 调试</a>

<ul>
<li>移除 Legacy node 调试器</li>
<li>Improved stepping in async functions and Node.js internals</li>
</ul></li>
</ul>

<h2 id="安装器-installer">安装器 (Installer)</h2>

<ul>
<li>可以在 Windows 11 通过 Microsoft Store 安装 VSCode</li>
</ul>

<h2 id="notebooks">Notebooks</h2>

<blockquote>
<p>更过关于 <a href="https://code.visualstudio.com/blogs/2021/08/05/notebooks">Notebook</a></p>
</blockquote>

<ul>
<li>Markdown 支持 <code>[text](#header-slug)</code> 链接快速调转，其中 <code>header-slug</code> 为标题将连续的空白字符替换为 <code>-</code> 得到的字符串，支持中文</li>
<li>处理大量输出时的性能改进，方法是将对象传输协议从 JSON 变更为 二进制本身</li>
<li><a href="https://code.visualstudio.com/updates/v1_60#_onnotebook-activation-event-improvement">onNotebook 激活事件改进</a></li>
<li>自定义布局设置按钮，添加到编辑器工具栏</li>
</ul>

<h2 id="语言特性-language-features">语言特性 (Language Features)</h2>

<ul>
<li><a href="https://code.visualstudio.com/updates/v1_60#_typescript-44">TypeScript 4.4</a></li>
<li>嵌入 JavaScript 和 TypeScript 提示， <code>command + ,</code> 搜索 <code>inlayHints</code> 开启</li>
<li>JavaScript 文件检查添加拼写检查</li>
<li><code>TypeScript</code> 语法服务器开关（语法服务器用于在项目加载阶段提供简单的提示能力），配置项为 <code>typescript.tsserver.useSyntaxServer</code></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>编辑器工具栏溢出菜单，添加 锁定编辑器组，可以用于禁止 markdown 在预览窗口打开问题件，

<ul>
<li>锁定后效果如下

<ul>
<li>除非手动拖动到该组，新的文件将不会再改组中打开</li>
<li>新编辑器将在最近使用的未锁定的组中打开，如果全部被锁定，则在最新使用的编辑组旁边创建一个新的组。</li>
<li>编辑器组的锁定状态在重新启动时会保持和恢复</li>
<li>还可以锁定空组，从而实现更稳定的编辑器布局</li>
<li>锁定的组由操作工具栏中的锁定图标（右上角）添加一个锁图标进行指示</li>
</ul></li>
<li>同时还添加如下命令

<ul>
<li><code>workbench.action.experimentalLockEditorGroup</code></li>
<li><code>workbench.action.experimentalUnlockEditorGroup</code></li>
<li><code>workbench.action.experimentalToggleEditorGroupLock</code></li>
</ul></li>
</ul></li>
<li>自动锁定编辑器组配置：<code>workbench.editor.experimentalAutoLockGroups</code></li>
<li>文件快速跳转支持通过 <code>&quot;&quot;</code> 实现精确包含匹配</li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li>Jupyter

<ul>
<li>Run By Line 一行一行的运行</li>
<li>Debugging 调试运行</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a>

<ul>
<li>接入 Testing API，支持测试视图</li>
<li>直接通过编辑器工具栏运行/调试按钮运行 Python 文件</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>

<ul>
<li>评论 Peek 视图，支持折叠/展开单个评论按钮 和 折叠/展开全部评论命令（<code>&gt;GitHub Pull Requests: Expand All Comments</code> 和 <code>&gt;GitHub Pull Requests: Collapse All Comments</code>）</li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Web 扩展，VSCode 已经支持完全在浏览器中运行，这种情况下扩展主机将在浏览器环境中运行，因此无法使用 Node 相关 API（如本地文件系统，进程相关 NodeAPI），但是仍可以使用 VSCode 所有 API，运行环境为 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">Browser WebWorker</a>。如果扩展不需要使用操作系统API，则可以在 <code>package.json</code> 声明扩展的 <code>&quot;browser&quot;</code> 入口点以支持 Web 模式。一旦一个扩展支持 Web 模式，则可以在 <code>github.dev</code> 中使用（或 <code>github1s.com</code> ）。体验纯 Web 版 VSCode，可以在 github 仓库页面按 <code>.</code> 键。关于 Web 扩展 的开发，参见官方扩展： <a href="https://code.visualstudio.com/api/extension-guides/web-extensions">Web Extensions</a>。</li>
<li><a href="https://code.visualstudio.com/api/references/contribution-points#contributes.commands">Commands 贡献点</a>，添加 <code>shortTitle</code> 属性</li>
<li>API 添加 <code>vscode.env.appHost</code> 属性，返回当前环境是在 桌面、Codespaces 还是 <code>github.dev</code></li>
<li><a href="https://code.visualstudio.com/updates/v1_60#_renderercontextworkspaceistrusted-for-notebook-renderers">RendererContext.workspace.isTrusted for notebook renderers</a></li>
<li>平台特定扩展，<a href="https://github.com/microsoft/vscode/issues/23251">issue #23251</a> ，这将允许扩展作者为不同平台（Windows、macOS、Linux）创建单独的扩展版本。方案已确定，将在九月的里程碑落地。</li>
<li><a href="https://code.visualstudio.com/updates/v1_60#_updated-codicons">Updated codicons</a></li>
<li><a href="https://code.visualstudio.com/updates/v1_60#_updates-to-walkthrough-contributions">演练贡献的更新</a></li>
</ul>

<h2 id="调试器扩展制作">调试器扩展制作</h2>

<p><a href="https://code.visualstudio.com/updates/v1_60#_debugger-extension-authoring">略</a></p>

<h2 id="调试适配器协议-debug-adapter-protocol-dap">调试适配器协议 (Debug Adapter Protocol) - DAP</h2>

<p><a href="https://code.visualstudio.com/updates/v1_60#_debug-adapter-protocol">略</a></p>

<h2 id="提案扩展-api-proposed-extension-apis">提案扩展 API (Proposed extension APIs)</h2>

<p><a href="https://code.visualstudio.com/updates/v1_60#_proposed-extension-apis">略</a></p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p><a href="https://code.visualstudio.com/updates/v1_60#_engineering">略</a></p>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/languages/julia">Julia 语言文档</a></li>
</ul>
]]></description></item><item><title>VSCode 1.61 (2021-09) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_61_2021-09/</link><pubDate>Mon, 18 Oct 2021 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_61_2021-09/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_61">https://code.visualstudio.com/updates/v1_61</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>编辑器支持仅拆分，不分组（右击编辑器标签，选择 Split in Group 菜单项），这对仅需对照当前文件的前后内容非常有用。</li>
<li>锁定的编辑器组（更多参见 <a href="/series/vscode/changelog/v1_60_2021-08/#%E9%A2%84%E8%A7%88%E7%89%B9%E6%80%A7-preview-features">V1.60 预览特性</a>），添加 <code>workbench.editor.autoLockGroups</code> 配置项可以配置那些编辑器将自动锁定</li>
<li>括号对连线着色，添加 <code>editor.guides.bracketPairs</code> 配置项，用于开启括号连线的多色显示</li>
<li>git 添加一个 Sync Changes 按钮（执行 push 或者 fetch）（默认工作空间 local 和 remote 不一致，且工作空间没有未提交的代码时展示）</li>
<li>终端

<ul>
<li>添加设置当前终端高度和宽度的命令，<code>Terminal: Set Fixed Dimensions command</code> (<code>workbench.action.terminal.setDimensions</code>）</li>
<li>添加自定义终端标题和描述的配置 <code>terminal.integrated.tabs.title</code> 和 <code>terminal.integrated.tabs.description</code></li>
<li>（预览）聚焦在终端 <code>option + z</code> 或者 终端 tab 上下文菜单 Toggle Size to Content Width。可以切换 terminal 输出溢出后是否换行</li>
</ul></li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>平台特定扩展，扩展现在可以为 VSCode 支持的每个平台（Windows、Linux、macOS）发布不同的 VSIX。从 VS Code 1.61.0 版本开始，VS Code 会寻找与当前平台匹配的扩展包。从 vsce 扩展发布工具 1.99.0 版本开始，支持发布特定于平台的扩展。更多参见：<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions">官方文档</a></li>
<li>启用文件系统提供程序以将文件声明为只读，细节参见：<a href="https://code.visualstudio.com/updates/v1_61#_enable-file-system-providers-to-declare-a-file-as-readonly">原文</a></li>
<li>支持对设置项进行分类，参考：<a href="https://github.com/microsoft/vscode/blob/32c20e923d49bb09b52074adc9ada45a9fb34c88/extensions/css-language-features/package.json#L37">CSS 扩展</a></li>
<li>Webview UI Toolkit for Visual Studio Code ，一个 UI 组件库，更多参见：<a href="https://github.com/microsoft/vscode-webview-ui-toolkit">github</a></li>
<li>虚拟工作空间扩展指南，细节参见：<a href="https://code.visualstudio.com/api/extension-guides/virtual-workspaces">原文</a></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>编辑器支持仅拆分，不分组（右击编辑器标签，选择 Split in Group 菜单项），这对仅需对照当前文件的前后内容非常有用。

<ul>
<li>添加了新设置： <code>workbench.editor.splitInGroupLayout</code> 配置拆分方向是水平还是垂直</li>
<li>添加了一系列命令

<ul>
<li><code>workbench.action.splitEditorInGroup</code></li>
<li><code>workbench.action.toggleSplitEditorInGroup</code></li>
<li><code>workbench.action.joinEditorInGroup</code></li>
<li><code>workbench.action.toggleSplitEditorInGroupLayout</code></li>
<li><code>workbench.action.focusFirstSideEditor</code></li>
<li><code>workbench.action.focusSecondSideEditor</code></li>
<li><code>workbench.action.focusOtherSideEditor</code></li>
</ul></li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/split-in-group.gif" alt="image" /></p>

<ul>
<li>锁定的编辑器组，更多参见 <a href="/series/vscode/changelog/v1_60_2021-08/#%E9%A2%84%E8%A7%88%E7%89%B9%E6%80%A7-preview-features">V1.60 预览特性</a>

<ul>
<li><code>workbench.editor.autoLockGroups</code> 配置项可以配置那些编辑器将自动锁定</li>
<li>添加如下命令

<ul>
<li><code>workbench.action.lockEditorGroup</code></li>
<li><code>workbench.action.unlockEditorGroup</code></li>
<li><code>workbench.action.toggleEditorGroupLock</code></li>
</ul></li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/locked-editor-group-setting.png" alt="image" /></p>

<ul>
<li>添加编辑器标签只读和已删除标识

<ul>
<li>删除：标题颜色为红色，删除线</li>
<li>只读：添加一个锁图标</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/editor-readonly-deleted.png" alt="image" /></p>

<ul>
<li>拆分一个编辑器到其他组，添加了如下命令

<ul>
<li><code>workbench.action.splitEditorToPreviousGroup</code>: Split into the previous group.</li>
<li><code>workbench.action.splitEditorToNextGroup</code>: Split into the next group.</li>
<li><code>workbench.action.splitEditorToAboveGroup</code>: Split into the group above the current one.</li>
<li><code>workbench.action.splitEditorToBelowGroup</code>: Split into the group below the current one.</li>
<li><code>workbench.action.splitEditorToLeftGroup</code>: Split into the group to the left of the current one.</li>
<li><code>workbench.action.splitEditorToRightGroup</code>: Split into the group to the right of the current one.</li>
<li><code>workbench.action.splitEditorToFirstGroup</code>: Split into first group.</li>
<li><code>workbench.action.splitEditorToLastGroup</code>: Split into last group.</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/split-into-group.gif" alt="image" /></p>

<ul>
<li>差异编辑器使用只显示文件名。只有文件名相同，才显示目录的差别</li>
<li>macOS：文件菜单更改

<ul>
<li>添加 “打开文件夹&hellip;”</li>
<li>Open Workspace&hellip; 重命名为 Open Workspace from File&hellip;</li>
</ul></li>
<li>遥感设置，添加 <code>telemetry.telemetryLevel</code> 配置项，<code>telemetry.enableTelemetry</code> 和 <code>telemetry.enableCrashReporter</code> 被废弃</li>
<li>小地图背景透明度配置 <code>minimap.foregroundOpacity</code> （在 <code>&quot;workbench.colorCustomizations&quot;</code> 配置项中自定义配置）</li>
<li>帮助菜单更新

<ul>
<li>Welcome 改为 Get Started</li>
<li>Introductory Videos 改为 Video Tutorials</li>
<li>Interactive Playground 改为 Editor Playground</li>
<li>添加 Show All Commands 菜单项</li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>括号对连线着色，添加 <code>editor.guides.bracketPairs</code> 配置项，用于开启括号连线的多色显示</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/bracket-pair-guides.png" alt="image" /></p>

<ul>
<li>缩进连线配置变更 <code>editor.renderIndentGuides</code> 和 <code>editor.highlightActiveIndentGuide</code> 设置已被弃用，取而代之的是 <code>editor.guides.indentation</code> 和 <code>editor.guides.highlightActiveIndentation</code></li>
</ul>

<h2 id="源代码控制-source-control">源代码控制 (Source Control)</h2>

<ul>
<li>添加一个 Sync Changes 按钮，用于和 remote 仓库进行同步（执行 push 或者 fetch）

<ul>
<li>通过 <code>git.showUnpublishedCommitsButton</code> 可以控制何时显示

<ul>
<li><code>whenEmpty</code> 工作空间 local 和 remote 不一致，且工作空间没有未提交的代码时展示（默认）</li>
<li><code>never</code> 从不显示</li>
<li><code>always</code> 工作空间 local 和 remote 不一致，总是显示</li>
</ul></li>
<li><code>scm.showActionButton</code> 可以覆盖各种源代码管理器的配置（优先级高于 <code>git.showUnpublishedCommitsButton</code>）</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/scm-sync-button.png" alt="image" /></p>

<ul>
<li>增加更改文件显示的限制 <code>git.statusLimit</code> 配置项，可以限制显示最多的变更文件数，默认是 10000。0 为不限制（可能存在性能问题）</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/scm-too-many-changes.png" alt="image" /></p>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>添加设置当前终端高度和宽度的命令，<code>Terminal: Set Fixed Dimensions command</code> (<code>workbench.action.terminal.setDimensions</code>）</li>
<li>添加自定义终端标题和描述的配置 <code>terminal.integrated.tabs.title</code> 和 <code>terminal.integrated.tabs.description</code>，可用的变量值如下

<ul>
<li><code>${cwd}</code> - The terminal&rsquo;s current working directory</li>
<li><code>${cwdFolder}</code> - The terminal&rsquo;s current working directory.</li>
<li><code>${workspaceFolder}</code> - The workspace in which the terminal was launched.</li>
<li><code>${local}</code> - Indicates a local terminal in a remote workspace.</li>
<li><code>${process}</code> - The name of the terminal process.</li>
<li><code>${separator}</code> - A conditional separator (&rdquo; - &ldquo;) that only shows when surrounded by variables with values or static text.</li>
<li><code>${sequence}</code> - The name provided to xterm.js by the process.</li>
<li><code>${task}</code> - Indicates this terminal is associated with a task.</li>
</ul></li>
<li>MacOS 支持 Emoji IMEs（通过 编辑 -&gt; 表情与符号 菜单项打开）</li>
<li>Alt buffer active context key 参见：<a href="https://code.visualstudio.com/updates/v1_61#_alt-buffer-active-context-key">原文</a></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>新的 JavaScript 和 TypeScript 语言状态项，可以通过 Hover 显示更多信息，并支持 pin 在状态栏中</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/ts-pin-version-status.gif" alt="image" /></p>

<ul>
<li>JavaScript 和 TypeScript 支持跨无标题文件的 IntelliSense （智能提示）</li>
<li>github.dev 支持打开的 JavaScript 和 TypeScript 文件的跨文件 IntelliSense （智能提示）能力</li>
<li>JSX tag 折叠时，展示结束标签</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>TypeScript 4.5 支持</li>
<li>在应用程序重新启动时恢复终端会话，通过 <code>terminal.integrated.persistentSessionReviveProcess</code> 配置项打开（需 shell 支持），参见：<a href="https://code.visualstudio.com/updates/v1_61#_restore-terminal-sessions-across-application-restarts">原文</a></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/buffer-restore.png" alt="image" /></p>

<ul>
<li>聚焦在终端 <code>option + z</code> 或者 终端 tab 上下文菜单 Toggle Size to Content Width。可以切换 terminal 输出溢出后是否换行</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_61/terminal-content-width.gif" alt="image" /></p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Jupyter

<ul>
<li>支持显示大纲（目录）</li>
<li>通过 New File 菜单创建新的 Notebooks</li>
<li>支持完整的 Debugging</li>
<li>支持 Remote Debugging</li>
<li>拆分出 <a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter-renderers">renderer extension</a>，以支持 github.dev</li>
</ul></li>
<li>Python

<ul>
<li>新的 Python walkthrough</li>
<li>提升无 <code>launch.json</code> debugging 体验</li>
<li>GitHub Pull Requests and Issues 扩展，参见：<a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0310">changelog</a></li>
</ul></li>
<li>Remote Development 细节参见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_61.md">Remote Development release notes</a>

<ul>
<li>DNS names in forwarded ports.</li>
<li>Easy container additional feature selection.</li>
<li>Remote - Containers extension can execute CLI commands in WSL.</li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>平台特定扩展，扩展现在可以为 VSCode 支持的每个平台（Windows、Linux、macOS）发布不同的 VSIX。从 VS Code 1.61.0 版本开始，VS Code 会寻找与当前平台匹配的扩展包。从 vsce 扩展发布工具 1.99.0 版本开始，支持发布特定于平台的扩展。更多参见：<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions">官方文档</a></li>
<li>testing APIs 变更，细节参见：<a href="https://code.visualstudio.com/updates/v1_61#_test-tags-and-nonerror-output">原文</a></li>
<li>启用文件系统提供程序以将文件声明为只读，细节参见：<a href="https://code.visualstudio.com/updates/v1_61#_enable-file-system-providers-to-declare-a-file-as-readonly">原文</a></li>
<li>支持对设置项进行分类，参考：<a href="https://github.com/microsoft/vscode/blob/32c20e923d49bb09b52074adc9ada45a9fb34c88/extensions/css-language-features/package.json#L37">CSS 扩展</a></li>
<li>类型层次 API 提案完成</li>
<li><code>WebviewOptions.enableForms</code>，细节参见：<a href="https://code.visualstudio.com/updates/v1_61#_webviewoptionsenableforms">原文</a></li>
<li>支持在测试数据上运行 web 扩展测试，细节参见：<a href="https://code.visualstudio.com/updates/v1_61#_running-web-extension-tests-on-test-data">原文</a></li>
<li>更新图标</li>
<li>Webview UI Toolkit for Visual Studio Code ，一个 UI 组件库，更多参见：<a href="https://github.com/microsoft/vscode-webview-ui-toolkit">github</a></li>
<li>虚拟工作空间扩展指南，细节参见：<a href="https://code.visualstudio.com/api/extension-guides/virtual-workspaces">原文</a></li>
</ul>

<h2 id="提案的扩展-api-proposed-extension-apis">提案的扩展 API (Proposed extension APIs)</h2>

<p><a href="https://code.visualstudio.com/updates/v1_61#_proposed-extension-apis">略</a></p>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>新版已支持 类型层次</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p><a href="https://code.visualstudio.com/updates/v1_61#_engineering">略</a></p>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<p>添加系列文档：<a href="https://code.visualstudio.com/remote/advancedcontainers/overview">容器高级配置</a></p>
]]></description></item><item><title>VSCode 1.62 (2021-10) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_62_2021-10/</link><pubDate>Sat, 13 Nov 2021 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_62_2021-10/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_62">https://code.visualstudio.com/updates/v1_62</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li><a href="https://vscode.dev">https://vscode.dev</a> 上线。

<ul>
<li>有打开和编辑本地目录的能力。</li>
<li>支持直接打开 github / azurerepos 中的仓库。</li>
<li>直接体验颜色主题 <code>https://vscode.dev/theme/extensionId</code>。</li>
<li>支持 Web 版的 Live Share 协同编辑 <code>https://vscode.dev/liveshare</code>。</li>
<li>通过基于 <a href="https://tree-sitter.github.io/tree-sitter">Tree-sitter</a> 技术实现的插件 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.anycode">anycode 扩展</a>，提供有限的代码智能能力。</li>
</ul></li>
<li>参数提示，突出显示当前参数。</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>Verified extension publishers （认证的扩展发布者），如 <a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">gitlen</a> 。如何验证，参见<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#verify-a-publisher">官方文档</a>。</li>
<li><code>MarkdownString</code> 上的新 <code>supportHtml</code> 属性允许呈现出现在 Markdown 文本中的原始 HTML 的安全子集。<code>supportHtml</code> 属性默认为 false。禁用后，VS Code 将删除出现在 Markdown 文本中的所有原始 HTML 标签。</li>
</ul>

<h2 id="vscode-dev">vscode.dev</h2>

<p>过去一段时间， <code>github1s.com</code> 以及 <code>github.dev</code> 陆续上线了基于 VSCode For Web 的产品。</p>

<p>本此更新，VSCode 发布了官方的 <code>vscode.dev</code>。目前如下能力</p>

<ul>
<li>有打开和编辑本地目录的能力</li>
<li>支持直接打开 github / azurerepos 中的仓库</li>
<li>直接体验颜色主题 <code>https://vscode.dev/theme/extensionId</code></li>
<li>支持 Web 版的 Live Share 协同编辑 <code>https://vscode.dev/liveshare</code></li>
<li>通过基于 <a href="https://tree-sitter.github.io/tree-sitter">Tree-sitter</a> 技术实现的插件 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.anycode">anycode 扩展</a>，提供有限的代码智能能力</li>
</ul>

<p>更多参见：<a href="https://code.visualstudio.com/blogs/2021/10/20/vscode-dev">官方博客</a> | <a href="https://code.visualstudio.com/docs/editor/vscode-web">官方文档</a></p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>设置编辑器可访问性提升

<ul>
<li>搜索后，自动滚动到顶部。</li>
<li>侧边分组支持键盘访问。</li>
<li>废弃的设置文本块显示一个图标。以前，废弃的文本只通过颜色与其他设置文本区分开来。</li>
<li>More UI elements within the Settings editor have the setting ID as their name.</li>
</ul></li>
<li>更新搜索图标。</li>
<li>参数提示，突出显示当前参数，主题 key 为 <code>editorHoverWidget.highlightForeground</code>。</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>参考线着色（通过 <code>editor.guides.bracketPairs</code> 配置项开启），可以通过 <code>editor.guides.bracketPairsHorizontal</code> 配置合适渲染水平参考线，颜色主题添加 <code>editorBracketPairGuide.background{1,...,6}</code> 和 <code>editorBracketPairGuide.activeBackground{1,...,6}</code> 参考线颜色。</li>
<li>添加 <code>editor.language.colorizedBracketPairs</code> 配置对哪些括号对进行着色</li>
<li>支持配置 hover 显示位置 <code>editor.hover.above</code>。</li>
<li>支持对控制字符直接显示为显示 Unicode 编码，可通过 <code>editor.renderControlCharacters</code> 配置，以环境 <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574">CVE-2021-42574</a> 安全问题。</li>
</ul>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li>Verified extension publishers （认证的扩展发布者），如 <a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">gitlen</a> 。如何验证，参见<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#verify-a-publisher">官方文档</a>。</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>特殊字符的新默认键绑定

<ul>
<li><code>ctrl+shift+2</code> Inputs the null character (<code>0x00</code>).</li>
<li><code>ctrl+shift+6</code>: Inputs the record separator character (<code>0x1E</code>).</li>
<li><code>ctrl+/</code>: Inputs the unit separator character (<code>0x1F</code>).</li>
</ul></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>配置 HTML 如何完成属性 <code>html.completion.attributeDefaultValue</code>。

<ul>
<li><code>doublequotes</code>: The value is placed in double quotes (default)。</li>
<li><code>singlequotes</code>: The value is placed in single quotes</li>
<li><code>empty</code>: The value is left empty。</li>
</ul></li>
<li>Emmet 提升，添加 <code>editor.emmet.action.updateTag</code> 命令。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>查找和替换支持捕获组。</li>
<li>添加 <code>notebook.displayOrder</code> 配置项，细节参见<a href="https://code.visualstudio.com/updates/v1_62#_better-selection-of-output-renderers-and-mimetypes">更新文档</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>，参见<a href="https://code.visualstudio.com/updates/v1_62#_jupyter">更新文档</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">ESLint</a>，参见<a href="https://code.visualstudio.com/updates/v1_62#_eslint">更新文档</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>，参见官方 <a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0320">changelog</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>，细节参见。 <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_62.md">changelog</a>

<ul>
<li>配置项 <code>remote.SSH.foldersSortOrder</code> 按字母顺序或最近使用对 SSH 目标进行排序。</li>
<li>Windows Subsystem for Linux指标让你迅速知道你是在使用 WSL1 还是 WSL2。</li>
<li>高级容器配置视频，涵盖<a href="https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history">如何保留 bash 历史记录</a>和<a href="https://code.visualstudio.com/remote/advancedcontainers/change-default-source-mount">使用 monorepos</a>。</li>
</ul></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>TypeScript 4.5 支持。</li>
</ul>

<h2 id="扩展创作-extension-authoring">扩展创作 (Extension authoring)</h2>

<ul>
<li><a href="https://github.com/microsoft/vscode/blob/9430f7848503b25ff1a629f2cb81b705e11672f5/src/vs/vscode.d.ts#L6071">file decorations</a> API 现在支持 emojis 作为徽章文本。</li>
<li><code>MarkdownString</code> 上的新 <code>supportHtml</code> 属性允许呈现出现在 Markdown 文本中的原始 HTML 的安全子集。<code>supportHtml</code> 属性默认为 false。禁用后，VS Code 将删除出现在 Markdown 文本中的所有原始 HTML 标签。</li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>文件监视更改，通过 <code>files.legacyWatcher</code> 进行配置。</li>
<li>支持 Electron 沙盒进展，下月稳定版将上线。</li>
</ul>

<h2 id="web-扩展-web-extensions">Web 扩展 (Web extensions)</h2>

<p>启用将代码作为 Web 扩展运行的扩展的扩展作者（以下列表截至 11 月 2 日）：参考 <a href="https://code.visualstudio.com/updates/v1_62#_web-extensions">原文</a>。</p>
]]></description></item><item><title>VSCode 1.63 (2021-11) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_63_2021-11/</link><pubDate>Sat, 13 Nov 2021 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_63_2021-11/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_63">https://code.visualstudio.com/updates/v1_63</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>颜色主题安装前预览，使用 <code>⌘K ⌘T</code> 打开主题选择器，选择第一个 <code>Browse Additional Color Themes</code>，即可快速预览颜色主题</li>
<li>特殊 Unicode 字符高亮</li>
<li>提升 Hover 响应速度：多个 Hover Provider 完成一个显示一个，不会因为某个 Provider 慢影响整体显示进度</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>编写插件 README 时很有用，通过 <code>&gt;Developer: Toggle Screencast Mode</code> 切换到截屏模式时，可以通过 <code>screencastMode.keyboardShortcutsFormat</code> 配置项，配置截屏模式显示命令名称。</li>
<li>Quick Pick API 提升

<ul>
<li>支持内联按钮</li>
<li>动态向下拉列表 Item 添加内联按钮时，保留滚动的位置</li>
</ul></li>
<li>设置编辑器提升 （更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_settings-editor-improvements">原文</a>）

<ul>
<li>通过 <code>order</code> 字段可以为配置项排序</li>
<li>Ungrouped category support</li>
<li>支持数字和整型对象</li>
</ul></li>
<li>默认值覆盖，可以通过 <code>package.json</code> 中添加 <code>configurationDefaults</code> 字段声明要覆盖那些默认值 <code>&quot;configurationDefaults&quot;: { &quot;files.autoSave&quot;: &quot;onFocusChange&quot; }</code> （注意：不能覆盖具有 <code>application</code> 或 <code>machine</code> 作用域的配置。）</li>
<li>发布预发布版本扩展 <code>vsce publish --pre-release</code>，更多参见：<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions">预发布扩展</a></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>颜色主题安装前预览，使用 <code>⌘K ⌘T</code> 打开主题选择器，选择第一个 <code>Browse Additional Color Themes</code>，即可快速预览颜色主题</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/browse-themes.gif" alt="image" /></p>

<ul>
<li>添加问题面板排序配置 <code>problems.sortOrder</code>，可选 <code>severity</code> 严重性 和 <code>position</code> 位置</li>

<li><p>配置文件，语言特定配置，支持多种语言。例如：通过如下方式，可以同时为 javascript 和 typescript 同时配置 <code>&quot;editor.maxTokenizationLineLength&quot;: 2500</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;[javascript][typescript]&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
<span style="color:#f92672">&#34;editor.maxTokenizationLineLength&#34;</span>: <span style="color:#ae81ff">2500</span>
}</code></pre></div></li>

<li><p>通过命令锁定到 2x2 布局 <code>&gt;View: Grid Editor Layout (2x2)</code> （更多其他布局通过 <code>View: Layout</code> 查看）</p></li>

<li><p>树和列表布局的选中多个时，第一次按 Esc，取消多选，但是仍会选中其中的一个；第二次按 Esc，取消选中全部</p></li>

<li><p>VSCode Web 版（如 github.dev）的 Webview 支持搜索 <code>cmd + f</code></p></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/webview-web-find.png" alt="image" /></p>

<ul>
<li>通过 <code>&gt;Developer: Toggle Screencast Mode</code> 切换到截屏模式时，可以通过 <code>screencastMode.keyboardShortcutsFormat</code> 配置项，配置截屏模式显示命令名称。</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/commands-screencast.png" alt="image" /></p>

<ul>
<li>VS Code 现在支持扩展的预发布版本，因此您可以选择安装它们并尝试来自扩展的最新的功能。 VSCode 在扩展安装下拉菜单中，显示了一个额外的安装预发布版本选项，用于安装预发布版本。</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/extensions-pre-release-install.png" alt="image" />
<img src="https://code.visualstudio.com/assets/updates/1_63/extensions-pre-release-install-indicators.png" alt="image" />
<img src="https://code.visualstudio.com/assets/updates/1_63/extensions-pre-release-indicators.png" alt="image" /></p>

<ul>
<li>Search 视图中的 Find 操作现在使用与编辑器中的 Find 操作相同的样式</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/find-actions.gif" alt="image" /></p>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>现在可以使用 <code>scm.diffDecorationsIgnoreTrimWhitespace</code> 配置项，设置显示在左侧装订线中的快速差异功能以忽略修剪空白。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><code>notebook.markup.fontSize</code> 配置项可以设置，可以通知控制笔记本 Markdown 内容的字体大小。此设置的默认值是当前编辑器字体大小的 120%。</li>
<li>支持 Markdown cells 的代码块高亮</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/notebook-fenced-codeblock.png" alt="image" /></p>

<ul>
<li>Markdown 预览的文件链接支持直接打开（以 <code>/</code> 开头的链接是相对于工作区根解析的。以 <code>./</code> 开头或仅以文件名开头的链接是相对于当前笔记本文件解析的）</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/notebook-file-links.gif" alt="image" /></p>

<ul>
<li>Markdown <code>http</code> 或 <code>https</code> 开头的文本会自动解析为链接</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/notebook-bare-link.png" alt="image" /></p>

<ul>
<li>Notebook 工具条会随着 Notebook 宽度变化变化自动隐藏按钮的标签，通过 <code>notebook.globalToolbarShowLabel</code> 配置项可以配置</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/notebook-toolbar-dynamic-label.gif" alt="image" /></p>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>特殊 Unicode 字符高亮

<ul>
<li>特殊的不可见的字符如，<code>U+3164</code>，通过 <code>editor.unicodeHighlight.invisibleCharacters</code> 配置项控制</li>
<li>字形和常见 ASCII 字符一样的字符，如中文冒号 <code>：</code>，通过 <code>editor.unicodeHighlight.ambiguousCharacters</code> 配置项控制</li>
<li>突出所有 非 ASCII 字符（默认为不信任的工作空间），通过 <code>editor.unicodeHighlight.nonBasicASCII</code> 配置</li>
<li>另外可以通过如下配置排除某些字符

<ul>
<li><code>editor.unicodeHighlight.allowedCharacters</code> 不突出显示的字符列表。</li>
<li><code>editor.unicodeHighlight.includeComments</code>  启用突出显示字符列表。</li>
</ul></li>
<li>Markdown 默认不启用该特性</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/unicode-highlighting-invisible.png" alt="image" />
<img src="https://code.visualstudio.com/assets/updates/1_63/unicode-highlighting-confusable.png" alt="image" /></p>

<ul>
<li>提升 Hover 响应速度：多个 Hover Provider 完成一个显示一个，不会因为某个 Provider 慢影响整体显示进度</li>
</ul>

<table>
<thead>
<tr>
<th>过去</th>
<th>现在</th>
</tr>
</thead>

<tbody>
<tr>
<td><img src="https://code.visualstudio.com/assets/updates/1_63/hover-providers-before.gif" alt="image" /></td>
<td><img src="https://code.visualstudio.com/assets/updates/1_63/hover-providers-after.gif" alt="image" /></td>
</tr>
</tbody>
</table>

<h2 id="任务-task">任务 (Task)</h2>

<ul>
<li>旧的 <code>terminal.integrated.automationShell.*</code> 设置已经废弃，可以使用 <code>terminal.integrated.automationProfile.*</code> 来自由地指定用于任务的终端的属性，包括 Shell、图标、颜色和 Shell 参数。</li>
<li>内置的 gulp 扩展支持解析 <code>gulpfile.ts</code> 配置的任务</li>
<li>NPM 脚本视图提升，会显示有关脚本的更多信息，另外通过 <code>npm.scriptExplorerExclude</code> 参数可以配置排除显示的脚本</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/npm-scripts-view.png" alt="image" /></p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>TypeScript

<ul>
<li>升级到 TypeScript 4.5，更多参见：<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-5">TypeScript blog</a></li>
<li>默认启用类方法签名完成，快速覆盖父类&amp;实现接口方法，可通过 <code>typescript.suggest.classMemberSnippets.enabled</code> and <code>javascript.suggest.classMemberSnippets.enabled</code> 配置</li>
<li>删除旧版本 TypeScript （4.1 之前）的语义化高亮</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/ts-method-completion.gif" alt="image" /></p>

<ul>
<li>JSX 属性值自动完成，默认会根据类型推测值类型而自动完成 <code>&quot;&quot;</code> 或者 <code>{}</code>。可以通过 <code>javascript.preferences.jsxAttributeCompletionStyle</code> and <code>typescript.preferences.jsxAttributeCompletionStyle</code> 配置</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/ts-attribute-completion.gif" alt="image" /></p>

<ul>
<li>Markdown

<ul>
<li>Markdown Preview 支持通过 Open With 方式打开，可以通过 <code>workbench.editorAssociations</code> 配置默认使用 Markdown Preview 打开配置 <code>&quot;workbench.editorAssociations&quot;: {&quot;*.md&quot;: &quot;vscode.markdown.preview.editor&quot;}</code></li>
<li>Markdown Preview 采用增量更新方式更新视图，以提升性能，减少抖动</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/md-custom-editor.gif" alt="image" /></p>

<ul>
<li>JSON

<ul>
<li>编辑 JSON 文件是，状态栏会显示语言指示器 <code>{}</code>，可显示内容是否已根据一个或多个 JSON Schema 进行验证。将鼠标悬停在指示器上会显示 JSON Schema 状态和用于打开 JSON Schema 的链接。</li>
<li>JSON Schema 缓存，<code>json.schemastore.org</code> 会缓存在本地，以减少网络请求</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/json-language-indicator.png" alt="image" /></p>

<ul>
<li><code>&gt;Emmet: Remove Tag</code> 可以正确处理缩进</li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/emmet-remove-tag.gif" alt="image" /></p>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li><a href="https://code.visualstudio.com/updates/v1_63#_azure-repos">Azure Repos 支持</a></li>
<li>左上角菜单提升，添加

<ul>
<li>Close Remote Workspace</li>
<li>Download Visual Studio Code</li>
<li>Go to Repository</li>
</ul></li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Java（更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_contributions-to-extensions">原文</a>）

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a>，添加了产品欢迎页</li>
<li>walkthroughs 涵盖安装 Java 运行时和有用的框架、打开和调试项目以及直接在 VS Code 中运行测试。</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/java-walkthrough.png" alt="image" /></p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>（更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_jupyter">原文</a>）

<ul>
<li>性能提升，打开重启速度提升 2 倍</li>
<li>kernel 失败提升

<ul>
<li>pip install 失败添加 Quick Fix</li>
<li>当内核在执行期间无法启动或停止时，已经进行了一些改进以提供更好和更有意义的错误消息。错误现在显示在单元格输出中以及有关如何解决问题的说明中。这可确保用户知道该问题并可以修复它，以防他们错过 VSCode 右下角显示的错误。</li>
</ul></li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/pip_install.gif" alt="image" />
<img src="https://code.visualstudio.com/assets/updates/1_63/kernel_override_python_builtins.gif" alt="image" /></p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a>（更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_python">原文</a>）

<ul>
<li>对不受信任和虚拟工作区提供有限支持</li>
<li>添加模块重命名支持</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/python-limited-support.png" alt="image" /></p>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/python-module-rename.gif" alt="image" /></p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>（参见：<a href="https://code.visualstudio.com/updates/v1_63#_remote-development">原文</a>）</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a> （参见：<a href="https://code.visualstudio.com/updates/v1_63#_github-pull-requests-and-issues">原文</a>）</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>更新 API 提案解构（采用多个文件），更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_updated-api-proposal-structure">原文</a></li>
<li>Quick Pick API 提升

<ul>
<li>支持内联按钮</li>
<li>动态向下拉列表 Item 添加内联按钮时，保留滚动的位置</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/quickpickitem-buttons.png" alt="image" /></p>

<ul>
<li>Authentication API 提升，参见：<a href="https://code.visualstudio.com/updates/v1_63#_authentication-api-improvements">原文</a></li>
<li>设置编辑器提升 （更多参见：<a href="https://code.visualstudio.com/updates/v1_63#_settings-editor-improvements">原文</a>）

<ul>
<li>通过 <code>order</code> 字段可以为配置项排序</li>
<li>Ungrouped category support</li>
<li>支持数字和整型对象</li>
</ul></li>
</ul>

<p><img src="https://code.visualstudio.com/assets/updates/1_63/ungrouped-config-settings-editor.png" alt="image" />
<img src="https://code.visualstudio.com/assets/updates/1_63/numeric-object-settings-editor.png" alt="image" /></p>

<ul>
<li>执行命令 API 的新签名（此更改仅影响 executeCommand 的类型，不会更改此函数的行为。）

<ul>
<li>旧的：<code>export function executeCommand&lt;T&gt;(command: string, ...rest: any[]): Thenable&lt;T | undefined&gt;;</code></li>
<li>新的：<code>export function executeCommand&lt;T = unknown&gt;(command: string, ...rest: any[]): Thenable&lt;T&gt;;</code></li>
</ul></li>
<li><a href="https://code.visualstudio.com/updates/v1_63#_html-custom-data-from-uris">HTML custom data from URIs</a></li>
<li>默认值覆盖，可以通过 <code>package.json</code> 中添加 <code>configurationDefaults</code> 字段声明要覆盖那些默认值 <code>&quot;configurationDefaults&quot;: { &quot;files.autoSave&quot;: &quot;onFocusChange&quot; }</code> （注意：不能覆盖具有 <code>application</code> 或 <code>machine</code> 作用域的配置。）</li>
<li><code>OutputChannel</code> 对象添加 <code>replace</code>，可以替换掉已经输出的所有内容</li>
<li>插件激活配置为 <code>workspaceContains</code> 时，如果在 7 秒内没有找到匹配的文件名，VS Code 现在将取消搜索并且不会激活扩展。</li>
<li>发布预发布版本扩展 <code>vsce publish --pre-release</code>，更多参见：<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions">预发布扩展</a></li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>参见：<a href="https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#version_3_17_0">various minor improvements</a></p>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_63#_debug-adapter-protocol">原文</a></p>

<h2 id="提案扩展-api-proposed-extension-apis">提案扩展 API (Proposed extension APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_63#_proposed-extension-apis">原文</a></p>
]]></description></item><item><title>VSCode 1.64 (2022-01) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_64_2022-01/</link><pubDate>Sun, 13 Feb 2022 12:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_64_2022-01/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_64">https://code.visualstudio.com/updates/v1_64</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>新增侧面板（右侧边面板），更有效的利用空间，建议开启，推荐 UI 布局配置为：

<ul>
<li>将大纲等常用的侧边栏功能移动到侧面板中（未来各种语言的扩展应该会更加充分的利用这块区域）

<ul>
<li><img src="/image/vscode/sidepanel_outline.gif" alt="image" /></li>
</ul></li>
<li>菜单栏 -&gt; 查看 -&gt; 外观 -&gt; 对齐面板（View &gt; Appearance &gt; Align Panel）将面板对齐（底部）设置为右对齐</li>
<li>将终端和调试控制台合并到一起（操作方式：打开终端，按住调试控制台标题，拖动到终端中）（该能力之前就有）</li>
</ul></li>
<li>对存储库发现的控制的配置项，<code>git.repositoryScanIgnoredFolders</code> 控制扫描期间应忽略的文件夹列表，对 MonoRepo 但主要只开发某一个的场景，或者 git submodule 的场景，非常有用</li>
<li>选中一段代码，执行 <code>&gt;Insert Snippet</code>，即可 Snippet 包围选中代码。</li>
<li>音频提示，当光标切换到某些特殊行时，发出提示声音，例如错误、断点或折叠的文本区域。通过 <code>audioCues.enabled</code> 进行配置，默认为 <code>auto</code> 开启屏幕阅读器时提示，可以通过配置为 <code>on</code> 总是提示体验</li>

<li><p>终端自动回复，例如如配置 Oh My Zsh 自动更新</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;terminal.integrated.autoReplies&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;[Oh My Zsh] Would you like to check for updates? [Y/n]&#34;</span>: <span style="color:#e6db74">&#34;Y\r&#34;</span>
}</code></pre></div></li>

<li><p>Markdown 新特性</p>

<ul>
<li>路径智能提示，超链接 <code>./</code> 或者 <code>/</code> 提示路径</li>
<li>锚点智能提示，超链接输入 <code>#</code> 号，将提示文档中的所有标题</li>
<li>删除线（<code>~~文本~~</code>）支持渲染在编辑器和预览中都渲染</li>
</ul></li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li><code>QuickPickItem</code> API 支持分割线和标签</li>
<li><a href="https://github.com/microsoft/vscode/blob/1a57cb85407249f380f0ebfb34c748a960e5430a/src/vscode-dts/vscode.d.ts#L9807">vscode.TerminalLocation</a> API，支持将终端创建到不同位置</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>新增侧面板，在 VSCode 窗口右侧添加一个侧面板，可以将侧边栏、面板、活动栏的内容拖拽过去。
<img src="/image/vscode/sidepanel_outline.gif" alt="image" />

<ul>
<li><a href="https://code.visualstudio.com/docs/getstarted/userinterface">VSCode UI 术语</a>

<ul>
<li>最左侧的图标条，术语为：活动栏（Activity Bar）</li>
<li>左侧的侧边栏，术语为：侧边栏（Side Bar）</li>
<li>中间主要编辑，术语为：编辑器组（Editor Groups）</li>
<li>最下方的图标条，术语为：状态栏（Status Bar）</li>
<li>下方的面板，术语称为：面板（Panel）</li>
<li>左侧的侧边栏，术语为：侧面板（Side Panel）</li>
</ul></li>
<li>术语解释

<ul>
<li>栏（Bar），拥有一个标题，和多个可这折叠的功能块，每个功能块占用该区域的一部分，可以折叠</li>
<li>面板（Panel），拥有包含多个标题的标题栏，标题栏的每个标题点击后面板内容就会随机切换，每个面板内容默认只有一个功能块（可以拖拽），右侧面板（侧面板）的标题将是一个图标，而底部面板（面板）标题是文字</li>
<li>在这版本更新后，VSCode 窗口的 <code>Bar</code> 可以同时有两个，可以位于底部或者左右侧之一，默认传统的底部面板可以移动到左侧的侧面板</li>
</ul></li>
<li>相关操作、命令和命令

<ul>
<li><code>&gt;view: Move View</code> 选择某个各种视图元素灵活移动到各种区域
<img src="/image/vscode/move-view-locations.png" alt="image" /></li>
<li><code>&gt;view: Move Views From Panel To Side Panel</code> 将面板（底部），移动到侧面板（右侧）</li>
<li><code>&gt;view: Move Views From SIde Panel To Panel</code> 将侧面板（右侧），移动到面板（底部）
<img src="/image/vscode/panel_location.gif" alt="image" /></li>
<li>侧面板（右侧）的图标，可以恢复默认位置
<img src="/image/vscode/move-view-locations.png" alt="image" /></li>
<li>面板（底部）对齐，配置面板空间更大，通过菜单栏 -&gt; 查看 -&gt; 外观 -&gt; 对齐面板（View &gt; Appearance &gt; Align Panel）可以选择

<ul>
<li>左 （Left）</li>
<li>右（推荐）（Right）</li>
<li>两端对齐 （Justify）</li>
<li>居中（默认） （Center）
<img src="/image/vscode/panel_alignment.gif" alt="image" /></li>
</ul></li>
<li>通过 <code>workbench.experimental.layoutControl.enabled</code> 配置项，可以在标题栏添加一个页面布局按钮（在 Mac 中，非全屏模式可以看见）
<img src="/image/vscode/customize_layout.gif" alt="image" /></li>
</ul></li>
</ul></li>
<li>设置编辑器

<ul>
<li>设置编辑器搜索支持枚举值匹配
<img src="/image/vscode/settings-editor-search-by-value.png" alt="image" /></li>
<li>新的搜索算法还优先考虑整个单词匹配，这意味着如果一个同时具有 Java 和 JavaScript 扩展名，则在搜索 <code>java</code> 时将首先显示 Java 设置。
<img src="/image/vscode/settings-editor-search-java-first.png" alt="image" /></li>
<li>最后，设置编辑器中的下拉菜单（枚举值选择）（例如 <code>files.autoSave</code>）和列表小部件（例如 <code>files.associations</code>）现在可用于触摸屏设备。</li>
</ul></li>
<li>设置同步

<ul>
<li>现在支持同步用户任务。

<ul>
<li><img src="/image/vscode/settings-sync-user-tasks.png" alt="image" /></li>
</ul></li>
<li>默认的 Settings Sync 机器名称现在包括 VS Code for Web 中的浏览器和产品名称。

<ul>
<li><img src="/image/vscode/settings-sync-machines.png" alt="image" /></li>
</ul></li>
</ul></li>
<li>资源管理器，撤销（Undo）行为的配置

<ul>
<li>禁用 Undo 配置，通过 <code>explorer.enableUndo</code> 配置项配置</li>
<li>Undo 的二次确认配置，通过 <code>explorer.confirmUndo</code> 配置项配置

<ul>
<li><code>default</code> 进行破坏性撤销时提示 （现在的默认值）</li>
<li><code>light</code> （之前的行为）聚焦时，将不会对 Undo 进行二次确认</li>
<li><code>verbose</code> 总是提示</li>
<li><img src="/image/vscode/explorer-undo.gif" alt="image" /></li>
</ul></li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>音频提示，当光标切换到某些特殊行时，发出提示声音，例如错误、断点或折叠的文本区域。通过 <code>audioCues.enabled</code> 进行配置

<ul>
<li><code>auto</code> 开启屏幕阅读器时提示</li>
<li><code>on</code> 总是提示</li>
<li><code>off</code> 关闭提示</li>
</ul></li>
<li>Unicode 高亮显示改进，添加一些配置改善误报

<ul>
<li><code>editor.unicodeHighlight.allowedLocales</code> （原文为：The new setting editor.unicodeHighlight.allowedLocales can be used to allow characters that are common in one or many configured locales. By default, this includes the current VS Code display language and the current OS language. At the moment, only locales translated in vscode-loc Language Packs are supported.）</li>
<li><code>editor.unicodeHighlight.includeStrings</code> 控制字符串中的字符是否也应进行 unicode 突出显示。</li>
</ul></li>
<li>编辑器折叠限制，<code>editor.foldingMaximumRegions</code> 配置项配置可折叠区域的最大数量，默认值为 5000</li>
<li>编辑器自适应语言特性请求的时序，某些语言特性是由用户打字触发的，这些特性耗时不同，该版本会自适应的调整多个请求的时序，以减轻负载提高性能</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>自动回复，终端现在具有选择加入功能，可在收到特定字符序列时自动回复。如 Windows 批处理脚本时，按 <code>Ctrl + C</code> 会询问用户 <code>Terminate batch job (Y/N)?</code> ，可以配置自动向终端中输入 <code>Y/r</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;terminal.integrated.autoReplies&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;Terminate batch job (Y/N)?&#34;</span>: <span style="color:#e6db74">&#34;Y\r&#34;</span>
}</code></pre></div>
<p><img src="/image/vscode/terminal-auto-reply.gif" alt="image" /></p>

<p>另外一个例子是，Oh My Zsh 更新提示</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;terminal.integrated.autoReplies&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;[Oh My Zsh] Would you like to check for updates? [Y/n]&#34;</span>: <span style="color:#e6db74">&#34;Y\r&#34;</span>
}</code></pre></div></li>

<li><p>增强的 VT 支持，添加了对操作系统命令 (OSC) 4/10/11/12 转义序列的支持，使应用程序能够控制终端的主题颜色。</p></li>

<li><p>添加命令，以打开终端中检测到的链接</p>

<ul>
<li><code>&gt; Terminal: Open Last Web Link...</code> 打开终端中最后一个 Web 链接，如 <code>https://github.com/microsoft/vscode</code></li>
<li><code>&gt; Terminal: Open Last File Link...</code> 打开终端中最后一个文件链接，如 <code>/Users/user/repo/file.txt</code></li>
<li><code>&gt; Terminal: Open Detected Link...</code> 展示检测到的所有链接 (web, file, word)</li>
</ul></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>对存储库发现的控制的配置项

<ul>
<li><code>git.repositoryScanMaxDepth</code> 控制扫描时使用的深度。</li>
<li><code>git.repositoryScanIgnoredFolders</code> 控制扫描期间应忽略的文件夹列表。</li>
</ul></li>
<li>更改（Changes） 列表排序配置项，<code>scm.defaultViewSortKey</code> （更改后需 Reload）

<ul>
<li><code>name</code> - 按文件名排序</li>
<li><code>path</code> - 按文件路径排序（默认）</li>
<li><code>status</code> - 按状态排序</li>
</ul></li>
<li>Git 添加命令 <code>Git: Drop All Stashes...</code> 清除所有存储条目</li>
<li>Git 输出，打印 git 命令执行时间和持续时间，以更好的追踪性能</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>支持查找 Markdown 渲染和单元格输出的内容，查找过滤器（漏斗图标）支持配置要查找的内容
<img src="/image/vscode/notebook-find-in-markup-output.gif" alt="image" /></li>
<li>单元格折叠 UI，笔记本单元格的左侧有一个蓝色条，表示它们已聚焦。此栏现在是交互式的 - 您可以单击顶部折叠单元格输入，单击底部折叠输出。
<img src="/image/vscode/collapse-gutter.gif" alt="image" /></li>
<li>Markdown 单元格折叠提示，当某个区域的 Markdown 单元格被折叠时，将显示一条消息，其中包含折叠单元格的数量，以使某些单元格被隐藏起来更加明显。
<img src="/image/vscode/cell-fold-hint.png" alt="image" /></li>
<li>单元格执行提示

<ul>
<li>首先，当一个单元格正在执行但没有滚动到视图中时，一个进度条将显示在编辑器窗格的顶部。</li>
<li>其次，在执行单元格时，会在笔记本工具栏中添加一个新按钮 <code>Go To Running Cell</code>。</li>
<li>第三，如果代码单元通过 <code>notebook.outline.showCodeCells</code> 和 <code>notebook.breadcrumbs.showCodeCells</code> 设置在大纲或面包屑中可见，它们将在执行时显示动画运行图标。
<img src="/image/vscode/cell-executing-spinner.gif" alt="image" /></li>
</ul></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>查看并编辑二进制数据，可以打开 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor">Hex Editor</a> 扩展，以二进制的方式编辑变量（目前只支持 JavaScript，其他语言需要相关扩展支持）。
<img src="/image/vscode/debug-memory.png" alt="image" /></li>
<li>JavaScript 调试，在断点暂停时，通过 Call Stack，右击某个堆栈，选择 <code>EXCLUDED CALLERS</code> 排除某调用者暂停（从该函数内部的断点不暂停）。
<img src="/image/vscode/js-debug-exclude-caller.gif" alt="image" /></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown

<ul>
<li>路径智能提示（相较第三方相关扩展来说，缺少图片预览）
<img src="/image/vscode/md-path-suggestions.png" alt="image" />

<ul>
<li>路径建议的工作方式类似于 CSS 和 HTML 文件中的路径 IntelliSense。以 <code>/</code> 开头的路径是相对于当前工作空间解析的，而以 <code>./</code> 开头或没有任何前缀的路径是相对于当前文件解析的。</li>
</ul></li>
<li>锚点智能提示，超链接输入 <code>#</code> 号，将提示文档中的所有标题
<img src="/image/vscode/md-header-suggestions.png" alt="image" /></li>
<li>删除线（<code>~~文本~~</code>）支持渲染在编辑器和预览中都渲染
<img src="/image/vscode/markdown-strike-through.png" alt="image" /></li>
</ul></li>
<li>TypeScript

<ul>
<li>支持 4.5.5 版本，更多参见：<a href="https://github.com/microsoft/typescript/issues?q=is%3Aissue+milestone%3A%22TypeScript+4.5.5%22+is%3Aclosed">important crashes and tooling bugs</a></li>
<li>用 JS/TS 的 Snippet 包围选中代码（选中代码，执行 &gt; <code>&gt;Insert Snippet</code>，实测其他语言也支持）</li>
</ul></li>
<li>HTML 在 <code>=</code> 之后自动插入引号，通过 <code>html.completion.attributeDefaultValue</code> 配置引号类型，通过 <code>html.autoCreateQuotes</code> 配置是否自动插入引号</li>
<li>JSON，<code>&gt;Clear schema cache</code> 命令，用于清除先前下载的模式的缓存。</li>
<li>LaTeX，添加对 LaTeX 基本语言支持，包括语法高亮和自动关闭对。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>远端仓库 Github

<ul>
<li>提交代码，在 VS Code for Web 中创建的提交现在已在 GitHub UI 中签名并标记为已验证。此外，维护者现在可以在使用 VS Code for Web 时承诺拉取从分叉提交的请求。这要归功于新的 GitHub GraphQL <a href="https://github.blog/changelog/2021-09-13-a-simpler-api-for-authoring-commits/">createCommitOnBranch</a> API。
<img src="/image/vscode/github-commit-signing-prs.gif" alt="image" /></li>
<li>以前，仅在将 GitHub 存储库克隆到本地或远程计算机后才支持创建空提交。您现在还可以使用 GitHub Repositories: Commit Empty 命令在 VS Code for Web 中创建空提交。</li>
<li>还添加了一个新配置，以启用自动下载低于给定大小的存储库的全部内容，以启用高级功能，如整个存储库文本搜索和跳转。设置 <code>remoteHub.experimental.fs.maxAutoDownloadSize</code> 控制存储库大小限制，然后在尝试下载完整内容时显示提示。默认情况下，<code>maxAutoDownloadSize</code> 未设置，以便在没有提示的情况下永远下载。</li>
</ul></li>
<li>VS Code for the Web 现在捆绑了 <a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Request and Issues</a> 以及 <a href="https://marketplace.visualstudio.com/items?itemName=GitHub.remotehub">GitHub Repositories</a> 的预发布版本。</li>
<li>PWA 和离线支持，VS Code for Web 采用了 PWA 模型，现在可以作为 PWA 安装在主机操作系统上。由于采用了这种方式，现在还可以启用一些离线功能。曾经访问过 <a href="https://vscode.dev/">vscode.dev</a> 或 <a href="https://insiders.vscode.dev/">insiders.vscode.dev</a> 的用户现在可以使用它来编辑本地文件，即使在离线时也是如此。
<img src="/image/vscode/pwa.png" alt="image" /></li>
</ul>

<h2 id="扩展贡献-contributions-to-extensions">扩展贡献 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a>

<ul>
<li>改进的解释器快速选择，Python 扩展的 <code>&gt;Python: Select Interpreter</code> 命令，将对解释器进行分组</li>
<li>添加对 <code>conda run</code> 的支持</li>
<li>通过智能选择能力选择 Python 代码所需的按键次数更少，因为在定义选择范围时会考虑代码的语义信息：
<img src="/image/vscode/python-smart-selection.gif" alt="image" /></li>
<li>折叠，以前区域仅通过缩进定义，这在某些情况下并不理想，例如多行字符串。现在折叠区域会适当地考虑语义信息，并且还支持<code>#region</code> 注释
<img src="/image/vscode/python-folding.gif" alt="image" /></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>

<ul>
<li>Jupyter 扩展现在在本地和远程 Jupyter 服务器之间切换时不再需要重新加载 VS Code。此外，该扩展现在在内核选择器中同时显示本地和远程内核。
<img src="/image/vscode/localAndRemoteJupyterTogether.gif" alt="image" /></li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor">Hex Editor</a>，打开任意大小文件都不会有性能问题。此外，它的布局宽度现在是可配置的，并且它具有更强大的查找/替换实现。在未来的迭代中将继续改进。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>，更多参见：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_64.md">发行记录</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>，更多参见：<a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0360">changelog for the 0.36.0</a></li>
</ul>

<h2 id="预览特性">预览特性</h2>

<ul>
<li>终端和 Shell 集成（通过 <code>&quot;terminal.integrated.enableShellIntegration&quot;: true</code> 开启），让 VSCode 终端可以得知 Shell 内部的一些信息。原理为：在执行 Shell 命令时，注入一个脚本（<a href="https://github.com/microsoft/vscode/blob/c64daad0ff2e7b5fd5e423e54af8a1da7aa2d712/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh">Shell 脚本源码</a> | <a href="https://github.com/microsoft/vscode/blob/c64daad0ff2e7b5fd5e423e54af8a1da7aa2d712/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts#L414">注入命令源码</a>），，这个脚本会修改命令提示符 <code>$PS1</code>，输出一些隐藏的字符，然后 VSCode 解析这些数据就能探查 Shell 内部情况，这些数据有：提示符位置、命令和命令输出、每个命令的当前工作目录 (cwd) 以及每个命令的退出代码等信息。使用这些信息。可以：

<ul>
<li>增强现有功能：

<ul>
<li>快速检测 cwd - 以前这个功能只能在 macOS 和 Linux 上进行使用，并且会启动一个进程或查询文件系统才能获取该信息。现在 Windows 也支持了。 cwd 用于链接检测和拆分终端选项卡时继承 cwd 等功能。</li>
<li>改进命令跟踪功能 - 之前此功能只存在于 macOS 上具有默认键绑定（Cmd+Up/Down），并使用一种天真的方法来猜测按 Enter 时行的位置。</li>
</ul></li>
<li>提供新的功能

<ul>
<li>运行最近的命令（<code>&gt;Terminal: Run recent command</code>） - 因为我们知道运行了哪些命令，所以我们可以公开一个命令，让您在快速选择中再次查看和运行它们。
<img src="/image/vscode/terminal-recent-command.png" alt="image" /></li>
<li>转到最近的目录 (<code>&gt;Terminal: Go to recent directory</code>) -  与上面类似，我们也允许导航到过去的目录。
<img src="/image/vscode/terminal-recent-directory.png" alt="image" /></li>
<li>相对于 cwd 的链接支持 - 我们现在知道终端缓冲区中每一行的 cwd，因此我们可以支持在终端中打开相对于 cwd 激活位置的链接。以前，当单击链接时，会打开一个快速选择，其中包含来自任何包含该名称匹配项的文件夹的结果。现在，将打开完全匹配的文件。</li>
</ul></li>
<li>该特性目标是在功能的可靠性足够好时默认打开 shell 集成。我们在参数注入方面采取的方法是尽可能不干扰。例如，我们不会像某些终端那样自动修改您的 shell 初始化脚本，而是截取进程的创建，检查参数，并在我们确信终端可以与它们一起运行时注入 shell 集成参数。希望无需用户进行任何配置即可使其正常运行，并且不会干扰您现有的 shell 设置。</li>
<li>当前支持的 shell 是用于 Windows 的 pwsh 和用于 Linux 和 macOS 的 pwsh、bash 和 zsh。</li>
<li>如前所述，这是一个实验性功能，边缘有些粗糙，并且存在一些已知问题：

<ul>
<li>尚不支持 <code>$PS2</code> 行延续。但是，pwsh 中的续行确实有效。</li>
<li>右侧提示符尚不支持（如某些 zsh 的提示符）</li>
<li><a href="https://github.com/microsoft/vscode/issues/141620">zsh 脚本有时不会激活</a>。</li>
<li>远程 VSCode 窗口支持是受限的。</li>
</ul></li>
</ul></li>

<li><p>资源管理器文件嵌套，可以在逻辑嵌套布局中显示同一目录中的文件。这有助于在视觉上将相关文件分组在一起并将文件折叠到“根”文件中以减少混乱。添加了几个新设置来控制此行为</p>

<ul>
<li><code>explorer.experimental.fileNesting.enabled</code> 是否启用该实验特性</li>
<li><code>explorer.experimental.fileNesting.expand</code> 是否默认展开</li>

<li><p><code>explorer.experimental.fileNesting.patterns</code> 控制文件如何嵌套</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;explorer.experimental.fileNesting.patterns&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;*.ts&#34;</span>: <span style="color:#e6db74">&#34;$(capture).js, $(capture).d.ts&#34;</span>,
    <span style="color:#f92672">&#34;*.js&#34;</span>: <span style="color:#e6db74">&#34;$(capture).js.map, $(capture).min.js, $(capture).d.ts&#34;</span>,
    <span style="color:#f92672">&#34;*.jsx&#34;</span>: <span style="color:#e6db74">&#34;$(capture).js&#34;</span>,
    <span style="color:#f92672">&#34;*.tsx&#34;</span>: <span style="color:#e6db74">&#34;$(capture).ts&#34;</span>,
    <span style="color:#f92672">&#34;tsconfig.json&#34;</span>: <span style="color:#e6db74">&#34;tsconfig.*.json&#34;</span>,
    <span style="color:#f92672">&#34;package.json&#34;</span>: <span style="color:#e6db74">&#34;package-lock.json, .npmrc, yarn.lock, .yarnrc&#34;</span>,
    <span style="color:#f92672">&#34;*.go&#34;</span>: <span style="color:#e6db74">&#34;$(capture).go, $(capture)_test.go&#34;</span>
}</code></pre></div></li>
</ul></li>
</ul>

<h2 id="扩展创作">扩展创作</h2>

<ul>
<li><p>语言默认图标。不显示文件图标的文件图标主题（如 Minimal 或 None）也不会使用语言图标。如果文件图标主题有扩展名或文件名的图标，主题图标优先级更高。文件图标主题可以通过在主题文件中定义 <code>showLanguageModeIcons: true|false</code> 来自定义新行为。</p>

<ul>
<li><code>showLanguageModeIcons: true</code> 即使主题没有指定文件图标，也会显示默认语言图标。</li>

<li><p><code>showLanguageModeIcons: false</code> 禁止使用默认语言图标。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
&#34;contributes&#34;: {
&#34;languages&#34;: [
    {
        &#34;id&#34;: &#34;latex&#34;,
        // ...
        &#34;icon&#34;: {
            &#34;light&#34;: &#34;./icons/latex-light.png&#34;,
            &#34;dark&#34;: &#34;./icons/latex-dark.png&#34;
        }
    }
]</pre></div></li>
</ul></li>

<li><p><code>QuickPickItem</code> API 支持分割线和标签，如果未指定 kind 属性，或者将其设置为 <code>QuickPickItemKind.Default</code>，则该项目将被视为普通 <code>QuickPickItem</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js">{
    <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;APIs&#39;</span>,
    <span style="color:#a6e22e">kind</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">QuickPickItemKind</span>.<span style="color:#a6e22e">Separator</span>
}
</code></pre></div></li>

<li><p><code>vscode.workspace.createFileSystemWatcher</code> 现在支持任何路径，现有的 vscode.workspace.createFileSystemWatcher API 得到了改进，允许您传递任何文件或文件夹路径以进行文件监视，即使它位于工作区之外。以前，文件观察器仅限于工作区中打开的文件夹。根据您传递给方法的 glob 模式，观察者将是递归的（例如，<code>**/*.js</code>）或非递归的（<code>*.js</code>）。递归观察者需要更多资源，因此我们建议尽可能使用简单的 glob 模式。例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ts" data-lang="ts"><span style="color:#75715e">// Watch a folder non-recursively
</span><span style="color:#75715e"></span><span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">workspace</span>.<span style="color:#a6e22e">createFileSystemWatcher</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">RelativePattern</span>(<span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">Uri</span>.<span style="color:#a6e22e">file</span>(<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">path</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">folder</span> <span style="color:#a6e22e">outside</span> <span style="color:#a6e22e">workspace</span><span style="color:#f92672">&gt;</span>), <span style="color:#e6db74">&#39;*.js&#39;</span>));

<span style="color:#75715e">// Watch the active text editor file
</span><span style="color:#75715e"></span><span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">workspace</span>.<span style="color:#a6e22e">createFileSystemWatcher</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">RelativePattern</span>(<span style="color:#a6e22e">vscode</span>.window.<span style="color:#a6e22e">activeTextEditor</span>.document.<span style="color:#a6e22e">uri</span>, <span style="color:#e6db74">&#39;*&#39;</span>));</code></pre></div>
<p>注意：作为此更改的一部分，我们对现有文件观察者进行了行为更改。仅使用 glob 模式（例如 vscode.workspace.createFileSystemWatcher(&rsquo;**&lsquo;)）指示的文件观察程序将不再接收在工作区之外更改的文件的事件。它只会从工作空间内的路径接收文件事件。如果用户没有打开的工作区，则不会再通过此方法传递任何事件。这样做是为了确保扩展不会收到来自工作区外部的意外事件。</p></li>

<li><p><a href="https://github.com/microsoft/vscode/blob/1a57cb85407249f380f0ebfb34c748a960e5430a/src/vscode-dts/vscode.d.ts#L9807">vscode.TerminalLocation</a> API，您可以使用新的 该 API 指定将在何处创建扩展终端。这可以通过提供 <a href="https://github.com/microsoft/vscode/blob/1a57cb85407249f380f0ebfb34c748a960e5430a/src/vscode-dts/vscode.d.ts#L6012">parentTerminal</a>、<a href="https://github.com/microsoft/vscode/blob/1a57cb85407249f380f0ebfb34c748a960e5430a/src/vscode-dts/vscode.d.ts#L5978">在编辑器区域和面板之间</a>进行选择等来创建拆分终端。</p></li>

<li><p><code>onWill</code> 事件的取消令牌，VS Code API 公开事件以参与文件操作，例如 <a href="https://github.com/microsoft/vscode/blob/f30dba54302d2c00356e90604ec27aceeeb38bb5/src/vscode-dts/vscode.d.ts#L11375">onWillRenameFiles</a>。这种参与可能会持续很长时间，因此用户可以取消它。在此版本中，扩展可以通过相应事件上的取消令牌（例如 <a href="https://github.com/microsoft/vscode/blob/f30dba54302d2c00356e90604ec27aceeeb38bb5/src/vscode-dts/vscode.d.ts#L10738">FileWillRenameEvent#token</a>）观察用户端取消。这也允许扩展取消昂贵的下层操作。</p></li>

<li><p>Git 扩展 API</p>

<ul>
<li>添加了一个新的 <code>Repository.add</code> 方法，以启用暂存文件的能力。</li>
<li>添加了 <code>Repository.tag</code> 和 <code>Repository.deleteTag</code> 方法以启用创建和删除标签的能力。</li>
</ul></li>

<li><p>onTaskType 激活事。原文为 Extension that provide tasks can limit their unneeded activations by using the new <code>onTaskType:foo</code> activation event. This is an improvement over activating on <code>onCommand:workbench.action.tasks.runTask</code> as <code>workbench.action.tasks.runTask</code> is usually too eager for task providing extensions.</p></li>
</ul>

<h2 id="调试器扩展创作-debugger-extension-authoring">调试器扩展创作 (Debugger extension authoring)</h2>

<ul>
<li>VS Code 现在实现了<a href="https://microsoft.github.io/debug-adapter-protocol">调试适配器协议</a>的内存相关功能，在这个版本中，VS Code 开始支持查看和编辑二进制数据，更多参见：<a href="https://code.visualstudio.com/updates/v1_64#_vs-code-now-implements-the-memoryrelated-features-of-the-debug-adapter-protocol">原文</a></li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_64#_language-server-protocol">原文</a></p>

<h2 id="proposed-extension-apis">Proposed extension APIs</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_64#_proposed-extension-apis">原文</a></p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>vscode-bisect 工具，一个用于对已发布的 VS Code Insiders 版本（网页版和桌面版）进行二等分的新工具可用于帮助诊断问题：<code>npx vscode-bisect</code>
<img src="/image/vscode/vscode-bisect.gif" alt="image" />
与 git bisect 类似，vscode-bisect 将启动一系列过去发布的 Insiders 构建，询问构建是否重现问题。最终结果是一系列引入该问题的提交。该实例将为用户数据使用专用的新文件夹，以免影响您的主要开发环境。</li>
<li>从源代码运行代码 Web 和 Server，从源代码运行 VS Code for Web 和 VS Code Server 的脚本已移至 scripts 文件夹：

<ul>
<li><code>./scripts/code-web.sh|bat</code> 从源代码启动 Web 代码（又名“无服务器）”并在其上打开浏览器。使用 &ndash;help 获得更多选项。</li>
<li><code>./scripts/code-server.sh|bat</code> 从源代码启动 VS Code Server。添加 &ndash;launch 以在浏览器中额外打开 Web UI。使用 &ndash;help 获得更多选项。</li>
<li><code>./scripts/test-web-integration.sh|bat</code> 用于远程 Web 测试。</li>
<li><code>./scripts/test-remote-integration.sh|bat</code> 用于远程测试。</li>
</ul></li>
<li>Extensions，在这个里程碑中，通过最大限度地减少 VS Code 对服务的查询次数来改进 Marketplace 交互。</li>
</ul>
]]></description></item><item><title>VSCode 1.65 (2022-02) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_65_2022-02/</link><pubDate>Sun, 06 Mar 2022 21:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_65_2022-02/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_65">https://code.visualstudio.com/updates/v1_65</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>新的编辑器历史导航，支持配置导航范围（全部、编辑器、编辑器组），支持 Notebook，添加基于编辑位置的历史导航和基于代码跳转的历史导航的相关命令</li>
<li>添加一个浅色高对比度主题</li>
<li>添加添加对问题和搜索结果的拖拽，以快速创建一个编辑器组</li>
<li>自动语言检测更加准确，并支持实时监测，可通过 <code>workbench.editor.historyBasedLanguageDetection</code> 配置型开启</li>
<li>用代码片段环绕，有一个新命令可以用片段包围当前选择。选择一些文本，从命令面板 (⇧⌘P) 中调用 <code>&gt;Surround With Snippet</code>。</li>
<li>差异编辑器管理

<ul>
<li>添加一个新命令 <code>Git: Close All Diff Editors</code>，可用于关闭所有打开的差异编辑器。</li>
<li>添加一个新设置 <code>git.closeDiffOnOperation</code> 可以在隐藏、提交、丢弃、暂存或取消暂存更改时自动关闭差异编辑器。</li>
</ul></li>
<li>减少 Unicode 提示针对字符串的误报</li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>语言状态项 API，<code>vscode.languages.createLanguageStatusItem(...)</code>。此 API 显示活动编辑器的语言特定信息。这可以是有关项目或工具集版本的一般信息，但也可以显示错误和警告</li>
<li>内联提示 API，用于在变量后方显示变量的类型。</li>
<li>评论 API 添加时间戳以及显示相对时间或绝对时间的配置</li>
<li>图标贡献 API，可以自定义图标，并支持在 Markdown 中通过 <code>$(IconID)</code> 使用</li>
<li>调试适配器协议 API，支持 &lsquo;lazy&rsquo; 变量</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>新的编辑器历史导航（前进/后退）

<ul>
<li>编辑器组感知的导航，历史记录会记录光标位置所属编辑器组（比如：某编辑器组的两个编辑器是同一个文件，在历史记录中可以区分属于那个编辑器），当编辑器组关闭后，所有关联的历史条目都将被丢弃。
<img src="/image/vscode/history-group-aware.gif" alt="image" /></li>
<li>支持配置，编辑器历史导航的范围 <code>workbench.editor.navigationScope</code>，如果将范围配置为 <code>editorGroup</code> 或 <code>editor</code>，则每个编辑器组或编辑器都将拥有自己的导航堆栈，可以单独导航。

<ul>
<li><code>default</code>: 导航适用于所有打开的编辑器组和编辑器。</li>
<li><code>editorGroup</code>: 编辑器导航仅限于活动编辑器组的已打开编辑器。</li>
<li><code>editor</code>: 编辑器导航仅限于活动编辑器。</li>
</ul></li>
<li>Notebook 支持
<img src="/image/vscode/history-notebooks.gif" alt="image" /></li>
<li>历史导航添加，编辑位置和导航位置的相关命令。默认情况下，在编辑器之间或在编辑器中导航时（例如，在切换笔记本单元格或更改文本编辑器中的选择时），都会添加编辑器导航位置。如果觉得记录了太多位置，则添加了新命令，可将位置减少为：

<ul>
<li>导航位置（Navigation locations）：例如，跳转到定义（Go to Definition）

<ul>
<li><code>workbench.action.navigateForwardInEditLocations</code> - 在编辑位置历史记录中前进。</li>
<li><code>workbench.action.navigateBackInEditLocations</code> - 在编辑位置历史记录中后退。</li>
<li><code>workbench.action.navigatePreviousInEditLocations</code> - 在编辑位置历史记录中转到上一个。</li>
<li><code>workbench.action.navigateToLastEditLocation</code> -  在编辑位置历史记录中转到最后一个（此命令之前已经存在）。</li>
<li>相关 Context keys

<ul>
<li><code>canNavigateBackInEditLocations</code> - 是否可以返回编辑位置。</li>
<li><code>canNavigateForwardInEditLocations</code> - 是否可以在编辑位置前进。</li>
<li><code>canNavigateToLastEditLocation</code> - 是否可以转到最后一个编辑位置。</li>
</ul></li>
</ul></li>
<li>编辑位置（Edit locations）：每当更改编辑器时。例如，在文本编辑器中插入时。

<ul>
<li><code>workbench.action.navigateForwardInNavigationLocations</code> - 在导航位置历史记录中前进。</li>
<li><code>workbench.action.navigateBackInNavigationLocations</code> - 在导航位置历史记录中后退。</li>
<li><code>workbench.action.navigatePreviousInNavigationLocations</code> - 在导航位置历史记录中转到上一个。</li>
<li><code>workbench.action.navigateToLastNavigationLocation</code> - 在导航位置历史记录中转到最后一个。</li>
<li>相关 Context keys

<ul>
<li><code>canNavigateBackInNavigationLocations</code> - 是否可以返回导航位置。</li>
<li><code>canNavigateForwardInNavigationLocations</code> - 是否可以在导航位置前进。</li>
<li><code>canNavigateToLastNavigationLocation</code> - 是否可以转到最后一个导航位置。</li>
</ul></li>
</ul></li>
</ul></li>
</ul></li>
<li>新的布局控制选项，上一个版本上 布局控制 按钮，可以通过 <code>workbench.experimental.layoutControl.enabled</code> 打开，这个版本添加 <code>workbench.experimental.layoutControl.type</code> 配置项目用来配置布局控制按钮的显示的内容：

<ul>
<li><code>menu</code>：以前的行为，在菜单栏显示单个按钮（默认）。</li>
<li><code>toggles</code>：一个新选项，在菜单栏显示三个按钮来切换：面板、侧栏和侧面板。</li>
<li><code>both</code>：一个新选项，显示切换按钮后跟菜单按钮，它仍然允许您相当快速地访问自定义布局快速选择。
<img src="/image/vscode/layout-control-options.png" alt="image" /></li>
</ul></li>
<li>浅色高对比度主题
<img src="/image/vscode/light-hc-theme.png" alt="image" /></li>
<li>音频提示，此版本添加了新的音频提示，包括警告、内联建议和调试器断点命中的音频提示。声音已经过调整，<code>audioCues.enabled</code> 已被弃用，取而代之的是单独的 <code>audioCues.*</code> 设置：
<img src="/image/vscode/audio-cues-settings.png" alt="image" />

<ul>
<li>默认情况下，屏幕阅读器用户会启用除 <code>lineHasWarning</code> 之外的所有音频提示（设置值自动）。</li>
<li><code>&gt;Help: List Audio Cues</code> 命令，可以快速配置音频。</li>
</ul></li>
<li>添加对问题和搜索结果的拖拽（以快速的创建编辑器组）
<img src="/image/vscode/dnd-problems.gif" alt="image" /></li>
<li>设置编辑器目录和内容分割线可以拖动
<img src="/image/vscode/settings-editor-split-view.gif" alt="image" /></li>
<li>改进的自动语言检测，<code>workbench.editor.historyBasedLanguageDetection</code> 配置项启动时，语言推测能力提升，支持文本内容变化后实时检测
<img src="/image/vscode/lang-detect.gif" alt="image" /></li>
<li>改进的语言扩展建议，语言功能扩展推荐现在会在推荐时考虑市场中其他突出的语言扩展。例如，如果您安装了 Apache NetBeans Java 扩展，VS Code 不推荐使用 Java 扩展包。</li>
<li>扩展树悬停的键盘快捷键 <code>Ctrl/Cmd+K, Ctrl/Cmd+I</code></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>用代码片段环绕，有一个新命令可以用片段包围当前选择。选择一些文本，从命令面板 (⇧⌘P) 中调用 <code>&gt;Surround With Snippet</code>，然后从下拉列表中选择一个片段。
<img src="/image/vscode/surround-with-snippet.gif" alt="image" /></li>
<li>内联提示添加可访问性（支持屏幕阅读器）</li>
<li>上下文 Unicode 突出显示，为了减少误报，如果周围的字符在视觉上指示非 ASCII 脚本，则不再突出显示不明确和不可见的 Unicode 字符。因此，在受信任的工作空间中，仅突出显示不可见或可能与 ASCII 字符混淆的字符。例外情况是包含在非 ASCII 字符的单词中的那些字符，其中至少一个字符不能与 ASCII 字符混淆。一个例子如下：

<ul>
<li>Before，str 变量有大量提示，但是 <code>&quot;user&quot;</code> 仍有提示
<img src="/image/vscode/unicode-context.dio-before.png" alt="image" /></li>
<li>After，str 没有提示，但是 <code>&quot;user&quot;</code> 仍有提示
<img src="/image/vscode/unicode-context.dio-after.png" alt="image" /></li>
</ul></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>多行粘贴警告，当 shell 不支持多行时，在终端中粘贴多行时，默认情况下会显示一个对话框。当我们将 Ctrl+V 直接传递给 shell 时，会为括号粘贴模式和 PowerShell 显示此警告。对话框上有一个不要再问我复选框，可以轻松禁用该功能。</li>
<li>终端链接改进，终端链接的实现在这个版本中有很大的重构。这种简化和提高了可维护性的特性同时也带来了：

<ul>
<li>链接亮点再次起作用。</li>
<li>缓存已解析链接，减少某些链接显示的延迟。</li>
<li>工作区搜索链接现在由与验证链接相同的代码处理，以提高一致性并改进行/列识别。</li>
<li>几个错误修复。</li>
</ul></li>
<li>打开文件链接命令改进，上一个版本引入的 <code>Open Last File Link</code> 和 <code>Open Detected Link...</code> 命令现在排除了文件夹，这应该会使它们更有用。</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>差异编辑器管理，这个里程碑我们做了一些改变，应该有助于管理差异编辑器。添加一个新命令 <code>Git: Close All Diff Editors</code>，可用于关闭所有打开的差异编辑器。还有一个新设置 <code>git.closeDiffOnOperation</code> 可以在隐藏、提交、丢弃、暂存或取消暂存更改时自动关闭差异编辑器。</li>
<li>Git 命令输出日志，执行 git 命令时，stderr 的内容会记录在 Git 输出窗口中。有一个新设置 git.commandsToLog，用于指定 Git 命令列表，这些命令将在 Git 输出窗口中记录 stdout 的内容。</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>Lazy 变量，访问变量的值可能会产生副作用或代价高昂。 VSCode 的通用调试器现在可以显示一个按钮，供用户按需获取变量值。这可用于支持新的“惰性”变量功能的调试扩展。目前，这仅由用于属性获取器的内置 JavaScript 调试器实现，但我们预计其他调试器扩展将很快跟进。
<img src="/image/vscode/lazy-vars.png" alt="image" /></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<p>您可以在任务中使用一个新的独立于平台的 userHome 变量。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;Test Home&#34;</span>,
  <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
  <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;ls ${userHome}&#34;</span>
}</code></pre></div>
<h2 id="notebooks">Notebooks</h2>

<ul>
<li>内置输出渲染器更新，我们将文本、图像、HTML 和代码渲染器从 VSCode 核心移至内置的输出渲染器扩展。通过此更改，VSCode 现在可以在这些输出类型上搜索文本。在下面的短视频中，搜索 <code>item</code> 最初有 3 个代码单元格的结果，但可以过滤以也包括单元格输出。
<img src="/image/vscode/notebook-search-in-text-output.gif" alt="image" /></li>
<li>笔记本单元格按钮滚动时具有粘性
<img src="/image/vscode/sticky-scroll.gif" alt="image" /></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-6-rc">TypeScript 4.6 支持</a></li>
<li>JavaScript 文件中的语法错误报告，感谢 TypeScript 4.6，VSCode 现在报告 JavaScript 文件中的一些常见语法错误。这包括解析错误，以及块范围变量的无效重新声明（可以通过 <code>&quot;javascript.validate.enable&quot;: false</code> 关闭）：
<img src="/image/vscode/js-syntax-errors.png" alt="image" /></li>
<li>可以为每种语言配置更多 JS/TS 设置：

<ul>
<li><code>javascript.preferences.quoteStyle</code>, <code>typescript.preferences.quoteStyle</code></li>
<li><code>javascript.preferences.importModuleSpecifier</code>, <code>typescript.preferences.importModuleSpecifier</code></li>
<li><code>javascript.preferences.importModuleSpecifierEnding</code>, <code>typescript.preferences.importModuleSpecifierEnding</code></li>
<li><code>javascript.preferences.jsxAttributeCompletionStyle</code>, <code>typescript.preferences.jsxAttributeCompletionStyle</code></li>
<li><code>javascript.preferences.renameShorthandProperties</code>, <code>typescript.preferences.renameShorthandProperties</code></li>
<li><code>javascript.preferences.useAliasesForRenames</code>, <code>typescript.preferences.useAliasesForRenames</code></li>
<li><code>javascript.suggest.enabled</code>, <code>typescript.suggest.enabled</code></li>
<li><code>javascript.suggest.completeJSDocs</code>, <code>typescript.suggest.completeJSDocs</code></li>
<li><code>javascript.suggest.jsdoc.generateReturns</code>, <code>typescript.suggest.jsdoc.generateReturns</code></li>
<li><code>javascript.autoClosingTags</code>, <code>typescript.autoClosingTags</code></li>
</ul></li>
<li>新的 Lua 语法高亮语法</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>重新打开本地文件和文件夹
<img src="/image/vscode/web-local-recent.gif" alt="image" /></li>
<li>Github 仓库，在 <code>vscode.dev</code> 和 <code>github.dev</code> 上编辑 GitHub 存储库时

<ul>
<li>合并冲突解决得到了改进。您的编辑器中现在有合并冲突装饰，具有接受当前更改、接受传入更改或接受两个更改的选项。
<img src="/image/vscode/remotehub-merge-conflicts.gif" alt="image" /></li>
<li>对于包含合并冲突的文件，源代码控制视图中还有一个阶段更改操作。
<img src="/image/vscode/remotehub-stage-conflicts.gif" alt="image" /></li>
<li>此外，您现在可以轻松地暂存和取消暂存 vscode.dev 和 github.dev 上 GitHub 存储库中特定文件夹下的所有更改。为此，请右键单击 Source Control 视图并选择 View as Tree。
<img src="/image/vscode/remotehub-stage-folder.gif" alt="image" /></li>
<li>工作区搜索和查找所有引用现在将默认下载并索引存储库的完整副本，而不是像以前那样默认提供部分结果。有几个设置可以配置此索引功能：

<ul>
<li><code>remoteHub.indexing.verboseDownloadNotification</code> - 控制下载通知是显示为弹出窗口（默认）还是显示在状态栏中。</li>
<li><code>remoteHub.indexing.maxIndexSize</code> - 控制要下载的索引的大小限制。如果超出此限制，将取消下载并提供部分结果。您可以将此设置留空以从不下载存储库并始终使用部分结果。</li>
</ul></li>
</ul></li>
<li>Azure 仓库，在这个里程碑中，我们将 Azure Repos 支持从使用特定的 Azure DevOps 身份验证提供程序切换到使用通用 Microsoft 身份验证提供程序（由 Settings Sync 使用）。当您访问 Azure 存储库时，系统会提示您再次登录，但您保存的所有更改都将保留。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor">Hex Editor</a> 的 data inspector 改进，不会总是显示，如果空间过小将通过悬浮窗展示。可以通过 <code>hexeditor.inspectorType</code> 进行配置。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a> 参见 <a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0380">changelog for the 0.38.0</a></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>终端 Shell 集成，可通过 <code>terminal.integrated.shellIntegration.enabled</code> 打开。</li>
<li>ESLint 支持 notebook</li>
<li>细节参见：<a href="https://code.visualstudio.com/updates/v1_65#_preview-features">原文</a></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>语言状态项。我们已经完成了语言状态项的 API。此 API 显示活动编辑器的语言特定信息。这可以是有关项目或工具集版本的一般信息，但也可以显示错误和警告。相关 API 为 <code>vscode.languages.createLanguageStatusItem(...)</code>
<img src="/image/vscode/language-status.png" alt="image" /></li>
<li>内联提示，Inlay Hint 提供程序 API 现已完成。它允许在源代码中嵌入附加信息。下图显示了 TypeScript 如何为推断类型添加内联提示。
<img src="/image/vscode/inlay-hints.png" alt="image" />
API 是围绕 <code>InlayHintsProvider</code> 构建的。它提供了 <code>InlayHint</code> 对象，这些对象有几个有趣的特性：

<ul>
<li>内联提示可以有工具提示和命令。</li>
<li>提示的标签可以由多个部分组成，也可以有工具提示和命令。</li>
<li>标签部件还可以具有关联的源位置，该位置启用语言功能，例如此部件的转到定义。</li>
</ul></li>
<li>状态栏焦点边框
<img src="/image/vscode/status-bar-focus-borders.gif" alt="image" />
主题作者可以通过配置两种新的主题颜色来自定义边框颜色：

<ul>
<li><code>statusBar.focusBorder</code>：焦点时整个状态栏的边框颜色。</li>
<li><code>statusBarItem.focusBorder</code>：焦点时状态栏项目的边框颜色。</li>
</ul></li>
<li>Testing refresh action and sortText，参见：<a href="https://code.visualstudio.com/updates/v1_65#_testing-refresh-action-and-sorttext">原文</a></li>
<li>评论时间戳，评论 API 现在让您可以为每个 <code>Comment</code> 添加 <code>timestamp</code>。此时间戳显示在“评论”小部件和“评论”视图中。默认情况下，时间戳显示为相对时间（例如，“2 周前”），但用户设置 <code>comments.useRelativeTime</code> 可用于显示准确时间。时间戳的悬停始终是准确的时间。</li>
<li><code>vscode-test</code> 包重命名为 <code>@vscode/test-electron</code></li>
<li>图标 codicons 更新</li>
<li>图标贡献点现已最终确定</li>
<li>文件图标主题：支持文件关联中的文件夹名称</li>
<li>Running remotely installed web extensions in VS Code for the Web with Codespaces</li>
<li>更多细节参见：<a href="https://code.visualstudio.com/updates/v1_65#_extension-authoring">原文</a></li>
</ul>

<h2 id="调试器扩展创作-debugger-extension-authoring">调试器扩展创作 (Debugger extension authoring)</h2>

<ul>
<li>支持 &ldquo;important&rdquo; 输出事件</li>
<li>支持 &lsquo;lazy&rsquo; 变量</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_65#_debugger-extension-authoring">原文</a></li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_65#_language-server-protocol">原文</a></p>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_65#_debug-adapter-protocol">原文</a></p>

<h2 id="提案的扩展-api-proposed-extension-apis">提案的扩展 API (Proposed extension APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_65#_proposed-extension-apis">原文</a></p>

<h2 id="新文档-new-documentation">新文档 (New documentation)</h2>

<ul>
<li>Java GUI applications，参见： <a href="https://code.visualstudio.com/docs/java/java-gui">Working with GUI applications in VS Code</a></li>
</ul>
]]></description></item><item><title>VSCode 1.66 (2022-03) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_66_2022-03/</link><pubDate>Fri, 15 Apr 2022 21:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_66_2022-03/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_66">https://code.visualstudio.com/updates/v1_66</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<ul>
<li>可以在时间线视图，查看文件的本地修改历史记录，以实现查看 diff 以及恢复。</li>
<li>UI 优化

<ul>
<li>终端搜索，滚动条高亮指示。</li>
<li>文件相对于代码库的改动左侧的装饰条，使用更好的颜色，并将优化样式。</li>
<li>括号对着色，支持按照括号类型进行分组，使用独立的颜色池 <code>editor.bracketPairColorization.independentColorPoolPerBracketType</code></li>
</ul></li>
<li>VS Code for the Web 支持拖拽打开本地目录。</li>
<li>Markdown shorthand 引用连接支持跳转到定义位置。</li>
<li>添加一个内置的 <a href="https://docutils.sourceforge.io/rst.html">rst (reStructuredText) 扩展</a></li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<p>本版本提供了如下可访问性的改进</p>

<ul>
<li>支持配置以减少工作台动画渲染。</li>
<li>使用图案和颜色对比提高，来提升源代码控制装饰器的可见性。</li>
<li>可以调整编辑器音频提示音量。</li>
<li>评论 UI 添加新命令和键盘快捷键。</li>
<li>主题作者现可以制作高对比度浅色主题。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>本地历史，对于非 git 管理的本地文件，可以在时间轴视图中显示打开的文件的本地历史记录。</p>

<p><img src="/image/vscode/local-history.gif" alt="image" /></p>

<p>每次保存编辑器时，都会在列表中添加一个新条目，每个本地历史条目都包含创建条目时文件的全部内容，并且在某些情况下，可以提供更多语义信息（例如，重构）。针对这些历史记录，可以：</p>

<ul>
<li>将更改与本地文件或以前的条目进行比较。</li>
<li>还原内容。</li>
<li>删除或重命名条目。</li>
</ul>

<p>添加如下命令以处理本地历史（这些命令没有默认键绑定，但您可以添加自己的键盘快捷键。）：</p>

<ul>
<li><code>workbench.action.localHistory.create</code> - 使用自定义名称为活动文件创建新的历史记录条目。</li>
<li><code>workbench.action.localHistory.deleteAll</code> - 删除所有文件中的所有历史记录条目。</li>
<li><code>workbench.action.localHistory.restoreViaPicker</code> - 列出所有的有历史记录的文件，选中后，可以选择历史记录后，将恢复到该历史。</li>
</ul>

<p>还有一些新设置可用于处理本地历史：</p>

<ul>
<li><code>workbench.localHistory.enabled</code> - 启用或禁用本地历史记录（默认值：true）。</li>
<li><code>workbench.localHistory.maxFileSize</code> - 创建本地历史记录条目时的文件大小限制（默认值：256 KB）。</li>
<li><code>workbench.localHistory.maxFileEntries</code> - 每个文件的本地历史条目限制（默认值：50）。</li>
<li><code>workbench.localHistory.exclude</code> - 用于从本地历史记录中排除某些文件的全局模式。</li>
<li><code>workbench.localHistory.mergeWindow</code> - 将进一步更改添加到本地文件历史记录中的最后一个条目的间隔（以秒为单位）（默认为 10 秒）。</li>
</ul>

<p>时间轴视图工具栏中的新过滤器操作允许您启用或禁用单个提供程序：</p>

<p><img src="/image/vscode/timeline-filter.png" alt="image" /></p>

<p>注意：根据您对 VS Code 的使用情况，本地历史记录条目存储在不同的位置。打开本地文件时，条目保存在本地用户数据文件夹中，打开远程文件时，它们将存储在远程用户数据文件夹中。当没有可用的文件系统时（例如，在某些情况下使用 VS Code for Web 时），条目将存储到 IndexedDB 中。</p></li>

<li><p>设置编辑器</p>

<ul>
<li><p>支持通过 <code>@lang:languageId</code> 过滤所有可以在该语言下使用的配置项。修改时，配置会写入 json 文件的 <code>[languageId]</code> 下。</p>

<p><img src="/image/vscode/settings-editor-lang-css-settings.gif" alt="image" /></p></li>

<li><p>某个设置在用户设置中配置了，然后又工作区设置了和默认值一样时，仍然会将配置写入工作区的配置文件，而不是之前的什么都不做。只有点击，齿轮菜单的重置此配置，才会删掉配置项。</p>

<p><img src="/image/vscode/settings-editor-workspace-override.gif" alt="image" /></p></li>
</ul></li>

<li><p>右下角弹窗通知的第一个按钮是 Primary 按钮，背景将使用强调色。</p></li>

<li><p>新的编辑器上下文 key（用于快捷键配置）</p>

<ul>
<li><code>activeEditorIsFirstInGroup</code> - 活动编辑器是否是其组中的第一个。</li>
<li><code>activeEditorIsLastInGroup</code> - 活动编辑器是否是其组中的最后一个。</li>
</ul></li>

<li><p>配置打开未知的二进制文件的 编辑器：<code>workbench.editor.defaultBinaryEditor</code>。</p></li>

<li><p>更流畅的身份验证体验</p></li>

<li><p>code 命令支持安装预发布版本，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">code --install-extension GitHub.vscode-pull-request-github@prerelease --install-extension GitHub.remotehub</code></pre></div></li>

<li><p>改进了平台特定的扩展更新，VS Code 现在支持将特定于平台的扩展更新为更特定的目标平台版本。例如，如果您使用的是 Windows 64 位，并且已经安装了适用于 Windows 32 位的 C/C++ 扩展，并且市场上有适用于 Windows 64 位的相同版本的扩展，VS Code 会自动将扩展更新为 64-位版本。</p></li>

<li><p>音频提示改进 <code>audioCues.volume</code> 可以配置音量（0-100, 默认 50)）。</p></li>

<li><p>上几个迭代添加的 Side Panel （侧边面板，即右侧侧边栏） 存在歧义，VSCode 更新的其在产品上的命名：</p>

<ul>
<li><code>Side Bar</code> -&gt; <code>Primary Side Bar</code> 主侧边栏（左侧边栏）。</li>
<li><code>Side Panel</code> -&gt; <code>Secondary Side Bar</code> 辅助侧边栏（右侧边栏）。</li>
</ul>

<p><img src="/image/vscode/focus-side-bar-commands.png" alt="image" /></p></li>

<li><p>减少工作台动画渲染配置项， <code>workbench.reduceMotion</code>（值是 <code>on</code>、<code>off</code> 或默认的 <code>auto</code>）。</p></li>
</ul>

<h2 id="评论-comments">评论 (Comments)</h2>

<p>当打开一个包含评论的文件时，评论视图将显示。这可以通过设置 <code>comments.openView</code> 来控制。</p>

<ul>
<li><p>添加评论可发现性，当将鼠标悬停在可以添加评论的行的任何部分时，<code>+</code>符号将显示在左侧装订线中。</p>

<p><img src="/image/vscode/comment-plus.gif" alt="image" /></p></li>

<li><p>评论可访问性改进。</p></li>

<li><p>如果在可评论范围内，则可以通过 <code>&gt;Add Comment on Current Line</code> 命令创建评论。</p></li>

<li><p>评论线程的 aria 标签，包括评论数和线程的标签。</p></li>

<li><p>添加 <code>&gt;Go to Next Comment Thread</code> 命令，跳转到下一条命令。</p></li>

<li><p>添加 <code>&gt;Go to Previous Comment Thread</code> 命令。</p></li>

<li><p>跳转到下一个和上一个评论的的键盘快捷键：<code>Alt+F9</code> 和 <code>Shift+Alt+F9</code>。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>终端搜索，显示找到的匹配项视图提升：</p>

<ul>
<li>搜索匹配项突出限制，可以在颜色主题中通过 <code>terminal.findMatch</code> 前缀配置。</li>
<li>滚动条显示匹配的位置条。</li>
</ul>

<p><img src="/image/vscode/find-scrollbar.png" alt="image" /></p></li>

<li><p>当开启了 <a href="https://code.visualstudio.com/updates/v1_66#_terminal-shell-integration">shell integration</a> 特性后：</p>

<ul>
<li><p>滚动条将在每条命令的位置显示颜色。</p>

<p><img src="/image/vscode/command-annotations.png" alt="image" /></p></li>

<li><p>通过 <code>Cmd+Up/Down</code> 快捷键可以实现如下效果。</p>

<p><img src="/image/vscode/command-navigation.gif" alt="image" /></p></li>

<li><p>复制的文本保留 HTML 样式，通过 <code>&gt;Terminal: Copy Selection as HTML</code> 命令或者上下文菜单可以复制 html 样式的终端文本（复制到富文本编辑器后会保留颜色和字体样式）。</p></li>
</ul></li>

<li><p><code>terminal.integrated.minimumContrastRatio</code> 配置项默认值配置为 4.5。设置每个单元格的前景色时，将改为尝试符合指定的对比度比率。示例值:</p>

<ul>
<li>1: 不执行任何操作，使用标准主题颜色。</li>
<li>4.5: 符合 WCAG AA 标准(最低)(默认)。</li>
<li>7: 符合 WCAG AAA 标准(增强)。</li>
<li>21: 黑底白字或白底黑字。</li>
</ul></li>
</ul>

<h2 id="源代码控制-source-control">源代码控制 (Source Control)</h2>

<ul>
<li>如果工作区包含多个 git 仓库，存储库列表将按照首字母排序。</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p><code>editor.quickSuggestions</code> 配置项支持配置为 <code>inline</code>，改为 <code>inline</code> 后，将不再显示下拉列表。</p>

<p><img src="/image/vscode/inline-quick-suggest.gif" alt="image" /></p></li>

<li><p>代码片段添加新的变量，<code>$CURSOR_INDEX</code> 和 <code>$CURSOR_NUMBER</code>，比如 markdown 场景，多光标场景实现有序列表。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;ordered_list&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34;markdown&#34;</span>,
    <span style="color:#f92672">&#34;prefix&#34;</span>: <span style="color:#e6db74">&#34;ol&#34;</span>,
    <span style="color:#f92672">&#34;body&#34;</span>: [
        <span style="color:#e6db74">&#34;$CURSOR_NUMBER. $0&#34;</span>
    ],
    <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Add ordered list&#34;</span>
}</code></pre></div></li>

<li><p>当前打开的编辑器文件被 git 管理时，源代码控制的左侧装饰条优化，使用图案和颜色以提高对比性。</p>

<p><img src="/image/vscode/new-scm-decorators.gif" alt="image" /></p></li>

<li><p>括号对着色，添加新的配置项 <code>editor.bracketPairColorization.independentColorPoolPerBracketType</code> 表示是否区分括号类型（小括号、中括号、大括号）来分别有自己的颜色池。</p>

<p><img src="/image/vscode/independentColorPoolPerBracketTypeEnabled.png" alt="image" />
<img src="/image/vscode/independentColorPoolPerBracketTypeDisabled.png" alt="image" /></p></li>

<li><p>语言检测改进，参见：<a href="https://code.visualstudio.com/updates/v1_66#_improved-language-detection">原文</a>。</p></li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li><p>支持拖拽目录到网页中直接打开该目录</p>

<p><img src="/image/vscode/web-dnd.gif" alt="image" /></p></li>

<li><p>GitHub Repositories 和 Azure Repos 扩展共同依赖 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-repositories">Remote Repositories</a> 扩展。</p></li>

<li><p>提醒同步存储库，当重新打开包含未提交更改的存储库时，默认情况下远程存储库不会显示存储库的最新版本。我们现在显示一个对话框来手动同步您的存储库，以便您的存储库与 GitHub 或 Azure Repos 上的内容保持同步。您可以使用 <code>remoteHub.uncommittedChangesOnEntry</code> 设置控制此对话框。</p>

<p><img src="/image/vscode/sync-dialog.png" alt="image" /></p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>搜索，在滚动条上添加装饰条。</p>

<p><img src="/image/vscode/notebook-find-scrollbar.gif" alt="image" /></p></li>

<li><p>添加，将焦点移到交互式窗口的命令</p>

<ul>
<li>interactive.input.focus - 将焦点移至交互式窗口中的输入编辑器。</li>
<li>interactive.history.focus - 将焦点移至交互式窗口中的历史记录。</li>
</ul></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li><p>JavaScript 调试器现在支持收集和可视化堆配置文件。堆配置文件允许您查看随时间分配的内存位置和数量。这些已作为选项添加到 <code>Debug:Take Performance Profile</code> 命令中，也可以通过 <code>CALL STACK</code> 视图中的记录按钮访问。</p>

<p><img src="/image/vscode/js-debug-memory-profile.png" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>内置 CSS 插件，添加格式化器，由 <a href="https://github.com/beautify-web/js-beautify">JS Beautify 库</a>提供能力。同时添加了如下设置（less 和 scss 也存在相同的设置）：</p>

<ul>
<li><code>css.format.enable</code> - 启用/禁用默认 CSS 格式化程序。</li>
<li><code>css.format.newlineBetweenRules</code> - 用空行分隔规则集。</li>
<li><code>css.format.newlineBetweenSelectors</code> - 用新行分隔选择器。</li>
<li><code>css.format.spaceAroundSelectorSeparator</code> - 确保选择器分隔符 <code>'&gt;'</code>、<code>'+'</code>、<code>'~'</code> 周围有一个空格字符（例如，a &gt; b）。</li>
</ul></li>

<li><p>HTML 中的 JavaScript 添加语义突出显示。</p></li>

<li><p>TypeScript 4.6.3。</p></li>

<li><p>Markdown shorthand 引用连接支持跳转到定义位置（<code>[my fancy link]</code> 可以快速调转到定义位置 <code>[my fancy link]: https://example.com</code>）。</p>

<p><img src="/image/vscode/markdown-ref-link.gif" alt="image" /></p></li>

<li><p>添加一个内置的 <a href="https://docutils.sourceforge.io/rst.html">rst (reStructuredText) 扩展</a></p>

<p><img src="/image/vscode/rst-sample.png" alt="image" /></p></li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python</p>

<ul>
<li><p>状态栏中解释器显示的更改，移动到了右侧</p>

<p><img src="/image/vscode/active-interpreter-display.png" alt="image" /></p></li>

<li><p>添加新建文件命令：<code>&gt;Python: New Python File</code></p></li>

<li><p>Pylint 能力抽到单独的 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.pylint">pylint 扩展</a> 中，但是 Python 主扩展的 pylint 能力尚未移除，如果需要移除，需添加 <code>&quot;python.linting.pylintEnabled&quot;: false</code> 配置。</p></li>
</ul></li>

<li><p>Jupyter</p>

<ul>
<li>Kernel 支持 更多 <a href="https://docs.conda.io/">conda</a> 环境，支持所有平台上的 <code>.env</code> 文件。</li>

<li><p>Data Viewer 支持序号列和命名索引列</p>

<p><img src="/image/vscode/named-index.png" alt="image" /></p></li>

<li><p>新建文件支持新建 Jupyter notebook。</p>

<p><img src="/image/vscode/new-notebook.png" alt="image" /></p></li>
</ul></li>

<li><p>Remote Development</p>

<ul>
<li>&ldquo;Open in Remote Container&rdquo; badge - Direct users of your repo to reopen in a custom development container.</li>
<li>SSH Remote 扩展，现在可以连接到远程 Apple Silicon/M1/ARM64 机器。</li>
</ul>

<p>更多详见 <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_66.md">Release Notes</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>终端 Shell 集成，参见：<a href="https://code.visualstudio.com/updates/v1_66#_terminal-shell-integration">原文</a>。</li>
<li>TypeScript 4.7 支持，参见：<a href="https://code.visualstudio.com/updates/v1_66#_typescript-47-support">原文</a>。</li>
<li>资源管理器文件嵌套完善（推荐使用：<a href="https://marketplace.visualstudio.com/items?itemName=antfu.file-nesting">File Nesting Updater</a>），参见：<a href="https://code.visualstudio.com/updates/v1_66#_explorer-file-nesting">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Notebook-aware document selectors ，添加 <code>vscode.DocumentSelector</code> 类型，更多参考：<a href="https://code.visualstudio.com/updates/v1_66#_notebookaware-document-selectors">原文</a>。</li>
<li>内联提示可以有编辑，InlayHint 类型现在可以有一个可选的lazy 的 textEdits 属性。当双击内联提示时，会应用这个属性。.例如，双击表示推断类型的提示应插入该类型注解。</li>
<li>output 频道支持自定义语言 ID，通过 <a href="https://github.com/microsoft/vscode/blob/dc2f5d8dd1790ac4fc6054e11b44e36884caa4be/src/vscode-dts/vscode.d.ts#L9415">createOutputChannel API</a> 添加。这将允许开发者通过传递语言 ID 为您的输出通道贡献标记或语法着色和 CodeLens 功能。</li>
<li>新的颜色主题类别：高对比度浅色主题（<code>hc-light</code>），在 VSCode API 中对应的 <a href="https://code.visualstudio.com/api/references/vscode-api#ColorTheme">ColorTheme.kind</a> 可以被设置为 <code>HighContrastLight</code>。颜色主题贡献支持定义高对比度浅色主题 (<code>highContrastLight</code>)。如果未指定，则默认使用 <code>light</code>。</li>
<li><code>NODE_MODULE_VERSION</code> 和 Node.js API 更新，参见：<a href="https://code.visualstudio.com/updates/v1_66#_nodemoduleversion-and-nodejs-api-update">原文</a>。</li>
<li>树拖拽 API。参见：<a href="https://code.visualstudio.com/updates/v1_66#_tree-drag-and-drop-api">原文</a>。</li>
</ul>

<h2 id="调试器扩展作者-debugger-extension-authoring">调试器扩展作者 (Debugger extension authoring)</h2>

<ul>
<li><p>调试适配器协议向 <code>CompletionItem</code> 对象添加了一个 <code>detail</code> 属性。 VSCode 现在支持此属性。在调试控制台的建议小部件中看到该属性配置的值。</p>

<p><img src="/image/vscode/debug-detail.png" alt="image" /></p></li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_66#_language-server-protocol">原文</a>。</p>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_66#_debug-adapter-protocol">原文</a>。</p>

<h2 id="提案的扩展-api-proposed-extension-apis">提案的扩展 API (Proposed extension APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_66#_proposed-extension-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 17</li>
</ul>

<p>更多参见：<a href="https://code.visualstudio.com/updates/v1_66#_engineering">原文</a>。</p>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/languages/r">R in VSCode 文档</a></li>
<li><a href="https://code.visualstudio.com/blogs/2022/03/08/the-tutorial-problem">The problem with tutorials</a></li>
</ul>
]]></description></item><item><title>VSCode 1.67 (2022-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_67_2022-04/</link><pubDate>Sun, 29 May 2022 21:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_67_2022-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_67">https://code.visualstudio.com/updates/v1_67</a></p>
</blockquote>

<h2 id="本次更新看点">本次更新看点</h2>

<h3 id="使用者">使用者</h3>

<ul>
<li>资源管理器目录项文件嵌套，建议通过启用 <code>explorer.fileNesting.enabled</code> 配置项开启，并安装 <a href="https://marketplace.visualstudio.com/items?itemName=antfu.file-nesting">File Nesting Config</a> 扩展自动更新嵌套模式。</li>
<li>设置编辑器，支持过滤按钮</li>
<li>建议通过将 <code>window.confirmBeforeClose</code> 配置项设置为 <code>keyboardOnly</code>，防止误触 <code>cmd + q</code> 导致 VSCode 退出。</li>

<li><p>Shift+单击，以禁用而非删除断点。</p>

<p><img src="/image/vscode/disable-breakpoints.gif" alt="image" /></p></li>

<li><p>内联提示 <code>editor.inlayHints.enabled</code> 配置添加新的可选项 <code>onUnlessPressed</code> 和 <code>offUnlessPressed</code>，可以通过 <code>Ctrl+Alt</code> 来按需开关内联提示。</p></li>

<li><p>Markdown 添加大量引用相关的支持。</p>

<ul>
<li><p>拖拽到编辑器以创建链接，可以通过 <code>&quot;markdown.editor.drop.enabled&quot;: false</code> 关闭该特性。</p>

<p><img src="/image/vscode/md-drop-external.gif" alt="image" /></p></li>

<li><p>支持查找对 Header 的所有引用。</p>

<p><img src="/image/vscode/md-header-references.png" alt="image" /></p></li>

<li><p>支持查找<a href="https://spec.commonmark.org/0.30/#reference-link">参考链接</a>的引用。</p>

<p><img src="/image/vscode/md-ref-references.png" alt="image" /></p></li>

<li><p>查找对当前 markdown 文档的所有文件的引用（<code>&gt;Markdown: Find all references to files</code>）。</p>

<p><img src="/image/vscode/md-file-references.gif" alt="image" /></p></li>

<li><p>查找所有对 URL 的引用（<code>&gt;Markdown: Find all references to URLs</code>）</p>

<p><img src="/image/vscode/md-url-references.png" alt="image" /></p></li>

<li><p>重命名标题 (<code>&gt;Markdown: Rename headers</code>)</p>

<p><img src="/image/vscode/md-rename-header.gif" alt="image" /></p></li>

<li><p>重命名参考链接</p>

<p><img src="/image/vscode/md-rename-references.png" alt="image" /></p></li>

<li><p>重命名 Markdown 文件名 (<code>F2</code>)</p>

<p><img src="/image/vscode/md-file-rename.png" alt="image" /></p></li>
</ul></li>

<li><p>JSON 添加 <code>json.validate.enable</code> 配置项（默认为 true）可以用来关闭 json schema 的验证。</p></li>

<li><p>Java 支持 内联提示 和 调试 lazy 变量。</p></li>

<li><p>添加 <a href="https://code.visualstudio.com/docs/languages/rust">Rust in VS Code 文档</a>。</p></li>
</ul>

<h3 id="扩展开发者">扩展开发者</h3>

<ul>
<li>VSCode URI 支持在新窗口中处理 <code>windowId=_blank</code>，以 git clone 为例： <code>vscode://vscode.git/clone?url=https%3A%2F%2Fgithub.com%2FMicrosoft%2Fvscode-vsce.git&amp;windowId=_blank</code></li>
<li>添加 <a href="https://code.visualstudio.com/api/ux-guidelines/overview">UX 指南</a> 最佳实践文档。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>资源管理器目录项文件嵌套，移除实验性，正式支持。</p>

<ul>
<li>通过如下配置项可以控制

<ul>
<li><code>explorer.fileNesting.enabled</code> 控制是否启用文件嵌套。它可以全局设置，也可以针对特定工作区设置。</li>
<li><code>explorer.fileNesting.expand</code> 控制默认情况下是否展开嵌套文件。</li>
<li><code>explorer.fileNesting.patterns</code> 控制文件的嵌套方式。默认配置为 TypeScript 和 JavaScript 项目提供嵌套智能。</li>
<li>之前实验性配置 <code>explorer.experimental.fileNesting.operateAsGroup</code> 已经被移除，目前针对一个组的操作就是会应用到整个组。</li>
</ul></li>

<li><p>一些例子：</p>

<ul>
<li><p>默认配置：</p>

<p><img src="/image/vscode/nest-default.png" alt="image" /></p></li>

<li><p>当文件与目录名称匹配时嵌套在 <code>index.ts</code> 下（<code>&quot;index.ts&quot;: &quot;${dirname}.ts&quot;</code>）：</p>

<p><img src="/image/vscode/nest-dirname.png" alt="image" /></p></li>

<li><p>嵌套与不同文件同名但添加了段的文件（<code>&quot;*&quot;: &quot;${basename}.*.${extname}&quot;</code>）：</p>

<p><img src="/image/vscode/nest-dirname-basename.png" alt="image" /></p></li>
</ul></li>

<li><p>推荐安装 <a href="https://marketplace.visualstudio.com/items?itemName=antfu.file-nesting">File Nesting Config</a> 扩展自动，更新嵌套模式。</p></li>
</ul></li>

<li><p>设置编辑器，搜索</p>

<ul>
<li><p>添加过滤器按钮。</p>

<p><img src="/image/vscode/se-filter-button.gif" alt="image" /></p></li>

<li><p>支持按照语言过滤，并将配置项应用到指定语言中，如 <code>@lang:markdown</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;[markdown]&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
<span style="color:#f92672">&#34;editor.wordBasedSuggestions&#34;</span>: <span style="color:#66d9ef">false</span>
}</code></pre></div></li>
</ul></li>

<li><p>添加 <code>files.refactoring.autoSave</code> 配置项（默认值为 true），配置思否在重构后自动保存。</p></li>

<li><p>添加 <code>workbench.editor.limit.excludeDirty</code> 配置项（默认值为 false），配置编辑器打开限制是否排除脏编辑器（未保存的编辑器）。</p></li>

<li><p>添加 <code>git.timeline.showUncommitted</code> （默认值为 false，因为本地文件时间线已经有了该特性了）配置项，是否展示未提交的文件的时间线。</p></li>

<li><p>编辑器打开失败时，在编辑器主页面，显示错误信息和快捷操作</p>

<p><img src="/image/vscode/editor-placeholder.gif" alt="image" /></p></li>

<li><p>状态栏语言状态</p>

<ul>
<li><p>添加显示格式化程序冲突提示 - 当安装了多个语言格式化程序但没有一个配置为默认格式化程序时会发生这种情况。此外，语言状态在包含严重状态时更加突出。</p>

<p><img src="/image/vscode/languageStatus.gif" alt="image" /></p></li>

<li><p>添加 <code>workbench.editor.languageDetectionHints</code> 配置项，配置是否在 无标题编辑器和 notebook 中，如果语言配置错误时，是否显示提示。</p>

<p><img src="/image/vscode/language-detection.gif" alt="image" /></p></li>
</ul></li>

<li><p>显示无效或不兼容的扩展</p>

<p><img src="/image/vscode/incompatible-extension.png" alt="image" /></p></li>

<li><p>退出前确认，添加 <code>window.confirmBeforeClose</code> （默认值为 <code>never</code>） 配置项，配置是否在关闭窗口前弹出确认框，可以配置为 <code>keyboardOnly</code> 只在键盘退出时提示。（注意：该设置并不是全新的，并且已经在 <a href="https://code.visualstudio.com/docs/editor/vscode-web">VS Code for Web</a> 中提供了一段时间）</p>

<p><img src="/image/vscode/confirm-quit.gif" alt="image" /></p></li>

<li><p>评论功能</p>

<ul>
<li><code>comments.openView</code> 默认值改为 <code>firstFile</code>。</li>

<li><p>多行评论 UI 支持。</p>

<p><img src="/image/vscode/multiline-comments.gif" alt="image" /></p></li>
</ul></li>

<li><p>VSCode URI 支持在新窗口中处理 <code>windowId=_blank</code>，以 git clone 为例： <code>vscode://vscode.git/clone?url=https%3A%2F%2Fgithub.com%2FMicrosoft%2Fvscode-vsce.git&amp;windowId=_blank</code></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>括号对着色，默认启用，即 <code>editor.bracketPairColorization.enabled</code> 默认值改为 true。</li>

<li><p>TextMate 语法可以将标记标记为不平衡（以支持 shell 语法中的 case in 语法中的 <code>)</code> 错误，参见官方：<a href="https://code.visualstudio.com/updates/v1_67#_textmate-grammars-can-mark-tokens-as-unbalanced">文档</a>）。</p>

<ul>
<li><p>之前</p>

<p><img src="/image/vscode/unbalanced-brackets-shell-old.png" alt="image" /></p></li>

<li><p>现在</p>

<p><img src="/image/vscode/unbalanced-brackets-shell-new.png" alt="image" /></p></li>
</ul></li>

<li><p>新的括号匹配算法，括号匹配现在使用与括号着色相同的数据结构。这既提高了准确性，又提高了性能。</p>

<ul>
<li><p>之前</p>

<p><img src="/image/vscode/bracket-matching-old.png" alt="image" /></p></li>

<li><p>现在</p>

<p><img src="/image/vscode/bracket-matching-new.png" alt="image" /></p></li>
</ul></li>

<li><p>括号装饰线提升（更多参见：<a href="https://code.visualstudio.com/updates/v1_67#_bracket-guide-improvements">原文</a>）</p>

<p><img src="/image/vscode/horizontal-bracket-guides.png" alt="image" /></p></li>

<li><p>内联提示 <code>editor.inlayHints.enabled</code> 配置添加新的可选项：</p>

<ul>
<li><code>on</code> - 启用内联提示。</li>
<li><code>off</code> - 关闭内联提示。</li>
<li><code>onUnlessPressed</code> - 使用 Ctrl+Alt 显示和隐藏内联提示。</li>
<li><code>offUnlessPressed</code> - 使用 Ctrl+Alt 隐藏和显示镶嵌提示。</li>
</ul></li>

<li><p>内联建议提升（更多参见：<a href="https://code.visualstudio.com/updates/v1_67#_improved-inline-suggestions">原文</a>）</p>

<p><img src="/image/vscode/inlineSuggest.gif" alt="image" /></p></li>

<li><p>将文本拖放到编辑器中</p>

<p><img src="/image/vscode/editor-drop.gif" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>查找结果计数</p>

<p><img src="/image/vscode/terminal-find-count.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>源代码控制存储库视图排序 <code>scm.repositories.sortOrder</code>，默认按照发现时间。</p>

<p><img src="/image/vscode/scm-repositories-view-sort.gif" alt="image" /></p></li>

<li><p><code>scm.diffDecorationsGutterPattern</code> 配置可以更改 diff 装饰器样式。</p>

<p><img src="/image/vscode/editor-diff-decorators.gif" alt="image" /></p></li>

<li><p>性能改进，将 <code>git.untrackedChanges</code> 设置设置为 hidden 的用户在使用大型存储库时将体验到更好的性能。这是通过在调用 <code>git status</code> 时传递 <code>-uno</code> 参数来实现的。</p></li>

<li><p>扩展的远程源提供程序 API，参见：<a href="https://code.visualstudio.com/updates/v1_67#_expanded-remote-source-providers-api">原文</a>。</p></li>

<li><p>使用 SSH 从 GitHub 克隆 <code>github.gitProtocol</code>，默认为 <code>https</code>，可以更改为 <code>ssh</code>。</p></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>自动展开惰性变量配置项 <code>debug.autoExpandLazyVariables</code>，默认为 false。</li>

<li><p>惰性变量新的 <code>eye</code> 按钮。</p>

<p><img src="/image/vscode/lazy-var-button.png" alt="image" /></p></li>

<li><p>Shift+单击，以禁用而非删除断点。</p>

<p><img src="/image/vscode/disable-breakpoints.gif" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown</p>

<ul>
<li><p>拖拽到编辑器以创建链接，可以通过 <code>&quot;markdown.editor.drop.enabled&quot;: false</code> 关闭该特性。</p>

<p><img src="/image/vscode/md-drop-external.gif" alt="image" /></p></li>

<li><p>支持查找对 Header 的所有引用。</p>

<p><img src="/image/vscode/md-header-references.png" alt="image" /></p></li>

<li><p>支持查找<a href="https://spec.commonmark.org/0.30/#reference-link">参考链接</a>的引用。</p>

<p><img src="/image/vscode/md-ref-references.png" alt="image" /></p></li>

<li><p>查找对当前 markdown 文档的所有文件的引用（<code>&gt;Markdown: Find all references to files</code>）。</p>

<p><img src="/image/vscode/md-file-references.gif" alt="image" /></p></li>

<li><p>查找所有对 URL 的引用（<code>&gt;Markdown: Find all references to URLs</code>）</p>

<p><img src="/image/vscode/md-url-references.png" alt="image" /></p></li>

<li><p>重命名标题 (<code>&gt;Markdown: Rename headers</code>)</p>

<p><img src="/image/vscode/md-rename-header.gif" alt="image" /></p></li>

<li><p>重命名参考链接</p>

<p><img src="/image/vscode/md-rename-references.png" alt="image" /></p></li>

<li><p>重命名 Markdown 文件名 (<code>F2</code>)</p>

<p><img src="/image/vscode/md-file-rename.png" alt="image" /></p></li>
</ul></li>

<li><p>JSON</p>

<ul>
<li>添加 <code>json.validate.enable</code> 配置项（默认为 true）可以用来关闭 json schema 的验证。</li>
</ul></li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>打开远程存储库选择器（<code>Open Remote Repository</code>）时，将使用 <code>window.openFoldersInNewWindow</code> 配置项来决定是否在新窗口中打开。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Java</p>

<ul>
<li>添加<a href="https://code.visualstudio.com/docs/editor/editingevolved#_inlay-hints">内联提示</a>支持
<img src="/image/vscode/java-inlay-hints.gif" alt="image" />

<ul>
<li>通过 <code>java.inlayHints.parameterNames.enabled</code> 来配置

<ul>
<li><code>literals</code> - 仅为实参为字面量参数启用函数参数内联提示（默认）。</li>
<li><code>all</code> - 为所有函数调用参数启用函数参数内联提示。</li>
<li><code>none</code> - 禁用函数参数内联提示。</li>
</ul></li>
</ul></li>

<li><p>调试支持 <a href="https://code.visualstudio.com/updates/v1_65#_support-for-lazy-variables">lazy 变量</a></p>

<p><img src="/image/vscode/java-lazy-variable.gif" alt="image" /></p></li>
</ul></li>

<li><p>Jupyter</p>

<ul>
<li>新增 <a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-powertoys">Jupyter PowerToys</a> 扩展。</li>
<li>支持 Web extension（可以在  vscode.dev / github.dev 中使用），只支持非 https 的 Jupyter servers（<code>jupyter --no-browser --NotebookApp.allow_origin_pat=https://.*\.vscode-cdn\.net</code>）。</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_67#_jupyter">原文</a>。</li>
</ul></li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a></p>

<ul>
<li>更改语言服务器时无需重新加载。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter">Black 格式化扩展</a>。</li>
<li>使用 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.isort">isort</a> 进行导入排序。</li>
</ul></li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>，参见：<a href="https://code.visualstudio.com/updates/v1_67#_remote-development">原文</a>。</p></li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>，参见：<a href="https://code.visualstudio.com/updates/v1_67#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>设置 Profile，是 settings、扩展和自定义 UI 的集合，可以用来共享配置和设备迁移。参见：<a href="https://code.visualstudio.com/updates/v1_67#_settings-profile">原文</a>。</li>
<li>TypeScript 4.7 支持，参见：<a href="https://code.visualstudio.com/updates/v1_67#_typescript-47-support">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Tab API，参见：<a href="https://code.visualstudio.com/updates/v1_67#_tab-api">原文</a>。</li>
<li>Notebook 更改和保存事件，参见：<a href="https://code.visualstudio.com/updates/v1_67#_notebook-change-and-save-events">原文</a>。</li>
<li>支持非递归工作区文件观察器，参见：<a href="https://code.visualstudio.com/updates/v1_67#_support-for-nonrecursive-workspace-file-watchers">原文</a>。</li>
<li>添加 <a href="https://code.visualstudio.com/api/ux-guidelines/overview">UX 指南</a> 最佳实践文档。</li>
</ul>

<h2 id="调试器扩展制作-debugger-extension-authoring">调试器扩展制作 (Debugger extension authoring)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_67#_debugger-extension-authoring">原文</a>。</p>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_67#_language-server-protocol">原文</a>。</p>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_67#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_67#_engineering">原文</a>。</p>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>添加 <a href="https://code.visualstudio.com/docs/languages/rust">Rust in VS Code 文档</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.68 (2022-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_68_2022-05/</link><pubDate>Fri, 01 Jul 2022 00:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_68_2022-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_68">https://code.visualstudio.com/updates/v1_68</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>配置显示语言，支持显示语言名字，支持展示所有可用的语言包（即使没有安装）。</p>

<p><img src="/image/vscode/configure-display-language.png" alt="image" /></p></li>

<li><p>问题面板使用表格视图（表格显示了每个问题的来源（语言服务或扩展），允许用户按来源过滤问题。），可以通过 <code>problems.defaultViewMode</code> 配置项配置默认模式，以及问题面板右侧图表切换视图。</p>

<p><img src="/image/vscode/problems-view-table.png" alt="image" />
<img src="/image/vscode/view-as-table-button.png" alt="image" /></p></li>

<li><p>VS Code for the Web 将自动识别浏览器的语言设置，而展示对应的展示语言，因此不在需要手动安装展示语言包，下边是德语的例子。
<img src="/image/vscode/translations-core.png" alt="image" /></p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>配置显示语言提升，支持显示语言名字，支持展示所有可用的语言包（即使没有安装）。</p>

<p><img src="/image/vscode/configure-display-language.png" alt="image" /></p></li>

<li><p>问题面板使用表格视图（表格显示了每个问题的来源（语言服务或扩展），允许用户按来源过滤问题。），可以通过 <code>problems.defaultViewMode</code> 配置项配置默认模式，以及问题面板右侧图表切换视图。</p>

<p><img src="/image/vscode/problems-view-table.png" alt="image" />
<img src="/image/vscode/view-as-table-button.png" alt="image" /></p></li>

<li><p>废弃的扩展标记（目前该列表由 VSCode 官方维护，如过认为一个扩展要被废弃，需要在这个<a href="https://github.com/microsoft/vscode-discussions/discussions/1">讨论</a>中告诉 VSCode 官方）</p>

<ul>
<li>不在维护的废弃扩展，仍可以安装。
<img src="/image/vscode/deprecated-extension.png" alt="image" /></li>
<li>一个扩展被代替的扩展而废弃，则该扩展将不能被安装。
<img src="/image/vscode/deprecated-extension-alternate.png" alt="image" /></li>
<li>一个已弃用的扩展，其功能内置于 VSCode，可通过配置设置启用。
<img src="/image/vscode/deprecated-extension-builtin.png" alt="image" /></li>
<li>VSCode 不会自动迁移或卸载已弃用的扩展。将有一个迁移按钮来指导您切换到推荐的扩展。
<img src="/image/vscode/deprecated-extension-migrate.png" alt="image" /></li>
</ul></li>

<li><p>打赏（赞助）扩展，扩展开发者，可以配置一个打赏（赞助） URL，这个 URL 将展示在扩展详情页面。
<img src="/image/vscode/sponsor-extension.png" alt="image" /></p></li>

<li><p>支持基于 <code>.gitignore</code> 资源管理器中的文件隐藏，配置项为 <code>explorer.excludeGitIgnore</code>。</p></li>

<li><p>编辑器之外的非操作系统 Hover 显示后窗，按住 alt （option） 键，可以让改窗口一直显示以方便复制内容。
<img src="/image/vscode/hover-lock.gif" alt="image" /></p></li>

<li><p>设置编辑器改进，支持通过类似于 <code>@lang:javascript</code> 展示<a href="https://code.visualstudio.com/docs/getstarted/settings#_languagespecific-editor-settings">特定语言覆盖配置</a>，并支持显示语言特定的默认值。
<img src="/image/vscode/settings-editor-language-specific-default.gif" alt="image" /></p></li>

<li><p>评论小组件最右侧按钮被定义为主按钮，并使用强调色。
<img src="/image/vscode/comment-primary-button.png" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>查找匹配的背景颜色，效果和编辑器类似（添加了 <code>terminal.findMatchBackground</code> 和 <code>terminal.findMatchHighlightBackground</code> 主题色）。
<img src="/image/vscode/terminal-find-bg.png" alt="image" /></li>
<li>对比度和最小对比度的改进（配置项为 <code>terminal.integrated.minimumContrastRatio</code>），参见；<a href="https://code.visualstudio.com/updates/v1_68#_improvements-to-contrast-and-the-minimum-contrast-ratio">原文</a>。</li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li><p>默认任务支持 Glob 模式。，当激活的编辑器的文件名与 <code>group.isDefault</code> glob 模式匹配时，则该任务被视为默认任务。一个例子如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.0.0&#34;</span>,
    <span style="color:#f92672">&#34;tasks&#34;</span>: [
        {
            <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;echo txt&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
            <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;echo TextFile&#34;</span>,
            <span style="color:#f92672">&#34;group&#34;</span>: {
                <span style="color:#f92672">&#34;kind&#34;</span>: <span style="color:#e6db74">&#34;build&#34;</span>,
                <span style="color:#f92672">&#34;isDefault&#34;</span>: <span style="color:#e6db74">&#34;**.txt&#34;</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">This</span> <span style="color:#960050;background-color:#1e0010">is</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">glob</span> <span style="color:#960050;background-color:#1e0010">pattern</span> <span style="color:#960050;background-color:#1e0010">which</span> <span style="color:#960050;background-color:#1e0010">will</span> <span style="color:#960050;background-color:#1e0010">only</span> <span style="color:#960050;background-color:#1e0010">match</span> <span style="color:#960050;background-color:#1e0010">when</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">active</span> <span style="color:#960050;background-color:#1e0010">file</span> <span style="color:#960050;background-color:#1e0010">has</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">.txt</span> <span style="color:#960050;background-color:#1e0010">extension.</span>
            }
        },
        {
            <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;echo js&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
            <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;echo JavascriptFile&#34;</span>,
            <span style="color:#f92672">&#34;group&#34;</span>: {
                <span style="color:#f92672">&#34;kind&#34;</span>: <span style="color:#e6db74">&#34;build&#34;</span>,
                <span style="color:#f92672">&#34;isDefault&#34;</span>: <span style="color:#e6db74">&#34;**.js&#34;</span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">This</span> <span style="color:#960050;background-color:#1e0010">is</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">glob</span> <span style="color:#960050;background-color:#1e0010">pattern</span> <span style="color:#960050;background-color:#1e0010">which</span> <span style="color:#960050;background-color:#1e0010">will</span> <span style="color:#960050;background-color:#1e0010">only</span> <span style="color:#960050;background-color:#1e0010">match</span> <span style="color:#960050;background-color:#1e0010">when</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">active</span> <span style="color:#960050;background-color:#1e0010">file</span> <span style="color:#960050;background-color:#1e0010">has</span> <span style="color:#960050;background-color:#1e0010">a</span> <span style="color:#960050;background-color:#1e0010">.js</span> <span style="color:#960050;background-color:#1e0010">extension.</span>
            },
        }
    ]
}</code></pre></div></li>
</ul>

<h2 id="源代码控制-source-control">源代码控制 (Source Control)</h2>

<ul>
<li>添加 <code>git.branchPrefix</code> 配置以配置新建一个分支时分支名前缀。</li>
<li>添加 <code>git.branchRandomName.enable</code> 以启用分支名自动生成，并通过 <code>git.branchRandomName.dictionary</code> 配置配置分支名的字典。
<img src="/image/vscode/branch-generation.gif" alt="image" /></li>
<li>添加 <code>git.branchProtection</code> 配置以保护分支，被保护的分支将禁止在 VSCode 中向其提交。而是引导创建一个新分支。可以通过 <code>git.branchProtectionPrompt</code> 微调该行为。</li>
<li><a href="https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository">GitHub PR 模板</a>支持。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_68#_notebooks">原文</a>。</p>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li><p>在没有 <code>launch.json</code> 的情况下运行和调试，调试器选择将显示建议的调试器。</p>

<p><img src="/image/vscode/select-debugger.png" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>JavaScript/TypeScript

<ul>
<li>绑定 <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/">TypeScript 4.7.3</a>，参见：<a href="https://code.visualstudio.com/updates/v1_68#_typescript-47">原文</a>。</li>
<li>转到源定义，可以避免跳转到 <code>.d.ts</code> 文件，而是尝试跳转到对应的 js 文件（可能不准确）。
<img src="/image/vscode/ts-go-to-source.gif" alt="image" />
支持对象方法的代码片段，通过 <code>&quot;typescript.suggest.classMemberSnippets.enabled&quot;: false</code> 或 <code>&quot;javascript.suggest.classMemberSnippets.enabled&quot;: false</code> 配置。
<img src="/image/vscode/ts-snippet-method.gif" alt="image" />
&amp; JavaScript/TypeScript 组织导入提升，参见：<a href="https://code.visualstudio.com/updates/v1_68#_group-aware-organize-imports">原文</a>。</li>
<li>在隐式项目中启用严格的 NULL 检查（通过 <code>&quot;js/ts.implicitProjectConfig.strictNullChecks&quot;: false</code> 配置）
<img src="/image/vscode/ts-strict-null.png" alt="image" /></li>
</ul></li>
<li>Markdown 支持<a href="https://www.markdownguide.org/basic-syntax/#reference-style-links">在参考链接</a>上跳转到定义。</li>
<li>Expanded JSON Schema 支持，参见：<a href="https://code.visualstudio.com/updates/v1_68#_expanded-json-schema-support">原文</a>。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>VS Code for the Web 将自动识别浏览器的语言设置，而展示对应的展示语言，因此不在需要手动安装展示语言包，下边是德语的例子。
<img src="/image/vscode/translations-core.png" alt="image" /></li>
<li>远程存储库，参见：<a href="https://code.visualstudio.com/updates/v1_68#_remote-repositories">原文</a>。
<br /></li>
</ul>

<h2 id="开发容器规范-development-container-specification">开发容器规范 (Development Container specification)</h2>

<p>微软和 github 主导的开发容器标准化，参见：<a href="https://containers.dev/">https://containers.dev/</a></p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python，参见：<a href="https://code.visualstudio.com/updates/v1_68#_python">原文</a>。</li>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_68#_jupyter">原文</a>。</li>
<li>Remote Development，参见：<a href="https://code.visualstudio.com/updates/v1_68#_remote-development">原文</a>。</li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_68#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>Markdown 引用（链接）校验，参见：<a href="https://code.visualstudio.com/updates/v1_68#_markdown-link-validation">原文</a>。</li>
<li>粘贴文件以插入 Markdown 链接，参见：<a href="https://code.visualstudio.com/updates/v1_68#_paste-files-to-insert-markdown-links">原文</a>。</li>
<li>终端 Shell 集成，参见：<a href="https://code.visualstudio.com/updates/v1_68#_terminal-shell-integration">原文</a>。</li>
<li>Window Controls Overlay on Windows，参见：<a href="https://code.visualstudio.com/updates/v1_68#_window-controls-overlay-on-windows">原文</a>。</li>
<li>Command Center，参见：<a href="https://code.visualstudio.com/updates/v1_68#_command-center">原文</a>。</li>
<li>Merge 编辑器，参见：<a href="https://code.visualstudio.com/updates/v1_68#_merge-editor">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>内联完成 API 稳定，参见：<a href="https://github.com/microsoft/vscode/blob/e3a8e502ad7263836d0bc34cbcefbfc7bd65104f/src/vscode-dts/vscode.d.ts#L12357">languages.registerInlineCompletionItemProvider</a>。</li>
<li>InputBox 验证消息严重性 API 稳定，通过 <code>window.showInputBox</code> 和 <code>window.createInputBox</code> <a href="https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.d.ts#L1990-L2002">API</a>。</li>
<li>笔记本编辑器 API，参见：<a href="https://code.visualstudio.com/updates/v1_68#_notebook-editor-api">原文</a>。</li>
<li>基于时间轴视图的扩展激活：<code>onView:timeline</code>，参见：<a href="https://code.visualstudio.com/updates/v1_68#_extension-activation-based-on-timeline-view">原文</a>。</li>
<li>文档：<a href="https://code.visualstudio.com/api/ux-guidelines">用户体验指南</a> 更新。</li>
<li>扩展打赏（赞助），<code>package.json</code> 添加 <code>sponsor</code> 字段。</li>
</ul>

<h2 id="提议的-api-proposed-apis">提议的 API (Proposed APIs#)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_68#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>不允许直接 push 到 main 分支（惊了，竟然之前一致可以 push）。</li>
<li>VS Code OSS build，参见：<a href="https://code.visualstudio.com/updates/v1_68#_vs-code-oss-build">原文</a>。</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>重新制作了 <a href="https://code.visualstudio.com/docs/introvideos/versioncontrol">Using Git with Visual Studio</a> 视频。</li>
<li>vscode.dev 链接添加到 <a href="https://code.visualstudio.com/download">VSCode 官网下载页面</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.69 (2022-06) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_69_2022-06/</link><pubDate>Sat, 23 Jul 2022 20:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_69_2022-06/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_69">https://code.visualstudio.com/updates/v1_69</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>VS Code Server (private preview)，参考该<a href="https://code.visualstudio.com/blogs/2022/07/07/vscode-server#_getting-started">博客</a>申请使用，了解该特性的细节，参见：<a href="#vs-code-server-private-preview">下文</a>。</li>
<li>3 步 merge 编辑器，通过 <code>git.mergeEditor</code> 配置项可以开启此功能。</li>
<li>标题栏命令中心，通过 <code>window.commandCenter</code> 配置项可以开启此功能。</li>
<li>Share 菜单，通过 文件 -&gt; 分享，可以复制 vscode.dev URL。</li>
<li>Shell 集成特性，稳定发布，下个版本将默认开启。参见：<a href="#终端-terminal">下文</a>。</li>
<li>调试

<ul>
<li>在一行里面有多个嵌套的函数调用时，通过 <code>cmd + f11</code> 等方式，可以快速选择要进入的函数。</li>
<li>同时调试多个程序时，cmd + p，输入 <code>debug consoles</code> 可以快速在调试控制台（调试会话）间切换。</li>
</ul></li>
<li>重构预览，通过 <code>&gt;Refactor with Preview...</code> 命令可以对重构进行预览。</li>
<li>拖动多行评论，按住评论 + 按钮可以拖动以添加多行评论。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>3 步 merge 编辑器，通过 <code>git.mergeEditor</code> 配置项可以开启此功能。</p>

<ul>
<li><p>可以通过上方两个窗口冲突点左侧的复选框，来选择更改。</p>

<p><img src="/image/vscode/merge-editor.gif" alt="image" /></p></li>

<li><p>也可以直接在下方窗口直接编辑内容。</p>

<p><img src="/image/vscode/merge-editor2.gif" alt="image" /></p></li>

<li><p>关闭合并编辑器或接受合并时，如果未解决所有冲突，则会显示警告。</p></li>

<li><p>上方窗口的复选框如果都选中，则先用用一个勾的，在应用二个勾的，乐意右键复选框，交换顺序。</p>

<p><img src="/image/vscode/merge-editor3.gif" alt="image" /></p></li>
</ul></li>

<li><p>标题栏命令中心，通过 <code>window.commandCenter</code> 配置项可以开启此功能。</p>

<p><img src="/image/vscode/cc-polish.png" alt="image" /></p></li>

<li><p>设置编辑器修改指示器。Hover 配置项右侧的修改指示器，可以看到当前配置的修改位置。</p>

<p><img src="/image/vscode/settings-editor-new-indicators.gif" alt="image" /></p></li>

<li><p>勿扰模式，点击右下角铃铛可以关闭通知。</p>

<p><img src="/image/vscode/do-not-disturb.jpeg" alt="image" /></p></li>

<li><p>在浅色和深色主题之间切换。</p>

<ul>
<li>通过 <code>&gt;Preferences: Toggle between Light/Dark Themes</code> 命令可以切换浅色/深色主题。</li>
<li>添加如下配置项，已配置默认的浅色/深色主题。

<ul>
<li><code>workbench.preferredDarkColorTheme</code></li>
<li><code>workbench.preferredLightColorTheme</code></li>
<li><code>workbench.preferredHighContrastColorTheme</code></li>
<li><code>workbench.preferredHighContrastLightColorTheme</code></li>
</ul></li>
</ul></li>

<li><p>小地图上下文菜单，右键编辑器右侧小地图可以快速设置小地图的样式。</p>

<p><img src="/image/vscode/minimap-context-menu.png" alt="image" /></p></li>

<li><p>Share 菜单，通过 文件 -&gt; 分享，可以复制 vscode.dev URL。</p>

<p><img src="/image/vscode/share-vscode-dev-link.gif" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>Shell 集成特性，可以通过 <code>terminal.integrated.shellIntegration.enabled</code>  配置项开启（将在下一个版本 1.70 正式默认启用），特性如下所示：</p>

<ul>
<li><p>在终端左侧展示可交互的装饰，单击可以做快速复制等操作；滚动条上显示标尺。可以通过如下配置项配置：</p>

<ul>
<li><code>terminal.integrated.shellIntegration.decorationIcon</code></li>
<li><code>terminal.integrated.shellIntegration.decorationIconSuccess</code></li>
<li><code>terminal.integrated.shellIntegration.decorationIconError</code></li>
</ul>

<p><img src="/image/vscode/terminal-si-decorations.png" alt="image" /></p>

<p><img src="/image/vscode/terminal-si-decoration-menu.png" alt="image" /></p></li>

<li><p>命令导航</p>

<ul>
<li><p>通过 <code>Ctrl/Cmd+Up</code> 和 <code>Ctrl/Cmd+Down</code> 可以快速定位到上一条/下一条命令。通过 <code>Ctrl/Cmd+Shift+Up</code> 和 <code>Ctrl/Cmd+Shift+Down</code> 快速从当前位置向上/向下快速选中输出。</p>

<p><img src="/image/vscode/terminal-si-command-nav.gif" alt="image" /></p></li>

<li><p>通过 <code>&gt;Terminal: Run Recent Command</code> 命令可以快速运行最近的命令。</p>

<p><img src="/image/vscode/terminal-si-recent-command.png" alt="image" /></p>

<ul>
<li>通过列表的剪切板图标键输出打开到一个编辑器中。</li>
<li>按住 Alt 将文本写入终端而不运行它。</li>
<li>前一个会话部分中存储的历史数量由 <code>terminal.integrated.shellIntegration.history</code> 设置决定。</li>

<li><p>目前默认情况下没有为 <code>&gt;Terminal: Run Recent Command</code> 分配键绑定，但作为示例，它可以通过以下键绑定连接到 Ctrl+Space：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;ctrl+space&#34;</span>,
    <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;workbench.action.terminal.runRecentCommand&#34;</span>,
    <span style="color:#f92672">&#34;when&#34;</span>: <span style="color:#e6db74">&#34;terminalFocus&#34;</span>
}<span style="color:#960050;background-color:#1e0010">,</span></code></pre></div></li>
</ul></li>
</ul></li>

<li><p>转到最近的目录，通过 <code>&gt;Terminal: Go to Recent Directory</code> 可以快速切换到历史的路径中。</p>

<p><img src="/image/vscode/terminal-si-go-to-dir.gif" alt="image" /></p>

<ul>
<li>可以按住 Alt 将文本写入终端而不运行它。</li>
</ul></li>

<li><p>当前工作目录检测</p></li>

<li><p>目前该能力在 powerlevel10k 上支持仍不太好。</p></li>
</ul></li>

<li><p>SetMark 序列支持，可以通过 <code>echo -e 'Mark this line\x1b]1337;SetMark\x07'</code> 在滚动条上打一个标记。</p></li>

<li><p>对简单 Powerline 字体，进行自定义渲染。可以在不安装 patched 字体的情况下渲染。</p></li>

<li><p>来自 VS Code 的消息的采用一致的格式</p></li>

<li><p>可访问性改进，参见：<a href="https://code.visualstudio.com/updates/v1_69#_accessibility-improvements">原文</a>。</p></li>

<li><p>流程重新连接并恢复改进，参见：<a href="https://code.visualstudio.com/updates/v1_69#_process-reconnection-and-revive-improvements">原文</a>。</p></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>装饰器，参见上文终端集成，更多参见：<a href="https://code.visualstudio.com/updates/v1_69#_decorations">原文</a>。</li>

<li><p>图标和颜色支持（将 kind 属性设置为 test 的任务默认使用烧杯图标。）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;test&#34;</span>,
<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
<span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;echo test&#34;</span>,
<span style="color:#f92672">&#34;icon&#34;</span>: { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;light-bulb&#34;</span>, <span style="color:#f92672">&#34;color&#34;</span>: <span style="color:#e6db74">&#34;terminal.ansiBlue&#34;</span> }
}</code></pre></div></li>
</ul>

<h2 id="源代码控制-source-control">源代码控制 (Source Control)</h2>

<ul>
<li><p>Commit &ldquo;action button&rdquo; for Git repositories</p>

<ul>
<li><code>git.postCommitCommand</code> 配置 commit 之后的操作。</li>
<li><code>git.showActionButton</code> 配置是否可以在源代码管理视图中显示操作按钮有哪些。</li>
<li><code>scm.showActionButton</code> 配置是否展示操作按钮。</li>
</ul>

<p><img src="/image/vscode/scm-commit-action-button.gif" alt="image" /></p></li>

<li><p>使用编辑器编写提交消息</p>

<ul>
<li><code>git.useEditorAsCommitInput</code> 配置开启通过 UI commit 时，是否在编辑器里面编写提交消息。</li>
<li><code>git.terminalGitEditor</code> 配置在终端里面 commit，是否在编辑器里面编写提交消息（需开启集成终端）。</li>
</ul>

<p><img src="/image/vscode/scm-git-editor.gif" alt="image" /></p></li>

<li><p>分支保护指标，通过 <code>git.branchProtection</code> 配置保护的分支。保护后，状态栏左下角的分支将添加一个锁标识。</p>

<p><img src="/image/vscode/scm-branch-protection-statusbar.png" alt="image" /></p></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>优化单步进入目标 UI

<ul>
<li>在一行里面有多个嵌套的函数调用时，可以通过右击某个函数名单步进入特定的目标函数</li>
<li>通过 cmd + f11 执行 <code>&gt;Debug: Step Into Target</code> 命令可以选择要进入的函数。</li>
</ul></li>

<li><p>有如下方式，可以快速在调试控制台间切换。</p>

<ul>
<li><p>通过 cmd + p，输入 <code>debug consoles</code> 可以浏览所有调试会话（通过 <code>?</code> 可以快速输入）。</p>

<p><img src="/image/vscode/goto-debug-quickaccess.gif" alt="image" /></p></li>

<li><p>通过 <code>&gt;Debug: Select Debug Console</code> 命令切换会话（目前存在翻译 bug，中文语言下为 <code>&gt;调试：调试控制台</code>）。</p></li>

<li><p>通过视图菜单访问控制台（目前存在翻译 bug，中文语言为 <code>view 调试控制台</code>）。</p>

<p><img src="/image/vscode/debug-view-menu.gif" alt="image" /></p></li>

<li><p>当焦点在调试控制台时，可以通过 <code>⇧⌘[</code> 和 <code>⇧⌘]</code> 快速切换调试会话。</p></li>
</ul></li>

<li><p>Loaded Scripts search and filtering，参见：<a href="https://code.visualstudio.com/updates/v1_69#_loaded-scripts-search-and-filtering">原文</a>。</p></li>

<li><p>JavaScript 调试</p>

<ul>
<li>通过 Call Stack 视图的指南针按钮可以切换到编译后的 js 代码，断点仍然有效。</li>

<li><p>通过调试条的指南针按钮快速切换到 ts 源码，断点仍然有效。</p>

<p><img src="/image/vscode/js-debug-toggle-sourcemaps.gif" alt="image" /></p></li>

<li><p>如果变量定义了 toString 方法，将在 VARIABLES 视图中显示 toString 的结果。</p>

<p><img src="/image/vscode/js-debug-custom-tostring.png" alt="image" /></p></li>

<li><p>JavaScript 调试器已率先支持上文提到单步进入目标。</p></li>

<li><p>JavaScript 调试中的未绑定断点警告图标。</p>

<p><img src="/image/vscode/bp-hover.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>重构预览，通过 <code>&gt;Refactor with Preview...</code> 命令可以对重构进行预览，点击变更的文件将打开 diff 编辑器。</p>

<p><img src="/image/vscode/refactor-preview.png" alt="image" />
<img src="/image/vscode/refactoring-editor.gif" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Markdown 允许您使用尖括号来编写包含空格或其他特殊字符的链接目标，如：<code>[Some link](&lt;path to file with spaces.md&gt;)</code></li>

<li><p>Emmet 添加 <code>emmet.useInlineCompletions</code> 设置配置可开启内联完成（下图关闭了 <code>editor.quickSuggestions</code>）。</p>

<p><img src="/image/vscode/emmet-inline-html.gif" alt="image" /></p></li>

<li><p>语言指示器中的 JSON 通知，当需要显示的折叠范围、文档符号或颜色装饰器过多时，VS Code 不再使用通知，而是使用 JSON 语言指示器来通知用户。</p></li>

<li><p>HTML 的 <code>html.format.endWithNewline</code> 配置项被移除，使用如下方式替代：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
&#34;[html]&#34;: {
    &#34;files.insertFinalNewline&#34;: true
}
}</pre></div></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_69#_support-for-innotebook-extension-recommendations">原文</a>。</p>

<h2 id="评论-comments">评论 (Comments)</h2>

<ul>
<li>新命令 <code>&gt;Comments: Toggle Editor Commenting</code> 切换所有编辑器评论功能，包括评论范围装订线装饰、在线悬停的 + 和所有编辑器评论小部件。在 Zen 模式下，评论将自动禁用。</li>

<li><p>拖动多行评论，按住评论 + 按钮可以拖动以添加多行评论。</p>

<p><img src="/image/vscode/drag-for-comment.gif" alt="image" /></p></li>
</ul>

<h2 id="企业-enterprise">企业 (Enterprise)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_69#_updatemode-group-policy-on-windows">原文</a>。</p>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li><p>主题测试器支持内置主题。如 <code>https://vscode.dev/theme/github.github-vscode-theme/GitHub%20Dark%20Default</code></p>

<p><img src="/image/vscode/theme-tester-marketplace.png" alt="image" /></p></li>

<li><p>对扩展的部分本地化支持，参见：<a href="https://code.visualstudio.com/updates/v1_69#_partial-localization-support-for-extensions">原文</a>。</p></li>

<li><p>配置显示语言命令，支持通过 <code>&gt;Configure Display Language</code> 命令来覆盖浏览器的默认配置。此外，可以使用 <code>&gt;Clear Display Language Preference</code> 命令删除此覆盖。</p></li>
</ul>

<h2 id="vs-code-server-private-preview">VS Code Server (private preview)</h2>

<p>VS Code Server (私有预览)，该特性按照原文的描述，最终目标可能是：在任意一台设备安装了 VSCode 后，可以 VSCode 可以通过 Server 模式启动 VSCode，只要保证这台设备联网。此时，通过微软的后端服务（后端）。可以通过在任意浏览器中通过 <code>vscode.dev</code>，连接到这台设备的 VSCode（不需要安装客户端，体验上和本地基本一致）。</p>

<p>可以简单的理解为，VSCode 提供了类似一种远程桌面的服务。在实原理上，微软的后端服务，提供了一套用户鉴权和内网穿透（可选）的能力。</p>

<p>下图是网络层面，对其实现原理的推测。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                         |                          |
      (用户内网)          |                          |       (用户内网)
        用户设备          |          机房             |        任意设备
   VS Code Server ----(互联网)---&gt; 微软的后端服务 &lt;--(互联网)-- 浏览器
                         |                          |
                         |                          |
                       内网穿透                  websocket
                       (如frp)                         </pre></div>
<p>现阶段，应该还在开发阶段，需要提交申请试用，更多参见：<a href="https://code.visualstudio.com/updates/v1_69#_vs-code-server-private-preview">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>ESLint 提供对 notebook 的检查。</p>

<p><img src="/image/vscode/eslint.png" alt="image" /></p></li>

<li><p>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_69#_jupyter">原文</a>。</p></li>

<li><p>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_69#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>TypeScript 4.8 支持，安装 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-next">TypeScript Nightly</a> 扩展获取支持。</li>
<li>Markdown 链接验证，参见：<a href="https://code.visualstudio.com/updates/v1_69#_markdown-link-validation">原文</a>。</li>
<li>Settings Profiles，参见：<a href="https://code.visualstudio.com/updates/v1_69#_settings-profiles">原文</a>。</li>
<li>Access edit sessions across VS Code for the Web and desktop，参见：<a href="https://code.visualstudio.com/updates/v1_69#_access-edit-sessions-across-vs-code-for-the-web-and-desktop">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Iterable vscode.d.ts collection types，参见：<a href="https://code.visualstudio.com/updates/v1_69#_iterable-vscodedts-collection-types">原文</a>。</li>
<li>Extensible notebook renderers，参见：<a href="https://code.visualstudio.com/updates/v1_69#_iterable-vscodedts-collection-types">原文</a>。</li>
<li>Read external files from DataTransfers，参见：<a href="https://code.visualstudio.com/updates/v1_69#_read-external-files-from-datatransfers">原文</a>。</li>
<li>High contrast light in webviews，参见：<a href="https://code.visualstudio.com/updates/v1_69#_high-contrast-light-in-webviews">原文</a>。</li>
<li>Icons in Test Item Labels，参见：<a href="https://code.visualstudio.com/updates/v1_69#_icons-in-test-item-labels">原文</a>。</li>
<li>Source Control input box enablement，参见：<a href="https://code.visualstudio.com/updates/v1_69#_source-control-input-box-enablement">原文</a>。</li>
<li>JSON word pattern change，参见：<a href="https://code.visualstudio.com/updates/v1_69#_json-word-pattern-change">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_69#_debug-adapter-protocol">原文</a>。</p>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_69#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 18 （Chromium <code>100.0.4896.160</code> 和 Node.js <code>16.13.2</code>）</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<p>TypeScript <a href="https://code.visualstudio.com/docs/typescript/typescript-editing">编辑</a>和<a href="https://code.visualstudio.com/docs/typescript/typescript-refactoring">重构</a>。</p>
]]></description></item><item><title>VSCode 1.70 (2022-07) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_70_2022-07/</link><pubDate>Sat, 20 Aug 2022 15:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_70_2022-07/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_70">https://code.visualstudio.com/updates/v1_70</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>在没有语言服务的情况下，可以使用  <code>&gt;Create Folding Range from Selection</code> (<code>⌘K ⌘,</code>) 命令手动折叠选中区域。通过 <code>Remove Manual Folding Ranges</code> (<code>⌘K ⌘.</code>)，可以删除该区域之前创建的折叠范围。</p>

<p><img src="/image/vscode/manual-folding-range.gif" alt="image" /></p></li>

<li><p>支持将 VSCode 配置为 git 的默认冲突处理编辑器，配置参见下文。</p></li>

<li><p>搜索结果，支持多选。</p>

<p><img src="/image/vscode/search-multiselect.gif" alt="image" /></p></li>

<li><p>树查找控件，通过 <code>cmd+f</code> 触发。</p>

<p><img src="/image/vscode/tree-filter.gif" alt="image" /></p></li>

<li><p>调试会话选择器（<code>&gt;Debug: Select Debug Session</code>），通过该命令可以快速选择启动的调试会话，参见：<a href="https://code.visualstudio.com/updates/v1_70#_picker-for-debug-sessions">原文</a>。</p>

<p><img src="/image/vscode/debug-sessions.gif" alt="image" /></p></li>

<li><p>【预览特性】 编辑器粘性滚动，通过 <code>editor.experimental.stickyScroll.enabled</code> 开启。</p>

<p><img src="/image/vscode/sticky-scroll.gif" alt="image" /></p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>更易用的标题栏自定义，右击标题栏可以快速显示隐藏相关模块。</p>

<p><img src="/image/vscode/title-bar-context-menu.png" alt="image" /></p></li>

<li><p>当窗口宽度变小时，标题栏的菜单项会被折叠起来。</p>

<p><img src="/image/vscode/menu-bar-folding.gif" alt="image" /></p></li>

<li><p>缩放时，标题栏也会跟着缩放。</p>

<p><img src="/image/vscode/macos-title-bar-zooming.gif" alt="image" /></p></li>

<li><p>选中一段文本后，通过 <code>&gt;Create Folding Range from Selection</code> (<code>⌘K ⌘,</code>)，可以手动折叠这一区域。光标位于手动折叠区域时，通过 <code>Remove Manual Folding Ranges</code> (<code>⌘K ⌘.</code>)，可以删除该区域之前创建的折叠范围（手动折叠范围在没有编程语言支持折叠的情况下特别有用）。</p>

<p><img src="/image/vscode/manual-folding-range.gif" alt="image" /></p></li>

<li><p>保留已折叠的折叠范围，以下图为例，63 行已被折叠，即使注释掉，63 行的折叠仍然保留。</p>

<p><img src="/image/vscode/restored-folding-ranges.gif" alt="image" /></p></li>

<li><p>隐藏折叠控件，可以通过 <code>&quot;editor.showFoldingControls&quot;: &quot;never&quot;</code> 配置，默认 hover 显示。</p></li>

<li><p>三步 merge 编辑器提升，已配置成默认选项。</p></li>

<li><p>通过命令行调起三步 merge 编辑器</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">-m --merge &lt;path1&gt; &lt;path2&gt; &lt;base&gt; &lt;result&gt; Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions, and the output file to save merge results.</pre></div>
<p>可以在 .gitconfig 中配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[merge]
tool = code
[mergetool &#34;code&#34;]
cmd = code --wait --merge $REMOTE $LOCAL $BASE $MERGED</pre></div></li>

<li><p>搜索，在文件名的右侧会显示 git 状态和命中数目的标签装饰。</p>

<p><img src="/image/vscode/search-decorations.png" alt="image" /></p></li>

<li><p>搜索结果，支持多选。</p>

<p><img src="/image/vscode/search-multiselect.gif" alt="image" /></p></li>

<li><p>树查找控件，通过 <code>cmd+f</code> 触发。</p>

<p><img src="/image/vscode/tree-filter.gif" alt="image" /></p></li>

<li><p>新建文件 <code>File &gt; New File...</code> 支持在选择文件类型时输入文件名。</p>

<p><img src="/image/vscode/new-file.png" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>Shell 集成已默认启用。

<ul>
<li>如果没有启用，可以参见 <a href="https://code.visualstudio.com/docs/terminal/shell-integration#_manual-installation">Manual installation</a> 进行手动安装（<code>powerlevel10k</code> 需要导出 <code>export ITERM_SHELL_INTEGRATION_INSTALLED=Yes</code> 环境变量）。</li>
<li>可以通过 <code>&quot;terminal.integrated.shellIntegration.enabled&quot;: &quot;false&quot;</code> 配置项禁用该能力。</li>
<li>关于集成终端，更多参见：<a href="https://code.visualstudio.com/docs/terminal/shell-integration">官方文档</a>。</li>
<li>通过 <code>terminal.integrated.shellIntegration.decorationsEnabled</code> 配置集成终端装饰符的效果。</li>
</ul></li>
<li>通过 <code>terminal.integrated.tabs.defaultIcon</code> 和 <code>terminal.integrated.tabs.defaultColor</code> 配置集成终端选项卡的颜色和图标。</li>
<li>扩展的 PowerShell 键绑定，<a href="https://code.visualstudio.com/updates/v1_70#_extended-powershell-keybindings">略</a>。</li>
<li><code>&gt;Terminal: run recent command</code> 命令可以跨 shell 搜索历史命令，用来代替 ctrl + r 的 shell 搜索功能。</li>
<li>其他终端集成提升，参见：<a href="https://code.visualstudio.com/updates/v1_70#_other-shell-integration-improvements">原文</a>。</li>
<li>渲染改进，参见：<a href="https://code.visualstudio.com/updates/v1_70#_rendering-improvements">原文</a>。</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>操作按钮优化

<ul>
<li>仅当高度确定本地分支位于远程跟踪分支之前或之后时，才会显示 同步更改 操作按钮。</li>
<li>提交操作按钮仅根据更改的文件列表，以及如下配置决定：

<ul>
<li><code>git.enableSmartCommit</code></li>
<li><code>git.suggestSmartCommit</code></li>
<li><code>git.smartCommitChanges</code></li>
</ul></li>
<li>提交操作按钮图标根据分支保护设置更新

<ul>
<li><code>git.branchProtection</code></li>
<li><code>git.branchProtectionPrompt</code></li>
</ul></li>
<li>改进了 rebase 冲突解决</li>
<li>提交消息添加拼写检查。</li>
</ul></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>JavaScript 调试，参见：<a href="https://code.visualstudio.com/updates/v1_70#_javascript-debugging">原文</a>。</li>

<li><p>调试会话选择器（<code>&gt;Debug: Select Debug Session</code>），通过该命令可以快速选择启动的调试会话，参见：<a href="https://code.visualstudio.com/updates/v1_70#_picker-for-debug-sessions">原文</a>。</p>

<p><img src="/image/vscode/debug-sessions.gif" alt="image" /></p></li>
</ul>

<h2 id="任务-debugging">任务 (Debugging)</h2>

<ul>
<li><code>&gt;Tasks: Run Task</code> (<code>workbench.action.tasks.runTask</code>) 命令支持按照任务名和类型过滤。</li>
<li>改进自动任务流程，通过 <code>task.allowAutomaticTasks</code> 配置。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>格式化 JSON 时保留换行符，通过 <code>json.format.keepLines</code> 配置项配置。</li>
<li>Notebooks，参见：<a href="https://code.visualstudio.com/updates/v1_70#_notebooks">原文</a>。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>设置显示语言，通过语言包扩展详情页的 <code>Set Display Language</code> 按钮配置显示语言，也可以通过 <code>Clear Display Language</code> 按钮删除语言设置。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python，参见：<a href="https://code.visualstudio.com/updates/v1_70#_python">原文</a>。</li>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_70#_jupyter">原文</a>。</li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_70#_jupyter">原文</a>。</li>
<li>Remote Development，参见：<a href="https://code.visualstudio.com/updates/v1_70#_jupyter">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li><p>编辑器粘性滚动，通过 <code>editor.experimental.stickyScroll.enabled</code> 开启。</p>

<p><img src="/image/vscode/sticky-scroll.gif" alt="image" /></p></li>

<li><p>TypeScript 4.8 支持。</p></li>

<li><p>Settings Profiles，参见：<a href="https://code.visualstudio.com/updates/v1_70#_settings-profiles">原文</a>。</p></li>

<li><p>任务重连，在窗口重新加载时，可以通过启用 task.experimental.reconnection 重新连接监视任务，这可以在扩展更改或 VS Code 版本更新后更快地恢复工作。</p></li>

<li><p>Code Actions，参见：<a href="https://code.visualstudio.com/updates/v1_70#_code-actions">原文</a>。</p></li>

<li><p>Edit Sessions，参见：<a href="https://code.visualstudio.com/updates/v1_70#_edit-sessions-across-vs-code-for-the-web-and-desktop">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>快捷键条件表达式 <code>when</code> 支持 <code>not in</code></li>
<li>添加 <code>htmlLanguageParticipants</code> 贡献点，极大地简化了 html 模板语言相关的扩展开发，参见：<a href="https://code.visualstudio.com/updates/v1_70#_htmllanguageparticipants-contribution-point">原文</a>。</li>

<li><p>拖拽到编辑器 API，更多参见：<a href="https://github.com/microsoft/vscode-extension-samples/tree/main/drop-on-document">example</a>。</p>

<p><img src="/image/vscode/api-drop.gif" alt="image" /></p></li>
</ul>

<h2 id="提议的-api-proposed-apis">提议的 API (Proposed APIs)</h2>

<ul>
<li>Webview 上下文菜单。</li>
<li>视图大小（占用的高度）。</li>
<li>可扩展的 HTML notebook 渲染器。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_70#_proposed-apis">原文</a>。</p>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_70#_debug-adapter-protocol">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Electron 沙箱支持的进展。</li>
<li>新的 Markdown 语言服务器，运行在独立的进程中。</li>
<li>Debian 软件包依赖项。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_70#_debian-package-dependencies">原文</a>。</p>

<h2 id="文档和扩展-documentation-and-extensions">文档和扩展 (Documentation and extensions)</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/remote/devcontainer-cli">开发容器 CLI 文档</a>。</li>
<li>Azure Developer CLI (azd)，参见：<a href="https://code.visualstudio.com/updates/v1_70#_azure-developer-cli-azd">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.71 (2022-08) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_71_2022-08/</link><pubDate>Sat, 17 Sep 2022 19:34:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_71_2022-08/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_71">https://code.visualstudio.com/updates/v1_71</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>FFmpeg 编解码器支持，Webview 支持了视频播放了。</p>

<p><img src="/image/vscode/codec.gif" alt="image" /></p></li>

<li><p>资源管理器重命名改进。进入重命名编辑操作时，可以按 F2 在选中名字、扩展名、全部名之间切换选中。</p>

<p><img src="/image/vscode/renameToggle.gif" alt="image" /></p></li>

<li><p>树视图展开模式，通过 <code>workbench.tree.expandMode</code> 配置项，可以控制展开树节点是单击展开还是双击展开。</p></li>

<li><p>粘性滚动，已去除实验性，可通过 <code>editor.stickyScroll.enabled</code> 配置项开启。</p></li>

<li><p>新的 Code Action 控件，取代之前简单的菜单，以实现更丰富的信息展示。</p>

<p><img src="/image/vscode/code-action-widget.png" alt="image" /></p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>合并编辑器提升</p>

<ul>
<li><p>在有冲突的文件中快速打开合并编辑器。</p>

<p><img src="/image/vscode/mergeHint.gif" alt="image" /></p></li>

<li><p>在打开文件时，不会修改文件内容。</p></li>

<li><p>通过编辑器右上角图标，可以快速打开旧版编辑器。</p>

<p><img src="/image/vscode/merge-editor-open-file-old-decorators.gif" alt="image" /></p></li>

<li><p>新旧编辑器的操作可以相互感知。</p>

<p><img src="/image/vscode/merge-editor-side-by-side.gif" alt="image" /></p></li>

<li><p>复选框改进，是其更加明确。</p></li>

<li><p>带来实验性的 diff 算法，可以通过 <code>&quot;mergeEditor.diffAlgorithm&quot;: &quot;experimental&quot;,</code> 开启。</p></li>
</ul></li>

<li><p>FFmpeg 编解码器支持，Webview 终于支持了视频播放了。</p>

<p><img src="/image/vscode/codec.gif" alt="image" /></p></li>

<li><p>资源管理器重命名改进。进入重命名编辑操作时，可以按 F2 在选中名字、扩展名、全部名之间切换选中。</p>

<p><img src="/image/vscode/renameToggle.gif" alt="image" /></p></li>

<li><p>按钮图标风格改为圆角。</p>

<p><img src="/image/vscode/rounded-buttons.png" alt="image" /></p></li>

<li><p>支持 Windows 上的窗口控件。</p>

<p><img src="/image/vscode/windows-snap-layout.png" alt="image" /></p></li>

<li><p>树视图展开模式，通过 <code>workbench.tree.expandMode</code> 配置项，可以控制展开树节点是单击展开还是双击展开。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>粘性滚动，已去除实验性，可通过 <code>editor.stickyScroll.enabled</code> 配置项开启。</p>

<ul>
<li><code>editor.stickyScroll.maxLineCount</code> 通过</li>
<li>可以通过 <code>Ctrl/Cmd + Click</code> 跳转到定义。</li>
</ul>

<p><img src="/image/vscode/sticky-scroll-ctrlclick.gif" alt="image" /></p></li>

<li><p>新的 Code Action 控件，取代之前简单的菜单，以实现更丰富的信息展示。</p>

<p><img src="/image/vscode/code-action-widget.png" alt="image" /></p></li>

<li><p>添加建议匹配配置 <code>editor.suggest.matchOnWordStartOnly</code>，默认为 true，表示只展示以该单词开头的建议项，关闭会导致匹配到的项目更多。</p>

<p><img src="/image/vscode/suggestMatchWordStart.gif" alt="image" /></p></li>
</ul>

<h2 id="源码版本控制-source-control">源码版本控制 (Source Control)</h2>

<ul>
<li>提交按钮行为提升

<ul>
<li><code>git.postCommitCommand</code> 配置成功提交后运行 git 命令。</li>
<li><code>git.rememberPostCommitCommand</code> 记住每个存储库最后执行的辅助操作。</li>
</ul></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>Shell 集成改进

<ul>
<li>支持 Fish，配置参见：<a href="https://code.visualstudio.com/docs/terminal/shell-integration#_manual-installation">文档</a>。</li>
<li>Windows 平台的 Git bash，配置参见：<a href="https://code.visualstudio.com/docs/terminal/shell-integration#_manual-installation">文档</a>。</li>
<li>Support for common alternative current working directory sequences: <code>OSC 6 ; scheme://&lt;cwd&gt; ST, OSC 1337 ; CurrentDir=&lt;cwd&gt; ST, OSC 9 ; 9 ; &lt;cwd&gt; ST</code></li>
<li>更好地处理各种 Shell 集成的<a href="https://github.com/microsoft/vscode/issues?q=is%3Aissue+assignee%3ATyriar+milestone%3A%22August+2022%22+is%3Aclosed+label%3Aterminal-shell-integration+label%3Abug">边缘 Case</a>。</li>
</ul></li>
<li>终端通过 <code>&quot;terminal.integrated.smoothScrolling&quot;: true</code> 配置项，开启平滑滚动。</li>

<li><p>支持 <a href="https://sw.kovidgoyal.net/kitty/underlines/">kitty</a> 终端首创的下划线样式和颜色的转移字符。</p>

<p><img src="/image/vscode/terminal-underlines.png" alt="image" /></p></li>

<li><p>改进渲染质量。</p>

<ul>
<li><p>修复放大缩小导致终端模糊的<a href="https://github.com/microsoft/vscode/issues/85154">问题</a>。</p>

<p><img src="/image/vscode/terminal-blurry.png" alt="image" /></p></li>

<li><p>当启用最小对比度并且需要翻转文本亮度以确保满足比率时，现在将保留文本的色调。</p>

<p><img src="/image/vscode/terminal-mcr-flip.png" alt="image" /></p></li>

<li><p>新的主题色配置 <code>terminal.inactiveSelectionBackground</code> 可以以实现焦点所在窗口的突出显示。</p>

<p><img src="/image/vscode/terminal-inactive.png" alt="image" /></p></li>

<li><p>自定义电力线字形渲染改进了边缘裁剪。这在半圆形字符上最为明显，现在应该是一条平滑的曲线。</p>

<p><img src="/image/vscode/terminal-powerline-clip.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>监视任务现在会在窗口重新加载时重新连接，从而在 VS Code 更新或扩展的状态更改时实现不间断的工作。默认情况下启用任务重新连接，但可以使用 <code>task.reconnection</code> 设置禁用。</li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li><code>launch.json</code> 添加 <code>suppressMultipleSessionWarning</code> 选项，来禁用多次启动同一个配置的警告。</li>
</ul>

<h2 id="评论-comments">评论 (Comments)</h2>

<ul>
<li><p>过滤，评论视图有一个新的过滤器，可以在其中按评论文本和已解决/未解决状态进行过滤。</p>

<p><img src="/image/vscode/comments-filtering.gif" alt="image" /></p></li>

<li><p>编辑器装饰，评论编辑器装订线装饰现在使用 <code>codicons</code> 并具有新样式。</p>

<p><img src="/image/vscode/comments-editor-decoration.gif" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>支持 TypeScript 4.8</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>

<ul>
<li>支持将图片以 markdown 语法粘贴到 Notebook 中。</li>
<li>使用 Pylance 改进了 JUPYTER 笔记本的智能感知</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_71#_jupyter">原文</a>。</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server">Live Preview</a>

<ul>
<li>支持多工作空间。</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_71#_live-preview">原文</a>。</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>，参见：<a href="https://code.visualstudio.com/updates/v1_71#_github-pull-requests-and-issues">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote Development</a>，参见：原文。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li><p>文件移动和重命名的 Markdown 链接对应更新，可通过 <code>markdown.experimental.updateLinksOnFileMove.enabled</code> 配置项体验。</p>

<p><img src="/image/vscode/md-link-update.gif" alt="image" /></p></li>

<li><p>Settings Profiles，更多参见：<a href="https://code.visualstudio.com/updates/v1_71#_settings-profiles">原文</a>。</p></li>

<li><p>在 vscode.dev 等远程开发的内容可以将未提交的内容保存在一个 Edit Session 中，在其他的设备或者环境中，可以通过 <code>Continue Working On</code> 按钮快速恢复，通过 <code>&quot;workbench.experimental.editSessions.enabled&quot;：true</code>，启用设置同步，然后在 Web 或桌面的 VS Code 中运行 <code>Edit Sessions: Sign In</code> 命令，更多参见：<a href="https://code.visualstudio.com/updates/v1_71#_bring-your-changes-with-you-when-moving-across-development-environments">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>通过 <code>TerminalExitStatus.reason</code> API 可以获取终端退出原因。</li>
<li>枚举配置项支持 <code>enumItemLabels</code> 字段，以支持在设置编辑器中展示更好的描述。</li>
<li>新的快捷键绑定条件变量 <code>activeWebviewPanelId</code> 用来判断所在 Webview，如 <code>&quot;when&quot;: &quot;activeWebviewPanelId == 'markdown.preview'&quot;</code>。</li>
<li>TypeScript server plugins on web，参见：<a href="https://code.visualstudio.com/updates/v1_71#_typescript-server-plugins-on-web">原文</a>。</li>
<li>Disabled tree items，参见：<a href="https://code.visualstudio.com/updates/v1_71#_disabled-tree-items">原文</a>。</li>
<li>Markdown 支持重构为 Language Server，参见：<a href="https://code.visualstudio.com/updates/v1_71#_markdown-language-server">原文</a>。</li>
<li>Upcoming change to context of &lsquo;view/title&rsquo; menu，参见：<a href="https://code.visualstudio.com/updates/v1_71#_upcoming-change-to-context-of-viewtitle-menu">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<ul>
<li>Proposal for a &lsquo;startDebugging&rsquo; request，更多参见：<a href="https://code.visualstudio.com/updates/v1_71#_proposal-for-a-startdebugging-request">原文</a>。</li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 19。</li>
<li>不再支持 Windows 7。</li>
</ul>
]]></description></item><item><title>VSCode 1.72 (2022-09) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_72_2022-09/</link><pubDate>Sat, 15 Oct 2022 19:34:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_72_2022-09/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_72">https://code.visualstudio.com/updates/v1_72</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>编辑器工具栏图标支持隐藏操作</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Hide-actions-from-editor-toolbar.mp4" type="video/mp4">
</video></li>

<li><p>搜索结果支持以树状结构展示。</p></li>

<li><p>添加 <code>git.pullBeforeCheckout</code> 配置项，以实现在 checkout 前，先 pull。</p></li>

<li><p>添加 <code>Git: Abort Merge</code> 命令取消 merge。</p></li>

<li><p>开启终端集成后，终端支持类似 Quick Fix 的能力，并可以通过 <code>audioCues.terminalQuickFix</code> 启用音频提示，例子如下：</p>

<ul>
<li><p>git 类似命令提示。</p>

<p><img src="/image/vscode/quick-fix-similar.png" alt="image" /></p></li>

<li><p>git 设置上游。</p>

<p><img src="/image/vscode/quick-fix-push.png" alt="image" /></p></li>

<li><p>git 创建 PR。</p>

<p><img src="/image/vscode/quick-fix-create-pr.png" alt="image" /></p></li>

<li><p>端口冲突。</p>

<p><img src="/image/vscode/quick-fix-free-port.png" alt="image" /></p></li>
</ul></li>

<li><p>Markdown</p>

<ul>
<li>通过 <code>&quot;markdown.validate.enabled&quot;: true</code> 开启 Markdown 链接校验。</li>
<li>CodeAction 添加：

<ul>
<li>提取普通链接到引用链接格式。</li>
<li>组织引用链接：移动底部，并按首字母排序，删除未使用的链接。</li>
</ul></li>
</ul></li>

<li><p>新增一个 <a href="https://chrome.google.com/webstore/detail/vs-code/kobakmhnkfaghloikphojodjebdelppk">Chrome 浏览器扩展</a>，可以在地址栏快速通过 code 命令快速打开 github 仓库。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/code-in-your-browser-omnibox.mp4" type="video/mp4">
</video></li>

<li><p>本地、vscode.dev 等编辑的内容，可以通过 <code>Continue Working On</code> 命令在具有完整开发环境的 Codespace 中继续开发，未提交的代码将保存在 Editor Session 中，进入 Codespace 后将自动恢复这些未提交的代码</p></li>

<li><p>体验提升</p>

<ul>
<li>树控件查找组件 <code>cmd+f</code> 调出查找组件后，可以上下拖动其位置</li>
<li>树控件查找组件，重新打开时，树视图 Find 控件将记住最后一个搜索词。</li>
<li>编辑器选中后向上或者向下移动鼠标的滚动速度，离编辑器越远，滚动越快。</li>
<li>编辑器 hover 提升，让鼠标更容易进入 hover 弹窗。</li>
</ul></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>编辑器工具栏图标支持隐藏操作</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Hide-actions-from-editor-toolbar.mp4" type="video/mp4">
</video></li>

<li><p>Merge 编辑器变更</p>

<ul>
<li><p>默认不再启用（<code>git.mergeEditor</code> 默认值为 false），可以通过普通的编辑器的右下角按钮打开。</p>

<p><img src="/image/vscode/merge-editor-open-in-merge-editor.png" alt="image" /></p></li>

<li><p>Merge 冲突选择时，从复选框更改为 Codelen。</p>

<p><img src="/image/vscode/merge-editor-checkboxes-vs-codelens.drawio.png" alt="image" /></p></li>

<li><p>修改 Merge 编辑器行为：参见原文 RESULT FILE RECOMPUTED FROM SCRATCH 部分。</p></li>

<li><p>合并编辑器称为标准的视图。</p></li>
</ul></li>

<li><p>树控件查找组件提升：</p>

<ul>
<li><p><code>cmd+f</code> 调出查找组件后，可以上下拖动其位置。</p>

<p><img src="/image/vscode/tree-move.gif" alt="image" /></p></li>

<li><p>重新打开时，树视图 Find 控件将记住最后一个搜索词。</p>

<p><img src="/image/vscode/tree-remember.gif" alt="image" /></p></li>

<li><p>内置预览支持部分格式的播放音视频文件。</p></li>
</ul></li>

<li><p>可以通过 <code>explorer.incrementalNaming</code> 配置复制一个文件后存在冲突时修改文件名的规则。</p>

<ul>
<li><code>sample</code>：在文件名后添加 <code>copy</code> （默认值）。</li>
<li><code>smart</code>：末尾添加一个数字。</li>
<li><code>disable</code>：禁用，提示是否覆盖。</li>
</ul></li>

<li><p>当 VSCode 打开程序自身的文件时将展示提示警告。</p>

<p><img src="/image/vscode/editing-vscode-application-warning.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>选中后向上或者向下移动鼠标的滚动速度，离编辑器越远，滚动越快。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Selectio-autoscroll.mp4" type="video/mp4">
</video></li>

<li><p>hover 提升，让鼠标更容易进入 hover 弹窗。</p>

<ul>
<li>之前</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Editor-hover-before.mp4" type="video/mp4">
</video>

<ul>
<li>之后</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Editor-hover-after.mp4" type="video/mp4">
</video></li>

<li><p>Code Action</p>

<ul>
<li>新的分组：对应重构的类型 <code>refactor.inline</code> 和 <code>refactor.move</code>。</li>
<li>新的颜色配置：使用 <code>editorWidget.*</code> 而不是 <code>menu.*</code>。</li>
</ul></li>
</ul>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li><p>扩展窗口的 <code>@updates</code> 添加展示最近更新的扩展视图。</p>

<p><img src="/image/vscode/extensions-recently-updated.png" alt="image" /></p></li>

<li><p>默认列表将在前面优先展示关注的扩展（需要更新或者 reload ）。</p>

<p><img src="/image/vscode/extensions-require-attention.png" alt="image" /></p></li>

<li><p>扩展市场图标的数字徽章，表示需要关注的扩展的数目。</p>

<p><img src="/image/vscode/extensions-badge.png" alt="image" /></p></li>

<li><p>支持忽略扩展更新。</p>

<p><img src="/image/vscode/extensions-ignore-updates.png" alt="image" /></p></li>

<li><p>支持取消忽略已忽略的扩展更新。</p>

<p><img src="/image/vscode/extensions-undo-ignore-updates.png" alt="image" /></p></li>

<li><p>可以按安装计数、评级、名称、发布日期和更新日期对已安装扩展的列表进行排序。</p>

<p><img src="/image/vscode/extensions-filter-sort.png" alt="image" /></p></li>
</ul>

<h2 id="搜索-search">搜索 (Search)</h2>

<ul>
<li><p>搜索结果支持以树状结构展示。</p>

<p><img src="/image/vscode/search-tree-view.gif" alt="image" /></p></li>

<li><p>添加配置项，<code>search.decorations.badges</code> 和 <code>search.decorations.colors</code>，控制搜索文件项是否展示装饰和颜色，默认均开启。</p>

<p><img src="/image/vscode/search-file-decoration-settings.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>发现嵌套 git 仓库最大层数 <code>git.repositoryScanMaxDepth</code> 默认为 1 层。</li>
<li>对包含密码的 ssh key 的支持。</li>
<li>添加 <code>git.pullBeforeCheckout</code> 配置项，以实现在 checkout 前，先 pull。</li>
<li>改进仓库 fetch：对于包含多个 remotes 的仓库，fetch 将显示选择器，让用户选择 fetch 哪个 remote。</li>
<li>添加 <code>Git: Abort Merge</code> 命令取消 merge。</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>开启终端集成后，终端支持类似 Quick Fix 的能力，并可以通过 <code>audioCues.terminalQuickFix</code> 启用音频提示，例子如下：</p>

<ul>
<li><p>git 类似命令提示。</p>

<p><img src="/image/vscode/quick-fix-similar.png" alt="image" /></p></li>

<li><p>git 设置上游。</p>

<p><img src="/image/vscode/quick-fix-push.png" alt="image" /></p></li>

<li><p>git 创建 PR。</p>

<p><img src="/image/vscode/quick-fix-create-pr.png" alt="image" /></p></li>

<li><p>端口冲突。</p>

<p><img src="/image/vscode/quick-fix-free-port.png" alt="image" /></p></li>
</ul></li>

<li><p>终端集成提升，参见：<a href="https://code.visualstudio.com/updates/v1_72#_shell-integration-improvements">原文</a>。</p></li>

<li><p>终端支持展示超链接。</p>

<ul>
<li><p>执行 <code>printf '\e]8;;https://code.visualstudio.com\e\\VS Code\e]8;;\e\\'</code> 效果如下：</p>

<p><img src="/image/vscode/terminal-hyperlink.png" alt="image" /></p></li>

<li><p>转义序列语法为： <code>\x1b]8;; &lt;URL&gt; \x1b\ &lt;Label&gt; \x1b]8;;\x1b\'</code></p></li>
</ul></li>

<li><p>新的 VT 转义序列支持，参见：<a href="https://code.visualstudio.com/updates/v1_72#_vt-feature-support">原文</a>。</p></li>

<li><p>终端支持响铃，可以通过 <code>terminal.integrated.enableBell</code> 配置项开启。</p></li>

<li><p>终端相关文档作为文章站点的顶级目录存在，包含多篇文档，参见：<a href="https://code.visualstudio.com/docs/terminal/basics">终端文档</a>。</p></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li><p><code>&gt;Tasks: Run Task</code> 添加 Pin 功能。</p>

<p><img src="/image/vscode/pinned-tasks.png" alt="image" /></p></li>

<li><p>添加任务完成声音，可以通过 <code>audioCues.taskEnded</code> 配置。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown</p>

<ul>
<li><p>Markdown 链接校验，通过 <code>&quot;markdown.validate.enabled&quot;: true</code> 配置项开启。有如下细粒度配置项：</p>

<ul>
<li><code>markdown.validate.fileLinks.enabled</code> 文件链接校验，如 <code>[link](/path/to/file.md)</code>。</li>
<li><code>markdown.validate.fragmentLinks.enabled</code> 锚点验证，如 <code>markdown.validate.fragmentLinks.enabled</code>。</li>
<li><code>markdown.validate.fileLinks.markdownFragmentLinks</code> 链接锚点，如 <code>[link](other-file.md#some-header)</code>。</li>
<li><code>markdown.validate.referenceLinks.enabled</code> 参考链接，如 <code>[link][ref]</code>。</li>
<li><code>markdown.validate.ignoredLinks</code> 跳过检验的连接列表。</li>
</ul></li>

<li><p>提取链接到引用链接。如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">[<span style="color:#f92672">Markdown</span>](<span style="color:#a6e22e">https://daringfireball.net/projects/markdown/</span>) and you: Adventures in [<span style="color:#f92672">Markdown linking</span>](<span style="color:#a6e22e">https://daringfireball.net/projects/markdown/</span>)!</code></pre></div>
<p>通过 <code>cmd + .</code> 提取，转为为如下引用链接的形式。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">[Markdown][md] and you: Adventures in [Markdown linking][md]!

[md]: https://daringfireball.net/projects/markdown/</code></pre></div></li>

<li><p>对引用链接进行优化：移动底部，并按首字母排序，删除未使用的链接。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">Some [link][example] and an image:

![An image of a cat][cat-gif]

[example]: http://example.com
[cat-gif]: /keyboard-cat.gif
[some unused link]: http://example.com/file2</code></pre></div>
<p>重构后为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">Some [link][example] and an image:

![An image of a cat][cat-gif]

[cat-gif]: /keyboard-cat.gif
[example]: http://example.com</code></pre></div></li>
</ul></li>

<li><p>CSS 语言支持现在理解 <code>@property</code> 和 <code>@layer</code> at-rules。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_72#_notebooks">原文</a>。</p>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li><p>新增一个 <a href="https://chrome.google.com/webstore/detail/vs-code/kobakmhnkfaghloikphojodjebdelppk">Chrome 浏览器扩展</a>，可以在地址栏快速通过 code 命令快速打开 github 仓库。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/code-in-your-browser-omnibox.mp4" type="video/mp4">
</video></li>

<li><p>本地、vscode.dev 等编辑的内容，可以通过 <code>Continue Working On</code> 命令在具有完整开发环境的 Codespace 中继续开发，未提交的代码将保存在 Editor Session 中，进入 Codespace 后将自动恢复这些未提交的代码，参见：<a href="https://code.visualstudio.com/docs/sourcecontrol/github#_continue-working-on">文档</a>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Continue-On-in-GitHub-Codespaces.mp4" type="video/mp4">
</video>

<p>该功能主要依赖 Editor Session，应该已经正式发布了。因为 ： <code>&quot;workbench.experimental.editSessions.enabled&quot;: true</code> 配置项找不到了）。关于 Edit Session，参见之前发布的说明：</p>

<ul>
<li><a href="https://code.visualstudio.com/updates/v1_70#_edit-sessions-across-vs-code-for-the-web-and-desktop">v1.70: Edit Sessions across VS Code for the Web and desktop</a></li>
<li><a href="https://code.visualstudio.com/updates/v1_71#_bring-your-changes-with-you-when-moving-across-development-environments">v1.71: Bring your changes with you when moving across development environments</a></li>
<li><a href="https://code.visualstudio.com/updates/v1_72#_take-your-changes-with-you-when-switching-development-environments">v1.72: Take your changes with you when switching development environments</a></li>
</ul></li>
</ul>

<h2 id="issue-reporting">Issue Reporting</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_72#_issue-reporting">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Remote Development，参见：<a href="https://code.visualstudio.com/updates/v1_72#_remote-development">原文</a>。</li>
<li>Dev Container Features，是 <a href="https://containers.dev/">Development Containers</a> 标准化方案中，用来解决开发时依赖（如 Go、Java JDK 等）和镜像分离的配置规范。feature 的定义包含 <code>install.sh</code> 脚本 和 参数声明。feature 定义可以打包 tar 包，并发布到 oci 仓库，在创建开发容器时，将根据用户参数调用 <code>install.sh</code> 脚本，安装该 feature。

<ul>
<li>参见：<a href="https://code.visualstudio.com/updates/v1_72#_dev-container-features">原文</a></li>
<li><a href="https://containers.dev/implementors/features/">规范：Dev Container Features reference [proposal]</a></li>
<li><a href="https://containers.dev/implementors/features-distribution/">规范：Dev container Features contribution and discovery [proposal]</a></li>
<li><a href="https://containers.dev/features">官方 feature 列表</a></li>
<li>该组织还定义了模板，参见：<a href="https://containers.dev/templates">官方模板列表</a>。</li>
</ul></li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_72#_github-pull-requests-and-issues">原文</a>。</li>
<li>GitHub Issue Notebooks，参见：<a href="https://code.visualstudio.com/updates/v1_72#_github-issue-notebooks">原文</a>。</li>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_72#_jupyter">原文</a>。</li>
<li>GitHub Enterprise Server authentication support，参见：<a href="https://code.visualstudio.com/updates/v1_72#_github-enterprise-server-authentication-support">原文</a>。</li>
<li>Python，参见：<a href="https://code.visualstudio.com/updates/v1_72#_python">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>Settings Profiles，参见：<a href="https://code.visualstudio.com/updates/v1_72#_settings-profiles">原文</a>。</li>
<li>WebAssembly and Python execution in the Web，参见：<a href="https://code.visualstudio.com/updates/v1_72#_webassembly-and-python-execution-in-the-web">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>支持在干净的环境调试扩展，参见：<a href="https://code.visualstudio.com/updates/v1_72#_extension-debugging-in-a-clean-environment">原文</a>。</li>
<li>改进的工作区编辑 API，参见：<a href="https://code.visualstudio.com/updates/v1_72#_extension-authoring">原文</a>。</li>
<li>在 webview 上贡献上下文菜单，参见：<a href="https://code.visualstudio.com/updates/v1_72#_contributed-webview-context-menus">原文</a>。</li>
<li>webviews 中活动主题的新主题变量，参见：<a href="https://code.visualstudio.com/updates/v1_72#_new-theme-variable-for-active-theme-in-webviews">原文</a>。</li>
<li>异步 Notebook 渲染器，参见：<a href="https://code.visualstudio.com/updates/v1_72#_async-notebook-renderers">原文</a>。</li>
<li>重构添加 <code>Refactor.move</code> 类型，参见：<a href="https://code.visualstudio.com/updates/v1_72#_refactormove-code-action-kind">原文</a>。</li>
<li>Selected tree items passed to view/title actions，参见：<a href="https://code.visualstudio.com/updates/v1_72#_selected-tree-items-passed-to-viewtitle-actions">原文</a>。</li>
<li>Tree view initialSize contribution finalized，参见：<a href="https://code.visualstudio.com/updates/v1_72#_tree-view-initialsize-contribution-finalized">原文</a>。</li>
<li>Tree viewBadge finalized，参见：<a href="https://code.visualstudio.com/updates/v1_72#_tree-viewbadge-finalized">原文</a>。</li>
<li>Unbound breakpoint warning icon，参见：<a href="https://code.visualstudio.com/updates/v1_72#_unbound-breakpoint-warning-icon">原文</a>。</li>
<li>建立了一个 <a href="https://github.com/microsoft/vscode-discussions/discussions">VSCode 社区论坛</a>。</li>
</ul>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见，<a href="https://code.visualstudio.com/updates/v1_72#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p>参见，<a href="https://code.visualstudio.com/updates/v1_72#_engineering">原文</a>。</p>
]]></description></item><item><title>VSCode 1.73 (2022-10) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_73_2022-10/</link><pubDate>Sat, 26 Nov 2022 18:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_73_2022-10/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_73">https://code.visualstudio.com/updates/v1_73</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>通过搜索结果上下文菜单快速将当前目录添加到包含的文件，排除的文件输入框中。</p>

<p><img src="/image/vscode/restrict-search-to-folder.gif" alt="image" /></p>

<p><img src="/image/vscode/exclude-folder-from-search.gif" alt="image" /></p></li>
</ul>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<ul>
<li>新增任务和终端相关音频提示。

<ul>
<li>任务完成 （配置项 <code>audioCues.taskCompleted</code>）。</li>
<li>任务失败 （配置项 <code>audioCues.taskFailed</code>）。</li>
<li>终端快速修复 - 如果快速修复在当前行上可用时播放 （配置项 <code>audioCues.terminalQuickFix</code>） 。</li>
</ul></li>
<li>修复屏幕阅读器模式下的禁用自动换行，可通过 <code>editor.wordWrap</code> 配置打开。</li>
<li>可视化配置 UI 搜索框添加 <code>@tag:accessibility</code> 以过滤辅助功能相关配置。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>通过搜索结果上下文菜单快速将当前目录添加到包含的文件，排除的文件输入框中。</p>

<p><img src="/image/vscode/restrict-search-to-folder.gif" alt="image" /></p>

<p><img src="/image/vscode/exclude-folder-from-search.gif" alt="image" /></p></li>

<li><p>标题栏命令中心提供常见的模式提示（命令中心可以通过 <code>window.commandCenter</code> 配置打开）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/command-center-home-view.mp4" type="video/mp4">
</video></li>

<li><p>设置编辑器工作区信任和策略标识（参见下图⚠️符号）。</p>

<p><img src="/image/vscode/settings-editor-indicators-keyboard.gif" alt="image" /></p></li>

<li><p>通过 <code>outline.collapseItems</code> 可以配置大的纲默认折叠状态（默认为展开）。</p></li>

<li><p>将 View (查看) 菜单自动换行等条目移动到 Appearance (外观) 的二级菜单中。</p></li>

<li><p>样式更新。</p>

<ul>
<li><p>输入框改为圆角。</p>

<p><img src="/image/vscode/rounded-corners-inputs.png" alt="image" /></p></li>

<li><p>快速选择样式更新：输入框改为圆角，增加左右边距。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/quick-pick-list-styles.mp4" type="video/mp4">
</video></li>

<li><p>更新列表和数视图图标。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/updated-list-view-icons.mp4" type="video/mp4">
</video></li>

<li><p>主题，副边栏前景色 <code>sideBar.foreground</code>。</p>

<p><img src="/image/vscode/secondary-sidebar-foreground.png" alt="image" /></p></li>

<li><p>去除 <code>'Too many folding ranges'</code> 通知，改为在状态栏语言部分悬停限制。</p>

<p><img src="/image/vscode/folding-limit-warning.png" alt="image" /></p></li>

<li><p>默认折叠提供者，通过如下方式可以配置使用某个折叠提供者。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;[javascript]&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
        <span style="color:#f92672">&#34;editor.defaultFoldingRangeProvider&#34;</span>: <span style="color:#e6db74">&#34;aeschli.better-folding&#34;</span>
}</code></pre></div></li>

<li><p>通过 <code>Developer: Set Log Level....</code> 命令，可以设置每个输出通道的日志级别。此外可以通过 <code>code --log vscode.git:debug</code> 命令在启动的时候指定。</p></li>

<li><p>新的 <code>list.collapseAllToFocus</code> 树视图命令，可以递归的折叠所有子树。</p></li>

<li><p>Merge 编辑器改进和提升，参见：<a href="https://code.visualstudio.com/updates/v1_73#_merge-editor">原文</a>。</p>

<ul>
<li><p>添加 Accept Combination 选项，智能的合并同一行冲突内容。</p>

<p><img src="/image/vscode/merge-accept-combination.gif" alt="image" /></p></li>

<li><p>支持展示 Base 内容。</p>

<p><img src="/image/vscode/compare-with-base.gif" alt="image" /></p></li>

<li><p>新增一个更好的实验性算法，在 Merge 编辑器中默认启用（<code>experimental</code>）。</p>

<ul>
<li>配置项 <code>&quot;mergeEditor.diffAlgorithm&quot;</code> 默认为 <code>experimental</code>，可通过 <code>smart</code> 改回。</li>
<li>配置项 <code>&quot;diffEditor.diffAlgorithm&quot;</code> 默认为 <code>smart</code>，可通过 <code>experimental</code> 在传统的 diff 编辑器中使用实验算法。</li>
</ul></li>

<li><p>在冲突中导航，通过 <code>n Conflict Remaining</code> 按钮可以在冲突位置快速跳转。</p>

<p><img src="/image/vscode/merge-conflict-counter.gif" alt="image" /></p></li>
</ul></li>
</ul></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown 引用的文件在移动后会提示重构（通过 <code>markdown.updateLinksOnFileMove.enabled</code> 配置项开启，可以通过 <code>markdown.updateLinksOnFileMove.include</code> 配置使用呢该特性的文件类型）。</p>

<p><img src="/image/vscode/md-link-update-2.gif" alt="image" /></p></li>

<li><p>Markdown 插入链接命令：<code>Markdown: Insert Link to File in Workspace</code> 和 <code>Markdown: Insert Image from Workspace</code></p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/markdown-insert-link-commands.mp4" type="video/mp4">
</video></li>

<li><p>Markdown 未使用和重复的参考链接（通过 <code>markdown.validate.enabled</code> 配置开启）。</p>

<p><img src="/image/vscode/md-duplicate-link-def.png" alt="image" /></p>

<p>细粒度配置为：</p>

<ul>
<li><code>markdown.validate.duplicateLinkDefinitions.enabled</code>。</li>
<li><code>markdown.validate.unusedLinkDefinitions.enabled</code>。</li>
</ul></li>

<li><p>Markdown 链接高亮显示（通过 <code>markdown.occurrencesHighlight.enabled</code> 配置开启）。</p>

<p><img src="/image/vscode/md-link-highlight.png" alt="image" /></p></li>

<li><p>新增 <a href="https://learn.microsoft.com/zh-cn/aspnet/core/razor-pages/?view=aspnetcore-7.0&amp;tabs=visual-studio">Razor</a> 语法高亮。</p></li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>分支创建和保护分支提示，参见：<a href="https://code.visualstudio.com/updates/v1_73#_improved-branch-creation-and-protection-workflows">原文</a>。</li>
<li>本地化 API 已经完成，参见：<a href="https://code.visualstudio.com/updates/v1_73#_localization-improvements-in-the-web">原文</a> 和 <a href="https://github.com/microsoft/vscode-l10n">vscode-l10n 仓库</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python</p>

<ul>
<li>导入排序功能移到独立的扩展 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.isort">isort</a>。</li>
<li>PYLANCE 默认关闭自动导入，可以通过 <code>&quot;python.analysis.autoImportCompletions&quot;: true</code> 配置项开启。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.pylint">pylint</a> 和 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.flake8">flake8</a> 功能移动到独立的扩展中。</li>
</ul></li>

<li><p>远程开发</p>

<ul>
<li>支持 Dev Container 的 Template 和 Features。</li>
<li>更多参见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_73.md">Remote Development release notes</a>。</li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_73#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>TypeScript 4.9，参见：<a href="https://code.visualstudio.com/updates/v1_73#_typescript-49">原文</a>。</li>
<li>Settings Profiles，参见：<a href="https://code.visualstudio.com/updates/v1_73#_settings-profiles">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Provide metadata for workspace edits，参见：<a href="https://code.visualstudio.com/updates/v1_73#_provide-metadata-for-workspace-edits">原文</a>。</li>
<li>出于安全性考虑，限制 MarkdownString 和 webviews 可以运行哪些 VSCode 命令，参见：<a href="https://code.visualstudio.com/updates/v1_73#_restrict-which-commands-can-be-run-by-markdownstring-and-in-webviews">原文</a>。</li>
<li>webview 和 webview 视图的 Consistent origin，参见：<a href="https://code.visualstudio.com/updates/v1_73#_consistent-origin-for-webviews-and-webview-views">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_73#_debug-adapter-protocol">原文</a>。</p>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_73#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>优化输入延迟，参见：<a href="https://code.visualstudio.com/updates/v1_73#_optimizing-for-input-latency">原文</a>。</li>
<li>Automatic renderer profiling，参见：<a href="https://code.visualstudio.com/updates/v1_73#_automatic-renderer-profiling">原文</a>。</li>
<li>Windows 11 Context menu，参见：<a href="https://code.visualstudio.com/updates/v1_73#_windows-11-context-menu">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.74 (2022-11) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_74_2022-11/</link><pubDate>Sun, 11 Dec 2022 20:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_74_2022-11/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_74">https://code.visualstudio.com/updates/v1_74</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>资源浏览器自动跳转，支持通过 <code>explorer.autoRevealExclude</code> 配置项，细粒度的配置哪些文件不自动跳转，这对于 <code>node_modules</code> 之类的文件特别有用，之前一不小心跳转过去，大量的 <code>node_modules</code> 会被展开导致的体验问题。</li>
<li>新增 <code>editor.indentSize</code> 配置项，支持配置缩进的数目，这在使用了 tab 且使用空格进行缩进且 tab 的展示长度和缩进空格不一致的场景。</li>

<li><p>Remote Tunnels 已作为一个预览特性添加 VSCode 稳定版中。</p>

<ul>
<li>具体操作为：在个人电脑中，下载安装 <a href="https://code.visualstudio.com">VSCode 桌面版</a>。点击左下角账户图标，点击打开远程隧道访问，按照提示操作即可。</li>
<li>命令行方式：

<ul>
<li>运行 <code>code tunnel --accept-server-license-terms</code>，或者通过。</li>
<li>打开 <a href="https://github.com/login/device，输入命令行输出的">https://github.com/login/device，输入命令行输出的</a> 8 位设备码，进行授权。</li>
<li>授权后，在浏览器中打开 <code>https://vscode.dev/tunnel/ecstatic-bunting/打开的目录</code>（终端中会输出）。</li>
<li>后面即可通过任意设备通过浏览器，访问这台个人电脑的 VSCode Server（类似远程多面）。</li>
<li>除了通过 VSCode 桌面版的 code 命令外，还支持通过独立 <a href="https://code.visualstudio.com/docs/editor/command-line#_advanced-cli-options">VS Code CLI</a> （打开<a href="https://code.visualstudio.com/#alt-downloads">链接</a>选择CLI）、 <a href="https://code.visualstudio.com/blogs/2022/07/07/vscode-server#_getting-started">code-server CLI</a>。</li>
</ul></li>
<li>除了通过浏览器的 vscode.dev 访问外，还可以通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-server">Remote - Tunnels</a> 扩展，使用 VSCode 桌面客户端访问。</li>
<li>更多参见： <a href="https://code.visualstudio.com/blogs/2022/12/07/remote-even-better">blog</a> | <a href="https://code.visualstudio.com/docs/remote/tunnels">doc</a>。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Turn-on-Remote-Tunnel-Access.mp4" type="video/mp4">
</video></li>

<li><p>扩展制作</p>

<ul>
<li>Log output channel，支持日志级别，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_implicit-activation-events-for-declared-extension-contributions">原文</a>。</li>
<li>扩展 README 支持 <code>&lt;video&gt;</code>，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_video-tag-support-in-extension-readme">原文</a>。</li>
</ul></li>
</ul>

<h2 id="辅助功能-accessibility">辅助功能 (Accessibility)</h2>

<ul>
<li>音频提示

<ul>
<li>Notebook 运行完成后，支持成功或失败音频提示。</li>
<li>diff review 模式下的音频提示，当 <code>Go to Next Difference</code> 被触发时，将播放特定音频，以指示光标是在新增行还是删除行中。</li>
<li>通过 <code>Help: List Audio Cues</code> 命令可以查看所有音频提示。</li>
</ul></li>

<li><p>设置编辑器键盘可导航型提升。</p>

<p><img src="/image/vscode/settings-indicator-tabbing.gif" alt="image" /></p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>资源浏览器自动跳转，支持通过 <code>explorer.autoRevealExclude</code> 配置项，细粒度的配置哪些文件不自动跳转，这对于 <code>node_modules</code> 之类的文件特别有用，之前一不小心跳转过去，大量的 <code>node_modules</code> 会被展开导致的体验问题。默认配置为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;explorer.autoRevealExclude&#34;</span>: {
        <span style="color:#f92672">&#34;**/node_modules&#34;</span>: <span style="color:#66d9ef">true</span>,
        <span style="color:#f92672">&#34;**/bower_components&#34;</span>: <span style="color:#66d9ef">true</span>
    }
}</code></pre></div></li>

<li><p>支持通过右键活动栏图标，选择上下文菜单的 Hide Badge，隐藏其的数字徽章。</p>

<p><img src="/image/vscode/hide-view-badge.gif" alt="image" /></p></li>

<li><p>修复合并编辑器的一些 bug，重点参见：<a href="https://code.visualstudio.com/updates/v1_74#_merge-editor">原文</a>。</p></li>

<li><p>添加 <code>Developer: Install Extension from Location...</code> 命令，支持选择磁盘中的 <code>.vsix</code> 文件进行安装。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>自动换行配置 <code>editor.wordBreak</code> 可以配置 <a href="https://en.wikipedia.org/wiki/CJK_characters">CJK</a> 末尾不自动换行。</li>
<li>对某些较新的支持调轴的（<a href="https://zh.m.wikipedia.org/zh-hans/%E5%8F%AF%E5%8F%98%E5%AD%97%E4%BD%93">可变字体</a>，<a href="https://www.cnblogs.com/coco1s/p/15944634.html">博客</a>） <a href="https://learn.microsoft.com/typography/opentype">OpenType</a> 字体，添加配置。新增 <code>editor.fontVariations</code> 、<code>editor.fontVariations</code> 配置。具体参见：<a href="https://code.visualstudio.com/updates/v1_74#_new-font-setting-for-opentype-fonts">原文</a>。</li>

<li><p>新增 <code>editor.indentSize</code> 配置项，支持配置缩进的数目，这在使用了 tab 且使用空格进行缩进且 tab 的展示长度和缩进空格不一致的场景，一个例子如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;editor.detectIndentation&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">false</span><span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;editor.insertSpaces&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">true</span><span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;editor.tabSize&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">8</span><span style="color:#960050;background-color:#1e0010">,</span>
<span style="color:#e6db74">&#34;editor.indentSize&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">2</span></code></pre></div></li>

<li><p>新增 <code>Accept Next Word Of Inline Suggestion</code> 配置项，参见：<a href="https://code.visualstudio.com/updates/v1_74#_command-to-partially-accept-inline-completions">原文</a>。</p></li>
</ul>

<h2 id="源代码控制-source-control">源代码控制 (Source Control)</h2>

<ul>
<li>自 git 2.35.2 起，在非当前用户为 owner 的目录下将禁止操作。当 VSCode 打开这类非安全的 git 仓库时，会在源代码管理视图显示 Welcome 视图，并展示错误通知。在 UI 和通知中，可以通过 <code>Manage Unsafe Repositories</code> 来管理这些仓库，这些将存储到 git 中的 <a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-safedirectory"><code>safe.directory</code></a> 配置中。</li>
<li>git UI 乐观更新，某些 git 命令操作，VSCode 的 UI 将立即成功，命令将在后台执行，这在某些大型仓库可能比较有用。可以通过 <code>git.optimisticUpdate</code> 关闭该特性。</li>
<li>Action 按钮交互提升。按钮的标签展示底层指定命令，并添加进度动画。</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>终端快速修复提升，UI 和编辑器保持一致。</p>

<p><img src="/image/vscode/terminal-action-widget.png" alt="image" /></p></li>

<li><p>终端下拉列表，添加 Run Task 和 Configure Tasks 菜单项。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li><p>Remote Tunnels 已作为一个预览特性添加 VSCode 稳定版中。</p>

<ul>
<li>具体操作为：在个人电脑中，下载安装 <a href="https://code.visualstudio.com">VSCode 桌面版</a>。点击左下角账户图标，点击打开远程隧道访问，按照提示操作即可。</li>
<li>命令行方式：

<ul>
<li>运行 <code>code tunnel --accept-server-license-terms</code>，或者通过。</li>
<li>打开 <a href="https://github.com/login/device，输入命令行输出的">https://github.com/login/device，输入命令行输出的</a> 8 位设备码，进行授权。</li>
<li>授权后，在浏览器中打开 <code>https://vscode.dev/tunnel/ecstatic-bunting/打开的目录</code>（终端中会输出）。</li>
<li>后面即可通过任意设备通过浏览器，访问这台个人电脑的 VSCode Server（类似远程多面）。</li>
<li>除了通过 VSCode 桌面版的 code 命令外，还支持通过独立 <a href="https://code.visualstudio.com/docs/editor/command-line#_advanced-cli-options">VS Code CLI</a> （打开<a href="https://code.visualstudio.com/#alt-downloads">链接</a>选择CLI）、 <a href="https://code.visualstudio.com/blogs/2022/07/07/vscode-server#_getting-started">code-server CLI</a>。</li>
</ul></li>
<li>除了通过浏览器的 vscode.dev 访问外，还可以通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-server">Remote - Tunnels</a> 扩展，使用 VSCode 桌面客户端访问。</li>
<li>更多参见： <a href="https://code.visualstudio.com/blogs/2022/12/07/remote-even-better">blog</a> | <a href="https://code.visualstudio.com/docs/remote/tunnels">doc</a>。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Turn-on-Remote-Tunnel-Access.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>JavaScript 调试

<ul>
<li>支持 console.profile。</li>
<li>支持 嵌套 sourcemaps。</li>
<li>调试配置 <code>serverReadyAction</code> 添加 <code>killOnServerStop</code>，参见：<a href="https://code.visualstudio.com/updates/v1_74#_killonserverstop-property-added-to-serverreadyaction">原文</a>。</li>
</ul></li>

<li><p>在 Call Stack 视图，有多个不同类型的调试会话。当焦点位于调试会话上时，breakpoints 视图将正确展示 Exception breakpoints 选中情况。</p>

<p><img src="/image/vscode/exception-breakpoints.gif" alt="image" /></p></li>
</ul>

<h2 id="评论-comments">评论 (Comments)</h2>

<ul>
<li>通过 <code>comments.visible</code> 可以配置评论的默认可见性。这个配置不影响 <code>Comments: Toggle Editor Commenting</code> 命令。</li>

<li><p>命令视图添加数字 badge，展示未解决的评论数目。</p>

<p><img src="/image/vscode/comments-view-badge.png" alt="image" /></p></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>Problem matcher &lsquo;search&rsquo; file location method，参见：<a href="https://code.visualstudio.com/updates/v1_74#_problem-matcher-search-file-location-method">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>带来 TypeScript 4.9</li>
<li>JavaScript 和 TypeScript支持通过 return 关键字，按 F12，跳转到函数定义。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>Kernel picker experiment: most recently used kernels，参见：<a href="https://code.visualstudio.com/updates/v1_74#_problem-matcher-search-file-location-method">原文</a>。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>源代码控制，参见：<a href="https://code.visualstudio.com/updates/v1_74#_vs-code-for-the-web">原文</a>。</li>
<li>Continue Working On 提升，参见：<a href="https://code.visualstudio.com/updates/v1_74#_improvements-to-continue-working-on">原文</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_74#_jupyter">原文</a>。</li>
<li>Remote Development extensions。

<ul>
<li>Dev Container 支持 GPU。</li>
<li>Dev Container Cygwin / Git Bash 套接字转发</li>
<li>通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-server">Remote - Tunnels</a> 扩展，可以无需 SSH 即可连接到远程主机。</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_remote-development-extensions">原文</a>。</li>
</ul></li>
<li>GitHub Pull Requests and Issues，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性">预览特性</h2>

<ul>
<li>Profiles，可通过 <code>workbench.experimental.settingsProfiles.enabled</code> 开启，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_github-pull-requests-and-issues">原文</a>。</li>
<li>扩展签名和验证，参见：<a href="https://code.visualstudio.com/updates/v1_74#_python-execution-in-the-web">原文</a>。</li>
<li>Python execution in the Web，参见：<a href="https://code.visualstudio.com/updates/v1_74#_python-execution-in-the-web">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>声明的扩展贡献的隐式激活事件，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_implicit-activation-events-for-declared-extension-contributions">原文</a>。</li>
<li>Log output channel，支持日志级别，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_implicit-activation-events-for-declared-extension-contributions">原文</a>。</li>
<li>Consistent origin for all webviews，参见：<a href="https://code.visualstudio.com/updates/v1_74#_implicit-activation-events-for-declared-extension-contributions">原文</a>。</li>
<li>扩展 README 支持 <code>&lt;video&gt;</code>，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_video-tag-support-in-extension-readme">原文</a>。</li>
<li>Comment thread additional actions，参见：<a href="https://code.visualstudio.com/updates/v1_74#_comment-thread-additional-actions">原文</a>。</li>
<li>vsce 被重命名为 @vscode/vsce，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_renaming-of-vsce-to-vscodevsce">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_74#_debug-adapter-protocol">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>方法和属性名重写，包大小减少 13%，代码加载时间减少 5%。</li>
<li>GitHub 和 Microsoft 身份验证扩展包大小改进。</li>
<li>Electron 沙盒进展，参见：<a href="https://code.visualstudio.com/updates/v1_74#_electron-sandbox-journey">原文</a>。</li>
<li>Windows 上默认重新启用窗口控件覆盖，参见：<a href="https://code.visualstudio.com/updates/v1_74#_electron-sandbox-journey">原文</a>。</li>
<li>内置扩展现在使用新的 <a href="https://code.visualstudio.com/api/references/vscode-api#l10n">l10n API</a> 而不是 vscode-nls，更多参见：<a href="https://code.visualstudio.com/updates/v1_74#_builtin-extensions-now-use-the-new-l10n-api-instead-of-vscodenls">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.75 (2023-01) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_75_2023-01/</link><pubDate>Sat, 11 Feb 2023 17:35:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_75_2023-01/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_75">https://code.visualstudio.com/updates/v1_75</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>VSCode 正式支持多 Profiles，同一设备支持多套隔离配置，并支持导入导出，细节参见下文。</li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<ul>
<li>diff 导航提升，转到上一个/下一个有音频指示是插入删除还是修改。被更改行的选中屏幕阅读器会播报。</li>
<li>添加了一个命令 <code>&gt;Terminal: Focus Accessible Buffer</code> （<code>⇧Tab</code>），终端输出的内容集中到一个窗口中，然后，可以通过键盘上下键移动光标让屏幕阅读器指定位置的输出。</li>

<li><p>添加了一个命令 <code>&gt;Terminal: Show Terminal Accessibility Help</code> 类似于 <code>Show Accessibility Help</code> 将展示终端可访问性帮助。</p>

<p><img src="/image/vscode/terminal-accessibility-help.png" alt="image" /></p></li>

<li><p>Workspace Trust 编辑器添加快捷键支持，可以通过 <code>Ctrl/Cmd+Enter</code>、<code>Ctrl/Cmd+Shift+Enter</code> 快捷键快速信任当期工作区目录、父目录。</p>

<p><img src="/image/vscode/trust-editor-shortcuts.png" alt="image" /></p></li>

<li><p>改进了设置编辑器指示器上的拥有多个修改指示器的键盘导航</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-user-navigating-a-setting-with-multiple-indicators-with-arrow-keys.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="配置文件-profiles">配置文件 (Profiles)</h2>

<p>预览了几个月的 Profiles 特性正式上线到该稳定版。</p>

<p>过去，VSCode 只有一套全局配置，没有办法为某种开发场景目定制一套专有配置。其次，配置没法分享。</p>

<p>现在，通过 Porfiles 机制，可以针对不同场景进行不同的配置。可以使用其他人分享的配置，但不影响自己的配置。</p>

<p>Profiles 包含：设置（<code>settings.json</code>）、任务配置（<code>tasks.json</code>）、代码片段（<code>snippets</code>）、扩展程序（<code>extensions</code>）、UI 状态（<code>globalState</code>）。这些内容将以 <code>profileName.code-profile</code> 文件储存。该文件是一个 json 文件，格式类似如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;profileName&#34;</span>,
    <span style="color:#f92672">&#34;settings&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#f92672">&#34;tasks&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#f92672">&#34;snippets&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#f92672">&#34;extensions&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#f92672">&#34;globalState&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
}</code></pre></div>
<p>可以通过左下角齿轮图标 -&gt; 配置文件使用此功能。</p>

<ul>
<li>第一部分：选择一个 Profiles 并应用。</li>
<li>显示内容 &hellip;： 打开当前 Profiles 的内容。</li>
<li>创建配置文件：

<ul>
<li>创建空配置文件 &hellip;：相当于打开刚安装的全新的干净的 VSCode。</li>
<li>从当前配置文件创建&hellip;：fork 一份当前的配置文件，在该配置文件上的修改不会影响之前的配置。</li>
</ul></li>
<li>Exports Profiles&hellip;：将配置文件导出到磁盘中或者直接发布到 github gist。</li>
<li>导入配置文件&hellip;：将一份配置文件导入到 VSCode 中并使用他。</li>
</ul>

<p><img src="/image/vscode/work-profile.png" alt="image" /></p>

<p>导出到 github gist 的配置文件，可以直接通过一个链接 vscode.dev 打开，并引导用户一键导入到本地 VSCode 中。</p>

<p><img src="/image/vscode/export-share-profile.gif" alt="image" /></p>

<blockquote>
<p>注意：当前版本，在 Remote 和 Github Codespaces 中，该功能存在问题，可以关注： <a href="https://github.com/microsoft/vscode/issues/165247">issue #165247</a></p>
</blockquote>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>提升多视图大小调整，在三个视图焦点位置，可以一次调整 3 个视图。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-user-dragging-corners-of-views-and-resizing-them-simultaneously.mp4" type="video/mp4">
</video></li>

<li><p>提升网格布局。如果编辑器的宽度被调整到最小化，则在调整整个工作台或侧边栏的大小时，网格现在将保留该状态。在下面的短视频中，右侧最小化编辑器的宽度随着整个编辑器区域的扩展而保持不变。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-user-resizing-views-in-the-editor-grid-and-demonstrating-how-a-minimized-view-maintains-its-state.mp4" type="video/mp4">
</video></li>

<li><p>自定义布局命令 <code>&gt;Customize Layout</code>，支持撤销操作。</p>

<p><img src="/image/vscode/customize-layout.png" alt="image" /></p></li>

<li><p>面板标题上下文菜单支持配置对齐方式。</p>

<p><img src="/image/vscode/panel-context-menu.png" alt="image" /></p></li>

<li><p>简化全局设置菜单（左下角齿轮）。</p>

<p><img src="/image/vscode/global-settings-menu.png" alt="image" /></p></li>

<li><p>树查找历史，支持通过 上下键浏览查找历史。</p></li>

<li><p>树查找支持连续匹配（点掉，波浪线放大镜图标）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-user-toggling-between-contiguous-and-fuzzy-find-in-the-explorer-tree.mp4" type="video/mp4">
</video></li>

<li><p>通过 <code>workbench.list.scrollByPage</code> 配置，控制点击列表的滚动条（如资源管理器）上下方按页滚动（编辑有专门的配置 <code>editor.scrollbar.scrollByPage</code>）。</p></li>

<li><p>通过 <code>workbench.list.typeNavigationMode</code> 配置，控制在列表页按键盘时，聚焦到匹配的项目。默认为 <code>automatic</code>，按任意键匹配，<code>trigger</code> 可以配置按下某个键后才进行匹配。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;/&#34;</span>,
    <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;list.toggleKeyboardNavigation&#34;</span>,
    <span style="color:#f92672">&#34;when&#34;</span>: <span style="color:#e6db74">&#34;listFocus&#34;</span>
}</code></pre></div></li>

<li><p>打开大文件的确认进行提示，通过 <code>workbench.editorLargeFileConfirmation</code> 配置，控制大文件的 MB。默认为 10M。</p>

<p><img src="/image/vscode/large-file-confirm.png" alt="image" /></p></li>

<li><p>文件 watcher 库支持 <code>files.watcherExclude</code> glob 模式，参见：<a href="https://code.visualstudio.com/docs/setup/linux#_visual-studio-code-is-unable-to-watch-for-file-changes-in-this-large-workspace-error-enospc">FAQ</a>、<a href="https://github.com/parcel-bundler/watcher/pull/106">PR</a>。</p></li>

<li><p>键盘快捷键编辑器提升。</p>

<ul>
<li><p>支持查看贡献该快捷键的扩展，通过 Source 列。</p>

<p><img src="/image/vscode/keyboard-shortcuts-extensions.png" alt="image" /></p></li>

<li><p>在扩展列表页设置菜单，支持查看当前扩展的快捷键。</p>

<p><img src="/image/vscode/extension-show-keyboard-shortcuts.png" alt="image" /></p></li>

<li><p>When 编辑支持只能提示。</p>

<p><img src="/image/vscode/when-context-key-suggestions.png" alt="image" /></p></li>

<li><p><code>Ctrl+K</code> 会显示所有以 <code>Ctrl+K</code> 开头的快捷键。</p></li>
</ul></li>

<li><p>通过 <code>application.shellEnvironmentResolutionTimeout</code> 配置项（仅限 macOS 和 Linux），配置 shell 环境解析超时（参见 <a href="https://code.visualstudio.com/docs/supporting/faq#_resolving-shell-environment-fails">FAQ</a>）。</p></li>

<li><p>shell 环境解析时会添加 <code>VSCODE_RESOLVING_ENVIRONMENT</code> 环境变量。</p></li>

<li><p>发行说明页添加一个选择框，快速配置不展示发行说明，对应的配置项为 <code>update.showReleaseNotes</code>。</p>

<p><img src="/image/vscode/release-notes.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>新增 <code>editor.suggest.selectionMode</code> 配置项，控制是否选中建议列表的第一个。无论该配置是什么，建议列表都会显示。此外，通过快捷键触发的建议列表(<code>cmd+i</code>)，本配置不生效。</p>

<ul>
<li><code>&quot;always&quot;</code> （默认） 总是选中第一个。</li>
<li><code>&quot;never&quot;</code> 从不选中第一个，可以通过上下键选择。</li>
<li><code>&quot;whenQuickSuggestion&quot;</code> 当建议列表是快速建议时，选中第一个。</li>
<li><code>&quot;whenTriggerCharacter&quot;</code> 当建议列表是通过字符触发时，选中第一个。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Suggest-Select-Mode.mp4" type="video/mp4">
</video></li>

<li><p>CodeAction 列表现在支持滚动。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-scrolling-a-long-list-of-Code-Actions.mp4" type="video/mp4">
</video></li>

<li><p>编辑器中显示的颜色装饰器数量限制为 500。这是为了防止在打开包含大量颜色的文件时出现性能问题。现在可以通过 <code>editor.colorDecoratorsLimit</code> 设置配置此限制。</p>

<p><img src="/image/vscode/css-color-decorators.png" alt="image" /></p></li>

<li><p>新增 <code>&gt;Go To Match...</code> 命令，可以快速跳转到第 n 个匹配项。</p>

<p><img src="/image/vscode/find-go-to-match.gif" alt="image" /></p></li>

<li><p>重新设计的的内联建议工具栏。在类似 <a href="https://code.visualstudio.com/updates/v1_75#_github-copilot">github Copilot</a> 扩展中有更好的体验。新的配置项 <code>&quot;editor.inlineSuggest.showToolbar&quot;: &quot;always&quot;</code> 可以配置内联建议工具栏总是可见。添加了 <code>Cmd + 右/左箭头</code> 按单词接收撤销建议。</p>

<p><img src="/image/vscode/inline-completions-hover.gif" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>新的默认快捷键。</p>

<ul>
<li><p>打开检测到的链接 - <code>CTRL/CMD+SHIFT+O</code>。</p>

<p><img src="/image/vscode/terminal-open-link.png" alt="image" /></p></li>

<li><p>转到最近的目录 - <code>CTRL/CMD+G</code>。</p>

<p><img src="/image/vscode/terminal-go-to-dir.png" alt="image" /></p></li>

<li><p>将 CTRL+G 发送到 Shell - <code>CTRL+ALT+G</code>。</p></li>

<li><p>运行最近命令 - <code>CTRL+ALT+R</code>。</p>

<p><img src="/image/vscode/terminal-run-command.png" alt="image" /></p></li>

<li><p>在打开屏幕阅读器时，<code>CTRL+ALT+R</code> 变为发送 <code>CTRL+R</code> 到终端，。<code>CTRL+R</code> 变为打开最近命令。</p></li>
</ul></li>

<li><p>终端链接检测提升，细节参见：<a href="https://code.visualstudio.com/updates/v1_75#_link-improvements">原文</a>。</p></li>

<li><p>当终端位于编辑器区域时，拖动文件进入终端时同时按住 shift，文件名将发送到终端里面，而不是直接打开该文件。</p>

<p><img src="/image/vscode/terminal-dnd.png" alt="image" /></p></li>

<li><p>不安全配置文件检测，参见：<a href="https://code.visualstudio.com/updates/v1_75#_unsafe-profile-detection">原文</a>。</p></li>

<li><p>溢出菜单添加 clear 终端菜单，通过右击溢出菜单，可以将溢出菜单项显示出来。</p>

<p><img src="/image/vscode/terminal-view-overflow.png" alt="image" />
<img src="/image/vscode/terminal-view-toggle.png" alt="image" /></p></li>

<li><p>在 Windows 中，如果选中了文本，<code>ctrl+c</code> 将变为复制文本操作，同时取消选中的内容，因此第二次按 <code>ctrl+c</code> 将发送 SIGINT 信号给终端。</p></li>

<li><p>通过 <code>terminal.integrated.tabStopWidth</code> 可以配置终端制表符展示长度。默认为 8。</p></li>

<li><p>电力线渲染优化，参见：<a href="https://code.visualstudio.com/updates/v1_75#_powerline-triangle-and-diagonal-line-custom-glyphs">原文</a>。</p></li>

<li><p>运行选中文本，如果选中多行，现在将作为单个输入输入到终端中。</p>

<ul>
<li><p>Before</p>

<p><img src="/image/vscode/terminal-selected-text-before.png" alt="image" /></p></li>

<li><p>After</p>

<p><img src="/image/vscode/terminal-selected-text-after.png" alt="image" /></p></li>
</ul></li>

<li><p>终端快速修复支持 Pwsh Preview feedback providers，参加：<a href="https://code.visualstudio.com/updates/v1_75#_quick-fixes-for-pwsh-preview-feedback-providers">原文</a>。</p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>新增 <code>&gt;Git: Stash Staged</code> 命令可以调用 <a href="https://github.blog/2022-01-24-highlights-from-git-2-35/">Git 2.35</a> <code>git stash --staged</code>。</li>
<li>新增 <code>Git: Delete Remote Tag</code> 命令可以删除远端标签。</li>

<li><p>当检测到当前打开的目录的父目录是 git 仓库时，VSCode 将不直接在源代码版本控制视图中打开，而是弹窗提示用户是否打开。可以通过 <code>git.openRepositoryInParentFolders</code> 配置恢复自动打开的行为。</p>

<p><img src="/image/vscode/git-repository-in-parent-folders.png" alt="image" /></p></li>

<li><p>当存在 git 操作未完成时，源代码版本控制视图的很多按钮将被禁用，以防止命令的冲突。</p></li>

<li><p>UI 提升，参见：<a href="https://code.visualstudio.com/updates/v1_75#_user-interface-improvements">原文</a>。</p></li>
</ul>

<h2 id="笔记本-notebook">笔记本 (Notebook)</h2>

<ul>
<li>Kernel 选择器提升，参加：<a href="https://code.visualstudio.com/updates/v1_75#_kernel-picker-improvements">原文</a>。</li>

<li><p>添加合并选择的 Cell 命令：<code>&gt;Join Selected Cells</code>。</p>

<p><img src="/image/vscode/notebook-join-cells.gif" alt="image" /></p></li>

<li><p>渲染优化，参见：<a href="https://code.visualstudio.com/updates/v1_75#_fallback-rendering-of-output-to-a-supported-mimetype">原文</a>。</p></li>

<li><p>新的文档，参见：<a href="https://code.visualstudio.com/updates/v1_75#_new-documentation">原文</a>。</p></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>提升 Node.JS 启动性能，参见：<a href="https://code.visualstudio.com/updates/v1_75#_javascript-debugging">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>JavaScript React 被重命名为  JavaScript JSX，TypeScript React 被重命名为 TypeScript JSX。因为 jsx 已经不仅仅被 React 使用。</li>
<li>Shell 语法高亮改为 <a href="https://github.com/jeff-hykin/better-shell-syntax">better-shell-syntax</a>。</li>
</ul>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li>VS 市场扩展签名，VSCode 市场会对现存的扩展进行签名。几个月后，VSCode 将禁止安装签名不一致的扩展，更多参见： <a href="https://github.com/microsoft/vscode-discussions/discussions/137">讨论</a>。</li>
<li>VSCode CLI 支持安装固定版本的扩展：<code>code --install-extension {publisher}.{name}@{version}</code>，通过此方式安装的扩展，VSCode 将不会自动更新。</li>
<li>配置同步将同步那些被固定版本的扩展的版本信息，这意味了，其他设备同步下来后仍然安装那个固定的版本，而不是新的版本。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a>，参见：<a href="https://code.visualstudio.com/updates/v1_75#_python">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server">Live Preview</a>，参见：<a href="https://code.visualstudio.com/updates/v1_75#_live-preview">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">ESLint</a>，参见：<a href="https://code.visualstudio.com/updates/v1_75#_eslint">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests and Issues</a>，参见：<a href="https://code.visualstudio.com/updates/v1_75#_github-pull-requests-and-issues">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot">GitHub Copilot</a>，参见：<a href="https://code.visualstudio.com/updates/v1_75#_github-copilot">原文</a></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>详见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_75.md">Remote Development release notes</a>。</p>

<ul>
<li>Remote Tunnels，支持睡眠抑制配置，防止设备睡眠而断连。

<ul>
<li>VSCode 桌面版开启，配置 <code>remote.tunnels.access.preventSleep</code> 配置项为 true。</li>
<li>code 命令开启，添加 <code>--no-sleep</code> 标志。</li>
</ul></li>

<li><p>Continue Working On（中文翻译为云更改，该功能细节参见前几版本 Changelog 或 <a href="https://code.visualstudio.com/docs/sourcecontrol/github#_continue-working-on">文档</a>）。</p>

<ul>
<li><p>支持同步本地创建的新分支。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-an-automatic-prompt-to-publish-your-branch-when-using-Continue-Working-On-in-a-Git-repository.mp4" type="video/mp4">
</video></li>

<li><p><code>vscode.dev</code> 中支持通过 <code>&gt;Continue Working in New Local Clone</code> 命令通过 <code>vscode://</code> 连接快速拉起本地 VSCode 桌面版，并 clone 下来代码，并继续工作。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-Continue-Working-On-a-Git-repository-in-a-new-local-clone-when-in-a-remote-window.mp4" type="video/mp4">
</video></li>

<li><p>在 VSCode 桌面版、vscode.dev、remote 中，点击远程指示器（左下角 <code>&gt;&lt;</code> 图标），菜单可以看到云同步的命令。</p>

<p><img src="/image/vscode/continue-on-remote-indicator.png" alt="image" /></p></li>

<li><p>注意，如果更改内容过多（如包含图片视频等），云更改可能失败。</p></li>

<li><p>目前，云更改处于实验阶段。自动云更改，需要通过 <code>workbench.experimental.cloudChanges.autoStore</code> 开启。</p></li>
</ul></li>
</ul>

<h2 id="预览特性">预览特性</h2>

<ul>
<li><p>新增 Dark+ V2 和 Light+ V2 实验主题，比之前默认主题更加现代化。</p>

<p><img src="/image/vscode/v2-themes.png" alt="image" /></p></li>

<li><p>TypeScript 5.0 support</p></li>

<li><p>命令面板添加常用部分，通过 <code>workbench.commandPalette.experimental.suggestCommands</code> 配置设置，更多参见：<a href="https://code.visualstudio.com/updates/v1_75#_commonly-used-section-in-the-command-palette">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Comment thread state，参见：<a href="https://code.visualstudio.com/updates/v1_75#_comment-thread-state">原文</a>。</li>
<li>注册配置时，可以使用 <code>ignoreSync</code> 来禁用配置同步。</li>
<li>遥感 API （<code>TelemetryLogger</code>）已冻结。</li>
</ul>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_75#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_75#_engineering">原文</a>。</p>
]]></description></item><item><title>VSCode 1.76 (2023-02) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_76_2023-02/</link><pubDate>Thu, 16 Mar 2023 19:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_76_2023-02/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_76">https://code.visualstudio.com/updates/v1_76</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-repositories"><code>Remote Repositories</code></a> 扩展，可以实现免于 clone 的浏览和编辑远程仓库（和 github.dev 原理一致），参见：<a href="https://code.visualstudio.com/blogs/2021/06/10/remote-repositories">VSCode 博客 / Remote Repositories</a>。</li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<ul>
<li><p>终端命令失败音效，在终端命令执行以非 0 退出时，将播放 <code>audioCues.terminalCommandFailed</code> 音效（可以通过 <code>&gt;Help: List Audio Cues</code> 试听）。</p>

<p><img src="/image/vscode/list-audio-cues-dropdown.png" alt="image" /></p></li>

<li><p>提升错误音效响应，参见：<a href="https://code.visualstudio.com/updates/v1_76#_improved-error-audio-cue-responsiveness">原文</a>。</p></li>

<li><p><code>&gt;Terminal: Focus Accessible Buffer</code> 命令提升，参见：<a href="https://code.visualstudio.com/updates/v1_76#_terminal-accessible-buffer-improvements">原文</a>。</p></li>

<li><p><code>&gt;Toggle Tab Key Moves Focus</code> 命令可以让光标脱离编辑器和终端，之后再按 <code>Tab</code> 光标将在整个工作台的各个元素中切换。新增 <code>editor.tabFocusMode</code> 和 <code>terminal.integrated.tabFocusMode</code> 用来配置 <code>Tab</code> 切换光标到编辑器或者终端后，再按 Tab 是输入 Tab 字符还是继续切换焦点。</p></li>

<li><p>Windows 上终端集成支持屏幕阅读器。</p></li>

<li><p>Terminal accessible help additions，参见：<a href="https://code.visualstudio.com/updates/v1_76#_terminal-accessible-help-additions">原文</a>。</p></li>
</ul>

<h2 id="配置文件-profiles">配置文件 (Profiles)</h2>

<ul>
<li><p>上一个<a href="/series/vscode/changelog/v1_75_2023-01/#配置文件-profiles">版本</a>，VSCode 正式发布了 Profiles 特性。现在通过将配置文件名称的前两个字母显示为管理活动栏图标上的配置文件徽章来指示当前的自定义配置文件。</p>

<p><img src="/image/vscode/profile-badge.png" alt="image" /></p></li>

<li><p>现在，支持包含远程工作区配置的中的 Profiles，下面的例子中，两个 Remote SSH 的工作空间，分别使用了 <code>&quot;Doc Writing&quot;</code>、 <code>&quot;Code&quot;</code> 两个配置。</p>

<p><img src="/image/vscode/remote-profiles.png" alt="image" /></p></li>

<li><p>Profiles 文档已上线，参见： <a href="https://code.visualstudio.com/docs/editor/profiles">用户手册 / Profiles in Visual Studio Code</a></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>JSONC (JSON documents with comments) 文件支持通过 <code>&gt;JSON: Sort Document</code> 命令，让使文件中的 JSON Object 按照 Key 的字母顺序进行排序。</li>
<li>优化多色括号对，参见：<a href="https://code.visualstudio.com/updates/v1_76#_independent-bracket-pairs-for-matching-and-colorization">原文</a>。</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>当 VSCode 作为 git commit 消息编辑器时，将提供语法高亮。</li>
<li>VSCode 文档， Source Control 章节，添加更多关于 git 和 github 的文档。

<ul>
<li><a href="https://code.visualstudio.com/docs/sourcecontrol/overview">Using Git source control in VS Code</a> - VSCode 集成 Git 概述。</li>
<li><a href="https://code.visualstudio.com/docs/sourcecontrol/intro-to-git">Introduction to Git</a> - Git 介绍。</li>
<li><a href="https://code.visualstudio.com/docs/sourcecontrol/github">Working with GitHub</a> - Move your code to <a href="https://github.com">GitHub</a> to share and collaborate with others.</li>
<li><a href="https://code.visualstudio.com/docs/sourcecontrol/faq">Frequently Asked Questions</a>。</li>
</ul></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>内核选择模式默认采用 MRU，参见： <a href="https://code.visualstudio.com/updates/v1_76#_kernel-picker-default-mode-mru">原文</a>。</li>
<li>笔记本渲染器性能诊断，参见：<a href="https://code.visualstudio.com/updates/v1_76#_notebook-renderer-performance-diagnostics">原文</a>。</li>
<li>更好地支持内置错误输出的快速调转链接的识别，参见：<a href="https://code.visualstudio.com/updates/v1_76#_notebook-renderer-performance-diagnostics">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown 连接，可以通过两个 <code>#</code> 号，智能提示整个工作空间的标题。</p>

<p><img src="/image/vscode/md-workspace-header-suggestion.png" alt="image" /></p>

<p>选择一个提示后，将自动的把文件添加进来。</p>

<p><img src="/image/vscode/md-workspace-header-suggestion-insert.png" alt="image" /></p>

<p>该特性可以通过 <code>markdown.suggest.paths.includeWorkspaceHeaderCompletions</code> 配置项配置。</p>

<ul>
<li><code>onDoubleHash</code> (默认值) - 当键入两个 <code>##</code> 时才提示。</li>
<li><code>onSingleOrDoubleHash</code> 当键入一个或两个 <code>##</code> 时才提示。</li>
<li><code>never</code> 关闭该特性。</li>
</ul>

<p>第一次提示时，需要扫描工作区的所有文件，在大型工作区会有延迟。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>参见：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_76.md">Remote Development release notes</a></p>

<ul>
<li><p>通过 <code>⌥⌘O</code> 快捷键（或通过左下角图标），可以打开远程菜单。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Opening-the-remote-menu-with-the-new-default-keybinding.mp4" type="video/mp4">
</video></li>

<li><p>简化了远程菜单的条目。</p></li>

<li><p><code>Install Additional Remote Extensions</code> 菜单项，将跳转到扩展市场，并搜索 <code>@recommended:remotes</code>。</p></li>

<li><p>通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-repositories"><code>Remote Repositories</code></a> 扩展，可以实现免于 clone 的浏览和编辑远程仓库（和 github.dev 原理一致），参见：<a href="https://code.visualstudio.com/blogs/2021/06/10/remote-repositories">VSCode 博客 / Remote Repositories</a>。</p></li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<p>支持对 <a href="https://git-lfs.com/">Git LFS</a> 的支持，更多参见：<a href="https://code.visualstudio.com/updates/v1_76#_vs-code-for-the-web">原文</a>。</p>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li><p>改进的扩展搜索相关性</p>

<p><img src="/image/vscode/search_before.png" alt="image" /></p></li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_76#_contributions-to-extensions">原文</a>。</p>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_76#_preview-features">原文</a>。</p>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><p>InputBox 的提示和验证消息，支持 markdown 链接，如：<code>[link text](link target)</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">result</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">vscode</span>.window.<span style="color:#a6e22e">showInputBox</span>({
<span style="color:#a6e22e">prompt</span><span style="color:#f92672">:</span>
    <span style="color:#e6db74">&#39;Please enter a valid email address [more info](https://aka.ms/vscode-email-validation)&#39;</span>,
<span style="color:#a6e22e">validateInput</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">text</span> =&gt; {
    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">text</span>.<span style="color:#a6e22e">indexOf</span>(<span style="color:#e6db74">&#39;@&#39;</span>) <span style="color:#f92672">===</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;Please enter a valid email address, [more info](https://aka.ms/vscode-email-validation)&#39;</span>;
    }
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">undefined</span>;
}
});
</code></pre></div>
<p>效果如下：</p>

<p><img src="/image/vscode/quickpick-prompt-links.png" alt="image" /></p>

<p><img src="/image/vscode/quickpick-validation-links.png" alt="image" /></p></li>

<li><p>对于 <code>*</code> 的激活事件，将进行性能问题警告。</p></li>

<li><p><code>package.json</code> 的 <code>When</code> 配置变更，参见：<a href="https://code.visualstudio.com/updates/v1_76#_upcoming-changes-in-when-clause-contexts-parsing">原文</a>。</p></li>

<li><p>即将升级到 Electron 22 变更可能带来一些影响，部分扩展需要进行重构才能运行，参见：<a href="https://code.visualstudio.com/updates/v1_76#_upcoming-electron-update-may-require-mandatory-changes-to-native-modules">原文</a>。</p></li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_76#_language-server-protocol">原文</a>。</p>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_76#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_76#_engineering">原文</a>。</p>
]]></description></item><item><title>VSCode 1.77 (2023-03) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_77_2023-03/</link><pubDate>Mon, 10 Apr 2023 23:30:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_77_2023-03/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_77">https://code.visualstudio.com/updates/v1_77</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<p>无</p>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<ul>
<li>终端可访问缓冲区改进，使用 <code>&gt;Terminal: Focus Accessible Buffer</code> 命令，快捷键 <code>⇧Tab</code> 可以打开，使用 <code>Esc</code> 和 <code>Tab</code> 可以退出。当开启 shell 集成，可以使用 <code>&gt;Terminal: Navigate Accessible Buffer</code> 命令，快捷键 <code>⇧⌘O</code>，在命令见导航。</li>
<li>Hover 控件支持 <code>up</code>, <code>down</code>, <code>home</code>, <code>end</code>, <code>page up</code>, <code>page down</code> 按键。</li>
<li>可通过 <code>&gt;Notifications: Accept Notification Primary Action</code> 命令接受通知的主要操作，而不需要鼠标点击。</li>
<li>可以通过 <code>&gt;View: Toggle Sticky Scroll</code> 开关粘性滚动导航。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>安装 GitHub Pull Requests and Issues 后，选择一段文本后，支持复制 github permalinks。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Copy-GitHub-permalinks.mp4" type="video/mp4">
</video></li>

<li><p>按文件内容推荐扩展</p></li>

<li><p>添加 Sticky Scroll 默认提供程序配置项 <code>editor.stickyScroll.defaultModel</code>。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>提升终端 Tab 标题 Hover，展示进程名，进程 id，进程命令，是否计划 Shell 集成，贡献扩展。</p>

<p><img src="/image/vscode/terminal-tab-hover.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>Git LFS commit support in Remote Repositories，参见：<a href="https://code.visualstudio.com/updates/v1_77#_git-lfs-commit-support-in-remote-repositories">原文</a>。</li>
<li>VSCode 文档添加 3 步 merge 编辑器文档： <a href="https://code.visualstudio.com/docs/sourcecontrol/overview#_3way-merge-editor">3-way merge editor documentation</a></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>通过 <code>notebook.formatOnSave.enabled</code> 配置项可以开启保存时自动格式化整个文档。</li>
<li>notebook 查找默认开启对输出的匹配。</li>
<li>支持通过 <code>notebook.output.scrolling</code> 开启输出功能，通过 <code>notebook.output.textLineLimit</code> 限制最大行数。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>TypeScript 更新至 5.0，参见：<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/">TypeScript 博客</a>。</li>

<li><p>字符串字面量类型 switch case 支持自动完成。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Completing-the-cases-of-a-switch-statement.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li><p>支持 .gitignore。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Gitignore-in-VS-Code-for-the-Web.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_77#_remote-development">原文</a>。</p>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li>Extension installation not blocked by signature verification failures，参见：<a href="https://code.visualstudio.com/updates/v1_77#_extension-installation-not-blocked-by-signature-verification-failures">原文</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python，移动符号重构。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move-symbol-refactoring-with-Pylance.mp4" type="video/mp4">
</video></li>

<li><p>Jupyter，Kernel 选择提升，参见：<a href="https://code.visualstudio.com/updates/v1_77#_kernel-picker-improvements-for-python-environments">原文</a>。</p></li>

<li><p>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_77#_github-pull-requests-and-issues">原文</a>。</p></li>

<li><p>GitHub Copilot，接入 chat 能力（类似 ChatGPT）。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>全局搜索支持搜索 Notebook 输出，参见：<a href="https://code.visualstudio.com/updates/v1_77#_notebook-search-support-for-outputs">原文</a>。</li>
<li>欢迎页展示远程连接选择器，参见：<a href="https://code.visualstudio.com/updates/v1_77#_notebook-search-support-for-outputs">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>即将到来的 Electron 22 更新可能需要更改原生模块。</li>
<li>测试 API 添加对连续测试的支持，参见：<a href="https://code.visualstudio.com/updates/v1_77#_finalized-support-for-continuous-test-runs">原文</a>。</li>
<li>新的 when 子句解析器，参见：<a href="https://code.visualstudio.com/updates/v1_77#_new-when-clause-parser">原文</a>。</li>
<li>源代码管理输入中的内联完成，参见：<a href="https://code.visualstudio.com/updates/v1_77#_inline-completions-in-source-control-input">原文</a>。</li>
</ul>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_77#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Windows 8 EOL 警告。</li>
<li>Base image updated for Snap package。</li>
<li>Exploring custom memory allocator for the extension host。</li>
</ul>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>添加新的编程语言主题。

<ul>
<li><a href="https://code.visualstudio.com/docs/languages/ruby">Ruby in VS Code</a>。</li>
<li><a href="https://code.visualstudio.com/docs/languages/polyglot">Polyglot Notebooks</a>。</li>
</ul></li>
</ul>
]]></description></item><item><title>VSCode 1.78 (2023-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_78_2023-04/</link><pubDate>Sun, 21 May 2023 00:49:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_78_2023-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_78">https://code.visualstudio.com/updates/v1_78</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>新的默认颜色主题 &lsquo;Dark Modern&rsquo; 和 &lsquo;Light Modern&rsquo;，取代了 &lsquo;Dark+&rsquo; 和 &lsquo;Light+&lsquo;。</li>

<li><p>Markdown 新增拖拽选择器。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Changing-how-an-image-is-dropped-using-the-drop-selector-widget.mp4" type="video/mp4">
</video></li>

<li><p>源代码变更消息输入框支持 Quick Fixes，下面是 Code Spell Checker 扩展进行拼写检查的例子。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Fixing-a-spelling-error-in-the-Source-Control-quickinput.mp4" type="video/mp4">
</video></li>

<li><p>Markdown 支持拖拽视频文件自动生成 video 标签。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Inserting-a-video-using-drag-and-drop.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<ul>
<li>Aria verbosity settings。</li>
<li>改进和统一的快速选择体验。</li>
<li>终端可访问缓冲区改进。</li>
<li>差异编辑器音频提示改进。</li>
<li>转到行/列，屏幕阅读器将阅读关联行的内容。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_78#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>新的默认颜色主题 &lsquo;Dark Modern&rsquo; 和 &lsquo;Light Modern&rsquo;，取代了 &lsquo;Dark+&rsquo; 和 &lsquo;Light+&lsquo;。</p>

<p><img src="/image/vscode/dark-light-modern-themes.png" alt="image" /></p></li>

<li><p>Profile 模板。VSCode 提供了一些针对各种场景的 Profile 配置模板，在创建 Profiles 的时候可以选择，点击：左下角齿轮 -&gt; Profiles -&gt; Create Profile&hellip; 即可打开打开模板列表。选择 Profile 模板后，您可以查看设置、扩展和其他数据，如果不想将个别项目包含在新 Profile 中，则可以将其删除。</p>

<p><img src="/image/vscode/profile-template-dropdown.png" alt="image" /></p>

<p><img src="/image/vscode/data-science-project-template.png" alt="image" /></p></li>

<li><p>编辑器左侧装饰栏，装饰渲染改进。所有调试相关的图标都显示在靠近行号的一侧。</p>

<p><img src="/image/vscode/glyph-decorations.png" alt="image" /></p></li>

<li><p>从图像预览中复制图像，现在可以使用 Ctrl+C 或右键单击预览并选择复制从内置图像预览中复制图像。复制的图像数据可以粘贴回 VSCode 或其他应用程序。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>拖拽选择器。例如将图片拖拽到 Markdown 编辑器中，将显示一个选择器图标，可以通过点击图标或者 <code>cmd + .</code> 显示列表，一旦开始键入或将光标移到插入的文本之外，下拉选择器就会消失。该特性可以通过 <code>&quot;editor.dropIntoEditor.showDropSelector&quot;: &quot;never&quot;</code> 关闭。扩展可以通过 <code>DocumentDropEditProvider</code> API 给拖拽选择器添加自定义项目。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Changing-how-an-image-is-dropped-using-the-drop-selector-widget.mp4" type="video/mp4">
</video></li>

<li><p>独立的颜色选择器，颜色选择器可以通过 <code>&gt;Show or Focus Standalone Color Picker</code> 命令呼出。可以通过 <code>editor.defaultColorDecorators</code> 配置项配置是否在任意语言的编辑器中展示颜色代码的装饰块 （例如，开启后 <code>rgb(206, 81, 81)</code> 字符串左侧将出现装饰块）。</p></li>

<li><p>snippet 变量添加时区偏移量。<code>CURRENT_TIMEZONE_OFFSET</code>。格式为 <code>+HHMM</code> 或 <code>-HHMM</code> (例如，<code>-0700</code>)，除此之外，其他的时间相关变量有 <code>CURRENT_YEAR</code>, <code>CURRENT_MONTH</code>, <code>CURRENT_DAY_NAME</code> 等。</p></li>

<li><p>diff 算法改进。VSCode 未来会将默认的 diff 算法设置为 <code>advanced</code>，目前仍然是 <code>legacy</code>。在多数情况下，新算法会产生更好的 diff，但对于某些代码来说可能会更慢。一些对比示例（legacy vs. advanced）参见：<a href="https://code.visualstudio.com/updates/v1_78#_diff-algorithm-improvements">原文</a>。</p></li>

<li><p>内联完成改进，代码进行了重写，修复了大量的 bug，参见：<a href="https://github.com/microsoft/vscode/issues?q=is%3Aclosed+is%3Aissue+milestone%3A%22April+2023%22+label%3Ainline-completions">Issue</a>。</p></li>
</ul>

<h2 id="扩展-extensions">扩展 (Extensions)</h2>

<ul>
<li>改进的扩展建议通知。将展示扩展作者。</li>

<li><p>通知已安装的已弃用扩展。</p>

<p><img src="/image/vscode/deprecated-extension-notification.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>源代码变更消息输入框支持 Quick Fixes，下面是 <a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker">Code Spell Checker</a> 扩展进行拼写检查的例子。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Fixing-a-spelling-error-in-the-Source-Control-quickinput.mp4" type="video/mp4">
</video></li>

<li><p>GitHub 存储库规则集。VSCode 已经允许您使用 <code>git.branchProtection</code> 设置定义分支保护。这个版本添加了一个新的实验性功能，它使用最近发布的 GitHub 存储库规则集来确定分支是否受到保护。如果您使用的是 GitHub 存储库规则集，则可以使用 <code>github.branchProtection</code> 设置启用此功能。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>类似 Markdown，支持将图片拖拽到 Notebook 的 Markdown cell，同样的也支持拖拽选择器。</p>

<p><img src="/image/vscode/notebook-drop.png" alt="image" />
<img src="/image/vscode/notebook-drop-attachment.png" alt="image" /></p></li>

<li><p>通过 <code>&gt;Notebook: Toggle Scroll Cell Output</code> (<code>Cmd+K Y</code>) 快速切换滚动模式。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Toggle-notebook-cell-scrolling.mp4" type="video/mp4">
</video></li>

<li><p>查找控件提升，可以通过 <code>notebook.find.scope</code> 配置项限制搜索范围，参见：<a href="https://code.visualstudio.com/updates/v1_78#_find-control-improvements">原文</a>。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown 支持拖拽视频文件自动生成 video 标签。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Inserting-a-video-using-drag-and-drop.mp4" type="video/mp4">
</video></li>

<li><p>HTML 中的 JavaScript 块，可以使用 <code>js/ts.implicitProjectConfig.strictNullChecks</code> 配置严格 null 值检查。</p></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<p>现在可以为单个测试打开连续运行。这需要一个支持连续运行的测试扩展，并采用了 supportsContinuousRun API 最后一次迭代的 API。</p>

<p>（关于 Continuous run 参见：<a href="https://github.com/microsoft/vscode/issues/134941">Issue</a>，v1.75 引入）</p>

<p><img src="/image/vscode/testing-continous-run.png" alt="image" /></p>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>将文件提交到 Git Large File Storage (LFS)。 github.dev 和 vscode.dev 支持提交到 github 托管的 LFS 而不需要安装 git lfs 扩展。</li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>参见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_78.md">Remote Development release notes</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python，参见：<a href="https://code.visualstudio.com/updates/v1_78#_python">原文</a>。</li>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_78#_jupyter">原文</a>。</li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_78#_github-pull-requests-and-issues">原文</a>。</li>
<li>GitHub Copilot，一些基于 chatgpt 的能力，参见：<a href="https://code.visualstudio.com/updates/v1_78#_github-copilot">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>TypeScript 5.1 支持。</li>

<li><p>使用 F2 重命名 jsx 标签。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Renaming-a-JSX-tag-using-F2.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Workspace edits can now create files directly from DataTransferFile，参见：<a href="https://code.visualstudio.com/updates/v1_78#_workspace-edits-can-now-create-files-directly-from-datatransferfile">原文</a>。</li>
<li>在 resolveCodeAction 中解析代码操作命令，参见：<a href="https://code.visualstudio.com/updates/v1_78#_resolve-code-action-commands-in-resolvecodeaction">原文</a>。</li>
<li>支持 <code>editor/lineNumber/context</code> 菜单。</li>
<li>身份验证 API 改进，参见：<a href="https://code.visualstudio.com/updates/v1_78#_authentication-api-improvements">原文</a>。</li>
</ul>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_78#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 22 的最后准备，参见：<a href="https://code.visualstudio.com/updates/v1_78#_electron-22-update">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.79 (2023-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_79_2023-05/</link><pubDate>Fri, 23 Jun 2023 20:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_79_2023-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_79">https://code.visualstudio.com/updates/v1_79</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>只读模式：通过 <code>files.readonlyInclude</code> 等配置项，可以配置指定目录的文件为只读（如 <code>node_modules</code>，防止意外修改）。</li>
<li>远程开发时，网络质量指示。当网络延迟达到一定程度是，左下角远程指示器的 hover 会展示延迟。</li>
<li><code>&gt;git: checkout to..</code> 可快速打开 github 或 github.dev。</li>
<li>JSX 标签编辑体验提升。a. 标签自动成对编辑； b. 重命名 html 标签，仅修改这一对标签。</li>
<li>支持 JSDoc <code>@param</code> 补全。</li>
<li>Markdown 编辑器支持粘贴剪切板或拖拽（按 shift）媒体文件，这些文件会自动保存到配置的目录，并生成对应的 Markdown 代码。</li>
<li>添加对 <a href="https://jsonlines.org/">JSON with Lines (jsonl)</a>  （每行一个json对象，通常用于日志、数据文件）语法高亮支持。</li>
<li>Electron 沙盒在该版本已正式启用，这是一个在给正在飞行的飞机更换引擎的过程，这个案例对架构升级类需求如何落地很有参考意义。整过程从 2020 年至今，经历了 3 年，详见博客：<a href="https://code.visualstudio.com/blogs/2022/11/28/vscode-sandbox#_process-sandboxing-in-a-nutshell">Migrating VS Code to Process Sandboxing</a>。</li>
</ul>

<h2 id="可访问性-accessibility">可访问性 (Accessibility)</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_79#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>只读模式，在某些开发场景中，将工作区的某些文件夹或文件显式标记为只读会很有帮助。例如，如果文件夹或文件内容由不同的进程管理（例如由 Node.js 包管理器管理的 node_modules 文件夹），将它们标记为只读可以避免无意的更改。可通过如下配置项进行配置：</p>

<ul>
<li><code>files.readonlyInclude</code> - 路径或 glob 模式，使文件在匹配时只读。</li>
<li><code>files.readonlyExclude</code> - 当文件与 files.readonlyIninclude 匹配时，用于跳过文件只读的路径或 glob 模式。</li>
<li><code>files.readonlyFromPermissions</code> - 磁盘上没有写权限的文件是否应该是只读的。</li>
</ul>

<p>根据设置规则，如果路径被视为只读，则无法从资源管理器中修改它（例如删除它），并且文本或 Notebook 编辑器是只读的。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Readonly-mode-set-for-a-node_modules-folder.mp4" type="video/mp4">
</video>

<p>除了如上配置外，还支持如下命令，来临时设置当前编辑器文件是否为只读。</p>

<ul>
<li><code>&gt;Set Active Editor Readonly in Session</code> - 标记活动的编辑器为只读。</li>
<li><code>&gt;Set Active Editor Writeable in Session</code> - 标记活动的编辑器为可写。</li>
<li><code>&gt;Toggle Active Editor Readonly in Session</code> - 活动的编辑器在只读和可写之间切换。</li>
<li><code>&gt;Reset Active Editor Readonly in Session</code> - 重设当前会话状态。</li>
</ul></li>

<li><p>Windows UNC 主机允许列表改进，参见：<a href="https://code.visualstudio.com/updates/v1_79#_windows-unc-host-allowlist-improvements">原文</a>。</p></li>

<li><p><code>workbench.editor.tabSizing</code> 配置添加 <code>fixed</code> 选项，即每个选项卡宽度相等。当空间有限时，选项卡将同等收缩到最小值。添加 <code>workbench.editor.tabSizingFixedMaxWidth</code> 设置选项卡的初始大小。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Fixed-tab-size.mp4" type="video/mp4">
</video>

<p>在此模式下，当您使用鼠标快速关闭选项卡时，选项卡的宽度保持稳定，以便通过单击同一点来关闭每个选项卡。当您将鼠标从编辑器选项卡区域移开时，宽度将会调整（和 Chrome Tab 类似，推荐配置）。</p></li>

<li><p>网络质量指示。当网络延迟达到一定程度是，左下角远程指示器的 hover 会展示延迟。</p>

<ul>
<li><p>高延迟 (web, desktop)</p>

<p><img src="/image/vscode/slow-network.png" alt="image" /></p></li>

<li><p>离线检测 (web only)</p>

<p><img src="/image/vscode/offline-indication.gif" alt="image" /></p></li>
</ul></li>

<li><p><a href="https://code.visualstudio.com/docs/sourcecontrol/github#_continue-working-on">Continue Working On</a> 提升，参见：<a href="https://code.visualstudio.com/updates/v1_79#_continue-working-on">原文</a>。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>Paste as，将一个文件粘贴到文本、notebook 编辑器时，会展示一个粘贴指示器（类似于上一个迭代拖拽指示器，可以通过 <code>cmd + .</code> 重新呼出）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Using-the-paste-as-control-to-change-how-an-image-is-inserted-into-a-notebook-Markdown-cell.mp4" type="video/mp4">
</video>

<p>可通过 <code>&quot;editor.pasteAs.showPasteSelector&quot;: &quot;never&quot;</code> 配置项关闭该特性。</p>

<p>也可以通过 <code>&gt;Paste As...</code> 命令从命令入口操作。</p></li>

<li><p>Quick suggestions and snippets 优化，在 snippet 中使用 tab 时，如果存在快速建议，则 tab 键的行为为接收建议，如果没有，则将光标跳转到下一个标定位置，更多参见：<a href="https://code.visualstudio.com/updates/v1_79#_quick-suggestions-and-snippets">原文</a>。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p><a href="https://code.visualstudio.com/docs/terminal/shell-integration">shell 集成</a>支持 fish shell 的自动集成。</p>

<p><img src="/image/vscode/terminal-fish-si.png" alt="image" /></p></li>

<li><p>终端支持上划线。</p>

<p><img src="/image/vscode/terminal-overline.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>默认分支名，改为 <code>main</code>。可通过 <code>git.defaultBranchName</code> 配置项配置。</li>

<li><p>分支选择器添加快速在 vscode.dev 或 GitHub 打开的按钮 （<code>&gt;git: checkout to..</code> 命令）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Open-a-branch-on-GitHub-com-from-the-branch-picker-com-from-the-branch-picker.mp4" type="video/mp4">
</video></li>

<li><p>Git status 使用相似性索引（与文件大小相比的添加/删除数量）来确定添加/删除对是否被视为重命名。现在，可以使用 <code>git.similarityThreshold</code> 设置来配置相似度阈值，该设置的值介于 0 到 100 之间。默认值为 50。</p></li>
</ul>

<h2 id="notebooks">Notebooks</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_79#_notebooks">原文</a>。</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>TypeScript 5.1</li>

<li><p>JSX 标签的首位关联编辑，该特性默认是关的，可以通过 <code>&quot;editor.linkedEditing&quot;: true</code> 配置项打开，也可以通过 <code>&gt;Start Linked Editing</code> 命令手动打开。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Linked-editing-some-JSX-tags.mp4" type="video/mp4">
</video></li>

<li><p>使用 F2 重命名时，仅重命名匹配的 JSX 标签。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Renaming-a-JSX-tag-using-F2.mp4" type="video/mp4">
</video>

<p>仅支持 <code>TypeScript 5.1+</code> 且重命名的是 HTML 原生标签时。可以使用 <code>javascript.preferences.renameMatchingJsxTags</code> 和 <code>typescript.preferences.renameMatchingJsxTags</code> 禁用此行为。</p></li>

<li><p>JSDoc <code>@param</code> 补全</p>

<p><img src="/image/vscode/js-param.png" alt="image" /></p>

<p>在 JS 中，可以选择自动类型声明占位符。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/at-param-completions-in-a-JavaScript-file.mp4" type="video/mp4">
</video></li>

<li><p>将外部媒体文件拖拽到 Markdown 编辑器按住 shift 松手后，文件将自动保存在当前文件所在目录，并生成 markdown 代码。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Coping-a-file-into-the-workspace-by-drag-and-dropping-it.mp4" type="video/mp4">
</video>

<p>此外，通过 <code>cmd + c</code> 同样可以把剪切板里的图片复制到文件所在目录，并生成 markdown 代码。</p>

<p>可以通过 <code>markdown.copyFiles.destination</code> 配置项配置，哪些 markdown 文件的媒体文件保存在哪个目录中，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;markdown.copyFiles.destination&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">当一个新文件被粘贴或拖拽在</span> <span style="color:#960050;background-color:#1e0010">/docs/api/readme.md</span> <span style="color:#960050;background-color:#1e0010">时，文件会保存在</span> <span style="color:#960050;background-color:#1e0010">/docs/api/images/readme/image.png。</span>
    <span style="color:#f92672">&#34;/docs/**/*&#34;</span>: <span style="color:#e6db74">&#34;images/${documentBaseName}/&#34;</span>,
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">甚至可以使用类似正则和类似</span> <span style="color:#960050;background-color:#1e0010">snippets</span> <span style="color:#960050;background-color:#1e0010">的语法。</span>
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">当一个新文件被粘贴或拖拽在</span> <span style="color:#960050;background-color:#1e0010">/docs2/api/readme.md</span> <span style="color:#960050;background-color:#1e0010">时，文件会保存在</span> <span style="color:#960050;background-color:#1e0010">/docs2/api/images/r/image.png.</span>
    <span style="color:#f92672">&#34;/docs2/**/*&#34;</span>: <span style="color:#e6db74">&#34;images/${documentBaseName/(.).*/$1/}/&#34;</span>,
    <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">/images/</span> <span style="color:#960050;background-color:#1e0010">表示</span> <span style="color:#960050;background-color:#1e0010">workspace</span> <span style="color:#960050;background-color:#1e0010">根目录的</span> <span style="color:#960050;background-color:#1e0010">images</span> <span style="color:#960050;background-color:#1e0010">目录。</span>
    <span style="color:#f92672">&#34;/docs3/**/*&#34;</span>: <span style="color:#e6db74">&#34;/images/&#34;</span>,
}</code></pre></div>
<p>可以通过 <code>markdown.copyFiles.overwriteBehavior</code> 配置项配置新的媒体文件是否覆盖旧的媒体文件，默认为添加序号。</p>

<p>该特性可以通过如下配置项关闭。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;markdown.editor.drop.copyIntoWorkspace&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;never&#34;</span>
<span style="color:#e6db74">&#34;markdown.editor.filePaste.copyIntoWorkspace&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;never&#34;</span></code></pre></div></li>

<li><p>Markdown 文件中 HTML 路径的智能提示，如 video。</p>

<p><img src="/image/vscode/md-html-support.png" alt="image" /></p></li>

<li><p>插入音频文件到 markdown，自动创建 <code>&lt;audio&gt;</code> 标签。</p></li>

<li><p>添加对 <a href="https://jsonlines.org/">JSON with Lines (jsonl)</a>  （每行一个json对象，通常用于日志、数据文件）语法高亮支持。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_79#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，参见：<a href="https://code.visualstudio.com/updates/v1_79#_github-copilot">原文</a>。</li>
<li>Python

<ul>
<li>在专用终端中运行 Python 文件 （<code>&gt;Python: Run Python File in Terminal</code> 命令）。</li>
<li>测试发现和运行被重写，可通过 <code>python.experiments.optInto</code> 添加 <code>pythonTestAdapter</code> 适配器试用该特性。</li>
<li>添加一个新的配置 <code>&quot;python.analysis.userFileIndexingLimit&quot;</code> 配置文件索引限制。</li>
</ul></li>
<li>Jupyter，参见：<a href="https://code.visualstudio.com/updates/v1_79#_jupyter">原文</a>。</li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_79#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview features)</h2>

<ul>
<li>insiders.vscode.dev 上的项目范围 JS/TS IntelliSense，参见：<a href="https://code.visualstudio.com/updates/v1_79#_project-wide-jsts-intellisense-on-insidersvscodedev">原文</a>。</li>
<li>在终端中支持查看图片（通过 <code>&quot;terminal.integrated.experimentalImageSupport&quot;: true</code> 配置项体验）。

<ul>
<li>cat <code>.six</code> 格式的图片。</li>
<li>安装并使用 imgcat 命令打开图片。</li>
<li>实测 remote ssh 好像不支持？</li>
</ul></li>
<li>其他参加：<a href="https://code.visualstudio.com/updates/v1_79#_typescript-52-support">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>提升 <code>vscode.fs</code> api 的性能。</li>
<li>更严格的状态栏 API，参见：<a href="https://code.visualstudio.com/updates/v1_79#_stricter-status-bar-api">原文</a>。</li>
<li>用于在任务完成时<a href="https://github.com/microsoft/vscode/blob/1899f626fdca44ff80c34ac0f0fe13fc0d3d0856/src/vscode-dts/vscode.d.ts#L7447-L7450">关闭</a>终端的任务呈现选项已最终确定。</li>
</ul>

<h2 id="提案-api-proposed-apis">提案 API (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_79#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Electron 沙盒在该版本已正式启用，这是一个在给正在飞行的飞机更换引擎的过程，这个案例对架构升级类需求如何落地很有参考意义。整过程从 2020 年至今，经历了 3 年，详见博客：<a href="https://code.visualstudio.com/blogs/2022/11/28/vscode-sandbox#_process-sandboxing-in-a-nutshell">Migrating VS Code to Process Sandboxing</a>。</li>
<li>仅重启扩展主机，目前应用在切换 profile 中，后续迭代仍在继续。</li>
<li>Windows 8 和 8.1 支持已结束。</li>
<li>Milestone automation，参见：<a href="https://code.visualstudio.com/updates/v1_79#_milestone-automation">原文</a>。</li>
</ul>

<h2 id="microsoft-build-中的-vs-code">Microsoft Build 中的 VS Code</h2>

<p>参见：<a href="https://code.visualstudio.com/updates/v1_79#_vs-code-at-microsoft-build">原文</a>。</p>
]]></description></item><item><title>VSCode 1.80 (2023-06) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_80_2023-06/</link><pubDate>Mon, 10 Jul 2023 00:45:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_80_2023-06/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_80">https://code.visualstudio.com/updates/v1_80</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>新增 <code>files.dialog.defaultPath</code> 配置项，可用来设置，文件选择器弹窗的默认路径。</li>
<li>通过配置项 <code>search.useIgnoreFiles</code> 可以控制在搜索时，是否使用 ignore 文件，默认为 <code>true</code> （即不会搜索 <code>.gitignore</code> 和 <code>.ignore</code> 匹配的文件）。</li>
<li>Hover 浮窗，支持通过窗口边缘调整浮窗大小。</li>
<li>终端支持多行超链接的识别和跳转，如 git 的 <code>@@</code>。</li>
<li>在面板区域新增测试输出面板，其输出内容通过 <code>xterm.js</code> 渲染，以支持终端转义字符，可通过 <code>&gt;Show Test Output</code> 命令打开。</li>
<li>Markdown 编辑器，新增 <code>markdown.editor.pasteUrlAsFormattedLink.enabled</code> 配置项（仅当 <code>editor.pasteAs.enabled</code> 为 true 时生效），默认为 false，当选中文字时，再粘贴一个链接时，该文字和链接将构造出一个 Markdown 链接。</li>
<li>通过安装 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">ms-python.debugpy</a> 可支持 Python 2.7 和 3.6 等旧版本 的 Debug 需求。。</li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_80#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>预览视频时，支持自动播放（<code>mediaPreview.video.autoPlay</code>）和循环播放（<code>mediaPreview.video.loop</code>），该配置项默认为 false。</li>

<li><p>尝试在只读编辑器上编辑时，展示 hover 提示，并可以通过 click here 快速打开设置，并展示 <code>files.readonly</code> 相关配置项。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Editor-readonly-message-indicator.mp4" type="video/mp4">
</video></li>

<li><p>新增 <code>files.dialog.defaultPath</code> 配置项，可用来设置，文件选择器弹窗的默认路径。</p></li>

<li><p>新增 <code>workbench.editor.doubleClickTabToToggleEditorGroupSizes</code> 配置项，可以配置是否双击标签栏最大化编辑器组，默认为 true，现在可以通过设置为 false 禁用该行为。</p></li>

<li><p>新增 <code>workbench.editor.tabSizingFixedMinWidth</code> 配置项，可以配置 <code>&quot;workbench.editor.tabSizing&quot;: &quot;fixed&quot;</code> 编辑器标签栏的最小宽度，默认为 50。</p></li>

<li><p>配置项 <code>workbench.editor.splitSizing</code> 新增选项 <code>auto</code> 并作为默认值：所有编辑器组平分相等的大小，除非手动更改过编辑器组的大小。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Editor-group-auto-split-sizing.mp4" type="video/mp4">
</video></li>

<li><p>通过配置项 <code>search.useIgnoreFiles</code> 可以控制在搜索时，是否使用 ignore 文件，默认为 <code>true</code> （即不会搜索 <code>.gitignore</code> 和 <code>.ignore</code> 匹配的文件）。</p></li>

<li><p>通过配置项 <code>comments.maxHeight</code> 控制评论小组件是滚动还是展开，默认为 true。</p></li>

<li><p>新增 <code>&gt;Help: Troubleshoot Issue</code> 命令，参见：<a href="https://code.visualstudio.com/updates/v1_80#_troubleshoot-issues-in-vs-code">原文</a>。</p></li>

<li><p>禁用 Chromium sandbox，参见：<a href="https://code.visualstudio.com/updates/v1_80#_disable-chromium-sandbox">原文</a> （原因参见：<a href="https://github.com/microsoft/vscode/issues/184687">issue</a>）。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>新增 <code>editor.smartSelect.selectSubwords</code> 配置项，默认为 true，是否应该选中子单词。</li>
<li>改进了 Emmet 对 JSX/TSX 中 CSS 模块的支持，参见：<a href="https://code.visualstudio.com/updates/v1_80#_improved-emmet-support-for-css-modules-in-jsxtsx">原文</a>。</li>

<li><p>Hover 浮窗，支持通过窗口边缘调整浮窗大小。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Resizable-hover-control.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>支持通过 <code>cat image.six</code> 或 <code>imgcat</code> 直接在终端中展示图片，详见：<a href="https://code.visualstudio.com/updates/v1_80#_image-support">原文</a>。</li>

<li><p>终端支持多行超链接的识别和跳转，如 git 的 <code>@@</code>，更多支持的格式参见：<a href="https://code.visualstudio.com/updates/v1_80#_multiline-and-range-link-formats">原文</a>。</p>

<p><img src="/image/vscode/terminal-link-git.png" alt="image" /></p></li>

<li><p>删除废弃的 shell 和 shell args 参数： <code>terminal.integrated.shell.*</code>、<code>terminal.integrated.shellArgs.*</code>。使用 <a href="https://code.visualstudio.com/docs/terminal/profiles">terminal profiles</a> 替代。</p></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li><p>在面板区域新增测试输出面板，其输出内容通过 <code>xterm.js</code> 渲染，以支持终端转义字符，可通过 <code>&gt;Show Test Output</code> 命令打开。</p>

<p><img src="/image/vscode/testing-terminal.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>关闭存储库状态将保存到工作区中，如需重新打开，可通过 <code>&gt;Git: Reopen Closed Repositories...</code> 命令打开。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>改进在 Remote 场景 Notebook 保存性能，之前每次保存都会发送全部数据，现在改为发送变更数据，目前可通过 <code>&quot;notebook.experimental.remoteSave&quot;: true</code> 配置开启。</li>
<li>笔记本全局工具栏重做，参见：<a href="https://code.visualstudio.com/updates/v1_80#_notebook-global-toolbar-rework">原文</a>。</li>
<li>交互式窗口在 reload 或重新打开后可以恢复之前的结果，参见：<a href="https://code.visualstudio.com/updates/v1_80#_interactive-window-backup-and-restore">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown 支持从图片预览窗口复制图片。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Copy-image-from-Markdown-preview.mp4" type="video/mp4">
</video></li>

<li><p>Markdown 编辑器，新增 <code>markdown.editor.pasteUrlAsFormattedLink.enabled</code> 配置项（仅当 <code>editor.pasteAs.enabled</code> 为 true 时生效），默认为 false，当选中文字时，再粘贴一个链接时，该文字和链接将构造出一个 Markdown 链接。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Paste-formatted-link-over-selected-text.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_80#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot 参见：<a href="https://code.visualstudio.com/updates/v1_80#_github-copilot">原文</a>。</li>
<li>Python

<ul>
<li>将 Python 调试能力移到 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">ms-python.debugpy</a> 扩展中。以实现使用最新版的 Python 插件的同时，支持 Python 2.7 和 3.6 等旧版本的 Debug 需求。</li>
<li>Pylance 支持本地化。</li>
<li>测试发现和运行重写仍在进行中，即将进入稳定状态。</li>
<li>更多，参见：<a href="https://code.visualstudio.com/updates/v1_80#_python">原文</a>。</li>
</ul></li>
<li>GitHub Pull Requests and Issues 参见：<a href="https://code.visualstudio.com/updates/v1_80#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_80#_preview-features">原文</a>。</p>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><a href="https://code.visualstudio.com/api/references/vscode-api#SecretStorage">SecretStorage API</a> 现在尝试使用 Electron 的 <a href="https://www.electronjs.org/docs/latest/api/safe-storage">safeStorage API</a> 而非 <a href="https://github.com/atom/node-keytar">keytar</a>。keytar 将被弃用。</li>
<li>提升 <code>vscode.fs.writeFile</code> 的性能。</li>
<li>Tree checkbox API， TreeItem checkboxState 已确定，参见：<a href="https://code.visualstudio.com/updates/v1_80#_tree-checkbox-api">原文</a>。</li>
<li>新增 <code>EnvironmentVariableCollection.description</code> 字段。</li>
</ul>

<h2 id="提案的-api-proposed-apis">提案的 API (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_80#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>在编译阶段修改导出的符号名，以减少代码尺寸，提高下载和加载速度。</li>
<li>编译阶段添加更多的校验和校验。</li>
<li>添加 Linux 性能测试机。</li>
<li>Event emitter 性能优化。</li>
<li>终端 pty 主机改进。</li>
</ul>

<p>更多，参见：<a href="https://code.visualstudio.com/updates/v1_80#_engineering">原文</a>。</p>

<h2 id="文档-documentation">文档 (Documentation)</h2>

<ul>
<li>新增 <a href="https://code.visualstudio.com/docs/csharp/get-started">C#</a> 一级菜单。</li>
<li>新增 <a href="https://code.visualstudio.com/docs/editor/glob-patterns">Glob Patterns Reference</a> 文章。</li>
</ul>
]]></description></item><item><title>VSCode 1.81 (2023-07) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_81_2023-07/</link><pubDate>Fri, 04 Aug 2023 23:55:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_81_2023-07/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_81">https://code.visualstudio.com/updates/v1_81</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>markdown 编辑器 <code>markdown.editor.pasteUrlAsFormattedLink.enabled</code> 配置添加 <code>smart</code> 可以更好的处理 url 粘贴。</li>
<li>更好的 diff 编辑器体验，使用 <code>&quot;diffEditor.experimental.useVersion2&quot;: true</code> 配置项开启。</li>
<li>Python，测试编辑器启用支持容错的 pytest case 发现，提高 pytest 测试 case 扫描的成功率。</li>
<li>更多的 pty 性能提升。</li>
<li>扩展制作：

<ul>
<li><code>workbench.action.openSettingsJson</code> 可定位到指定配置。</li>
<li><code>QuickPickItem</code> 添加对图标的支持。</li>
</ul></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略</p>

<h2 id="profiles">Profiles</h2>

<ul>
<li>支持创建仅包含部分内容的Profiles，那么这个Profiles使用后，该Profiles不包含的配置将使用 VSCode 的默认值。</li>

<li><p>设置编辑器 UI 配置项设置按钮添加 <code>Apply Setting to all Profiles</code> 选项。</p>

<p><img src="/image/vscode/profiles_apply_setting_all.png" alt="image" /></p>

<p>这会将设置的值应用于所有Profiles。任何Profiles对此设置的任何更新都会应用于所有Profiles。</p>

<p>可以通过取消选中 <code>Apply Setting to all Profiles</code> 选项来恢复此改动。</p></li>

<li><p>扩展 UI，设置按钮添加 <code>Apply an extension to all profiles</code> 选项</p>

<p><img src="/image/vscode/profiles_apply_extension_all.png" alt="image" /></p>

<p>这使得此扩展可在所有Profiles中使用。</p>

<p>可以通过取消选中 <code>Apply an extension to all profiles</code> 选项来恢复此状态。</p></li>

<li><p><code>&gt;Preferences: Open User Settings (JSON)</code> （ID <code>workbench.action.openSettingsJson</code>） 命令将打开当前使用的 Profiles 的 json 文件。<code>&gt;Preferences: Open Application Settings (JSON)</code> (ID <code>workbench.action.openApplicationSettingsJson</code>) 命令将打开应用程序粒度的配置文件（即默认 profiles）。</p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>在 VSCode 存在月度更新时，可通过左下角齿轮图标 <code>Show Update Release Notes</code> 菜单项，在更新之前预览发行说明。</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><code>markdown.editor.pasteUrlAsFormattedLink.enabled</code> 配置项添加一个 <code>smart</code> 选项，该选项可以根据上下文只能判断是否生成一个 <code>[]()</code>，比如一个连接粘贴在一个形如 <code>[abc]()</code> 的小括号里面，则不会再生成一个 <code>[]()</code>。</li>
<li>异步文档标记化，修复了一些 bug，可以通过 <code>editor.experimental.asyncTokenization</code> 配置项尝鲜启用。未来将会逐步设为默认。</li>
</ul>

<h2 id="diff-编辑器-diff-editor">diff 编辑器 (Diff editor)</h2>

<ul>
<li>diff 编辑器 v2 版本可通过 <code>&quot;diffEditor.experimental.useVersion2&quot;: true</code> 尝鲜使用。</li>

<li><p>折叠未更改的区域，通过 <code>diffEditor.experimental.collapseUnchangedRegions</code> 可以折叠未更改区域，可通过点击或拖动图标展示隐藏区域。</p>

<p><img src="/image/vscode/diffEditor_collapseUnchangedRegions.png" alt="image" /></p></li>

<li><p>差异区域算法，更好的处理文本不变仅缩进改变的场景。</p>

<p>之前</p>

<p><img src="/image/vscode/diff-editor-without-hunk-alignment.png" alt="image" /></p>

<p>之后</p>

<p><img src="/image/vscode/diff-editor-with-hunk-alignment.png" alt="image" /></p></li>

<li><p>新的差异算法已被设为默认，算法说明，参见：之前的<a href="https://code.visualstudio.com/updates/v1_78#_diff-algorithm-improvements">发行说明</a>。</p></li>

<li><p>diff 降噪，减少干扰。</p>

<p>之前</p>

<p><img src="/image/vscode/diff-algorithm-before.png" alt="image" /></p>

<p>之后</p>

<p><img src="/image/vscode/diff-algorithm-after.png" alt="image" /></p></li>

<li><p>新增 <code>&gt;Diff Editor: Switch Sides</code> 命令，可以将 diff 编辑器的左右互换。</p></li>

<li><p>更多参见 <a href="https://code.visualstudio.com/updates/v1_80#_new-diff-editor">v1.80 预览特性</a>中的说明。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>内联终端选项卡中的自定​​义悬停</p>

<p><img src="/image/vscode/terminal-hover.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>支持位于符号链接路径中的 git 库，更多参见：<a href="https://code.visualstudio.com/updates/v1_81#_support-git-repositories-with-symbolic-links">原文</a>。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>JavaScript 调试器支持使用 fnm 来切换 NodeJS 版本（通过 <code>runtimeVersion</code> 配置的）。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>支持对关闭的 Notebook 的富文本进行搜索。</li>
<li>改进了大型流输出的性能。</li>
<li>粘性滚动的支持。</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_81#_notebooks">原文</a>。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>vscode.dev 现在始终加载最新版本的内置扩展。这意味着当您打开 vscode.dev 时，您将不再看到 “需要重新加载” 通知以将内置扩展更新到最新版本。</li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_81#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，略，参见：<a href="https://code.visualstudio.com/updates/v1_81#_github-copilot">原文</a>。</li>

<li><p>Python</p>

<ul>
<li>测试浏览器支持容错 pytest case 发现，可通过 <code>pythonTestAdapter</code> 配置项启用或退回。</li>

<li><p>调试器配置，<code>args</code> 参数可以配置 <code>${command:pickArgs}</code>，以实现在启动调试时动态填写命令行参数的能力。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Debugging_a_Python_file_providing_arguments_through_the_Python_ File_with Arguments.mp4" type="video/mp4">
</video></li>

<li><p>调试器提供了 NodeJS 扩展 API 包，<a href="https://www.npmjs.com/package/@vscode/python-extension"><code>@vscode/python-extension</code></a>，第三方开发者可以扩展 Python 的能力。</p></li>
</ul></li>

<li><p>其他，参见：<a href="https://code.visualstudio.com/updates/v1_81#_jupyter">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><p><code>workbench.action.openSettingsJson</code> 命令参数，添加 <code>revealSetting</code>，可以定位到指定的配置 Key 的位置。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#a6e22e">vscode</span>.<span style="color:#a6e22e">commands</span>.<span style="color:#a6e22e">executeCommand</span>(<span style="color:#e6db74">&#39;workbench.action.openSettingsJson&#39;</span>, {
    <span style="color:#a6e22e">revealSetting</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;editor.renderWhitespace&#39;</span> }
});
</code></pre></div></li>

<li><p>新的 TestController.invalidateTestResults 方法，可以向用户展示那些测试结果已经不可用。</p></li>

<li><p>添加对使用 Kerberos 认证的网络代理的支持。</p></li>

<li><p>QuickPickItem 中图标 API 已确定，通过 iconPath 选项指定。</p>

<p><img src="/image/vscode/icons-in-quick-pick.png" alt="image" /></p></li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>修改符号名缩小 JavaScript 代码体积，参见博客： <a href="https://code.visualstudio.com/blogs/2023/07/20/mangling-vscode">Shrinking VS Code with name mangling</a>。</li>
<li>更多 pty 主机改进，略，参见：<a href="https://code.visualstudio.com/updates/v1_81#_more-pty-host-improvements">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.82 (2023-08) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_82_2023-08/</link><pubDate>Sat, 16 Sep 2023 23:22:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_82_2023-08/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_82">https://code.visualstudio.com/updates/v1_82</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>内置端口转发：通过该能力，可以将一个本地启动的 HTTP/HTTPS 端口转发到公网中，连接互联网的任意用户都可以通过类似 <code>https://xxx-3000.asse.devtunnels.ms/</code> 的域名访问到这个本地端口（中国大陆地区无法使用）。</li>

<li><p>粘性滚动（可通过： <code>View: Toggle Sticky Scroll</code> 命令开启）：横向滚动条滚动跟随；按住 <code>shift</code> hover 在粘性视图的文本上，可显示当前块的最后一行，单击可以将光标移动到这一行；折叠图标也添加到了粘性视图左侧。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-improvements.mp4" type="video/mp4">
</video></li>

<li><p>触发了代码操作（Code Action）和快速修复（Quick Fix）导航菜单后，可以通过输入菜单项中包含的字符快速选中选项（之前只能通过上下键操作）。</p>

<p><img src="/image/vscode/action-control-fuzzy-search.gif" alt="image" /></p></li>

<li><p>差异编辑器，可识别代码移动 （<code>&quot;diffEditor.experimental.showMoves&quot;: true</code>）。</p>

<p><img src="/image/vscode/diffEditor-movedCodeDetection.gif" alt="image" /></p></li>

<li><p>通过 cmd + p，输入 <code>%</code>，即可按照文本内容进行搜索，并进行快速跳转。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Quick-Search-demo.mp4" type="video/mp4">
</video></li>

<li><p>编辑器区域。分屏打开多个编辑区或终端时，通过 <code>&quot;accessibility.dimUnfocused.enabled&quot;</code> 配置项，可将未获得焦点的窗口变暗来更加容易的区分焦点。</p>

<p><img src="/image/vscode/dim-unfocused.png" alt="image" /></p></li>

<li><p>更新到 Electron 25，绑定的 Chromium 更新到 114.0.5735.289， Node.js 更新到 18.15.0。由于 nodejs 版本从 16 更新到了 18，扩展作者需要关注一些兼容性变更，参见：<a href="https://code.visualstudio.com/updates/v1_82#_update-highlights-for-nodejs">原文</a>。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>内置端口转发：通过该能力，可以将一个本地启动的 HTTP/HTTPS 端口转发到公网中，连接互联网的任意用户都可以通过类似 <code>https://xxx-3000.asse.devtunnels.ms/</code> 的域名访问到这个本地端口。使用时需登录 github/微软 账号，并提供公开和私有选项，私有选项只有登录了相同的账号，才能访问该。换句话说，微软给 VSCode 提供了一个免费内网穿透的能力。此外，该能力在中国大陆地区无法使用。更多详见：文档 <a href="https://code.visualstudio.com/docs/editor/port-forwarding">Local Port Forwarding</a>。</p>

<p><img src="/image/vscode/ports-view.png" alt="image" /></p></li>

<li><p>位于标题栏的命令中心（带有搜索图标的输入框），已经默认开启（如需关闭，可通过右击标题栏关闭显示）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Command-Center.mp4" type="video/mp4">
</video></li>

<li><p>新的设置项 <code>workbench.editor.preventPinnedEditorClose</code> 默认为 <code>keyboardAndMouse</code>，表示 pin 住的编辑器，不能通过快捷键和鼠标中键关闭。</p></li>

<li><p>状态栏新增和更新主题颜色，参见：<a href="https://code.visualstudio.com/updates/v1_82#_new-and-updated-themable-colors-for-the-status-bar">原文</a>。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>粘性滚动 UI 改进（粘性滚动可通过： <code>View: Toggle Sticky Scroll</code> 命令开启）。</p>

<ul>
<li>默认情况下，横向滚动条滚动时，粘性视图的文本也会移动。（可通过 <code>editor.stickyScroll.scrollWithEditor</code> 配置项更改）</li>
<li>按住 <code>shift</code> hover 在粘性视图的文本上，可显示当前块的最后一行，单击可以将光标移动到这一行。</li>
<li>折叠图标也添加到了粘性视图左侧。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-improvements.mp4" type="video/mp4">
</video></li>

<li><p>保存时对 JSON 进行排序（通过 <code>json.sortOnSave.enable</code> 配置可以开启）。</p></li>

<li><p>触发了代码操作（Code Action）和快速修复（Quick Fix）导航菜单后，可以通过输入菜单项中包含的字符快速选中选项（之前只能通过上下键操作）。</p>

<p><img src="/image/vscode/action-control-fuzzy-search.gif" alt="image" /></p></li>
</ul>

<h2 id="差异编辑器-diff-editor">差异编辑器 (Diff Editor)</h2>

<ul>
<li><p>代码移动检测，并通过 UI 展示出来，通过 <code>&quot;diffEditor.experimental.showMoves&quot;: true</code> 配置可以打开该特性。</p>

<p><img src="/image/vscode/diffEditor-movedCodeDetection.gif" alt="image" /></p></li>

<li><p>通过 <code>&quot;diffEditor.hideUnchangedRegions.enabled&quot;: true</code> 可以隐藏未改变的代码块。</p>

<p><img src="/image/vscode/diffEditor-collapsedCodeHeaders.gif" alt="image" /></p></li>

<li><p>动态布局：如果差异编辑器的宽度太小，编辑器会自动切换到内联视图。如果编辑器再次足够宽，则恢复之前的布局。通过 <code>&quot;diffEditor.useInlineViewWhenSpaceIsLimited&quot;: false</code> 可禁用此行为。</p></li>

<li><p>按钮切换状态优化，通过是否显示背景色来展示切换还是未切换（之前是通过字体颜色，不符合直觉）。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>通过 <code>terminal.integrated.hideOnStartup</code> 配置项，可以控制是否展示终端视图。</p>

<ul>
<li><code>never</code> 默认，从不隐藏终端。</li>
<li><code>whenEmpty</code> 没有持久化会话被恢复时隐藏中隐藏终端视图。</li>
<li><code>always</code> 总是隐藏。</li>
</ul></li>

<li><p>当向终端粘贴文本看到 <code>[201~</code> 之类的文本时，说明终端不支持 <code>bracketed paste mode</code>，可通过 <code>terminal.integrated.ignoreBracketedPasteMode</code> 禁用，详见：<a href="https://code.visualstudio.com/docs/terminal/basics#_i-see-1-or-201-when-i-paste-something">文档</a>。</p></li>

<li><p>通过 <code>terminal.integrated.focusAfterRun</code> 配置型，可以控制执行 <code>Terminal: Run Selected Text In Active Terminal</code> 命令后，是否聚焦在终端中。</p></li>

<li><p>类似与编辑器搜索，终端搜索控件可以通过左侧边框调整控件宽度。</p>

<p><img src="/image/vscode/terminal-find-resize.png" alt="image" /></p></li>

<li><p>在禁用 GPU 渲染的情况下（DOM 渲染模式），提高了终端的渲染性能。</p></li>

<li><p>更好的选中渲染，参见：<a href="https://code.visualstudio.com/updates/v1_82#_better-selection-rendering">原文</a>。</p></li>

<li><p>遵守暗淡文本最小对比度的一半，参见：<a href="https://code.visualstudio.com/updates/v1_82#_respect-half-minimum-contrast-ratio-for-dimmed-text">原文</a>。</p></li>

<li><p>通过 <code>terminal.integrated.cursorStyleInactive</code> 配置项，配置未聚焦时，光标的外观。</p></li>

<li><p>改善打开检测到的链接的行为，参见：<a href="https://code.visualstudio.com/updates/v1_82#_improved-terminal-open-detected-link-behavior">原文</a>。</p></li>

<li><p>新的连接检测格式，参见：<a href="https://code.visualstudio.com/updates/v1_82#_new-link-formats">原文</a>。</p></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>JavaScript 调试器

<ul>
<li>自动将 WebAssembly 模块反编译成文本模式然后进行调。</li>
<li>Source map 加载速度提升。</li>
<li>更多参见：<a href="https://code.visualstudio.com/updates/v1_82#_javascript-debugger">原文</a>。</li>
</ul></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li><p>改进调试状态区域。</p>

<p><img src="/image/vscode/testing-status-area.png" alt="image" /></p></li>

<li><p>链接检测在测试终端中的输出现已工作。</p></li>

<li><p>改进了测试相关输出的体验，参见：<a href="https://code.visualstudio.com/updates/v1_82#_improved-experience-for-testcorrelated-output">原文</a>。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>复制单元格输出，参见：<a href="https://code.visualstudio.com/updates/v1_82#_copy-cell-output">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>JavaScript / TypeScript</p>

<ul>
<li>TypeScript 升级到 5.2，参见：<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-2">TypeScript blog</a>。</li>
<li>支持移动文件重构，将自动修改引用位置的导入。</li>
<li>内联变量重构：支持将变量内联到代码中，将变量对应的表达式内联到 return 语句中。</li>

<li><p>参数提示可点击，通过 <code>&quot;editor.inlayHints.enabled&quot;: &quot;on&quot;</code> 配置项开启后（默认开启），通过 cmd + 单击即可跳转到函数参数声明位置。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Ctrl_Cmd-clicking-on-a-parameter-inlay-hint-to-jump-to-its-declaration.mp4" type="video/mp4">
</video></li>
</ul></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li>略，参见：<a href="https://code.visualstudio.com/updates/v1_82#_remote-development">原文</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，略，参见：<a href="https://code.visualstudio.com/updates/v1_82#_github-copilot">原文</a>。</li>
<li>Python

<ul>
<li>新增文档： <a href="https://code.visualstudio.com/docs/python/formatting">Python Formatting</a>。</li>
<li>创建新的终端时，通过 VSCode 新的 API 设置终端的环境变量，而非发送 active 命令，可通过 <code>&quot;python.experiments.optInto&quot;: [&quot;pythonTerminalEnvVarActivation&quot;]</code> 配置项启用该配置。</li>
<li>Recreate or use existing .venv environment，参见：<a href="https://code.visualstudio.com/updates/v1_82#_recreate-or-use-existing-venv-environment">原文</a>。</li>
</ul></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>支持文本搜索，通过 cmd + p，输入 <code>%</code>，即可按照文本内容进行搜索，并进行快速跳转。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Quick-Search-demo.mp4" type="video/mp4">
</video></li>

<li><p>编辑器区域。分屏打开多个编辑区或终端时，通过 <code>&quot;accessibility.dimUnfocused.enabled&quot;</code> 配置项，可将未获得焦点的窗口变暗来更加容易的区分焦点（通过 <code>&quot;accessibility.dimUnfocused.opacity&quot;</code> 配置项，可配置光度）。</p>

<p><img src="/image/vscode/dim-unfocused.png" alt="image" /></p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><code>DocumentRangeFormattingEditProvider</code> API 支持批量范围格式化。</li>
<li><code>EnvironmentVariableCollection</code> API 支持将作用域限制到单个工作空间。</li>
<li>Configure when a EnvironmentVariableMutator is applied，参见：<a href="https://code.visualstudio.com/updates/v1_82#_configure-when-a-environmentvariablemutator-is-applied">原文</a>。</li>
</ul>

<h2 id="提案-api-proposed-apis">提案 API (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_82#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 25，绑定的 Chromium 更新到 114.0.5735.289， Node.js 更新到 18.15.0。由于 nodejs 版本从 16 更新到了 18，扩展作者需要关注一些兼容性变更，参见：<a href="https://code.visualstudio.com/updates/v1_82#_update-highlights-for-nodejs">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.83 (2023-09) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_83_2023-09/</link><pubDate>Sun, 15 Oct 2023 16:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_83_2023-09/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_83">https://code.visualstudio.com/updates/v1_83</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>命令面板列表，展示提供和输入关键字相似的结果。</p>

<p><img src="/image/vscode/similar-commands-3.png" alt="image" /></p></li>

<li><p>编辑器 Tab 栏高度配置，通过 <code>window.density.editorTabHeight</code> 配置为 <code>compact</code>，可以减少 Tab 高度。</p>

<p><img src="/image/vscode/editor-tab-height-compact.png" alt="image" /></p></li>

<li><p>将编辑器选项卡固定在单独的行上，通过 <code>workbench.editor.pinnedTabsOnSeparateRow</code> 设置项来配置。</p>

<p><img src="/image/vscode/pinned-tabs-on-separate-row.gif" alt="image" /></p></li>

<li><p>改进了编辑器操作的溢出行为，一些常用的图标，将不会被收到 <code>...</code> 溢出菜单里。</p></li>

<li><p>快速修复 <code>Ctrl+.</code> (命令ID <code>editor.action.quickFix</code>) 新增一个 <code>editor.codeActionWidget.includeNearbyQuickfixes</code> 配置项，可以激活光标附近的快速修复，而不要求光标在那一行。</p>

<p><img src="/image/vscode/nearest-quick-fix.gif" alt="image" /></p></li>

<li><p>新增一个配置 <code>&quot;debug.toolBarLocation&quot;: &quot;commandCenter&quot;</code> ，可以在调试时将调试控制条展示到命令中心。</p>

<p><img src="/image/vscode/cc-debugtoolbar.png" alt="image" /></p></li>

<li><p>浮动编辑器窗口探索，预计最早于 10 月 Insiders 版推出。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Floating-editor-windows.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_83#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>命令面板列表，展示提供和输入关键字相似的结果。</p>

<p><img src="/image/vscode/similar-commands-3.png" alt="image" /></p></li>

<li><p>Profile 图标，在创建 Profile 的时候，可以给 Profile 配置一个图标。</p>

<p><img src="/image/vscode/profile-icon-selection.png" alt="image" /></p>

<p>激活后，会展示在做左下角</p>

<p><img src="/image/vscode/active-profile-icon.png" alt="image" /></p></li>

<li><p>编辑器 Tab 栏高度配置，通过 <code>window.density.editorTabHeight</code> 配置为 <code>compact</code>，可以减少 Tab 高度。</p>

<p><img src="/image/vscode/editor-tab-height-compact.png" alt="image" /></p></li>

<li><p>将编辑器选项卡固定在单独的行上，通过 <code>workbench.editor.pinnedTabsOnSeparateRow</code> 设置项来配置。</p>

<p><img src="/image/vscode/pinned-tabs-on-separate-row.gif" alt="image" /></p></li>

<li><p>设置编辑器搜索调整，略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_settings-editor-search-adjustments">原文</a>。</p></li>

<li><p>改进了编辑器操作的溢出行为，一些常用的图标，将不会被收到 <code>...</code> 溢出菜单里。</p></li>

<li><p>颜色主题选择器现在显示主题标识符，而不是仅仅显示翻译名。</p>

<p><img src="/image/vscode/color-theme-id.png" alt="image" /></p></li>
</ul>

<h2 id="评论-comments">评论 (Comments)</h2>

<ul>
<li><p>评论编辑器的高度随着编辑内容的行数自动增长。</p>

<p><img src="/image/vscode/expanding_comment_editor.gif" alt="image" /></p></li>

<li><p>设置项 <code>comments.openView</code> 新增一个 <code>firstFileUnresolved</code> 选项，第一次打开带有未解析注释的文件时。打开 comments 视图。</p></li>

<li><p>默认情况下，解决的评论将被默认被折叠。可以通过设置 <code>comments.collapseOnResolve</code> 禁用此功能。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p><code>editor.codeActionsOnSave</code> 配置的格式由 <code>map&lt;string, bool&gt;</code> 变为 <code>map&lt;string, string&gt;</code> value 部分可选项变为：</p>

<ul>
<li><code>explicit</code>：当显式的（如 cmd + s）保存时，才触发 action。和之前的 <code>true</code> 一样。</li>
<li><code>always</code>：在显式（如 cmd + s）保存，或者开启了自动保存自动保存时，以及窗口或焦点更改自动保存时，都会触发代码操作。</li>
<li><code>never</code>：保存时从不触发代码操作。和之前的 <code>false</code> 一样。</li>
</ul></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_notebooks">原文</a>。</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>Perl 6 语言已重命名为 Raku（语言标识符 raku），并且 Raku 将自动选择作为 .raku 文件的语言。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>JavaScript 调试器，JavaScript 调试器现在可以调试编译到 WebAssembly 中的代码（如果它包含 DWARF 调试信息）。例如，可以调试使用 Emscripten 编译的 C++ 代码：</p>

<p><img src="/image/vscode/wasm-dwarf.png" alt="image" /></p></li>
</ul>

<p>其他，略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_webassembly-debugging">原文</a>。</p>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，略，参见：<a href="https://code.visualstudio.com/updates/v1_83#_github-copilot">原文</a>。</li>
<li>Jupyter，略，参见：<a href="https://code.visualstudio.com/updates/v1_83#_jupyter">原文</a>。</li>

<li><p>Python</p>

<ul>
<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">Python 调试器</a>，添加 <code>debugpy.debugJustMyCode</code> 配置项，可以统一配置是否调试库。</p>

<p><img src="/image/vscode/debugpy-debug-just-my-code.png" alt="image" /></p></li>

<li><p>新增 <code>pylint.lintOnChange</code> 配置项，支持在键入的时候执行 lint。</p></li>

<li><p>Mypy extension reporting scope and daemon mode，略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_mypy-extension-reporting-scope-and-daemon-mode">原文</a>。</p></li>

<li><p>Update on call argument inlay hints setting，略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_update-on-call-argument-inlay-hints-setting">原文</a>。</p></li>

<li><p>废弃 Python 3.7 支持。</p></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_83#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>快速修复 <code>Ctrl+.</code> (命令ID <code>editor.action.quickFix</code>) 新增一个 <code>editor.codeActionWidget.includeNearbyQuickfixes</code> 配置项，可以激活光标附近的快速修复，而不要求光标在那一行。</p>

<p><img src="/image/vscode/nearest-quick-fix.gif" alt="image" /></p></li>

<li><p>源代码管理，新增同步视图，可以展示本地未提交到远端的情况。</p>

<p><img src="/image/vscode/scm-sync-view.png" alt="image" /></p></li>

<li><p>新增一个配置 <code>&quot;debug.toolBarLocation&quot;: &quot;commandCenter&quot;</code> ，可以在调试时将调试控制条展示到命令中心。</p>

<p><img src="/image/vscode/cc-debugtoolbar.png" alt="image" /></p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>语言语法配置支持类似的 <code>&quot;\\p{Letter}+&quot;</code> 来配置 Unicode 字符类转义验证，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_support-for-unicode-character-class-escapes-for-string-setting-validation">原文</a>。</li>
<li>贡献到终端菜单，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_contribute-to-terminal-menus">原文</a>。</li>
<li>新增 <code>env.onDidChangeShell</code> 事件。</li>
<li>keytar 从 VS Code 中删除，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_keytar-removed-from-vs-code">原文</a>。</li>
</ul>

<h2 id="语言服务器协议-language-server-protocol">语言服务器协议 (Language Server Protocol)</h2>

<p>略，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_language-server-protocol">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li><p>浮动编辑器窗口探索，预计最早于 10 月 Insiders 版推出。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Floating-editor-windows.mp4" type="video/mp4">
</video></li>

<li><p>vscode.dev 现在是跨源隔离的，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_vscodedev-is-now-cross-origin-isolated">原文</a>。</p></li>

<li><p>设置同步故障排除，更多参见：<a href="https://code.visualstudio.com/updates/v1_83#_settings-sync-troubleshooting">原文</a>。</p></li>
</ul>
]]></description></item><item><title>VSCode 1.84 (2023-10) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_84_2023-10/</link><pubDate>Sun, 15 Oct 2023 16:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_84_2023-10/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_84">https://code.visualstudio.com/updates/v1_84</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>VSCode UI “重大” 改进！，最左侧巨丑的图标长条终于有了改进。这些图标现在可以在主侧栏上方横向放置。而设置、用户等图标放到标题栏的右侧。</p>

<p><img src="/image/vscode/activity_bar_position.gif" alt="image" /></p></li>

<li><p>编辑器组支持最大化。</p>

<p><img src="/image/vscode/maximize-editor-group.gif" alt="image" /></p></li>

<li><p>多文档高亮。当窗口存在多个文档编辑器时，在任意文档选中文字，在其他的文档中匹配的文字也会高亮显示（通过 <code>editor.multiDocumentOccurrencesHighlight</code> 配置项打开）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Multi-document-highlighting-in-VS-Code.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<ul>
<li>清除、格式化和保存选择加入的音频提示。</li>
<li>Windows 放大镜现在可以正确跟随 VS Code 中的光标。</li>
</ul>

<p>其他，略，参见：<a href="https://code.visualstudio.com/updates/v1_84#_accessible-view-improvements">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>自定义活动栏位置（活动栏为默认视图最左侧的一列图标），可以像辅助侧栏一样放到主侧栏的上方横向放置。而设置、用户等图标放到标题栏的右侧。可通过下图方式或 <code>&quot;workbench.activityBar.location&quot;: &quot;top&quot;</code> 配置项目设置。</p>

<p><img src="/image/vscode/activity_bar_position.gif" alt="image" /></p></li>

<li><p>新增 <code>workbench.editor.showTabs</code> 配置项来设置编辑器选项卡，<code>none</code> 隐藏，<code>multiple</code> （默认）总是显示，<code>single</code> 只显示当前打开的编辑器的选项卡标题。</p>

<p><img src="/image/vscode/hide-tab-bar.gif" alt="image" /></p></li>

<li><p>编辑器组支持最大化，点击编辑器组右上角溢出菜单的 maximize group (cmd+K cmd+M)，或者在设置项 <code>&quot;workbench.editor.doubleClickTabToToggleEditorGroupSizes&quot;</code> 为 <code>&quot;maximize&quot;</code> 时双击标题，将最大化当前编辑器组。</p>

<p><img src="/image/vscode/maximize-editor-group.gif" alt="image" /></p></li>

<li><p>设置编辑器搜索支持近义词、近似语义匹配，类似于上一个版本的命令面板的匹配。</p>

<p><img src="/image/vscode/se-natlang-search-2.png" alt="image" /></p></li>

<li><p><code>vscode://</code> 协议打开二次确认，该行为可通过如下配置项禁用：</p>

<ul>
<li><code>security.promptForLocalFileProtocolHandling</code> - 打开本地链接 （<code>vscode://file/path/to/file</code>）</li>
<li><code>security.promptForRemoteFileProtocolHandling</code> - 打开远端链接 （<code>vscode://vscode-remote/ssh-remote+[USER@]HOST[:PORT]/path/to/file</code>）</li>
</ul>

<p><img src="/image/vscode/confirm-protocol-link.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>（上版本为预览特性）快速修复 <code>Ctrl+.</code> (命令ID <code>editor.action.quickFix</code>) 新增一个 <code>editor.codeActionWidget.includeNearbyQuickfixes</code> 配置项，可以激活光标附近的快速修复，而不要求光标在那一行。</p>

<p><img src="/image/vscode/nearest-quick-fix.gif" alt="image" /></p></li>

<li><p>多文档高亮，通过 <code>editor.multiDocumentOccurrencesHighlight</code> 配置项可打开。当窗口存在多个文档编辑器时，在任意文档选中文字，在其他的文档中匹配的文字也会高亮显示。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Multi-document-highlighting-in-VS-Code.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>Force push using &ndash;force-if-includes，详见：<a href="https://code.visualstudio.com/updates/v1_84#_force-push-using-forceifincludes">原文</a>。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>使用 Shift+Enter 通过笔记本执行时如何显示下一个 Cell 已得到改进，以帮助专注于输出。这也减少了重新执行已经有输出的 Cell 时的单元移动量。</li>
<li>IPython 堆栈跟踪渲染。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_84#_notebooks">原文</a>。</p>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>JavaScript 调试器</p>

<ul>
<li><p>改进事件监听断点视图，采用树形复选框视图。</p>

<p><img src="/image/vscode/js-debug-event-listener-bps.png" alt="image" /></p></li>

<li><p>Better handling of sourcemap renames，参见：<a href="https://code.visualstudio.com/updates/v1_84#_better-handling-of-sourcemap-renames">原文</a>。</p></li>
</ul></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_84#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，详见：<a href="https://code.visualstudio.com/updates/v1_84#_github-copilot">原文</a>。</li>

<li><p>Python</p>

<ul>
<li><p>改进了在终端中运行行（shift + enter）特性，通过 <code>&quot;python.experiments.optInto&quot;: [&quot;pythonREPLSmartSend&quot;]</code> 配置项可以智能的根据语义选择发送到 REPL 的内容。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/A-series-of-Python-code-selections-being-iteratively-executed-on-Shift-Enter.mp4" type="video/mp4">
</video></li>

<li><p>Python linting 扩展的改进，略，参见：<a href="https://code.visualstudio.com/updates/v1_84#_improvements-to-python-linting-extensions">原文</a>。</p></li>

<li><p>启用 Python 扩展内置 lint。</p></li>

<li><p>当打开未启用 python 虚拟环境时，会弹出创建虚拟环境的通知。通过 <code>python.python.createEnvironment.trigger</code> 设置为 <code>off</code> 关闭通知。</p></li>

<li><p>虚拟环境停用助手，详见：<a href="https://code.visualstudio.com/updates/v1_84#_virtual-environment-deactivation-helper">原文</a>。</p></li>

<li><p>测试输出的改进，详见：<a href="https://code.visualstudio.com/updates/v1_84#_improvements-to-test-output">原文</a>。</p></li>

<li><p>Python 调试器扩展现在提供特定于平台的版本，因此每次更新时仅安装必要的特定于平台的文件。这可以减少扩展的大小并有助于缩短启动时间。</p></li>

<li><p>Tensorboard 相关能力从 Python 扩展中移到新的 <a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard">Tensorboard 扩展</a>。</p></li>
</ul></li>

<li><p>Jupyter，略，详见：<a href="https://code.visualstudio.com/updates/v1_84#_jupyter">原文</a>。</p></li>

<li><p>新增 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-speech">VS Code Speech 扩展</a>，配合 GitHub Copilot Chat 可以语音输入。</p></li>

<li><p>GitHub Pull Requests and Issues，略，详见：<a href="https://code.visualstudio.com/updates/v1_84#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>浮动编辑器窗口，VS Code Insiders 已可用。</li>
<li>实验性 wasm-wasi-core 扩展中添加了对 WASM/WASI 语言服务器的支持，详见：<a href="https://code.visualstudio.com/updates/v1_84#_wasmwasi-support-for-language-servers">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>VSCode 扩展程序测试执行器改进，略，详见：<a href="https://code.visualstudio.com/updates/v1_84#_improved-test-runner">原文</a>。</li>
<li>TestMessage.contextValue API 最终确定，略，详见：<a href="https://code.visualstudio.com/updates/v1_84#_finalized-testmessagecontextvalue-api">原文</a>。</li>

<li><p>codicons 更新，详见：<a href="https://code.visualstudio.com/updates/v1_84#_updated-codicons">原文</a>。</p>

<p><img src="/image/vscode/codicons-oct-2023-release.png" alt="image" /></p></li>

<li><p>新的颜色主题项 <code>textPreformat.background</code>：预格式化文本段的背景颜色</p></li>

<li><p>Root folder icons per name，详见：<a href="https://code.visualstudio.com/updates/v1_84#_root-folder-icons-per-name">原文</a>。</p></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_84#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>结束对 Windows 32-bit 平台的支持。</li>
</ul>

<h2 id="扩展和文档-extensions-and-documentation">扩展和文档 (Extensions and documentation)</h2>

<ul>
<li>Gradle for Java，使用 <a href="https://build-server-protocol.github.io/">BSP 协议</a>来构建。</li>
<li>新增在 VSCode 中开发 Python FastAPI 的 <a href="https://code.visualstudio.com/docs/python/tutorial-fastapi">《FastAPI 指南》</a> 文档。</li>
<li>新增 <a href="https://code.visualstudio.com/updates/v1_84#_custom-layout-user-guide">《自定义布局用户指南》</a> 文档</li>
</ul>
]]></description></item><item><title>VSCode 1.85 (2023-11) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_85_2023-11/</link><pubDate>Sat, 09 Dec 2023 14:42:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_85_2023-11/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_85">https://code.visualstudio.com/updates/v1_85</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>轻量级浮动编辑器窗口。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Floating-window-by-drag-and-drop.mp4" type="video/mp4">
</video></li>

<li><p>可以粘贴，来自系统的文件浏览器中复制文件或目录。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Copy-a-file-from-Windows-Explorer-to-the-VS-Code-File-Explorer.mp4" type="video/mp4">
</video></li>

<li><p>git 视图，新增 Incoming/Outgoing changes 视图，用来展示本地分支和远端分支的 diff。</p>

<p><img src="/image/vscode/scm-incoming-outgoing.png" alt="image" /></p></li>

<li><p>光标在在终端悬停，会在其左侧显示一个突出显示栏。这对于不清楚一个命令从哪里开始到哪里结束很有用。</p>

<p><img src="/image/vscode/terminal-command-highlighting.png" alt="image" /></p></li>

<li><p>粘性滚动，新增对终端 （<code>&quot;terminal.integrated.stickyScroll.enabled&quot;: true</code>） 和树形控件 （ <code>&quot;workbench.tree.enableStickyScroll&quot;: true</code>） 的支持。</p>

<p><img src="/image/vscode/terminal-sticky-scroll.png" alt="image" /></p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-in-the-File-Explorer.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>浮动编辑器窗口，可以通过拖拽、tab 标签右键菜单、命令中心等，将 VSCode 一个 VSCode 编辑器、编辑器组在独立的轻量级窗口打开。该窗口可以和原窗口保持状态同步（自动完成、编辑内容等），也可以使用复杂的编辑器组布局。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Floating-window-by-drag-and-drop.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Command-to-Copy-Editor-into-New-Window.mp4" type="video/mp4">
</video>

<p><img src="/image/vscode/float_3.png" alt="image" /></p></li>

<li><p>可以粘贴，来自系统的文件浏览器中复制文件或目录。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Copy-a-file-from-Windows-Explorer-to-the-VS-Code-File-Explorer.mp4" type="video/mp4">
</video></li>

<li><p>扩展自动更新控制，可以控制哪些扩展。</p>

<p><img src="/image/vscode/select-auto-update-extensions.png" alt="image" /></p>

<p><img src="/image/vscode/auto-update-mode.png" alt="image" /></p></li>

<li><p>以下新的 Profile 图标可添加到你的 Profile 中。</p>

<p><img src="/image/vscode/new-profile-icons.png" alt="image" /></p></li>

<li><p>设置编辑器搜索改进和错误修复，参见：<a href="https://code.visualstudio.com/updates/v1_85#_settings-editor-search-improvements-and-bug-fixes">原文</a>。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><code>editor.codeActionsOnSave</code> 配置项（在保存时自动执行的 CodeAction），选项由 bool 更新为如下枚举值：

<ul>
<li><code>explicit</code> - 仅当手动触发保存时才触发（如 <code>cmd + s</code>），等价于之前的 true。</li>
<li><code>always</code> - 当显式保存以及从窗口或焦点更改自动保存时触发代码操作。</li>
<li><code>never</code> - 永远不会在保存时触发代码操作，等价于之前的 false。</li>
</ul></li>

<li><p>多文档高亮支持语义层面匹配。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Semantic-multi-document-highlighting-across-TypeScript-files.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>新增 Incoming/Outgoing changes 视图，用来展示本地分支和远端分支的 diff，可通过 <code>scm.showIncomingChanges</code> 配置项配置是否显示。</p>

<p><img src="/image/vscode/scm-incoming-outgoing.png" alt="image" /></p></li>

<li><p>新增 scm.inputMaxLines 配置项，配置 commit 消息输入框显式的最大行数。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>终端新增粘性滚动，可通过 <code>&quot;terminal.integrated.stickyScroll.enabled&quot;: true</code> 配置项开启，目前处于实验性，未来会默认开启。</p>

<p><img src="/image/vscode/terminal-sticky-scroll.png" alt="image" /></p></li>

<li><p>光标在在终端悬停，会在其左侧显示一个突出显示栏。这对于不清楚一个命令从哪里开始、另一个命令从哪里结束的普通终端提示很有用。</p>

<p><img src="/image/vscode/terminal-command-highlighting.png" alt="image" /></p></li>

<li><p>Shell 集成和命令导航改进，参见：<a href="https://code.visualstudio.com/updates/v1_85#_shell-integration-and-command-navigation-improvements">原文</a>。</p></li>

<li><p>改进下划线渲染，参见：<a href="https://code.visualstudio.com/updates/v1_85#_improved-underline-rendering">原文</a>。</p></li>

<li><p>新增 Git pull 的快速修复。</p></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<p>可以将 npm.packageManager 设置设置为 Bun，以启用对 package.json 中定义的 Bun 脚本的检测和运行。</p>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>JavaScript 调试器，略，参见：<a href="https://code.visualstudio.com/updates/v1_85#_javascript-debugger">原文</a>。</li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<p>测试结果查看终端现在支持查找控件。</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>TypeScript 5.3 发布。</li>
<li><code>&quot;typescript.workspaceSymbols.excludeLibrarySymbols&quot;: false</code> 配置，可以从工作区符号搜索中排除的 node_module 符号。</li>
<li>内嵌提示支持通过 <code>cmd + 单击</code> 跳转到定义。</li>
<li>Prefer using &lsquo;type&rsquo; for auto imports，参见：<a href="https://code.visualstudio.com/updates/v1_85#_prefer-using-type-for-auto-imports">原文</a>。</li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_85#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，参见：<a href="https://code.visualstudio.com/updates/v1_85#_github-copilot">原文</a>。</li>
<li>GitHub Pull Requests and Issues，参见：<a href="https://code.visualstudio.com/updates/v1_85#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>在树视图上的粘性滚动，可通过 <code>&quot;workbench.tree.enableStickyScroll&quot;: true</code> 配置项打开，可通过 <code>workbench.tree.stickyScrollMaxItemCount</code> 配置最大行数。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-in-the-File-Explorer.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-in-the-GitHub-Pull-Requests-and-Issues-extension-Pull-Request-tree-view.mp4" type="video/mp4">
</video></li>

<li><p>多文件 diff，可通过 <code>&quot;multiDiffEditor.experimental.enabled&quot;: true</code> 配置项打开。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Multiple-file-diff-view.mp4" type="video/mp4">
</video></li>

<li><p>搜索过滤支持其他等效字母，如韩语字母。</p>

<p><img src="/image/vscode/korean-filtering.png" alt="image" /></p></li>

<li><p>通过 <code>problems.visibility</code> 配置项可以关闭错误在编辑器中的展示（如，波浪线等），更多参见：<a href="https://code.visualstudio.com/updates/v1_85#_hide-problem-decorations">原文</a>。</p></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_85#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>新的 CDN 站点 <code>vscode.download.prss.microsoft.com</code>。</li>
<li>macOS 10.13 和 10.14 支持已结束。</li>
</ul>
]]></description></item><item><title>VSCode 1.86 (2024-01) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_86_2024-01/</link><pubDate>Sat, 09 Dec 2023 14:42:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_86_2024-01/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_86">https://code.visualstudio.com/updates/v1_86</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>WARNING: Linux 最低要求更新：从此版本开始，VSCode 桌面仅与基于 glibc 2.28 或更高版本以及 glibcxx 3.4.25 或更高版本的 Linux 发行版兼容，例如 Debian 10、RHEL 8 或 Ubuntu 20.04 以上版本。这将导致 VSCode 远程开发（Remote SSH）无法连接到遗留的 Linux 平台 😞，临时非降级解决办法参见： <a href="https://github.com/rectcircle/patch-vscode-1-86-for-old-linux。">https://github.com/rectcircle/patch-vscode-1-86-for-old-linux。</a></li>
<li><code>command + +</code> / <code>command + -</code> 窗口缩放的默认生效范围限制在当前窗口。</li>

<li><p>自动保存行为优化：</p>

<ul>
<li><p>可以为某个语言单独配置自动保存选项，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;[markdown]&#34;</span>: {
        <span style="color:#f92672">&#34;files.autoSave&#34;</span>: <span style="color:#e6db74">&#34;afterDelay&#34;</span>
    }
}</code></pre></div></li>

<li><p>新增 <code>files.autoSaveWhenNoErrors</code> 配置项，可在错误时禁用自动保存。</p></li>

<li><p>新增 <code>files.autoSaveWorkspaceFilesOnly</code> 配置项，仅自动保存工作区文件。</p></li>
</ul></li>

<li><p>允许仅禁用某个扩展的通知消息。</p>

<p><img src="/image/vscode/turn-off-notifications-1.png" alt="image" /></p>

<p><img src="/image/vscode/turn-off-notifications-2.png" alt="image" /></p></li>

<li><p>支持一键切换差异编辑器的左右。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Swap-left-and-right-sides-of-the-Diff-Editor.mp4" type="video/mp4">
</video></li>

<li><p>输出面板</p>

<ul>
<li>可通过 <code>View: Toggle Word Wrap</code> （或 <code>option + z</code>） 切换自动换行。</li>
<li>在输出面板右上角溢出菜单 （三个点） 新增 <code>Open Output in New Window</code> 可以将会输出面板在新窗口中打开。</li>
</ul></li>

<li><p>在 diff 编辑器中 review 多个文件：</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Use-the-multi-file-diff-view-to-compare-files-changes-across-multiple-files.mp4" type="video/mp4">
</video></li>

<li><p>源代码管理视图，右键上下文菜单可以关闭其他存储库。</p></li>

<li><p>新增 <code>触发断点</code> （Triggered breakpoints） 类型，该类型断点可以配置在某个断点命中后，这个断点才会命中。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Add-a-triggered-breakpoint-that-depends-on-another-breakpoint-to-be-hit.mp4" type="video/mp4">
</video></li>

<li><p>按住 alt (option) 单击编辑器左侧运行测试的按钮，将以 debug 模式运行测试。</p></li>
</ul>

<h2 id="accessibility">Accessibility</h2>

<ul>
<li><p>新增 <code>accessibility.voice.keywordActivation</code> 配置项。也可以通过 <code>Hey Code&quot;</code> 快速唤起 GitHub Copilot Chat （需安装 <a href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat">GitHub Copilot Chat</a> 和 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-speech">VS Code Speech</a> 扩展）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Use-the-Hey-Code-voice-command-to-activate-voice-chat.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>编辑器拖拽出来作为独立的窗口后，重启 VSCode 将以关闭之前的位置和大小恢复这些窗口。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Auxiliary-windows-restore-after-reloading-or-restarting-VSCode.mp4" type="video/mp4">
</video></li>

<li><p>树视图中的粘性滚动特性默认开启。可通过 <code>&quot;workbench.tree.enableStickyScroll&quot;: false</code> 配置项关闭，可通过 <code>workbench.tree.stickyScrollMaxItemCount</code> 最大粘性行数。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-in-the-File-Explorer.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-in-the-GitHub-Pull-Requests-and-Issues-extension-Pull-Request-tree-view.mp4" type="video/mp4">
</video></li>

<li><p>之前，按 <code>command + +</code> / <code>command + -</code> 时，会缩放所有的窗口，现在这个快捷建的行为只会对当前窗口有效。如果仍想改变全局窗口的缩放，则可以通过修改 <code>window.zoomLevel</code> 配置项实现。如果想将 <code>command + +</code> 快捷键切换为旧的行为，可以修改 <code>window.zoomPerWindow</code> 配置项。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Configure-per-window-zoom-levels-and-show-zoom-indicator-in-Status-Bar.mp4" type="video/mp4">
</video>

<p>此外，当进行缩放后当前后，可通过状态栏图标修改。</p>

<p><img src="/image/vscode/zoom.png" alt="image" /></p></li>

<li><p>自动保存行为优化：</p>

<ul>
<li><p>可以为某个语言单独配置自动保存选项，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;[markdown]&#34;</span>: {
        <span style="color:#f92672">&#34;files.autoSave&#34;</span>: <span style="color:#e6db74">&#34;afterDelay&#34;</span>
    }
}</code></pre></div></li>

<li><p>新增 <code>files.autoSaveWhenNoErrors</code> 配置项，可在错误时禁用自动保存。</p></li>

<li><p>新增 <code>files.autoSaveWorkspaceFilesOnly</code> 配置项，仅自动保存工作区文件。</p></li>
</ul></li>

<li><p>允许仅禁用某个扩展的通知消息。</p>

<p><img src="/image/vscode/turn-off-notifications-1.png" alt="image" /></p>

<p><img src="/image/vscode/turn-off-notifications-2.png" alt="image" /></p></li>

<li><p>支持一键切换差异编辑器的左右。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Swap-left-and-right-sides-of-the-Diff-Editor.mp4" type="video/mp4">
</video></li>

<li><p>使用 code 命令编辑标准输出时，默认会命令会自动退出，除非添加 <code>--wait</code> 命令，例如 <code>echo Hello World | code -</code>。</p></li>

<li><p>新增 <code>window.customTitleBarVisibility</code> 配置项，可配置自定义标题栏可见性，详见：<a href="https://code.visualstudio.com/updates/v1_86#_support-custom-title-bar-with-native-title-bar">原文</a>。</p></li>

<li><p>新增 <code>window.systemColorTheme</code> 配置项，可以配置上下文菜单是 dark 还是 light 的行为。</p>

<p><img src="/image/vscode/system-theme.png" alt="image" /></p></li>

<li><p>新增 <code>window.confirmSaveUntitledWorkspace</code> 配置项，控制切换到新的工作空间时，当前工作空间是未保存的时，是否弹窗保存。</p></li>

<li><p>输出面板</p>

<ul>
<li>可通过 <code>View: Toggle Word Wrap</code> （或 <code>option + z</code>） 切换自动换行。</li>
<li>在输出面板右上角溢出菜单 （三个点） 新增 <code>Open Output in New Window</code> 可以将会输出面板在新窗口中打开。</li>
</ul></li>

<li><p>code 命令，新增 <code>--update-extensions</code> 命令来更新已安装的扩展。</p></li>

<li><p>Quick Pick 视图的 Hover 视图切换为自定义渲染，以提升展示速度。</p>

<p><img src="/image/vscode/quick-pick-hovers.png" alt="image" /></p></li>
</ul>

<h2 id="在-diff-编辑器中-review-多个文件-review-multiple-files-in-diff-editor">在 diff 编辑器中 review 多个文件 (Review multiple files in diff editor)</h2>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Use-the-multi-file-diff-view-to-compare-files-changes-across-multiple-files.mp4" type="video/mp4">
</video>

<ul>
<li>GitHub Pull Requests and Issues 扩展可通过 <code>&quot;githubPullRequests.focusedMode&quot;: &quot;multiDiff&quot;</code> 配置项来使用 multiple files in diff editor 打开 PR。</li>
<li>在 github.dev 可以使用 multi-file diff editor 来 review 代码。</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>新增 <code>&gt;Paste As...</code> 命令可以将复制内容粘贴为 HTML 格式。</p>

<p><img src="/image/vscode/paste-html.png" alt="image" /></p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Pasting-content-copied-from-a-webpage-into-a-html-file-The-Live-Preview-extension-is-being-used-to-show-a-preview-of-the-HTML.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>Commit 消息输入框：</p>

<ul>
<li>新增 <code>scm.inputMinLineCount</code> 配置项，可配置输入框的最小行数。</li>
<li><code>scm.inputMaxLines</code> 配置项被重命名为 <code>scm.inputMaxLineCount</code>。</li>

<li><p>可以使用语言特殊编辑器配置，配置Commit 消息输入框的行为，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;[scminput]&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;editor.rulers&#34;</span>: [
        <span style="color:#ae81ff">50</span>,
        <span style="color:#ae81ff">72</span>
    ],
    <span style="color:#f92672">&#34;editor.wordWrap&#34;</span>: <span style="color:#e6db74">&#34;off&#34;</span>
}</code></pre></div></li>
</ul></li>

<li><p>源代码管理视图，右键上下文菜单可以关闭其他存储库。</p></li>

<li><p>Incoming/Outgoing changes improvements 略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_incomingoutgoing-changes-improvements">原文</a>。</p></li>

<li><p>Ability to merge tags 略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_ability-to-merge-tags">原文</a>。</p></li>

<li><p>可通过 <code>&gt;Git: View Stash...</code> 查看 stash 的 diff。</p></li>

<li><p>Commit signing using SSH keys，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_ability-to-merge-tags">原文</a>。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>支持拖拽到独立窗口。</li>
<li>Built-in variable view。</li>
<li>粘性滚动。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_86#_notebooks">原文</a>。</p>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>背景色显示在选中色的后面。</li>
<li>新增配置项 <code>&quot;terminal.integrated.mouseWheelZoom&quot;</code>，支持通过滚轮缩放终端。</li>
<li>改进粘贴多行的警告，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_multiline-paste-warning-improvements">原文</a>。</li>
<li>文件定位输出支持 <code>file://</code> 协议，支持 <code>#&lt;line&gt;</code> 格式定位到具体行。</li>
<li>新增终端语音指令，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_terminal-voice-commands">原文</a>。</li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>新的 <code>${/}</code> 变量可以用来代替之前的 <code>${pathSeparator}</code> （用来拼比 unix 和 windows 的文件路径分隔符的差异）。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>新增 <code>触发断点</code> （Triggered breakpoints） 类型，该类型断点可以配置在某个断点命中后，这个断点才会命中。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Add-a-triggered-breakpoint-that-depends-on-another-breakpoint-to-be-hit.mp4" type="video/mp4">
</video></li>

<li><p>新增 <code>debug.closeReadonlyTabsOnEnd</code> 配置项，在调试会话结束时关闭只读文件。</p></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li>按住 alt (option) 单击编辑器左侧运行测试的按钮，将以 debug 模式运行测试。</li>
<li>Finalized TestRunProfile.isDefault/onDidChangeDefault APIs for extension authors，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_finalized-testrunprofileisdefaultondidchangedefault-apis-for-extension-authors">原文</a>。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown</p>

<ul>
<li><code>markdown.editor.pasteUrlAsFormattedLink.enabled</code> 新增默认值 <code>smartWithSelection</code>，仅在选中文本时并粘贴链接时触发自动生成 markdown 连接。</li>
<li>新增 <code>markdown.editor.filePaste.audioSnippet</code> 配置项，可配置智能粘贴音频或视频文件的 URL 时生成代码的片段。</li>
</ul></li>

<li><p>使用 <a href="https://github.com/radium-v/Better-Less">Better-Less</a> 作为默认的 Less 语法高亮。</p></li>

<li><p>使用 <a href="https://github.com/worlpaker/go-syntax">Go Syntax</a> 作为默认的 Go 语法高亮。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>需要特别注意的是，由于 VSCode 的 Linux minimum requirements 更新，在旧版本的 （不满足 glibc 2.28 or later, and glibcxx 3.4.25 or later, such as Debian 10, RHEL 8, or Ubuntu 20.04.） 的 Linux 将无法进行远程开发。</p>

<p>临时非降级的解决办法参见： <a href="https://github.com/rectcircle/patch-vscode-1-86-for-old-linux">https://github.com/rectcircle/patch-vscode-1-86-for-old-linux</a> 。</p>

<p>其他，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，详见：<a href="https://code.visualstudio.com/updates/v1_86#_github-copilot">原文</a>。</li>
<li>Python：

<ul>
<li>扩展的 Debug 能力被抽离到 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">Python Debugger</a> 扩展，该扩展将随 Python 扩展的安装自动安装，在 launch.json 中使用 <code>&quot;type&quot;: &quot;debugpy&quot;</code> 使用新的调试器。</li>
<li>其他略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_python">原文</a>。</li>
</ul></li>
<li>Jupyter，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_jupyter">原文</a>。</li>
<li>GitHub Pull Requests and Issues，略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_github-pull-requests-and-issues">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>TypeScript 5.4 beta support，详见：<a href="https://code.visualstudio.com/updates/v1_86#_typescript-54-beta-support">原文</a>。</li>

<li><p>快速搜索提升， <code>cmd+p</code>， <code>% 关键字</code>，可点击右侧图标，在搜索视图查看。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Editor-previews-Quick-Search-results-and-direct-navigation-from-Quick-Search-to-Search-view.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_proposed-apis">原文</a>。</p>

<h2 id="已完成的-api-finalized-apis">已完成的 API (Finalized APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_86#_finalized-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Housekeeping，年末清理一波 Issue，详见：<a href="https://code.visualstudio.com/updates/v1_86#_housekeeping">原文</a>。</li>
<li>Markdown 语言服务 0.4 发布。</li>
<li>新的 localize2 函数使制作 ILocalizedStrings 变得更容易，详见：<a href="https://code.visualstudio.com/updates/v1_86#_new-localize2-function-to-make-crafting-ilocalizedstrings-more-easily">原文</a>。</li>
<li>更新到 Electron 27 。</li>
<li>Linux 最低要求更新：从此版本开始，VSCode 桌面仅与基于 glibc 2.28 或更高版本以及 glibcxx 3.4.25 或更高版本的 Linux 发行版兼容，例如 Debian 10、RHEL 8 或 Ubuntu 20.04 以上版本。</li>
</ul>
]]></description></item><item><title>VSCode 1.87 (2024-02) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_87_2024-02/</link><pubDate>Sun, 31 Mar 2024 21:50:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_87_2024-02/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_87">https://code.visualstudio.com/updates/v1_87</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>上一版本，VSCode 提升了对 Linux 最小 glibc 版本的要求到了 2.28 这导致 VSCode remote 无法连接到遗留的 Linux 操作系统，遭到了社区的强烈不满。在此版本， VSCode 已经承诺，VSCode 仍支持在旧版 glibc 的 Linux 平台使用，直至 2025 年 1 月。详见： <a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">FAQ</a> 和 <a href="https://github.com/microsoft/vscode/issues/203375">Issue</a>。</li>
</ul>

<h2 id="accessibility">Accessibility</h2>

<ul>
<li>使用 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-speech">VS Code Speech 扩展</a>，可以实现语音输入，通过 <code>Voice: Start Dictation in Editor</code> (Ctrl+Alt+V) 。并支持 26 种语言，可通过 <code>accessibility.voice.speechLanguage</code> 配置。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>Show Release Notes 命令，将支持快速跳转到设置。</li>
<li>更透明的大语言模型的访问，略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_transparency-and-control-of-language-model-access">原文</a>。</li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>编辑器粘性滚动现在已经默认开启，<code>editor.stickyScroll.maxLineCount</code> 默认值从 10 调整到 20。</li>

<li><p>多光标内联完成。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Multi-cursor_inline_Completions_.mp4" type="video/mp4">
</video></li>

<li><p>重构预览支持 multi diff editor。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Multi-diff_Editor_opened_from_Refactor_preview_.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>终端集成开启后，命令左侧小蓝点 hover 将展示命令执行时间和耗时。</p>

<p><img src="/image/vscode/terminal_duration.png" alt="image" /></p></li>

<li><p>终端新增用于放大、缩小和重置的新命令。</p>

<ul>
<li>Terminal: Increase Font Size (workbench.action.terminal.fontZoomIn)</li>
<li>Terminal: Decrease Font Size (workbench.action.terminal.fontZoomOut)</li>
<li>Terminal: Reset Font Size (workbench.action.terminal.fontZoomReset)</li>
</ul></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>可以使用 <code>window.title</code> 设置自定义窗口标题。在本次迭代中，我们添加了两个可与此设置一起使用的新变量：<code>${activeRepositoryName}</code> 和 <code>${activeRepositoryBranchName}</code>。这些变量分别替换为活动存储库和活动分支的名称。</li>
<li>新增 <code>git.inputValidationSubjectLength</code> 和 <code>git.inputValidationLength</code> 以及 <code>git.inputValidation</code> 配置提交输入验证。</li>
<li>新增 <code>scm.showIncomingChanges</code>、<code>scm.showOutgoingChanges</code>、<code>scm.showChangesSummary</code> 配置项，配置是否展示传入/传出更改。</li>
<li>新增 <code>&gt;Close All Unmodified Editors</code> 命令，关闭所有未修改的编辑器。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>我们现在通过 <code>notebook.editorOptionsCustomizations</code> 设置支持特定于笔记本的缩进设置。此设置允许用户通过 <code>editor.tabSize</code>、<code>editor.indentSize</code> 和 <code>editor.insertSpaces</code> 设置为笔记本设置特定的缩进样式。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>VSCode 支持调试适配器协议 (DAP) 的新功能，允许您设置不同的断点“模式”。此功能通常由本机代码的调试器使用，例如，设置硬件与软件断点。可以使用断点上下文菜单中的“编辑模式”操作来更改断点的模式。</p>

<p><img src="/image/vscode/bp-modes.png" alt="image" /></p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>上一版本，VSCode 提升了对 Linux 最小 glibc 版本的要求到了 2.28 这导致 VSCode remote 无法连接到遗留的 Linux 操作系统，遭到了社区的强烈不满。在此版本， VSCode 已经承诺，VSCode 仍支持在旧版 glibc 的 Linux 平台使用，直至 2025 年 1 月。详见： <a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">FAQ</a> 和 <a href="https://github.com/microsoft/vscode/issues/203375">Issue</a>。</p>

<p>更多参见：<a href="https://code.visualstudio.com/updates/v1_87#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_contributions-to-extensions">原文</a>。</p>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>For extension authors: Preview of @vscode/l10n-dev and Azure AI Translator 略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_for-extension-authors-preview-of-vscodel10ndev-and-azure-ai-translator">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>使用测试 CLI 进行扩展的扩展作者可以通过更新到最新版本的 @vscode/test-cli 包来生成测试覆盖率。参见：<a href="https://code.visualstudio.com/updates/v1_87#_test-coverage-in-extensions">原文</a>。</li>
<li>Test configurations in launch.json ，略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_test-configurations-in-launchjson">原文</a>。</li>
<li>Contributing Additional Data in Issue Reporter ，略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_contributing-additional-data-in-issue-reporter">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，参见：<a href="https://code.visualstudio.com/updates/v1_87#_proposed-apis">原文</a>。</p>
]]></description></item><item><title>VSCode 1.88 (2024-03) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_88_2024-03/</link><pubDate>Sun, 28 Apr 2024 20:57:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_88_2024-03/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_88">https://code.visualstudio.com/updates/v1_88</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>通过设置 <code>workbench.activityBar.location</code> 为 <code>bottom</code>，可以将活动栏移动到侧边栏底部。</p>

<p><img src="/image/vscode/activity-bar-positions.png" alt="image" /></p></li>

<li><p>快速选择列表中，按住 <code>cmd + 上下</code>，可以快速跳转到下一个分隔符。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move-between-separators-using-keybindings.mp4" type="video/mp4">
</video></li>

<li><p>更新扩展后，现在可以重新启动扩展，而不必重新加载窗口（连接到 WSL 或 SSH 或 Dev Container 等远程服务器时，您仍然需要重新加载窗口来更新扩展）。</p>

<p><img src="/image/vscode/restart-extensions.png" alt="image" /></p></li>

<li><p>按 F2 进行符号重命名时，可以按 <code>cmd + enter</code> 可以进行预览。鼠标 hover 或聚焦到重构选项上时，按 <code>cmd + enter</code> 可以进行预览。</p></li>

<li><p>文件浏览器，新增传入更改装饰，以更早的避免冲突。</p>

<p><img src="/image/vscode/scm-incoming-changes-decorators.png" alt="image" /></p></li>

<li><p>测试覆盖率支持。</p>

<ul>
<li><p>测试浏览器添加 <code>run-with-coverage</code> 按钮。</p>

<p><img src="/image/vscode/run-with-coverage.png" alt="image" /></p></li>

<li><p>测试浏览器添加测试覆盖率视图，通过 <code>&gt;Toggle Inline Coverage</code> 命令，可以将覆盖信息展示到代码上。</p>

<p><img src="/image/vscode/test-coverage.png" alt="image" /></p></li>
</ul></li>

<li><p><code>cmd + p</code>， 键入 <code>issue</code>，选择一个扩展，即可通过填写一个表单，快速给指定插件的 github 提交一个 issue。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<ul>
<li>语音输入触发时的提示音（语音输入功能可通过按住 <code>cmd+option+v</code> 触发，或 <code>&gt;Voice: Start Dictation in Editor</code> 命令触发，上个版本引入）。

<ul>
<li><code>accessibility.signals.voiceRecordingStarted</code> 配置项可以配置，当打开语音输入的时候是否播放提示音。</li>
<li><code>accessibility.signals.voiceRecordingStopped</code> 配置项可以配置，当关闭语音输入的时候是否播放提示音。</li>
</ul></li>
<li>改进了 diff 编辑器的可访问性，略，详见<a href="https://code.visualstudio.com/updates/v1_88#_improved-diff-editor-accessibility">原文</a> 。</li>
<li>可访问的查看聊天代码块命令，略，详见<a href="https://code.visualstudio.com/updates/v1_88#_accessible-view-chat-code-block-commands">原文</a>。</li>
<li>笔记本单元 aria 标签更新，略，详见<a href="https://code.visualstudio.com/updates/v1_88#_notebook-cell-aria-label-updates">原文</a>。</li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>浮动窗口支持自定义编辑器（如 markdown 预览等），对于基于技术原因，窗口拖拽出来后状态可能会重置。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Custom-editors-support-in-floating-windows.mp4" type="video/mp4">
</video></li>

<li><p>编辑器的标题支持通过 blob 模式匹配重新，并通过 <code>${filename}</code>, <code>${extname}</code>, <code>${dirname}</code>, <code>${dirname(N)}</code> 重新自定义，相关配置项为 <code>workbench.editor.customLabels.patterns</code> 和 <code>workbench.editor.customLabels.enabled</code>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Adding-an-entry-for-a-custom-editor-label.mp4" type="video/mp4">
</video></li>

<li><p>通过 <code>&gt;View: Toggle Locked Scrolling Across Editors</code> 命令，可以设置所有跨编辑器滚动锁定（可以通过状态栏按钮取消）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Synchronize-scrolling-two-editors.mp4" type="video/mp4">
</video></li>

<li><p>通过设置 <code>workbench.activityBar.location</code> 为 <code>bottom</code>，可以将活动栏移动到侧边栏底部。</p>

<p><img src="/image/vscode/activity-bar-positions.png" alt="image" /></p></li>

<li><p>通过 <code>search.searchEditor.singleClickBehaviour</code> 可以配置搜索编辑器的鼠标单击行为为 <code>Peek Definition</code> （使用内联编辑器打开文件的当前行）。</p></li>

<li><p>快速搜索提升。</p>

<ul>
<li><p>文件路径分割符粘性滚动。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Sticky-Scroll-support-in-the-quick-pick.mp4" type="video/mp4">
</video></li>

<li><p>鼠标悬浮或聚焦到到文件路径分割符上，会展示打开文件按钮，点击后会打开该路径。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Separator-buttons-show-up-when-you-hover-or-focus-an-item-in-that-section.mp4" type="video/mp4">
</video></li>
</ul></li>

<li><p>快速选择列表中，按住 <code>cmd + 上下</code>，可以快速跳转到下一个分隔符。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move-between-separators-using-keybindings.mp4" type="video/mp4">
</video></li>

<li><p>多选类型的快速选择的选项，支持配置为禁用操作（用于表达，用户无法操作的默认选中或默认不选中）。</p>

<p><img src="/image/vscode/manage-trusted-extensions.png" alt="image" /></p></li>

<li><p>扩展更新改进</p>

<ul>
<li><p>更新扩展后，现在可以重新启动扩展，而不必重新加载窗口（连接到 WSL 或 SSH 或 Dev Container 等远程服务器时，您仍然需要重新加载窗口来更新扩展）。</p>

<p><img src="/image/vscode/restart-extensions.png" alt="image" /></p></li>

<li><p>当启用扩展自动更新时，VSCode 现在会更新与可更新的 VSCode 较新版本兼容的扩展。如果新版本的扩展与当前版本的 VS Code 不兼容，则只有在更新 VS Code 后才会启用新版本的扩展。</p></li>
</ul></li>

<li><p>当评论允许回复时，评论视图中评论线程的上下文菜单现在包含恢复操作。这使您能够快速跳转到回复输入框并开始输入回复。</p>

<p><img src="/image/vscode/context-menu-comment.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>小地图区域标题，以 TypeScript 为例，通过  <code>//#region xxx</code> 标记的区域，会在小地图区域显示出来，以方便浏览文件时快速定位</p>

<p><img src="/image/vscode/minimap-sections.png" alt="image" /></p></li>

<li><p>按 F2 进行符号重命名时，可以按 <code>cmd + enter</code> 可以进行预览。鼠标 hover 或聚焦到重构选项上时，按 <code>cmd + enter</code> 可以进行预览。</p></li>

<li><p>差异编辑器现在有一个单独的用于 Stage 和 Revert 控件的装订线。这些操作能够暂存或恢复更改的代码块。</p>

<p><img src="/image/vscode/diffEditor-stage-revert-demo.gif" alt="image" /></p></li>

<li><p>重命名可以提供建议。</p>

<p><img src="/image/vscode/rename-new-ux.gif" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>文件浏览器，新增传入更改装饰，以更早的避免冲突。</p>

<p><img src="/image/vscode/scm-incoming-changes-decorators.png" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>调试终端 Shell 集成已自动启用，更多参见 <a href="https://code.visualstudio.com/docs/terminal/shell-integration">Shell 集成</a>。</li>

<li><p>运行最近的命令改进。shell 集成支持的运行最近命令 (<code>⌃⌥R</code>) 现在会滚动到并显示上次运行该命令的时间（如果可能）。运行命令或取消快速选择会将终端返回到之前的状态。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Running-a-recent-command-will-preview-the-last-time-its-run-in-the-terminal-temporarily.mp4" type="video/mp4">
</video></li>

<li><p>打开检测到的链接改进。打开检测到的链接命令 (<code>⇧⌘G</code>) 现在可以在编辑器中预览链接结果，并在终端中突出显示链接源。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Previews-are-shown-in-both-the-editor-and-terminal-when-scrolling-through-links.mp4" type="video/mp4">
</video>

<p>此外，重复的链接现在已从列表中过滤掉，并且所有链接都以一致的格式呈现。</p>

<p><img src="/image/vscode/terminal-open-detected-link-format.png" alt="image" /></p></li>

<li><p>单词链接的附加上下文。</p>

<p><img src="/image/vscode/terminal-word-link-context.png" alt="image" /></p></li>

<li><p>新的链接格式， <code>FILE  path:line:column</code>。</p></li>

<li><p>终端粘性滚动支持透明度颜色主题配置项 <code>terminalStickyScroll.background</code>。</p></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li><p>测试覆盖率支持。</p>

<ul>
<li><p>测试浏览器添加 <code>run-with-coverage</code> 按钮。</p>

<p><img src="/image/vscode/run-with-coverage.png" alt="image" /></p></li>

<li><p>测试浏览器添加测试覆盖率视图，通过 <code>&gt;Toggle Inline Coverage</code> 命令，可以将覆盖信息展示到代码上。</p>

<p><img src="/image/vscode/test-coverage.png" alt="image" /></p></li>

<li><p>关于测试覆盖率 API 参见：<a href="https://code.visualstudio.com/api/extension-guides/testing#test-coverage">Testing API 文档 - 测试覆盖率章节</a>。</p></li>
</ul></li>

<li><p>测试消息中的颜色代码支持。</p></li>

<li><p>Color code support in test messages，详见：<a href="https://code.visualstudio.com/updates/v1_88#_color-code-support-in-test-messages">原文</a>。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>集成 TypeScript 5.4 版本，详见：<a href="https://code.visualstudio.com/updates/v1_88#_typescript-54">原文</a>。</li>

<li><p>在 Markdown 中更智能地插入图像和链接。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Smartly-dropping-an-image-into-a-Markdown-file-Markdown-image-syntax-is-inserted-when-it-can-be-used-but-not-used-in-code-blocks.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，详见<a href="https://code.visualstudio.com/updates/v1_88#_notebooks">原文</a>。</p>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，详见<a href="https://code.visualstudio.com/updates/v1_88#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-speech">VS Code Speech</a>。

<ul>
<li>延迟激活，仅在 VSCode 中请求语音转文本服务时激活。</li>
<li>使用显示语言作为默认语音语言。</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot">GitHub Copilot</a>，略，详见<a href="https://code.visualstudio.com/updates/v1_88#_github-copilot">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</a>。

<ul>
<li>创建调试配置时，改进了对 Flask 和 DJANGO 项目的识别。</li>
<li>支持 HATCH 类型环境识别。</li>
<li>PIPENV、PYENV 和 POETRY 项目的自动环境选择，对于 pyenv，扩展会查看 <code>.python-version</code> 文件以自动为工作区选择适当的解释器。</li>
<li>报告问题命令的改进，略，详见<a href="https://code.visualstudio.com/updates/v1_88#_report-issue-command-improvements">原文</a>。</li>
</ul></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">GitHub Pull Requests</a>，略，详见：<a href="https://code.visualstudio.com/updates/v1_88#_github-pull-requests">原文</a>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter">Jupyter</a>，略，详见：<a href="https://code.visualstudio.com/updates/v1_88#_jupyter">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li><code>cmd + p</code>， 键入 <code>issue</code>，选择一个扩展，即可通过填写一个表单，快速给指定插件的 github 提交一个 issue。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>Rescaling overlapping glyph in the terminal，参见：<a href="https://code.visualstudio.com/updates/v1_88#_rescaling-overlapping-glyph-in-the-terminal">原文</a>。</li>
<li>本地工作区扩展，支持将扩展安装到 <code>.vscode/extensions</code> 目录中，更多详见：<a href="https://code.visualstudio.com/updates/v1_88#_local-workspace-extensions">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_88#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Electron 更新至  28。此更新附带 Chromium 120.0.6099.291 和 Node.js 18.18.2。</li>
</ul>
]]></description></item><item><title>VSCode 1.89 (2024-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_89_2024-04/</link><pubDate>Sun, 19 May 2024 18:19:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_89_2024-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_89">https://code.visualstudio.com/updates/v1_89</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>更改树状控件（如资源管理器）的搜索快捷键为 <code>Ctrl+Alt+F</code>（<code>cmd+option+f</code>），避免之前的 <code>cmd+f</code> 的误触发。</li>

<li><p>搜索结果树递归展开。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Recursively-open-search-tree-nodes.mp4" type="video/mp4">
</video></li>

<li><p><a href="https://code.visualstudio.com/docs/terminal/shell-integration#_automatic-script-injection">终端集成</a> 已支持 git bash。</p></li>

<li><p>Markdown 支持在智能提示、Hover 预览图片视频。</p>

<p><img src="/image/vscode/md-path-completion-preview.png" alt="image" /></p>

<p><img src="/image/vscode/md-hover-preview.png" alt="image" /></p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>语言模型使用量报告。</p>

<p><img src="/image/vscode/language-models-usage.png" alt="image" /></p></li>

<li><p>本地工作区扩展，将扩展程序解压到 <code>.vscode/extensions</code> 目录后，在信任此工作区后，该目录中的扩展将会加载。</p></li>

<li><p><code>cmd + p</code> 快速打开，支持<a href="https://code.visualstudio.com/docs/getstarted/userinterface#_customize-tab-labels">自定义标签</a>。</p>

<p><img src="/image/vscode/custom-labels-quick-open.png" alt="image" /></p></li>

<li><p>右上角按钮，右键上下文菜单新增，快速绑定自定义快捷键菜单项。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video_that_shows_how_to_customize_the_keybinding_for_the_split_editor_action.mp4" type="video/mp4">
</video></li>

<li><p>更改树状控件（如资源管理器）的搜索快捷键为 <code>Ctrl+Alt+F</code>（<code>cmd+option+f</code>），避免之前的 <code>cmd+f</code> 的误触发。</p></li>

<li><p>通过 <code>window.autoDetectColorScheme</code> 配置项可以配置 VSCode 颜色主题随系深色/浅色模式自动切换。开启该配置项后，<code>workbench.colorTheme</code> 将被忽略，将使用如下两个配置项：</p>

<ul>
<li><code>workbench.preferredDarkColorTheme</code> 操作系统切换为暗色时，VSCode 切换到的主题。</li>
<li><code>workbench.preferredLightColorTheme</code> 操作系统切换为亮色时，VSCode 切换到的主题。</li>
</ul>

<p>当开启 <code>window.autoDetectColorScheme</code> 后，如果操作系统为暗色，则 <code>&gt;Preferences: Color Theme</code> 命令将只展示暗色主题。</p>

<p><img src="/image/vscode/configuring_dark_mode.png" alt="image" /></p>

<p><code>&gt;Preferences: Color Theme</code> 命令，搜索栏右侧按钮可以快速打开 <code>window.autoDetectColorScheme</code> 配置项。</p>

<p><img src="/image/vscode/configure-detect-mode.png" alt="image" /></p></li>

<li><p>评论支持粘贴为 markdown。</p>

<p><img src="/image/vscode/paste-markdown-link-comment.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>Save/restore open editors when switching branches，详见：<a href="https://code.visualstudio.com/updates/v1_89#_saverestore-open-editors-when-switching-branches">原文</a>。</li>
<li>新增 <code>&gt;Git: View Staged Changes</code>、<code>&gt;Git: View Changes</code>、<code>Git: View Untracked Changes</code> 命令。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_notebooks">原文</a>。</p>

<h2 id="搜索-search">搜索 (Search)</h2>

<ul>
<li><p>快速搜索，已去除实验性（命令 id 为 <code>workbench.action.quickTextSearch</code>）。通过 <code>&gt;Search: Quick Search</code> 命令，或 <code>cmd+p</code> 输入 <code>%</code>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Quick-Search-demo2.mp4" type="video/mp4">
</video></li>

<li><p>搜索结果树递归展开。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Recursively-open-search-tree-nodes.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><a href="https://code.visualstudio.com/docs/terminal/shell-integration#_automatic-script-injection">终端集成</a> 已支持 git bash。</li>
<li>配置项 <code>terminal.integrated.middleClickBehavior</code> 用于配置鼠标滚轮单击行为。</li>
<li>支持扩展 ANSI 超链接，支持 file, http, https, mailto, vscode and vscode-insiders。可通过 <code>terminal.integrated.allowedLinkSchemes</code> 配置项。</li>

<li><p>终端的新图标选择器。</p>

<p><img src="/image/vscode/terminal-icon-picker.png" alt="image" /></p></li>

<li><p>Support for window size reporting，详见：<a href="https://code.visualstudio.com/updates/v1_89#_support-for-window-size-reporting">原文</a>。</p></li>

<li><p>终端 canvas 渲染器即将废弃，将于下一个版本移除。</p></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>JavaScript 调试器查找可执行文件会在 <code>node_modules/.bin</code> 中查找。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
    &#34;name&#34;: &#34;Run Tests&#34;,
    &#34;type&#34;: &#34;node&#34;,
    &#34;request&#34;: &#34;launch&#34;,
-   &#34;runtimeExecutable&#34;: &#34;${workspaceFolder}/node_modules/.bin/mocha&#34;,
-   &#34;windows&#34;: {
-       &#34;runtimeExecutable&#34;: &#34;${workspaceFolder}/node_modules/.bin/mocha.cmd&#34;
-   },
+   &#34;runtimeExecutable&#34;: &#34;mocha&#34;,
}</pre></div></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>Markdown</p>

<ul>
<li><p>路径补全中的图像预览</p>

<p><img src="/image/vscode/md-path-completion-preview.png" alt="image" /></p></li>

<li><p>悬停预览图片和视频</p>

<p><img src="/image/vscode/md-hover-preview.png" alt="image" /></p></li>

<li><p>标题重命名提升，详见：<a href="https://code.visualstudio.com/updates/v1_89#_improved-markdown-header-renaming">原文</a>。</p></li>
</ul></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>详见：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_89.md">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_github-copilot">原文</a>。</li>
<li>Python

<ul>
<li>CodeAction 新增 &ldquo;实现所有继承的抽象类&rdquo;。</li>
<li>新增基于语义分析的自动缩进，<code>python.analysis.autoIndent</code>。</li>
<li>Python 扩展移除 Debugpy，改为使用 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">Python Debugger</a>。</li>
<li>其他，详见：<a href="https://code.visualstudio.com/updates/v1_89#_python">原文</a>。</li>
</ul></li>
<li>Hex Editor，略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_hex-editor">原文</a>。</li>
<li>GitHub Pull Requests，略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_github-pull-requests">原文</a>。</li>
<li>TypeScript，略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_typescript">原文</a>。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>VSCode 原生的针对 PowerShell 的智能提示。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/VS-Code-native-PowerShell-intellisense.mp4" type="video/mp4">
</video></li>

<li><p>Markdown 代码粘贴自动处理图片视频、引用链接。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Links-being-automatically-updated-when-copy-and-pasting-between-Markdown-files.mp4" type="video/mp4">
</video></li>

<li><p>支持 TypeScript 5.5 beta。</p></li>
</ul>

<h2 id="api">API</h2>

<ul>
<li>改进对评论输入编辑器语言特点的支持，详见：<a href="https://code.visualstudio.com/updates/v1_89#_improved-support-for-language-features-in-comment-input-editors">原文</a>。</li>

<li><p>窗口活动 API 已最终完成。该 API 提供了一个简单的附加 <code>WindowState.active</code> 布尔值，扩展程序可以用它来确定窗口最近是否被交互过。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#a6e22e">vscode</span>.window.<span style="color:#a6e22e">onDidChangeWindowState</span>(<span style="color:#a6e22e">e</span> =&gt; <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Is the user active?&#39;</span>, <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">active</span>));
</code></pre></div></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_89#_proposed-apis">原文</a>。</p>
]]></description></item><item><title>VSCode 1.90 (2024-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_90_2024-05/</link><pubDate>Fri, 07 Jun 2024 22:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_90_2024-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_90">https://code.visualstudio.com/updates/v1_90</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>编辑器标签支持多选（shift + 单击：连续选中；cmd + 单击：选中一个）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Select-multiple-tabs-and-move_close-them.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_90#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>编辑器标签支持多选（shift + 单击：连续选中；cmd + 单击：选中一个）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Select-multiple-tabs-and-move_close-them.mp4" type="video/mp4">
</video></li>

<li><p>Always show editor actions（未复现），详见：<a href="https://code.visualstudio.com/updates/v1_90#_always-show-editor-actions">原文</a>。</p></li>

<li><p>添加 <code>disable-lcd-text</code> 运行时参数，可通过 <code>argv.json</code> 配置（次像素渲染，参见：<a href="https://guoyunhe.me/2014/12/14/subpixel-render-how-to-detect-rgb-bgr-and-other/comment-page-1/">文章</a>），左边是 <code>disable-lcd-text=true</code>，右边时 <code>disable-lcd-text=false</code>(对于高分辨率屏幕，次像素渲染没有必要)。</p>

<p><img src="/image/vscode/h-side-by-side.png" alt="image" /></p></li>

<li><p>新增 <code>window.newWindowProfile</code> 配置项，用于配置打开新窗口使用的 profile，默认为继承当前窗口配置。</p>

<p><img src="/image/vscode/profile-new-window.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>新增一系列命令可以用来配置快捷建，详见：<a href="https://code.visualstudio.com/updates/v1_90#_focus-inputresource-group-commands">原文</a>。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>在 Cell 中查找。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Notebook-Find-in-Cell-Selection.mp4" type="video/mp4">
</video></li>

<li><p>Notebook Format Code Actions，详见：<a href="https://code.visualstudio.com/updates/v1_90#_notebook-format-code-actions">原文</a>。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>⚠️ 删除 canvas 渲染器已移除，详见：<a href="https://code.visualstudio.com/docs/terminal/appearance#_gpu-acceleration">终端文档</a>。</li>
<li>调整终端中重叠字形的大小，详见 <code>terminal.integrated.rescaleOverlappingGlyphs</code> 配置，详见：<a href="https://code.visualstudio.com/updates/v1_90#_rescaling-overlapping-glyph-in-the-terminal">原文</a>。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot，详见：<a href="https://code.visualstudio.com/updates/v1_90#_github-copilot">原文</a>。

<ul>
<li>通过手动选择文件添加上下文文件。</li>
<li>支持通过 <code>#web</code> 出发互联网搜索。</li>
<li>回答的代码块支持跳转定义等能力。</li>
<li>回答的文件路径添加跳转。</li>
<li>将 inline chat 移到到 chat view。</li>
<li>重命名自动建议。</li>
</ul></li>
<li>Python

<ul>
<li>重写 pytest 发现器，减少错误。</li>
<li>实验性：带有 intellisense 和语法高亮的 Python 本地 REPL。将配置项 <code>&quot;python.REPL.sendToNativeREPL&quot;</code> 设置为 true 后，选中 python 代码按 <code>shfit + enter</code> （右键，运行 Python，在 Python REPL 中运行选择/行） 后，会将代码在类似 jupyter 的 VSCode 原生的 REPL 中执行而非终端。</li>
</ul></li>
<li>GitHub Pull Requests and Issues，详见： <a href="https://github.com/microsoft/vscode-pull-request-github/blob/main/CHANGELOG.md#0900">changelog for the 0.90.0</a>。</li>
<li>VS Code Speech

<ul>
<li>添加 <code>accessibility.voice.autoSynthesize</code> 配置项，当使用语音输入时，回答也会阅读出来。</li>
<li>回答框添加语音按钮，可以语音输出内容。</li>
</ul></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>VSCode 原生的对 PowerShell 的智能感知，详见：<a href="https://code.visualstudio.com/updates/v1_90#_vs-codenative-intellisense-for-powershell">原文</a>。</li>
<li>TypeScript 5.5，详见：<a href="https://code.visualstudio.com/updates/v1_90#_typescript-55">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>VSCode 扩展项目初始化时，默认使用 esbuild 构建，详见：<a href="https://code.visualstudio.com/updates/v1_90#_use-esbuild-for-extensions">原文</a>。</li>
<li>Chat and Language Model API 最终完成。让扩展可以调用 Github Copilot 使用语言模型，详见：<a href="https://code.visualstudio.com/updates/v1_90#_chat-and-language-model-api">原文</a>，<a href="https://code.visualstudio.com/api/extension-guides/chat">Chat extensions 文档</a>。</li>
<li>Extending GitHub Copilot through GitHub Apps，详见：<a href="https://code.visualstudio.com/updates/v1_90#_extending-github-copilot-through-github-apps">原文</a>。</li>
<li>Debug Stack Focus API，详见：<a href="https://code.visualstudio.com/updates/v1_90#_debug-stack-focus-api">原文</a>。</li>
<li>TestRunRequest.preserveFocus API，配置是否聚焦到输出页面，详见：<a href="https://code.visualstudio.com/updates/v1_90#_debug-stack-focus-api">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_90#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>追踪内存效率，详见：<a href="https://code.visualstudio.com/updates/v1_90#_tracking-memory-efficiency-on-startup">原文</a>。</li>
<li>更新到 Electron 29 （Chromium 122.0.6261.156 and Node.js 20.9.0）。</li>
</ul>
]]></description></item><item><title>VSCode 1.91 (2024-06) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_91_2024-06/</link><pubDate>Fri, 07 Jun 2024 22:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_91_2024-06/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_91">https://code.visualstudio.com/updates/v1_91</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>新增 传入/传出更改图 的预览功能，通过 <code>scm.experimental.showHistoryGraph</code> 配置项可开启。</p>

<p><img src="/image/vscode/incoming-outgoing-changes.png" alt="image" /></p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_91#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li>搜索新增 <code>search.ripgrep.maxThreads</code> 配置项，用于设置搜索线程数，默认为 0，引擎将自动确认该值。</li>
<li>通过 <code>files.candidateGuessEncodings</code> ，配置一组在启用 <code>files.autoGuessEncoding</code> 时应考虑的编码。配置顺序决定优先级。通过该功能，可以将可能检测到的编码限制在较小的范围内，并可以决定哪些编码检测优先级更高。</li>

<li><p>Profile 编辑器，目前处于实验性功能，可通过 <code>workbench.experimental.enableNewProfilesUI</code> 开启。开启后 Profile 的所有操作将在一个独立的窗口中操作。</p>

<p><img src="/image/vscode/profiles-editor-action.png" alt="image" /></p>

<p><img src="/image/vscode/profiles-editor.png" alt="image" /></p></li>

<li><p>菜单栏新增菜单： 文件 -&gt; 新增使用 Profile 新建窗口。</p>

<p><img src="/image/vscode/profile-new-window-actions.png" alt="image" /></p></li>

<li><p>扩展列表上下文菜单（右键），添加更多选择项。</p>

<ul>
<li>安装扩展而不同步。</li>
<li>安装扩展的特定版本。以前，必须先安装扩展的最新版本，然后才能选择特定版本。</li>
</ul>

<p><img src="/image/vscode/extension-install-actions.png" alt="image" /></p></li>

<li><p>在<a href="https://code.visualstudio.com/docs/getstarted/userinterface#_customize-tab-labels">自定义标签</a>中访问文件扩展名（文件后缀），如 <code>${extname(N)}</code>、<code>${extname}</code>。</p></li>

<li><p>合并多个扩展中的自定义标签默认配置，详见：<a href="https://code.visualstudio.com/updates/v1_91#_merge-custom-label-patterns-from-multiple-extensions">原文</a>。</p></li>

<li><p>如果主题设置了不喜欢的颜色或边框，现在可以使用默认值将其设置回原始值：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;workbench.colorCustomizations&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#f92672">&#34;diffEditor.removedTextBorder&#34;</span>: <span style="color:#e6db74">&#34;default&#34;</span>
}</code></pre></div></li>

<li><p>折叠的省略号颜色可以通过 <code>editor.foldPlaceholderForeground</code> 设置。</p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>为保存时的代码操作配置项 <code>editor.codeActionsOnSave</code> 提供智能提示，会根据安装的扩展情况进行可选项的提示。该功能，详见：<a href="https://code.visualstudio.com/docs/typescript/typescript-refactoring#_code-actions-on-save">Code Actions on Save</a>。</li>

<li><p>如果通过 <code>files.readonlyInclude</code> 设置将文件配置为只读。现在可通过编辑器消息快速切换文件的只读状态。</p>

<p><img src="/image/vscode/quick-toggle-readonly.png" alt="image" /></p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>新增 传入/传出更改图 的预览功能，通过 <code>scm.experimental.showHistoryGraph</code> 配置项可开启。</p>

<p><img src="/image/vscode/incoming-outgoing-changes.png" alt="image" /></p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_91#_notebooks">原文</a>。</p>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>现在支持操作系统命令 (OSC) 52 转义序列。终端中运行的任何程序都可以使用该命令，但主要用途是 tmux 的剪贴板访问。</li>

<li><p>新增 分支、行号、锁定 Powerline 自定义字形，开启 GPU 加速后即可正常展示。</p>

<p><img src="/image/vscode/terminal-powerline.png" alt="image" /></p></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>JavaScript 调试器现在可以根据程序的作用域，在 Hover 时以及内联值位置（通过 <code>debug.inlineValues</code> 设置启用）显示 shadowed 变量的正确值。</p>

<p><img src="/image/vscode/debug-shadowed.png" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>支持 TypeScript 5.5 ，详见 <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/">TypeScript 5.5 blog post</a>。</li>

<li><p>支持对 JavaScript 和 TypeScript 的正则表达式的语法检查。</p>

<p><img src="/image/vscode/ts-regexp-invalid-group.png" alt="image" /></p>

<p><img src="/image/vscode/ts-new-escape-from-regexp-error.png" alt="image" /></p></li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>GitHub Copilot</p>

<ul>
<li><p>添加实验性（<code>inlineChat.experimental.textButtons</code>）的紧凑内敛聊天 UI。</p>

<p><img src="/image/vscode/inline-chat.png" alt="image" /></p></li>

<li><p>终端内联提示。</p>

<p><img src="/image/vscode/copilot-terminal-hint.png" alt="image" /></p></li>

<li><p>聊天视图中代码块上的 &ldquo;在编辑器中应用&rdquo; 命令使用语言模型来确定将更改应用到当前编辑器的最佳方法。</p></li>
</ul></li>

<li><p>Python</p>

<ul>
<li>使用 <a href="https://github.com/microsoft/python-environment-tools">python-environment-tools</a> 发现 python 环境。</li>
<li>智能发送到<a href="https://devblogs.microsoft.com/python/python-in-visual-studio-code-june-2024-release/#vs-code-native-repl-for-python-with-intellisense-and-syntax-highlighting">原生 REPL</a>，详见：<a href="https://code.visualstudio.com/updates/v1_91#_smart-send-in-native-repl">原文</a>。</li>

<li><p>Pylance 现在支持在悬停时渲染 reStructuredText 文档字符串（docstrings）（实验性 <code>python.analysis.supportRestructuredText</code>）。</p>

<p><img src="/image/vscode/pylance-restructuredtext.png" alt="image" /></p></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，略，详见：<a href="https://code.visualstudio.com/updates/v1_91#_github-pull-requests-and-issues">原文</a>。</p></li>

<li><p>ESLint，略，详见：<a href="https://code.visualstudio.com/updates/v1_91#_eslint">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Chat and Language Model API，了解更多信息，详见：<a href="https://code.visualstudio.com/blogs/2024/06/24/extensions-are-all-you-need">announcement 博客文章</a>，<a href="https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample">扩展示例</a>，<a href="https://code.visualstudio.com/api/extension-guides/chat">Chat extensibility documentation</a>。</li>
<li>当多个扩展为同一对象设置提供默认值时，这些默认值现在会合并在一起。这样可以防止扩展程序之间发生冲突。</li>
<li>最终完成 DebugSessionOptions.testRun API。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<ul>
<li>详见：<a href="https://code.visualstudio.com/updates/v1_91#_debug-adapter-protocol">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>语言模型的工具和函数，详见：<a href="https://code.visualstudio.com/updates/v1_91#_tools-and-functions-for-language-models">原文</a>。</li>
<li>身份验证 getSessions 现在改为 getAccounts，详见：<a href="https://code.visualstudio.com/updates/v1_91#_authentication-getsessions-is-now-getaccounts">原文</a>。</li>
<li>Comment thread reveal，详见：<a href="https://code.visualstudio.com/updates/v1_91#_comment-thread-reveal">原文</a></li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>将 NLS 与 AMD 加载器分离，在这个里程碑上，我们开始在 VSCode 中使用异步模块定义（AMD）加载器来移除对核心本地语言支持（NLS）的依赖。我们未来的目标是使用 ECMAScript 模块 (ESM) 加载，并完全放弃 AMD。为了向这个方向迈进，我们删除了 AMD 加载器插件的依赖关系。您应该不会注意到行为上的任何差异，而且我们以前支持的所有翻译在网页和桌面版中都仍然支持。</li>
</ul>
]]></description></item><item><title>VSCode 1.92 (2024-07) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_92_2024-07/</link><pubDate>Sun, 11 Aug 2024 00:01:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_92_2024-07/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_92">https://code.visualstudio.com/updates/v1_92</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>底部面板支持移动到顶部。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move_Workbench_Panel_to_the_top.mp4" type="video/mp4">
</video></li>

<li><p>终端 shell 集成，监听 git 的 <code>add</code>, <code>checkout</code>, <code>commit</code>, <code>fetch</code>, <code>pull</code>, <code>push</code> 等命令，结束后立即更新 UI。</p></li>

<li><p>新的终端滚动条。</p>

<p><img src="/image/vscode/terminal-scroll-bar.png" alt="image" /></p></li>

<li><p>在测试视图变量面板，展示变量类型，通过 <code>debug.showVariableTypes</code> 配置项设置。</p>

<p><img src="/image/vscode/debug-types.png" alt="image" /></p></li>

<li><p>真正的内联差异，引入了 <code>diffEditor.experimental.useTrueInlineView</code> 设置（默认关闭）。开启后单行更改将内联呈现（之前是按行的维度）：</p>

<p><img src="/image/vscode/diffEditor_trueInlineView.png" alt="image" /></p>

<p>之前</p>

<p><img src="/image/vscode/diffEditor_defaultInlineView.png" alt="image" /></p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_92#_improved-debugging-experience">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>底部面板支持移动到顶部。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move_Workbench_Panel_to_the_top.mp4" type="video/mp4">
</video></li>

<li><p>继续改进实验性的 Profile 编辑器，使其更加用户友好，并具有与设置编辑器一致的外观和感觉（通过 <code>workbench.experimental.enableNewProfilesUI</code> 配置）。</p>

<p><img src="/image/vscode/profiles-editor-1-92.png" alt="image" /></p>

<p><img src="/image/vscode/profiles-editor-action-1-92.png" alt="image" /></p></li>

<li><p>覆盖已存在的 Profile。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Override-existing-Profile.mp4" type="video/mp4">
</video></li>

<li><p>提升扩展更新体验。</p>

<ul>
<li><p>将扩展更新通知移动到三个点菜单。</p>

<p><img src="/image/vscode/manage-autoupdate.png" alt="image" /></p></li>

<li><p>支持单个扩展的自动更新配置。</p>

<p><img src="/image/vscode/extension-autoupdate.png" alt="image" /></p></li>

<li><p>默认禁用通过 VSIX 安装的扩展的自动更新。</p></li>

<li><p>对更新扩展的更多控制，现在，当将已安装的没有可执行代码的扩展版本更新为具有可执行代码的版本时，需要用户同意。这使您可以控制在应用此类更新之前对其进行审查。以下视频演示了将无代码的扩展更新为有代码的版本时的体验。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/User-consent-required-to-update-extension-with-no-code-to-a-version-with-code.mp4" type="video/mp4">
</video></li>

<li><p>修复设置编辑器跳转问题（详见：<a href="https://code.visualstudio.com/updates/v1_92#_settings-editor-jump-issue-fixed">原文</a>）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Settings-Editor-not-jumping-after-setting-check-mark-checked.mp4" type="video/mp4">
</video></li>

<li><p>添加对 settings 编辑器 URL 格式处理 <code>vscode://settings/setting.name</code> (<code>vscode-insiders://settings/setting.name</code>)。如：<a href="vscode://settings/editor.fontSize">vscode://settings/editor.fontSize</a>。该功能主要用在发行说明。</p>

<p><img src="/image/vscode/setting-url-in-release-notes.gif" alt="image" /></p></li>

<li><p>新增 <a href="vscode://settings/workbench.externalBrowser">workbench.externalBrowser</a> 配置项，用于配置打开链接的浏览器，可以配置为绝对路径或者浏览器别名（如：<code>edge</code>, <code>chrome</code>, or <code>firefox</code>）。</p>

<p><img src="/image/vscode/default-browser.gif" alt="image" /></p></li>

<li><p>新增 <code>explorer.autoOpenDroppedFile</code> 配置，设置为 <code>false</code> 可配置禁用拖放时自动打开文件。</p></li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>改进灯泡显式位置，可以通过 <code>editor.lightbulb.enabled</code> 切换。</p>

<p><img src="/image/vscode/lightbulb-positioning.png" alt="image" /></p></li>
</ul>

<h2 id="差异编辑器-diff-editor">差异编辑器 (Diff Editor)</h2>

<ul>
<li><p>聊天视图/内联聊天中的差异编辑器布局，并使其更加紧凑。</p>

<ul>
<li><p>之前</p>

<p><img src="/image/vscode/diffEditor_inlineChat_before.png" alt="image" /></p></li>

<li><p>现在</p>

<p><img src="/image/vscode/diffEditor_inlineChat_after.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>Incoming/Outgoing 变更图。</p>

<p><img src="/image/vscode/incoming-outgoing-changes-1-92.png" alt="image" /></p></li>

<li><p>终端 shell 集成，监听 git 的 <code>add</code>, <code>checkout</code>, <code>commit</code>, <code>fetch</code>, <code>pull</code>, <code>push</code> 等命令，结束后立即更新 UI。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>多单元格注释。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Notebook-Multi-Cell-Commenting.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>新的终端滚动条。</p>

<p><img src="/image/vscode/terminal-scroll-bar.png" alt="image" /></p></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>在测试视图变量面板，展示变量类型，通过 <code>debug.showVariableTypes</code> 配置项设置。</p>

<p><img src="/image/vscode/debug-types.png" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>更新粘贴上的 Markdown 链接。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Links-being-updated-when-copy-and-pasting-between-Markdown-files.mp4" type="video/mp4">
</video></li>

<li><p>粘贴和拖拽文件到 CSS 文件中，自动生成 <code>url()</code>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Dropping-and-pasting-an-image-file-to-insert-a-url.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_92#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>GitHub Copilot 详见：<a href="https://code.visualstudio.com/updates/v1_92#_github-copilot">原文</a>。

<ul>
<li>Chat View 从 GPT-4-Turbo 切换到 GPT-4o。</li>
<li>从 <code>github.com</code> 中匹配相似的代码。</li>
<li>可以通过 <code>⌘/</code> 手动向 Chat View 添加附件。</li>
<li>改进 <code>@workspace /new</code>，可以在文件或项目创建期间使用聊天变量（例如 <code>#selection</code>）。另外，可以将生成的文件和文件夹保存在现有工作区中。</li>
<li>通过 <code>@vscode /runCommand</code> 实现，从聊天中访问 VSCode 命令。</li>
</ul></li>

<li><p>Python 详见：<a href="https://code.visualstudio.com/updates/v1_92#_python">原文</a>。</p>

<ul>
<li>使用 <a href="https://github.com/microsoft/vscode-docs/blob/main/release-notes/v1_91.md#python-environment-discovery-using-python-environment-tools">Python 环境工具</a>改进 Python 环境的发现，可以通过设置 <code>&quot;python.locator&quot;: &quot;native&quot;</code> 开启该能力。</li>
<li>原生 REPL （通过 <code>&quot;python.REPL.sendToNativeREPL&quot;: true</code> 配置项开启） 现在将展示成功/失败 UI。</li>
<li>源代码中的内联变量值。</li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy">Python 调试器</a>支持内联变量值能力，通过 <code>&quot;debugpy.showPythonInlineValues&quot;:true</code> 配置项配置。</p>

<p><img src="/image/vscode/show-python-inline-variables.png" alt="image" /></p></li>

<li><p>改进的调试欢迎视图，添加一个按钮，用于在编辑器中打开 Python 文件时快速访问自动 Python 配置。</p></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，详见：<a href="https://code.visualstudio.com/updates/v1_92#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>使用 https Node.js 模块的扩展现在可以使用需要基本身份验证的网络代理。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>真正的内联差异，引入了 <code>diffEditor.experimental.useTrueInlineView</code> 设置（默认关闭）。开启后单行更改将内联呈现（之前是按行的维度）：</p>

<p><img src="/image/vscode/diffEditor_trueInlineView.png" alt="image" /></p>

<p>之前</p>

<p><img src="/image/vscode/diffEditor_defaultInlineView.png" alt="image" /></p></li>

<li><p>适用于 PowerShell 的 VS Code 原生 IntelliSense，略，详见：<a href="https://code.visualstudio.com/updates/v1_92#_vs-codenative-intellisense-for-powershell">原文</a>。</p></li>

<li><p>TypeScript 5.6 支持</p></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>QuickInputButtonLocation 在输入右侧显示按钮，详见：<a href="https://code.visualstudio.com/updates/v1_92#_quickinputbuttonlocation-to-show-buttons-to-the-right-of-the-input">原文</a>。</li>
<li>测试增强功能，详见：<a href="https://code.visualstudio.com/updates/v1_92#_testing-enhancements">原文</a>。

<ul>
<li>将代码与测试相关联。</li>
<li>测试失败时的调用堆栈。</li>
<li>可归因的测试覆盖率。</li>
</ul></li>
<li>Search APIs，详见：<a href="https://code.visualstudio.com/updates/v1_92#_search-apis">原文</a>。</li>
</ul>

<h2 id="官网-website">官网 (Website)</h2>

<p><a href="https://code.visualstudio.com/">VSCode 官网</a>，进行了重新设计，支持了明暗主题。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>将 Markdown 语言服务器移至单独的存储库： <a href="https://github.com/microsoft/vscode-markdown-languageserver">microsoft/vscode-markdown-languageserver</a>。</li>
<li>逐步使用 ESM 模块替代 AMD 模块。</li>
<li>xterm.js depending upon VS Code，详见：<a href="https://code.visualstudio.com/updates/v1_92#_xtermjs-depending-upon-vs-code">原文</a>。</li>
<li>更新到 Electron 30 ，详见： <a href="https://code.visualstudio.com/updates/v1_92#_electron-30-update">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.93 (2024-08) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_93_2024-08/</link><pubDate>Sat, 07 Sep 2024 20:52:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_93_2024-08/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_93">https://code.visualstudio.com/updates/v1_93</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>正式上线 Profile 编辑器，现在可以通过统一的页面管理编辑 Profile（ 通过左下角或右上角齿轮图标的 Profile 菜单项打开）。</p>

<p><img src="/image/vscode/profiles-editor-1-93.png" alt="image" /></p></li>

<li><p>资源管理器新增反向排序配置项 <a href="vscode://settings/explorer.sortOrderReverse"><code>explorer.sortOrderReverse</code></a></p></li>

<li><p>VS Code for the Web 对 JS 类型项目提供了项目级别的智能提示以及包类型建议。</p></li>

<li><p>移动多个终端 tab。</p></li>

<li><p>调试变量视图， Watch 视图：支持通过 cmd + 单击跳转到定义。</p></li>

<li><p>Debug Console 支持通过 <code>⌥⌘F</code> 打开搜索控件进行搜索。</p></li>

<li><p>Python 支持 Django 单元测试。</p>

<p><img src="/image/vscode/django-unittests.png" alt="image" /></p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>正式上线 Profile 编辑器，现在可以通过统一的页面管理编辑 Profile。</p>

<p><img src="/image/vscode/profiles-editor-1-93.png" alt="image" /></p>

<p>打开方式如下：</p>

<ul>
<li><p>菜单 <code>Code &gt; Settings &gt; Profiles</code>。</p>

<p><img src="/image/vscode/profiles-editor-menu-item.png" alt="image" /></p></li>

<li><p>活动栏设置图标。</p>

<p><img src="/image/vscode/profiles-editor-via-manage.png" alt="image" /></p></li>
</ul>

<p>更多详见： <a href="https://code.visualstudio.com/docs/editor/profiles">官方文档 Profile</a></p></li>

<li><p>添加新的配置项 <a href="vscode://settings/window.experimentalControlOverlay"><code>window.experimentalControlOverlay</code></a> 来显示 native 窗口控件，详见：<a href="https://code.visualstudio.com/updates/v1_93#_linux-support-for-window-control-overlays">原文</a>。</p></li>

<li><p>评论支持选择：按在文件中的位置，按日期对评论进行排序。</p>

<p><img src="/image/vscode/comment-sorting-options.png" alt="image" /></p></li>

<li><p>从设置编辑器复制设置 URL。</p>

<p><img src="/image/vscode/copy-setting-url.gif" alt="image" /></p></li>

<li><p>资源管理器新增反向排序配置项 <a href="vscode://settings/explorer.sortOrderReverse"><code>explorer.sortOrderReverse</code></a></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>灯泡改进：现在决定将 <a href="vscode://settings/editor.lightbulb.enabled"><code>editor.lightbulb.enabled</code></a> 配置项的默认值设置为 <code>onCode</code>。这意味着灯泡图标仅在光标位于源代码行时才会显示，并且显示频率会降低。</li>
<li>添加对操作列表的颜色主题的控制，详见：<a href="https://code.visualstudio.com/updates/v1_93#_color-theming-for-action-lists">原文</a>。</li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<ul>
<li>改进测试生成：现在通过查找现有测试文件并将新测试生成到该文件中并将其附加到末尾来改进测试生成流程。如果还没有测试文件，Copilot 会为生成的测试创建一个新的测试文件。</li>
<li>代码操作中的生成测试和文档的菜单项被重命名为 <code>xxx using Copilot</code>。</li>
<li>改进聊天历史：新的聊天会通过 AI 总结对话生成合适的标题。</li>
<li>空窗口将默认保存聊天会话。</li>
<li>Quick Chat 添加一个附加上下文的图标。</li>
<li>点踩按钮添加反馈细节。</li>
<li>新增代码生成指令（实验性）：可以配置代码结构，代码风格等。</li>
<li>聊天视图中的自动聊天参与者检测（实验性）。</li>
<li>使用最近的编码文件作为内联聊天上下文（实验性）。</li>
<li>检测当前输入的内容是代码还是内联聊天的提示词，并自动触发内联聊天（实验性）。</li>
<li>支持生成调试配置。</li>
<li>如果测试覆盖率信息可用，会添加 CodeLens 一键生成测试。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_93#_github-copilot">原文</a>。</p>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>SQL 语言名被重命名为 MS SQL。</li>
<li>新的 YAML 语法。</li>
<li>VS Code for the Web 对 JS 类型项目提供了项目级别的智能提示以及包类型建议。</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>新增 <code>Source Control Graph</code> 视图，目前用来展示远端更新的提交。</p>

<p><img src="/image/vscode/scm-graph.png" alt="image" /></p></li>

<li><p>新增 reftable format 存储后端的支持。</p></li>

<li><p>新增 <a href="vscode://settings/scm.compactFolders"><code>scm.compactFolders</code></a> 配置项。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>终端集成支持 Julia 和 NuShell。</li>
<li>移动多个终端 tab。</li>
<li>颜色主题配置支持配置 Command guide （鼠标 hover 在终端输出上左侧展示的竖线），详见：<a href="https://code.visualstudio.com/updates/v1_93#_command-guide-setting-and-color-theming">原文</a>。</li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>在差异视图中显示或隐藏未更改的单元格。</li>
<li>管理差异视图中的空白。</li>
<li>笔记本执行计数的粘性滚动。</li>
</ul>

<p>详见：<a href="https://code.visualstudio.com/updates/v1_93#_notebooks">原文</a>。</p>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>在进程退出时（退出码非零）保持任务终端打开。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>调试变量视图， Watch 视图：支持通过 cmd + 单击跳转到定义。</li>
<li>Debug Console 支持通过 <code>⌥⌘F</code> 打开搜索控件进行搜索。</li>
<li>调试配置的<a href="https://code.visualstudio.com/docs/editor/variables-reference#_input-variables">输入变量</a>，将自动填入上次输入的值（仅当没有配置默认值时启用该特性）。</li>
<li>JavaScript 调试器：添加实验性的网络试图，可展示网络请求（支持浏览器和 nodejs 22.6.0 以上，并启用 <a href="https://nodejs.org/en/blog/release/v22.6.0#experimental-network-inspection-support-in-nodejs"><code>--experimental-network-inspection</code></a> 选项）。</li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li><p>支持显示错误堆栈以及其附近代码。</p>

<p><img src="/image/vscode/test-stack.png" alt="image" /></p></li>
</ul>

<h2 id="安装器-installer">安装器 (Installer)</h2>

<p>Debian 软件包现在会提示您确认是否要添加packages.microsoft.com 存储库。这使您能够在之后使用 apt 更新软件包。</p>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_remote-development">原文</a>。</p>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<ul>
<li>支持 <a href="vscode://settings/git.openDiffOnClick"><code>git.openDiffOnClick</code></a> 配置项。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python：</p>

<ul>
<li><p>支持 Django 单元测试。</p>

<p><img src="/image/vscode/django-unittests.png" alt="image" /></p></li>

<li><p>原生 REPL 改进，略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_native-repl-improvements">原文</a>。</p></li>

<li><p>内联提示支持跳转到定义。</p>

<p><img src="/image/vscode/pylance-gotodef-inlayhints.png" alt="image" /></p></li>

<li><p>调试测试时支持重新启动。</p></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>终端 Shell 集成 API：这个 API 使扩展能够侦听终端中运行的命令、读取其原始输出、退出代码和命令行。详见：<a href="https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api">原文</a>。</li>
<li>Authentication account API：略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_authentication-account-api">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_debug-adapter-protocol">原文</a>。</p>

<h2 id="webassemblies-in-vs-code">WebAssemblies in VS Code</h2>

<p>现在可以使用 wasm 开发 VSCode 扩展，详见：<a href="https://code.visualstudio.com/updates/v1_93#_webassemblies-in-vs-code">原文</a>。</p>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>终端智能感知提升，略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_terminal-intellisense-improvements">原文</a>。</li>
<li>Conpty 打包在 VSCode 中，略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_conpty-shipping-in-product">原文</a>。</li>
<li>TypeScript 5.6 支持。</li>
<li>新的问题报告器实现。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_proposed-apis">原文</a>。</p>

<h2 id="website">Website</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_93#_website">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<p>在此里程碑期间，完成了为 VS Code Core 采用 ESM 的大部分工作。我们的目标是使用 ECMAScript 模块 (ESM) 完全加载和删除 AMD。我们将于 9 月开始发布支持 ESM 的 Insider 版本，并计划将 ESM 交付到稳定版，以便在 10 月发布下一个版本。</p>
]]></description></item><item><title>VSCode 1.94 (2024-09) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_94_2024-09/</link><pubDate>Fri, 11 Oct 2024 20:29:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_94_2024-09/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_94">https://code.visualstudio.com/updates/v1_94</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>Profiles 支持配置工作区和目录。</p>

<p><img src="/image/vscode/profiles-editor-folders-workspaces.png" alt="image" /></p></li>

<li><p>在资源管理器，通过 <code>⌥⌘F</code> 支持按照文件名搜索，支持切换到模糊搜索。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Searching-for-files-in-explorer-find.mp4" type="video/mp4">
</video></li>

<li><p>源代码管理图视图的提升。</p>

<ul>
<li><p>如果多仓支持仓库选择。</p>

<p><img src="/image/vscode/scm-repository-picker-button.png" alt="image" /></p></li>

<li><p>历史项引用选择，默认是 Auto，可以过滤分支。</p>

<p><img src="/image/vscode/scm-reference-quick-pick.png" alt="image" /></p></li>

<li><p>历史项的上下文菜单。</p>

<p><img src="/image/vscode/scm-context-menu.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<blockquote>
<p>⚠️ 需付费订阅才能使用</p>
</blockquote>

<ul>
<li><p>在聊天中切换语言模型到 GPT-4o （<a href="https://github.com/o1-waitlist-signup">申请提前使用</a>）。</p>

<p><img src="/image/vscode/copilot-model-picker.png" alt="image" /></p></li>

<li><p>内联聊天已升级到 GPT-4o。</p></li>

<li><p>聊天中的公共代码匹配。</p>

<p><img src="/image/vscode/code-references.png" alt="image" /></p></li>

<li><p>聊天中的文件建议，通过 <code>#&lt;filename&gt;</code> 触发。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/File-suggestions-when-typing-filename.mp4" type="video/mp4">
</video></li>

<li><p>改进了聊天响应中的文件链接，支持拖拽，悬停可展示全路径，右键可以展示上下文菜单。</p>

<p><img src="/image/vscode/copilot-path-overview.png" alt="image" /></p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Dragging-and-dropping-a-workspace-file-from-copilot-into-the-editor.mp4" type="video/mp4">
</video></li>

<li><p>支持拖拽文件到聊天窗口作为上下文，对于 Inline Chat，按住 Shift 并放下文件即可将其添加为上下文。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Dragging-files-and-editors-into-chat.mp4" type="video/mp4">
</video></li>

<li><p>聊天历史包含文件附件。</p></li>

<li><p>Python native REPL 支持 Inline Chat 完成。</p></li>

<li><p>Notebook 的 Inline Chat 支持 Accept &amp; Run 按钮。</p></li>

<li><p>Notebook 的 Inline Chat 支持引用变量。</p></li>

<li><p>改进聊天视图体验。</p></li>

<li><p>语义搜索结果（预览）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Semantic-Search-in-Search-View.mp4" type="video/mp4">
</video></li>

<li><p>修复测试失败（预览），详见：<a href="https://code.visualstudio.com/updates/v1_94#_fix-test-failure-preview">原文</a>。</p></li>

<li><p>自动测试设置（实验），详见：<a href="https://code.visualstudio.com/updates/v1_94#_automated-test-setup-experimental">原文</a>。</p></li>

<li><p>从聊天启动调试（实验），详见：<a href="https://code.visualstudio.com/updates/v1_94#_start-debugging-from-chat-experimental">原文</a>。</p></li>

<li><p>Chat in Command Center (Experimental)，详见：<a href="https://code.visualstudio.com/updates/v1_94#_chat-in-command-center-experimental">原文</a>。</p></li>

<li><p>Improved temporal context (Experimental)，详见：<a href="https://code.visualstudio.com/updates/v1_94#_improved-temporal-context-experimental">原文</a>。</p></li>

<li><p>自定义指令（实验），详见：<a href="https://code.visualstudio.com/updates/v1_94#_custom-instructions-experimental">原文</a>。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_94#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>支持配置某个扩展使用账户。</p>

<p>可通过如下方式打开选项。</p>

<p><img src="/image/vscode/accountPreferenceManageTrustedExtensions.png" alt="image" /></p>

<p><img src="/image/vscode/accountPreferenceContextMenu.png" alt="image" /></p>

<p><img src="/image/vscode/accountPreferenceGear.png" alt="image" /></p>

<p>打开后可以为扩展选择账号。</p>

<p><img src="/image/vscode/accountPreferenceQuickPick.png" alt="image" /></p></li>

<li><p>Profiles 支持配置工作区和目录。</p>

<p><img src="/image/vscode/profiles-editor-folders-workspaces.png" alt="image" /></p></li>

<li><p>支持跨 profiles 更新扩展。</p></li>

<li><p>扩展视图中的警告，当存在任何无效扩展或由于版本不兼容而被禁用的扩展时，扩展视图会显示警告标志和相关信息。</p>

<p><img src="/image/vscode/extensions-warning-ux.png" alt="image" /></p></li>

<li><p>在资源管理器，通过 <code>⌥⌘F</code> 支持按照文件名搜索，支持切换到模糊搜索。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Searching-for-files-in-explorer-find.mp4" type="video/mp4">
</video></li>

<li><p>Release notes 配置项，可以点击进行进行一些快速操作。</p>

<p><img src="/image/vscode/setting-url-in-release-notes.gif" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>内联提示改进：添加了 <a href="vscode://settings/editor.inlayHints.maximumLength">editor.inlayHints.maximumLength</a> 设置，该设置控制截断多少个字符嵌入提示。</li>
<li>实验性 Edit Context，略，详见：<a href="https://code.visualstudio.com/updates/v1_94#_experimental-edit-context">原文</a>。</li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>源代码管理图视图的提升。</p>

<ul>
<li><p>如果多仓支持仓库选择。</p>

<p><img src="/image/vscode/scm-repository-picker-button.png" alt="image" /></p></li>

<li><p>历史项引用选择，默认是 Auto，可以过滤分支。</p>

<p><img src="/image/vscode/scm-reference-quick-pick.png" alt="image" /></p></li>

<li><p>历史项的上下文菜单。</p>

<p><img src="/image/vscode/scm-context-menu.png" alt="image" /></p></li>

<li><p>一些配置项如下所示：</p>

<ul>
<li><a href="vscode://settings/scm.graph.badges">scm.graph.badges</a> 控制展示在右侧图标。</li>
<li><a href="vscode://settings/scm.graph.pageOnScroll">scm.graph.pageOnScroll</a> 滚动到页尾是否自动加载下一页，默认为 true。</li>
<li><a href="vscode://settings/scm.graph.pageSize">scm.graph.pageSize</a> 每页大小，默认为 50。</li>
</ul></li>
</ul></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>多光标支持跨 Cell（预览），通过 <a href="vscode://settings/notebook.multiCursor.enabled">notebook.multiCursor.enabled</a> 配置项可以开启。</li>

<li><p>diff 支持展示元信息。</p>

<p><img src="/image/vscode/notebook-diff-document-metadata.png" alt="image" /></p></li>

<li><p>折叠差异视图中未更改的区域，可通过 <a href="vscode://settings/diffEditor.hideUnchangedRegions.enabled">diffEditor.hideUnchangedRegions.enabled</a> 配置。</p>

<p><img src="/image/vscode/notebook-unchanged-region.png" alt="image" /></p></li>

<li><p>Notebook serialization in web worker (Experimental) 详见： <a href="https://code.visualstudio.com/updates/v1_94#_notebook-serialization-in-web-worker-experimental">原文</a>。</p></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li>DAP 协议支持 ANSI 转义字符着色，可在变量视图、监视视图、悬停和调试控制台中渲染颜色。</li>

<li><p>JavaScript 调试器提升对 HTML 元素的展示。</p>

<p><img src="/image/vscode/js-debug-html.png" alt="image" /></p></li>

<li><p>编写 JavaScript 调试配置时，可以给出 node_modules 中的相关命令如 <code>vitest</code> 或 <code>nest</code> 的建议。</p></li>

<li><p>更干净的加载源视图，详见：<a href="https://code.visualstudio.com/updates/v1_94#_cleaner-loaded-sources-view">原文</a>。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>引入 TypeScript 5.6 的支持，详见：<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/">TypeScript 博客</a> 和 <a href="https://code.visualstudio.com/updates/v1_94#_typescript-56">原文</a>。</li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_94#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python

<ul>
<li>Python 扩展测试覆盖度，详尽：<a href="https://code.visualstudio.com/docs/python/testing#_run-tests-with-coverage">文档</a>。</li>
<li>默认 Python 问题匹配器，在 tasks.json 中，可通过 <code>&quot;problemMatcher&quot;: &quot;$python&quot;</code> 配置使用。</li>
<li>通过 <a href="vscode://settings/python.terminal.shellIntegration.enabled"><code>python.terminal.shellIntegration.enabled</code></a> 配置项（需重启 VSCode 并激活 Python 扩展）为 Python REPL 启用终端集成（在终端里面输入 python 命令，可以看到左侧有终端集成的小圆点）。</li>
<li>新增 Pylance 语言服务器模式配置项 <a href="vscode://settings/python.analysis.languageServerMode"><code>python.analysis.languageServerMode</code></a>，可选值 <code>light</code> 和 <code>default</code>。</li>
</ul></li>
<li>GitHub Pull Requests，略，详见：<a href="https://code.visualstudio.com/updates/v1_94#_github-pull-requests">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>在桌面 App 中删除 custom allocator，详见：<a href="https://code.visualstudio.com/updates/v1_94#_remove-custom-allocator-in-the-desktop-app">原文</a>。</li>
</ul>

<h2 id="调试适配器协议-debug-adapter-protocol">调试适配器协议 (Debug Adapter Protocol)</h2>

<p>在<a href="https://microsoft.github.io/debug-adapter-protocol">调试适配器协议</a>中形式化了如何在变量显示和输出中对文本进行着色和样式设置。着色通过 ANSI 控制序列进行工作，并要求客户端和调试适配器在其初始化请求和功能中分别支持 ANSIStyling。</p>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>支持登录多个 GitHub 账户，详见：<a href="https://code.visualstudio.com/updates/v1_94#_multiple-github-accounts">原文</a>。</li>
<li>MSAL-based Microsoft Authentication ， 详见：<a href="https://code.visualstudio.com/updates/v1_94#_msalbased-microsoft-authentication">原文</a>。</li>
<li>可通过安装 <a href="https://code.visualstudio.com/updates/v1_94#_msalbased-microsoft-authentication">TypeScript Nightly 扩展</a> 启用 TypeScript 5.7 支持。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>语言模型工具 (<code>LanguageModelTool</code>) API 继续迭代，详见： <a href="https://code.visualstudio.com/updates/v1_94#_tools-for-language-models">原文</a>。</li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>全面切换到 ESM，主工作台包大小减少了 10% 以上。</li>
<li>使用 npm 替换 yarn 作为默认的包管理器（性能上 npm 已经不差了， 减少依赖数量提升供应链安全）。</li>
</ul>
]]></description></item><item><title>VSCode 1.95 (2024-10) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_95_2024-10/</link><pubDate>Mon, 11 Nov 2024 01:06:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_95_2024-10/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_95">https://code.visualstudio.com/updates/v1_95</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>设置编辑器支持 <code>@tag:experimental</code> 或 <code>@tag:preview</code> 过滤。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Searching-for-preview-settings-in-Settings-editor.mp4" type="video/mp4">
</video></li>

<li><p>profiles 支持更多图标。</p>

<p><img src="/image/vscode/v1_95_profile-icons.png" alt="image" /></p></li>

<li><p>底部面板标签支持展示图标，通过 <a href="vscode://settings/workbench.panel.showLabels"><code>workbench.panel.showLabels</code></a> 配置项设置为 false 展示图标（或者右键底部面板标签栏选择 Show Icons 切换）。</p>

<p><img src="/image/vscode/panel-showLabels-off.png" alt="image" /></p></li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<ul>
<li><p>【预览特性】 启动一个 Copilot Edits 代码编辑会话，通过 <a href="vscode://settings/github.copilot.chat.edits.enabled"><code>github.copilot.chat.edits.enabled</code></a> 配置项开启。代码编辑会话可以根据聊天需求，自动批量修改添加的多个文件的代码，并支持多轮对话逐步迭代。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/copilot-edits-hero.mp4" type="video/mp4">
</video>

<p>使用方法如下：</p>

<ul>
<li><p>从 聊天 菜单中选择  Open Copilot Edits 或按 <code>Ctrl+Shift+I</code> 来启动编辑会话。</p>

<p><img src="/image/vscode/copilot-command-center-open-edit-session.png" alt="image" /></p></li>

<li><p>将相关文件添加到工作集中，以向 Copilot 指示您要处理哪些文件。</p></li>

<li><p>输入提示，告诉 Copilot 您想要进行的编辑！例如，向所有页面添加一个简单的导航栏或使用 vitest 而不是 jest。</p></li>
</ul>

<p>更多详见：<a href="https://code.visualstudio.com/docs/copilot/copilot-edits">Copilot Edits</a></p></li>

<li><p>聊天视图默认位置放置到第二侧边栏（右侧）。</p></li>
</ul>

<p><img src="/image/vscode/chat-new-location.png" alt="image" /></p>

<ul>
<li><p>命令中心右侧添加聊天菜单，可通过 <a href="vscode://settings/chat.commandCenter.enabled"><code>chat.commandCenter.enabled</code></a> 配置项关闭。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Chat-in-Secondary-Side-Bar_.mp4" type="video/mp4">
</video></li>

<li><p>【预览特性】 Copilot 代码审查，有两种使用方法：</p>

<ul>
<li>审查选中：在编辑器中选择代码，然后从编辑器上下文菜单中选择 Copilot &gt; 审核和评论，或使用 <code>&gt;GitHub Copilot: Review and Comment</code> 命令。</li>

<li><p>审查更改：要更深入地审核所有未提交的更改，可以选择源代码管理视图中的 Copilot 代码审核按钮，也可以在 GitHub.com 上的拉取请求中执行此操作。 （加入<a href="https://gh.io/copilot-code-review-waitlist">候补名单</a>，向所有 Copilot 订阅者开放）</p>

<p><img src="/image/vscode/review_diff.png" alt="image" /></p></li>
</ul>

<p>Copilot 的反馈在编辑器中显示为为评论，附加到代码行中。如果可能，评论包括可操作的代码建议，可以通过一项操作应用这些建议。</p>

<p><img src="/image/vscode/reviewing_selection.png" alt="image" /></p>

<p>更多详见： <a href="https://gh.io/copilot-code-review-docs">官方文档</a>。</p>

<p>另外，可以通过 <a href="vscode://settings/github.copilot.chat.reviewSelection.instructions">github.copilot.chat.reviewSelection.instructions</a> 配置项，通过自然语言描述代码评审要求。类似于 <a href="https://code.visualstudio.com/docs/copilot/copilot-customization">VS Code 中 GitHub Copilot 的自定义指令</a>。</p></li>

<li><p>【实验性特性】 自动检测聊天参与者，通过 <a href="vscode://settings/chat.experimental.detectParticipant.enabled"><code>chat.experimental.detectParticipant.enabled</code></a> 配置项开启，可以自动根据提问，将提问发送给合适的聊天参与者（如 @workspace）。如果这不是你想要的，可通过 rerun without 按钮来将问题直接发送给 Copilot。</p>

<p><img src="/image/vscode/participant-detection.png" alt="image" /></p>

<p>另外可以通过发送按钮下拉菜单选择发送的目标。</p>

<p><img src="/image/vscode/chat-send-commands.png" alt="image" /></p></li>

<li><p>控制当前编辑器上下文，Copilot 默认会将当前打开的编辑器作为上下文发送给 Copilot。现在可以通过如下图方式方式禁止发送当前编辑器的上下文。</p>

<p><img src="/image/vscode/implicit-context.png" alt="image" /></p></li>

<li><p>Copilot 回复的项目的符号，将添加链接，单击可跳转到符号位置。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Symbols-links-being-rendered-in-a-Copilot-response.mp4" type="video/mp4">
</video></li>

<li><p>在问题悬停中添加使用 Copilot 修复操作按钮</p>

<p><img src="/image/vscode/copilot-fix-problem-hover.png" alt="image" /></p></li>

<li><p>为工作区索引过程提供更多 UI 展示。</p>

<ul>
<li><p>在状态栏中展示索引中状态。</p>

<p><img src="/image/vscode/copilot-workspace-ui-progress.png" alt="image" /></p></li>

<li><p>在索引中和 Copilot 聊天时，会基于简单的索引进行回答，并展示警告：正在进行索引，回答可能不准确。</p>

<p><img src="/image/vscode/copilot-workspace-ui-warning.png" alt="image" /></p></li>
</ul></li>

<li><p>Chat follow-up improvements，详见：<a href="https://code.visualstudio.com/updates/v1_95#_chat-followup-improvements">原文</a>。</p></li>

<li><p>【实验性特性】 按语义搜索中的相关性排序，详见：<a href="https://code.visualstudio.com/updates/v1_95#_sort-by-relevance-in-semantic-search-experimental">原文</a>。</p></li>
</ul>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>支持登录多个 Github 账号，详见：<a href="https://code.visualstudio.com/updates/v1_95#_multiple-github-accounts">原文</a>。</p>

<p><img src="/image/vscode/multi-github-accounts.png" alt="image" /></p></li>

<li><p>更改帐户首选项时，添加其他帐户按钮。</p>

<p><img src="/image/vscode/use-new-account.png" alt="image" /></p></li>

<li><p>设置编辑器支持 <code>@tag:experimental</code> 或 <code>@tag:preview</code> 过滤。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Searching-for-preview-settings-in-Settings-editor.mp4" type="video/mp4">
</video></li>

<li><p>profiles 支持更多图标。</p>

<p><img src="/image/vscode/v1_95_profile-icons.png" alt="image" /></p></li>

<li><p>底部面板标签支持展示图标，通过 <a href="vscode://settings/workbench.panel.showLabels"><code>workbench.panel.showLabels</code></a> 配置项设置为 false 展示图标（或者右键底部面板标签栏选择 Show Icons 切换）。</p>

<ul>
<li><p><code>workbench.panel.showLabels: true</code></p>

<p><img src="/image/vscode/panel-showLabels-on.png" alt="image" /></p></li>

<li><p><code>workbench.panel.showLabels: false</code></p>

<p><img src="/image/vscode/panel-showLabels-off.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>通过 <a href="vscode://settings/editor.occurrencesHighlightDelay"><code>editor.occurrencesHighlightDelay</code></a> 设置，让您可以控制编辑器中高亮显示出现之前的延迟时间。在使用语义高亮时，降低延迟值可使编辑器的响应速度更快。</li>
</ul>

<h2 id="vs-code-for-the-web">VS Code for the Web</h2>

<p>从 Chrome 浏览器或 Edge 浏览器 129 版本开始，使用打开 <a href="https://insiders.vscode.dev">https://insiders.vscode.dev</a> 时，本地文件夹现在支持文件事件。如果在浏览器外对已打开工作区的文件和文件夹进行更改，这些更改会立即反映在浏览器内。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>扩展 Copilot 的扩展的展示（Copilot 提供了一套 API，可以让其他 VSCode 扩展来利用 Copilto LLM 能力来扩展 Copilot 的能力，参见： <a href="https://code.visualstudio.com/docs/copilot/copilot-extensibility-overview">GitHub Copilot extensibility in VS Code</a>），详见：<a href="https://code.visualstudio.com/updates/v1_95#_copilot-extensions-showcase">原文</a>。</li>

<li><p>Python</p>

<ul>
<li><p>原生 Python REPL 支持变量试图。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Opening-the-variable-view-within-the-debug-panel-after-executing-code-in-the-native-REPL.mp4" type="video/mp4">
</video></li>

<li><p>Pylance 生成文档字符串。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Generating-docstring-templates-for-a-function-with-Pylance-by-invoking-Ctrl+Space-inside-a-pair-of-triple-quotes_.mp4" type="video/mp4">
</video></li>

<li><p>折叠所有文档字符串，通过 <code>&gt;Pylance: Fold All Docstrings</code> 和 <code>&gt;Pylance: Unfold All Docstrings</code> 命令触发。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Folding-and-unfolding-docstrings-with-Pylances-new-commands_.mp4" type="video/mp4">
</video></li>

<li><p>Improved import suggestions，详见：<a href="https://code.visualstudio.com/updates/v1_95#_improved-import-suggestions">原文</a></p></li>

<li><p>实验性 AI Code Action： 实现抽象类，详见：<a href="https://code.visualstudio.com/updates/v1_95#_experimental-ai-code-action-implement-abstract-classes">原文</a>。</p></li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_95#_extension-authoring">原文</a>。</p>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>TypeScript 5.7 通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-next">TypeScript Nightly 扩展</a>提供。</li>

<li><p>粘贴 JavaScript 和 TypeScript 代码时，自动导入。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Update-imports-on-paste-for-JavaScript-and-TypeScript.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_95#_proposed-apis">原文</a></p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>LLM 提示词构件库。</li>
<li><a href="https://vscode.dev">VSCode Web</a> 完全切换到 ESM (ECMAScript Modules)。</li>
<li>迁移到 ESLint 9。</li>
<li>更新到 Electron 32（Chromium 128.0.6613.186 and Node.js 20.18.0）。</li>
</ul>
]]></description></item><item><title>VSCode 1.96 (2024-11) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_96_2024-11/</link><pubDate>Wed, 15 Jan 2025 11:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_96_2024-11/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_96">https://code.visualstudio.com/updates/v1_96</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>Gitub Copilot 新增了<a href="https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot">免费计划</a>，可以体验。</li>
<li>编辑器搜索历史持久化，在 VSCode 重启后可以恢复，通过 <a href="vscode://settings/editor.find.history"><code>editor.find.history</code></a> 可配置。</li>

<li><p>编辑器支持覆盖模式（用输入内容覆盖光标后方字符）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Overtype-mode.mp4" type="video/mp4">
</video></li>

<li><p>添加了对使用编辑器装饰和状态栏项目显示 blame 信息的实验性支持（类似于 gitlens）。可以使用 <a href="vscode://settings/git.blame.editorDecoration.enabled"><code>git.blame.editorDecoration.enabled</code></a> 和 <a href="vscode://settings/git.blame.statusBarItem.enabled"><code>git.blame.statusBarItem.enabled</code></a> 设置启用此功能。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Show-git-blame-information-in-editor-and-Status-Bar_.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<ul>
<li><p>Gitub Copilot 新增了<a href="https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot">免费计划</a>，限制如下：</p>

<ul>
<li>每月 2000 次 IDE 代码完成。</li>
<li>每月 50 次聊天消息。</li>
<li>不支持手机。</li>
<li>不支持 Windows terminal。</li>
<li>不支持 CLI。</li>
<li>不支持 Copilot CR 总结。</li>
<li>不支持聊天技能。</li>
<li>不支持排除特殊文件等企业版能力。</li>
</ul>

<p>官方文档参见： <a href="https://code.visualstudio.com/docs/copilot/overview">GitHub Copilot in VS Code</a></p>

<p><img src="/image/vscode/copilot-chat-view-new-user.png" alt="image" /></p></li>

<li><p>Copilot Edits，上一个里程碑是，我们推出了 Copilot Edits（目前处于预览版），它允许您使用自然语言一次快速编辑多个文件。可以通过打开命令中心中的 Copilot 菜单，然后选择打开 Copilot 编辑，或触发 Ctrl+Shift+I 来尝试 Copilot 编辑。</p>

<ul>
<li><p>进度和编辑器控制。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Copilot-Edits-changing-a-file.mp4" type="video/mp4">
</video></li>

<li><p>将 Chat 会话移动到 Copilot 编辑会话。</p>

<p><img src="/image/vscode/chat-move.png" alt="image" /></p></li>

<li><p>工作集建议，可用通过 “相关文件” ，推荐相关文件。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Add-suggested-files-to-Copilot-Edits-working-set_.mp4" type="video/mp4">
</video></li>

<li><p>重启后恢复编辑会话。</p></li>

<li><p>从资源管理器、搜索和编辑器添加到工作集。</p>

<p><img src="/image/vscode/add-file-to-edits.png" alt="image" /></p></li>

<li><p>在终端可以通过 <code>copilot-debug 启动命令</code> 启动调试。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Use-the-copilot-debug-command-to-debug-a-Go-program_.mp4" type="video/mp4">
</video></li>

<li><p>程序退出后，您可以选择重新运行程序或查看、保存或重新生成用于调试程序的 VS Code 启动配置。</p>

<p><img src="/image/vscode/copilot-debug.png" alt="image" /></p>

<p>Copilot 的调试功能（包括 copilot-debug 和 /startDebugging 意图）现在可根据需要在调试前进行编译步骤的代码生成 preLaunchTasks。对于编译语言（例如 Rust 和 C++）来说，通常就是这种情况。</p></li>
</ul></li>

<li><p>添加上下文。可以将符号和文件夹作为上下文包含在 Copilot Chat 和 Copilot Edits 中，从而使您可以在工作流程中更轻松地引用相关信息。</p>

<ul>
<li><p>符号</p>

<p>支持从大纲、面包屑拖拽。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Dragging-and-dropping-symbols-from-the-outline-view-and-editor-breadcrumbs-into-copilot-chat.mp4" type="video/mp4">
</video>

<p>支持 <code>#sym</code> 智能提示。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Writing-sym-allows-to-see-the-completion-item-sym-to-open-a-global-symbol-picker.mp4" type="video/mp4">
</video></li>

<li><p>目录</p>

<p>支持从资源管理器、面包屑或其他视图拖拽。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Dragging-and-dropping-the-@types-folder-into-copilot-chat-and-asking-how-to-implement-a-share-provider.mp4" type="video/mp4">
</video></li>
</ul></li>

<li><p><a href="https://code.visualstudio.com/docs/copilot/copilot-extensibility-overview">Copilot 扩展</a>将在插件状态页（插件详情，功能，运行时状态）展示 Copilto 用量图。</p></li>

<li><p>git 提交消息自动生成格式支持通过 <a href="vscode://settings/github.copilot.chat.commitMessageGeneration.instructions"><code>github.copilot.chat.commitMessageGeneration.instructions</code></a> 配置生成格式（详见： <a href="https://code.visualstudio.com/docs/copilot/copilot-customization">官方文档</a>）。</p></li>

<li><p>改进伪代码改进，此功能允许在编辑器中键入伪代码，然后将其用作内联聊天的提示。您还可以通过按 Ctrl+I 来触发此流程。当检测到当前行为伪代码时，将内联提示： <code>请 cmd+I 以继续处理 Copilot</code></p>

<p><img src="/image/vscode/inline-chat-nl-hint.png" alt="image" /></p></li>

<li><p>终端内联聊天焕然一新，使外观和感觉更接近编辑器内联聊天：</p>

<p><img src="/image/vscode/copilot-terminal-chat.png" alt="image" /></p></li>

<li><p>@workspace 的性能改进，略，详见：<a href="https://code.visualstudio.com/updates/v1_96#_performance-improvements-for-workspace">原文</a>。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_96#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>搜索结果改进。已安装的扩展现在会显示在搜索结果的顶部。这使得在 Marketplace 中搜索时可以更轻松地查找和管理已安装的扩展。</p>

<p><img src="/image/vscode/extension-search-order.png" alt="image" /></p></li>

<li><p>扩展搜索列表右键上下文，添加下载 VSIX 包菜单。</p>

<p><img src="/image/vscode/extensions-download.png" alt="image" /></p></li>

<li><p>扩展详情页右侧，添加磁盘使用空间统计。</p>

<p><img src="/image/vscode/extension-memory-usage-on-disk.png" alt="image" /></p></li>

<li><p>在资源管理器中查找改进。</p>

<ul>
<li><p>搜索结果匹配目录将展示匹配数目。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Find-in-the-Explorer-highlights-all-matching-results-and-adds-a-badge-to-folders-indicating-the-number-of-matches-inside_.mp4" type="video/mp4">
</video></li>

<li><p>过滤器切换仍然可用。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Find-in-the-Explorer-with-filter-mode-enabled-shows-only-the-files-and-folders-that-match-the-search-term_.mp4" type="video/mp4">
</video></li>

<li><p>当滚动到文件资源管理器的顶部时，会在顶部创建额外的空间，确保控件不会阻碍搜索结果的展示。</p>

<p><img src="/image/vscode/explorer_find_empty_space.png" alt="image" /></p></li>
</ul></li>

<li><p>在主侧栏和辅助侧栏之间移动视图。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Move-view-containers.mp4" type="video/mp4">
</video></li>

<li><p>标题栏支持右键支持隐藏标题区域中的导航控件。</p>

<p><img src="/image/vscode/nav-controls.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li>配置粘贴和拖拽行为，略，详见：<a href="https://code.visualstudio.com/updates/v1_96#_configure-paste-and-drop-behavior">原文</a>。</li>
<li>编辑器搜索历史持久化，在 VSCode 重启后可以恢复，通过 <a href="vscode://settings/editor.find.history"><code>editor.find.history</code></a> 可配置。</li>

<li><p>编辑器支持覆盖模式（用输入内容覆盖光标后方字符）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Overtype-mode.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>添加了对使用编辑器装饰和状态栏项目显示 blame 信息的实验性支持。可以使用 <a href="vscode://settings/git.blame.editorDecoration.enabled"><code>git.blame.editorDecoration.enabled</code></a> 和 <a href="vscode://settings/git.blame.statusBarItem.enabled"><code>git.blame.statusBarItem.enabled</code></a> 设置启用此功能。可以将鼠标悬停在 blame 信息上以查看更多提交详细信息。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Show-git-blame-information-in-editor-and-Status-Bar_.mp4" type="video/mp4">
</video>

<p>可以通过 <a href="vscode://settings/git.blame.editorDecoration.template"><code>git.blame.editorDecoration.template</code></a> 和 <a href="vscode://settings/git.blame.statusBarItem.template"><code>git.blame.statusBarItem.template</code></a> 配置展示模板。</p></li>

<li><p>源代码控制图标题添加推拉图标。</p>

<p><img src="/image/vscode/source-control-graph-title-actions.png" alt="image" /></p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>跨单元格选择突出显示。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Notebook-selection-highlighting-demo.mp4" type="video/mp4">
</video></li>

<li><p>多光标：选择所有出现的查找匹配项。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Notebook-select-all-occurrences-multicursor-demo.mp4" type="video/mp4">
</video></li>

<li><p>运行 Markdown 部分中的单元格。笔记本现在具有向 Markdown 单元格的单元格工具栏公开的“运行部分中的单元格”操作。如果 Markdown 单元格有标题，则执行该部分和子部分中包含的所有单元格。如果没有标题，则如果可能的话，将执行周围部分中的所有单元格。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Notebook-run-in-section-from-markdown-cell-toolbar-demo.mp4" type="video/mp4">
</video></li>

<li><p>单元格执行时间详细程度，通过 <a href="vscode://settings/notebook.cellExecutionTimeVerbosity"><code>notebook.cellExecutionTimeVerbosity</code></a>。</p>

<p><img src="/image/vscode/notebook-verbose-execution-time.png" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>终端支持连字，通过 <a href="vscode://settings/terminal.integrated.fontLigatures"><code>terminal.integrated.fontLigatures</code></a> 配置。</p>

<p><img src="/image/vscode/terminal-ligatures.png" alt="image" /></p>

<p>必须通过 <a href="vscode://settings/terminal.integrated.fontFamily"><code>terminal.integrated.fontFamily</code></a> 配置使用支持连字的字体。</p></li>

<li><p><code>terminal.integrated.tabs.title</code> 和 <code>terminal.integrated.tabs.description</code> 配置支持预置变量：</p>

<ul>
<li><code>${shellType}</code></li>
<li><code>${shellCommand}</code></li>
<li><code>${shellPromptInput}</code></li>
</ul></li>

<li><p>运行最近的命令现在显示历史源文件。</p></li>

<li><p>新的代码行识别方式格式 <code>/path/to/file.ext, &lt;line&gt;</code>。</p></li>
</ul>

<h2 id="测试-testing">测试 (Testing)</h2>

<ul>
<li><p>当可归因覆盖率可用时，“测试覆盖率”视图、编辑器操作中、打开时的“测试覆盖率”工具栏中（通过“测试：测试覆盖率工具栏”命令）或只需使用“测试：过滤覆盖率”按钮即可使用过滤器按钮测试命令。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Demo-of-the-per-test-coverage-feature.mp4" type="video/mp4">
</video></li>

<li><p>重新设计内联失败消息。</p>

<p><img src="/image/vscode/test-errors.png" alt="image" /></p></li>

<li><p>Improvements to the continuous run UI，详见：<a href="https://code.visualstudio.com/updates/v1_96#_improvements-to-the-continuous-run-ui">原文</a>。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>支持 TypeScript 5.7，详见 <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/">TypeScript blog</a>。</li>

<li><p>粘贴 JavaScript 和 TypeScript 的导入，支持局部变量的自动导出。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Imports-are-automatically-updated-when-pasting-code-between-files-in-JS-and-TS.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li>添加 remote-ssh Copilot chat 参与者。</li>
<li>增强 session 日志。</li>
</ul>

<p>更多详见：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_96.md">原文</a>。</p>

<h2 id="企业支持-enterprise-support">企业支持 (Enterprise support)</h2>

<ul>
<li>配置允许的扩展，详见官方文档： <a href="https://code.visualstudio.com/docs/setup/enterprise#configure-allowed-extensions">Enterprise support</a>。</li>
</ul>

<h2 id="使用预安装的扩展设置-vs-code-set-up-vs-code-with-preinstalled-extensions">使用预安装的扩展设置 VS Code (Set up VS Code with preinstalled extensions)</h2>

<blockquote>
<p>注意：目前仅在 Windows 上支持预安装扩展。</p>
</blockquote>

<p>您可以使用一组预安装的扩展（引导程序）来设置 VS Code。如果准备预安装了 VS Code 的计算机映像、虚拟机或云工作站并且用户可以立即使用特定扩展，则此功能非常有用。</p>

<p>步骤如下：</p>

<ul>
<li>在 VS Code 安装目录中创建文件夹 bootstrap\extensions。</li>
<li>下载要预安装的扩展的 VSIX 文件并将其放置在 bootstrap\extensions 文件夹中。</li>
<li>当用户首次启动 VS Code 时，bootstrap\extensions 文件夹中的所有扩展都会在后台静默安装。</li>
</ul>

<p>用户仍然可以卸载预安装的扩展。卸载扩展后重新启动 VS Code 将不会重新安装该扩展。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python

<ul>
<li>发布 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs">Python Environments 扩展</a>预览版，此扩展简化了 Python 环境管理，提供了用于创建、删除和管理环境的 UI，以及用于安装和卸载包的包管理。</li>
<li>Python 测试增强，详见：<a href="https://code.visualstudio.com/updates/v1_96#_python-testing-enhancements">原文</a>。</li>
<li>Python REPL 增强，详见：<a href="https://code.visualstudio.com/updates/v1_96#_python-repl-enhancements">原文</a>。</li>
<li>Pylance 新增 <code>&quot;full&quot;</code> 模式配置，详见：<a href="https://code.visualstudio.com/updates/v1_96#_pylance-full-language-server-mode">原文</a>。</li>
</ul></li>

<li><p>TypeScript</p>

<ul>
<li><p>实验性的可展开/折叠的 Hover 提示，通过 <a href="vscode://settings/typescript.experimental.expandableHover"><code>typescript.experimental.expandableHover</code></a> 配置可以打开（仅支持  TypeScript 5.8 以上版本）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/TypeScript-Expandable-Hover.mp4" type="video/mp4">
</video></li>
</ul></li>

<li><p>Microsoft 帐户现在使用 MSAL（在 Windows 上支持 WAM），而不是浏览器实现登录。</p>

<p><img src="/image/vscode/showingBrokerOSDialog.png" alt="image" /></p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>提供 <code>@vscode/chat-extension-utils</code> npm 包，以简化聊天参与者的开发，该工具可以和 <a href="https://github.com/microsoft/vscode-prompt-tsx">@vscode/prompt-tsx</a> 配合使用。</li>
<li>可归因测试覆盖 API，详见：<a href="https://code.visualstudio.com/updates/v1_96#_attributable-coverage-api">原文</a>。</li>
<li>支持三方扩展为 JavaScript 调试终端做出贡献，详见：<a href="https://github.com/microsoft/vscode-js-debug/blob/main/EXTENSION_AUTHORS.md#extension-api">文档</a>。</li>
<li><code>fetch</code> 函数支持 Proxy。</li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li><p>Github Copilot 粘贴代码以附加聊天上下文（<code>&quot;editor.pasteAs.preferences&quot;: [&quot;chat.attach.text&quot;]</code>）。</p>

<p><img src="/image/vscode/paste-code-context.gif" alt="image" /></p></li>

<li><p>终端提示支持更多 shells (pwsh v7+, zsh, bash, fish)，可通过 <a href="vscode://settings/terminal.integrated.suggest.enabled"><code>terminal.integrated.suggest.enabled</code></a> 和 <a href="vscode://settings/terminal.integrated.suggest.enableExtensionCompletions"><code>terminal.integrated.suggest.enableExtensionCompletions</code></a> 配置项启用，目前仅支持 <code>cd</code>, <code>code</code>, <code>code-insiders</code> 参数。</p>

<p><img src="/image/vscode/terminal-completions.gif" alt="image" /></p></li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li><p>快速选择的值选中 API。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">qp</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">vscode</span>.window.<span style="color:#a6e22e">createQuickPick</span>();
<span style="color:#a6e22e">qp</span>.<span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;12345678&#39;</span>;
<span style="color:#a6e22e">qp</span>.<span style="color:#a6e22e">valueSelection</span> <span style="color:#f92672">=</span> [<span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">6</span>];
<span style="color:#a6e22e">qp</span>.<span style="color:#a6e22e">items</span> <span style="color:#f92672">=</span> [
{ <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;12345678&#39;</span>, <span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;desc 1&#39;</span> },
{ <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;12345678&#39;</span>, <span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;desc 2&#39;</span> },
{ <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;12345678&#39;</span>, <span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;desc 3&#39;</span> }
];
<span style="color:#a6e22e">qp</span>.<span style="color:#a6e22e">show</span>();
</code></pre></div>
<p><img src="/image/vscode/valueSelectionQuickPick.png" alt="image" /></p></li>

<li><p>本机窗口句柄 API，略，详见：<a href="https://code.visualstudio.com/updates/v1_96#_proposed-native-window-handle-api">原文</a>。</p></li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>从 vscode-unpkg API 检查扩展更新，该服务实现了 10 分钟 TTL 的服务器端缓存，以减少对扩展市场基础设施的压力。可通过 <a href="vscode://settings/extensions.gallery.useUnpkgResourceApi"><code>extensions.gallery.useUnpkgResourceApi</code></a> 配置项禁用。</li>
<li>编辑器中 GPU 加速的基础工作，详见：<a href="https://code.visualstudio.com/updates/v1_96#_ground-work-for-gpu-acceleration-in-the-editor">原文</a>。</li>
<li>macOS 10.15 的 EOL 警告</li>
</ul>
]]></description></item><item><title>VSCode 1.97 (2025-01) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_97_2025-01/</link><pubDate>Thu, 20 Feb 2025 19:25:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_97_2025-01/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_97">https://code.visualstudio.com/updates/v1_97</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>GitHub Copilot 预览下一个编辑建议 (Copilot NES) 特性（使用 <a href="vscode://settings/github.copilot.nextEditSuggestions.enabled"><code>github.copilot.nextEditSuggestions.enabled</code></a> 配置项开启）。</p>

<p><img src="/image/vscode/nes-arrow-directions.gif" alt="image" /></p>

<p><img src="/image/vscode/gutter-menu-highlighted-updated.png" alt="image" /></p></li>

<li><p>Copilot Edits 在已正式发布。</p></li>

<li><p>命令面板和快速输入窗口可拖拽。
<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-moving-the-Command-Palette-around-the-screen.mp4" type="video/mp4">
</video></p></li>

<li><p>输出面板支持过滤和聚合。
<img src="/image/vscode/output-view-filtering.png" alt="image" />
<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-how-to-create-a-compound-log-that-combines-the-log-messages-from-two-other-logs.mp4" type="video/mp4">
</video></p></li>

<li><p>对于 GitHub 仓库，在 Git blame 悬停窗口中，添加在 GitHub 中打开的按钮。</p>

<p><img src="/image/vscode/scm-graph-hover.png" alt="image" /></p></li>

<li><p>终端默认支持连字字体。</p></li>

<li><p>关闭最后一个终端后，是否自动关闭面板，可通过 <a href="vscode://settings/terminal.integrated.hideOnLastClosed"><code>terminal.integrated.hideOnLastClosed</code></a> 配置项配置。</p></li>

<li><p>通过 <code>⌥⌘F</code> 在调试变量视图过滤和搜索变量名和值。
<img src="/image/vscode/debug-search-values.png" alt="image" /></p></li>

<li><p>VSCode Remote 对 Linux Legacy 服务器（GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25）的支持即将结束，到 v1.99 版本后，将不再支持。</p></li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<ul>
<li><p>Copilot 下一个编辑建议（预览）(Copilot NES)，使用 <a href="vscode://settings/github.copilot.nextEditSuggestions.enabled"><code>github.copilot.nextEditSuggestions.enabled</code></a> 配置项开启，可以通过 tab 快速接受，并触发下一个建议。详见：<a href="https://code.visualstudio.com/docs/copilot/ai-powered-suggestions#_next-edit-suggestions-preview">文档</a>。</p>

<p><img src="/image/vscode/nes-arrow-directions.gif" alt="image" /></p>

<p><img src="/image/vscode/gutter-menu-highlighted-updated.png" alt="image" /></p></li>

<li><p>Copilot Edits 在已正式发布。</p>

<ul>
<li><p>改进编辑器控制控件，切换到并排视图时，编辑器控件的编辑控制仍然可见。</p>

<p><img src="/image/vscode/edits-accept-hunk.png" alt="image" /></p></li>

<li><p>新增 <a href="vscode://settings/chat.editing.autoAcceptDelay"><code>chat.editing.autoAcceptDelay</code></a> 配置项，可配置自动接受建议的延迟时间，编辑器控制将展示自动接收的进度。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-a-gradient-on-the-Accept-button-for-Copilot-Edits-indicating-the-auto-accept-progress.mp4" type="video/mp4">
</video></li>
</ul></li>

<li><p>在编辑器中应用改进：</p>

<ul>
<li><p>悬停显示了文件块的生成文件路径。</p>

<p><img src="/image/vscode/apply-code-block-hover.png" alt="image" /></p></li>

<li><p>如果代码块是针对不存在的文件，则会提示在哪里创建文件。这可以处于 Copilot，无标题编辑器中建议的文件路径，也可以是在当前活动的编辑中。</p></li>

<li><p>计算和应用更改时，使用和 Copilot Edits 相同的 UI。</p></li>
</ul></li>

<li><p>时间上下文在编辑或生成代码时会有所帮助，通过告知语言模型有关您最近与之交互的文件。正在实验和衡量其有效性，可以通过 <a href="vscode://settings/github.copilot.chat.editor.temporalContext.enabled"><code>github.copilot.chat.editor.temporalContext.enabled</code></a> 和 <a href="vscode://settings/github.copilot.chat.edits.temporalContext.enabled"><code>github.copilot.chat.edits.temporalContext.enabled</code></a> 开启。</p></li>

<li><p>工作区索引状态 UI。</p>

<p><img src="/image/vscode/copilot-workspace-status.png" alt="image" /></p></li>

<li><p>构建远程工作区索引。</p></li>

<li><p>工作区搜索改进，详见：<a href="https://code.visualstudio.com/updates/v1_97#_workspace-search-improvements">原文</a>。</p></li>

<li><p>git 更改上下文变量，在编写聊天或编辑查询时，您现在可以使用 <code>#changes</code> 上下文变量在GIT源控制中修改的文件。例如：<code>总结我工作区中的 #changes</code>。</p>

<p><img src="/image/vscode/copilot-chat-git-changes.png" alt="image" /></p></li>

<li><p>可用模型新增： <code>OpenAI’s o3-mini</code> 和 <code>Gemini 2.0 Flash</code>。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_97#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>命令面板和快速输入窗口可拖拽。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-moving-the-Command-Palette-around-the-screen.mp4" type="video/mp4">
</video></li>

<li><p>信任扩展发布者，当第一次从发布者中安装扩展名时，您现在将看到一个对话框，以帮助您评估扩展发布者的可信度。此功能有助于确保您只能从受信任来源安装扩展，从而增强开发环境的安全性。
<img src="/image/vscode/trust-publisher-dialog.png" alt="image" />
也可以通过 <code>&gt;Extensions: Manage Trusted Extensions Publishers</code> 管理扩展信任情况。
<img src="/image/vscode/manage-trusted-publishers.png" alt="image" /></p></li>

<li><p>输出面板过滤。
<img src="/image/vscode/output-view-filtering.png" alt="image" /></p></li>

<li><p>日志聚合查看。日志分布在多个日志中，现在，可以在单个复合日志视图中查看多个日志。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-how-to-create-a-compound-log-that-combines-the-log-messages-from-two-other-logs.mp4" type="video/mp4">
</video></li>

<li><p>输出面板移除菜单添加：导出和导入日志功能。</p></li>

<li><p>设置编辑器搜索问题修复，详见：<a href="https://code.visualstudio.com/updates/v1_97#_settings-editor-search-fixes">原文</a>。</p></li>

<li><p>扩展过滤能力增强增强，新增 <code>@outdated</code> 和 <code>@recentlyUpdated</code>。</p>

<p><img src="/image/vscode/extension-filters.png" alt="image" /></p></li>

<li><p>支持 SVG 图像预览。</p>

<p><img src="/image/vscode/image-svg-preview.png" alt="image" /></p></li>

<li><p>vscode cli 添加 <code>--remove</code> 参数，支持从 <a href="https://code.visualstudio.com/docs/editor/workspaces/multi-root-workspaces">multi-root 工作区</a>中移除文件夹。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">code --remove /path/to/rootfolder</code></pre></div></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>替换空间，输入历史持久化，可通过 <a href="vscode://settings/editor.find.replaceHistory"><code>editor.find.replaceHistory</code></a> 配置项关闭。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-persistence-of-editor-replace-history-across-VS-Code-reloads.mp4" type="video/mp4">
</video></li>

<li><p>评论。</p>

<ul>
<li>关闭未提交的评论时二次确认，可通过 <a href="vscode://settings/comments.thread.confirmOnCollapse"><code>comments.thread.confirmOnCollapse</code></a> 配置项关闭。</li>
<li>评论编辑器中的快速操作。</li>
</ul>

<p><img src="/image/vscode/quick-actions-in-comment.gif" alt="image" /></p></li>
</ul>

<h2 id="源代码版本管理-source-control">源代码版本管理 (Source Control)</h2>

<ul>
<li><p>Git blame 信息。在状态栏中显示正在编辑的 git blame 信息，并改进了悬停在编辑器装饰或状态栏项目上时所显示的信息。可通过 <a href="vscode://settings/git.blame.statusBarItem.enabled"><code>git.blame.statusBarItem.enabled</code></a> 配置项启用。可通过 <a href="vscode://settings/git.blame.editorDecoration.enabled">git.blame.editorDecoration.enabled</a> 配置项启用。</p>

<p><img src="/image/vscode/scm-git-blame.png" alt="image" /></p></li>

<li><p>对于 GitHub 仓库，在 Git blame 悬停窗口中，添加在 GitHub 中打开的按钮。</p>

<p><img src="/image/vscode/scm-graph-hover.png" alt="image" /></p></li>

<li><p>在悬停窗口中，展示作者头像，可通过 <a href="vscode://settings/github.showAvatar"><code>github.showAvatar</code></a> 配置项关闭。</p></li>

<li><p>在源代码管理图视图上下文菜单添加 Checkout、删除分支、删除标签。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>内联显示 Cell 执行值。
<img src="/image/vscode/nb-inline-values.png" alt="image" /></p></li>

<li><p>Markdown Cell 支持自定义字体，可通过 <a href="vscode://settings/notebook.markup.markdown.fontFamily"><code>notebook.markup.markdown.fontFamily</code></a> 配置。</p>

<p><img src="/image/vscode/markdown-cell-font-family.png" alt="image" /></p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>字体连字特性默认启用。

<ul>
<li>通过 <a href="vscode://settings/terminal.integrated.fontLigatures.enabled"><code>terminal.integrated.fontLigatures.enabled</code></a> 配置项开启字体连字（需通过 <a href="vscode://settings/terminal.integrated.fontFamily"><code>terminal.integrated.fontFamily</code></a> 配置支持连字的字体）。</li>
<li>光标选中时，连字将会禁用。</li>
<li>通过 <a href="vscode://settings/terminal.integrated.fontLigatures.featureSettings"><code>terminal.integrated.fontLigatures.featureSettings</code></a> 配置项，透传给 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings">font-feature-settings</a> CSS 属性。</li>
<li>在 VSCode 使用的库识别连字不支持时，可以使用 terminal.integrated.fontLigatures.fallbackLigatures 手动设置连字的字符序列。</li>
</ul></li>

<li><p>支持 ConEmu 的进度展示控制字符 <code>ESC ] 9 ; 4</code>，并支持通过 <code>${progress}</code> 展示到终端标题（<a href="vscode://settings/terminal.integrated.tabs.title"><code>terminal.integrated.tabs.title</code></a>）和描述（<a href="vscode://settings/terminal.integrated.tabs.description"><code>terminal.integrated.tabs.description</code></a>）中。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-a-progress-indicator-in-the-terminal-title-while-running-a-winget-command.mp4" type="video/mp4">
</video></li>

<li><p>终端开启粘性滚动时（<a href="vscode://settings/terminal.integrated.stickyScroll.enabled"><code>terminal.integrated.stickyScroll.enabled</code></a> 配置项），将在结尾显示省略号。</p></li>
</ul>

<p><img src="/image/vscode/terminal-sticky-scroll-ellipsis.png" alt="image" /></p>

<ul>
<li>关闭最后一个终端后，是否自动关闭面板，可通过 <a href="vscode://settings/terminal.integrated.hideOnLastClosed"><code>terminal.integrated.hideOnLastClosed</code></a> 配置项配置。</li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li><code>${columnNumber}</code> 列表变量可以在 <a href="https://code.visualstudio.com/docs/editor/tasks">tasks.json</a> 和 <a href="https://code.visualstudio.com/docs/editor/debugging#_launch-configurations">launch.json</a> 中使用。全部变量，详见：<a href="https://code.visualstudio.com/docs/editor/variables-reference">文档</a>。</li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>通过 <code>⌥⌘F</code> 在调试变量视图过滤和搜索变量名和值。
<img src="/image/vscode/debug-search-values.png" alt="image" /></p></li>

<li><p>改进调试控制台的选中体验。</p></li>

<li><p>JavaScript 调试器。可以使用 <code>&gt;debug: Pretty Print</code> 命令，将正在调试的 JavaScript 文件进行格式化并定位到断点的行。</p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>发布 TypeScript 5.7.3，详见： <a href="https://code.visualstudio.com/updates/v1_97#_typescript-573">官方文档</a>。</li>

<li><p>Markdown</p>

<ul>
<li><p>预览页，图片右击可在新窗口打开。</p>

<p><img src="/image/vscode/md-preview-open-image.png" alt="image" /></p></li>

<li><p>Markdown 链接验证将展示到状态栏。</p>

<p><img src="/image/vscode/md-link-status-item.png" alt="image" /></p></li>
</ul></li>

<li><p>新的 Ruby 语法高亮语法。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>VSCode 正在快速接近对 Linux Legacy 服务器的支持结束。 VSCode v1.98（2025年2月）将是支持 Linux 遗留服务器的最后一个版本（支持 GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25）的版本。到 v1.99，无法再连接到这些服务器。</p>

<p>详见：<a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_97.md">Remote Development 发布记录</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Microsoft Account，略，详见：<a href="https://code.visualstudio.com/updates/v1_97#_microsoft-account-now-uses-msal-with-wam-support-on-windows">原文</a>。</li>

<li><p>Python</p>

<ul>
<li><p>从终端一键打开 VSCode 原生 REPL（通过 <a href="vscode://settings/python.terminal.shellIntegration.enabled"><code>python.terminal.shellIntegration.enabled</code></a> 配置开启）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-Native-REPL-entry-point-link-in-terminal-REPL.mp4" type="video/mp4">
</video></li>

<li><p>无配置调试，详见：<a href="https://code.visualstudio.com/updates/v1_97#_no-config-debug">原文</a>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-no-config-debug-feature-for-Python.mp4" type="video/mp4">
</video></li>

<li><p>Test 发现取消。</p>

<p><img src="/image/vscode/test-discovery-cancelation.png" alt="image" /></p></li>

<li><p>跳转到实现。
<img src="/image/vscode/pylance-go-to-implementation.png" alt="image" /></p></li>

<li><p>AI Code Action: Generate Symbol (Experimental)，详见：<a href="https://code.visualstudio.com/updates/v1_97#_ai-code-action-generate-symbol-experimental">原文</a>。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-invoking-a-new-class.mp4" type="video/mp4">
</video></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，略，详见：<a href="https://code.visualstudio.com/updates/v1_97#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="预览特性-preview-features">预览特性 (Preview Features)</h2>

<ul>
<li>Copilot Edits 新增实验性的代理模式。在代理模式，Copilot 会端到端的，自动的，搜索工作区上下文，编辑文件，执行终端命令，检查错误，目前在灰度中，可在 <a href="https://code.visualstudio.com/insiders/">VSCode Insiders</a> 中体验。详见：<a href="https://code.visualstudio.com/updates/v1_97#_agent-mode-experimental">原文</a>。</li>
<li>代理的代码仓库搜索，通过 <a href="vscode://settings/github.copilot.chat.edits.codesearch.enabled"><code>github.copilot.chat.edits.codesearch.enabled</code></a> 配置项开启。代理的代码仓库搜索指的是在 Copilot Edits 中添加 <code>#codebase</code> 指令时，使用其他工具搜索代码（如：文件、文本、git 状态、目录），而不是仅仅语义搜索。</li>
<li>在 <a href="https://code.visualstudio.com/insiders/">VSCode Insiders</a> 中预览， Copilot 视觉，可给 Copilot 发送图片。详见：<a href="https://code.visualstudio.com/updates/v1_97#_copilot-vision-in-vs-code-insiders-preview">原文</a>。</li>
<li>可重复使用的提示词，详见：<a href="https://code.visualstudio.com/updates/v1_97#_reusable-prompts-experimental">原文</a>。</li>
<li>Linux 平台自定义标题栏，详见：<a href="https://code.visualstudio.com/updates/v1_97#_custom-title-bar-on-linux-experimental">原文</a>。</li>
<li>TypeScript 5.8 beta 支持，详见：<a href="https://code.visualstudio.com/updates/v1_97#_typescript-58-beta-support">原文</a>。</li>
<li>终端完成支持更多 Shell，详见：<a href="https://code.visualstudio.com/updates/v1_97#_terminal-completions-for-more-shells">原文</a>。</li>
<li>基于 <a href="https://tree-sitter.github.io/tree-sitter/">Tree-Sitter</a> 的语法高亮，通过 <a href="vscode://settings/editor.experimental.preferTreeSitter"><code>editor.experimental.preferTreeSitter</code></a> 配置开启 TypeScript 的实验性支持。与 <a href="https://macromates.com/manual/en/language_grammars">TextMate grammars</a> 相比，Tree-Sitter 性能更好，准确性更高（具体可以看 Zed 以及 Tree-Sitter 核心贡献者的文章 <a href="https://zed.dev/blog/syntax-aware-editing">Enabling low-latency, syntax-aware editing using Tree-sitter</a>）。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>文档粘贴 API，详见：<a href="https://code.visualstudio.com/updates/v1_97#_document-paste-api">原文</a>。</li>
<li><code>OpenDialogOptions</code> 的 <code>openLabel</code> 属性在简单文件选择器中支持（简单文件选择器可通过 <a href="vscode://settings/files.simpleDialog"><code>files.simpleDialog</code></a> 配置项启用），详见：<a href="https://code.visualstudio.com/updates/v1_97#_file-openlabel-shows-in-the-simple-file-picker">原文</a>。</li>
<li>文件层级评论 API，详见：<a href="https://code.visualstudio.com/updates/v1_97#_filelevel-comments-api">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>终端完成提供者，详见：<a href="https://code.visualstudio.com/updates/v1_97#_terminal-completion-provider">原文</a>。</li>
<li>终端 Shell 类型，详见：<a href="https://code.visualstudio.com/updates/v1_97#_terminal-shell-type">原文</a>。</li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Housekeeping，年末清理一波 Issue。</li>
<li>优化 TypeScript 工作区中 Watch 文件的资源占用。</li>
</ul>
]]></description></item><item><title>VSCode 1.98 (2025-02) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_98_2025-02/</link><pubDate>Thu, 03 Apr 2025 12:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_98_2025-02/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_98">https://code.visualstudio.com/updates/v1_98</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>GitHub Copilot</p>

<ul>
<li>修改自动完成模型，通过 <code>&gt;Change Completions Model</code> 命令可配置（实测：<code>github.copilot@1.293.0</code> 找不到该命令）。</li>
<li>可用模型新增：<code>GPT 4.5 (Preview)</code> 和 <code>Claude 3.7 Sonnet (Preview)</code>。</li>

<li><p>预览支持 Copilot Vision （多模态），支持将图像作为输入，目前支持 <code>GPT 4o</code> 模型。</p>

<p><img src="/image/vscode/image-attachments.gif" alt="image" /></p></li>

<li><p>实验性支持 Copilot 状态概述视图，展示剩余 quota 和重置时间，自动完成配置开关，快捷键配置。</p>

<p><img src="/image/vscode/copilot-status.png" alt="image" /></p></li>
</ul></li>

<li><p>默认情况下，Linux上启用了自定义标题栏。通过自定义标题栏可访问布局控件，Copilot 菜单等。</p>

<p><img src="/image/vscode/custom-title.png" alt="image" /></p></li>

<li><p>Peek 引用支持拖拽。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-drag-and-drop-of-a-peek-reference-as-new-editor-group.mp4" type="video/mp4">
</video></li>

<li><p>丢弃未跟踪的更改时，不再永久删除，而是移动到回收站，可通过 <a href="vscode://settings/git.discardUntrackedChangesToTrash">git.discardUntrackedChangesToTrash</a>。</p>

<p><img src="/image/vscode/scm-move-to-trash.png" alt="image" /></p></li>

<li><p>Debug 内联值 Hover 展示全部的值，以方便展示更长的值。</p>

<p><img src="/image/vscode/debug-inline-values-rich-hover.png" alt="image" /></p></li>

<li><p>当前版本是对 Linux Legacy 服务器（GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25）支持的最后一个版本，到下一个版本 v1.99 版本后，将不再支持。</p></li>
</ul>

<h2 id="github-copilot">GitHub Copilot</h2>

<ul>
<li><p>Copilot Edits</p>

<ul>
<li><p>实验性特性：支持代理模式改进（可在 VS Code Insiders 体验，在代理模式下，Copilot可以自动搜索您的工作空间以查看相关上下文，编辑文件，检查错误并运行终端命令（用户确认后）以完成任务端到端）：</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-editing-a-suggested-terminal-command-in-Chat.mp4" type="video/mp4">
</video>

<ul>
<li>内联显示终端命令，因此可以跟踪运行哪些命令。</li>
<li>可以在聊天响应中在聊天响应中编辑建议的终端命令。</li>
<li>使用 <code>⌘ENTER</code> 快捷方式确认终端命令。</li>
<li>实验性特性：代理模式展示搜索了哪些上下文。
<img src="/image/vscode/agent-mode-search-results.png" alt="image" /></li>
<li>更多详见： <a href="https://code.visualstudio.com/docs/copilot/copilot-edits#_use-agent-mode-preview">Copilot Edits agent mode</a> 和  <a href="https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode">agent mode announcement blog post</a>。</li>
</ul></li>
</ul></li>

<li><p>Notebook 预览支持 Copilot Edits，略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_notebook-support-in-copilot-edits-preview">原文</a>。</p></li>

<li><p>改进文件修改和编辑器的集成。</p>

<ul>
<li>当代码没有改动时，编辑器将不会滚动。</li>
<li>编辑内容审查操作从 &ldquo;Accept&rdquo; 改为 &ldquo;Keep&rdquo;，从 &ldquo;Discard&rdquo; 改为 &ldquo;Undo&rdquo;。</li>
<li>完成审查操作后，将自动打开下一个待审查文件。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-that-changes-from-Copilot-Edits-are-saved-automatically-and-the-user-decided-to-keep-them.mp4" type="video/mp4">
</video></li>

<li><p>Refreshed UI，将 Copilot Edits UI 和 Copilot Chat 对齐。详见：<a href="https://code.visualstudio.com/updates/v1_98#_refreshed-ui">原文</a>。</p></li>

<li><p>删除 Copilot Edits 限制：不在限制最高 10 个文件上下文、不在在客户端限制每 10 分钟最高 14 个任务（服务端频控仍然存在）。</p></li>

<li><p>自定义指令正式可用。可以再 <code>.github/copilot-instructions.md</code> 填写提示词，告知模型特定需求。更多参见： <a href="https://code.visualstudio.com/docs/copilot/copilot-customization">custom instructions in Copilot</a>。</p></li>

<li><p>请求 github 时，更加流畅的进行身份验证。</p>

<p><img src="/image/vscode/confirmation-auth-dialog.png" alt="image" /></p></li>

<li><p>添加 <code>#codebase</code> 上下文标签后，Copilot 可以利用更多的工具来查找代码，如：基于嵌入的语义搜索、文本搜索、文件搜索、git 修改文件、项目结构、读文件、读目录、工作区符号搜索。</p></li>

<li><p>支持将问题和文件夹附加到聊天上下文。</p></li>

<li><p>预览的 Next Edit Suggestions 新增 <a href="vscode://settings/editor.inlineSuggest.edits.showCollapsed">editor.inlineSuggest.edits.showCollapsed</a> 配置（根据文档和试用没有理解到这个配置的作用），详见：<a href="https://code.visualstudio.com/updates/v1_98#_collapsed-mode-for-next-edit-suggestions-preview">官方文档</a>。</p></li>

<li><p>修改自动完成模型，通过 <code>&gt;Change Completions Model</code> 命令可配置（实测：<code>github.copilot@1.293.0</code> 找不到该命令）。</p></li>

<li><p>可用模型新增：<code>GPT 4.5 (Preview)</code> 和 <code>Claude 3.7 Sonnet (Preview)</code>。</p></li>

<li><p>预览支持 Copilot Vision （多模态），支持将图像作为输入，目前支持 <code>GPT 4o</code> 模型。</p>

<p><img src="/image/vscode/image-attachments.gif" alt="image" /></p></li>

<li><p>实验性支持 Copilot 状态概述视图，展示剩余 quota 和重置时间，自动完成配置开关，快捷键配置。</p>

<p><img src="/image/vscode/copilot-status.png" alt="image" /></p></li>

<li><p>实验性支持 TypeScript context for inline completions，略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_typescript-context-for-inline-completions-experimental">原文</a>。</p></li>

<li><p>自定义指令支持对 pr 的标题和描述的设置，更多详见： <a href="https://code.visualstudio.com/docs/copilot/copilot-customization">customizing Copilot in VS Code</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;github.copilot.chat.pullRequestDescriptionGeneration.instructions&#34;</span>: [
        {
        <span style="color:#f92672">&#34;text&#34;</span>: <span style="color:#e6db74">&#34;Prefix every PR title with an emoji.&#34;</span>
        }
    ]
}</code></pre></div></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_accessibility">原文</a>。</p>

<h2 id="工作台-workbench">工作台 (Workbench)</h2>

<ul>
<li><p>默认情况下，Linux上启用了自定义标题栏。通过自定义标题栏可访问布局控件，Copilot 菜单等。可以通过 <a href="vscode://settings/window.titleBarStyle"><code>window.titleBarStyle</code></a> 禁用。</p>

<p><img src="/image/vscode/custom-title.png" alt="image" /></p>

<p><img src="/image/vscode/restore-title.png" alt="image" /></p></li>

<li><p>第二侧边栏可通过 <a href="vscode://settings/workbench.secondarySideBar.showLabels"><code>workbench.secondarySideBar.showLabels</code></a> 设置标签显示文字还是图标（仅在 <code>workbench.activityBar.location</code> 为非 top 时有效）。</p>

<p><img src="/image/vscode/aux-sidebar.png" alt="image" /></p></li>

<li><p>预览性，优化设置编辑器，搜索算法，略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_new-settings-editor-keymatching-algorithm-preview">原文</a>。</p></li>

<li><p>在<a href="https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_simple-file-dialog">简单文件选择器</a>中支持隐藏掩藏文件（可通过 <a href="vscode://settings/files.simpleDialog.enable"><code>files.simpleDialog.enable</code></a> 配置项开启简单文件选择器）。</p>

<p><img src="/image/vscode/hide-dot-file.png" alt="image" /></p></li>
</ul>

<h2 id="编辑器-editor">编辑器 (Editor)</h2>

<ul>
<li><p>Peek 引用支持拖拽。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-drag-and-drop-of-a-peek-reference-as-new-editor-group.mp4" type="video/mp4">
</video></li>

<li><p>通过 <a href="vscode://settings/editor.occurrencesHighlightDelay"><code>editor.occurrencesHighlightDelay</code></a> 配置项可以控制突出显示的延迟。</p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li>源代码管理视图的子视图的标题被简化为 存储库（Repositories）、更改（Changes）、图形（Graph）。</li>

<li><p>丢弃未跟踪的更改时，不再永久删除，而是移动到回收站，可通过 <a href="vscode://settings/git.discardUntrackedChangesToTrash">git.discardUntrackedChangesToTrash</a>。</p>

<p><img src="/image/vscode/scm-move-to-trash.png" alt="image" /></p></li>

<li><p>实验性特性：新增诊断提交 Hook，新增 <a href="vscode://settings/git.diagnosticsCommitHook.Enabled"><code>git.diagnosticsCommitHook.Enabled</code></a> 和 <a href="vscode://settings/git.diagnosticsCommitHook.Sources"><code>git.diagnosticsCommitHook.Sources</code></a> 配置，可以在提交时检查代码质量，检查 VSCode 诊断的问题是否完全解决。</p>

<p><img src="/image/vscode/scm-diagnostics-commit-hook.png" alt="image" /></p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li><p>实验性支持：内联笔记本差异视图，可通过 <a href="vscode://settings/notebook.diff.experimental.toggleInline"><code>notebook.diff.experimental.toggleInline</code></a> 配置项启用。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-toggling-an-edit-suggestion-from-side-by-side-to-an-inline-diff-for-notebooks.mp4" type="video/mp4">
</video></li>

<li><p>内联值 Hover 保持空格换行格式。</p>

<p><img src="/image/vscode/nb-inline-values-rich-hover.png" alt="image" /></p></li>
</ul>

<h2 id="终端-intellisense-terminal-intellisense-preview">终端 Intellisense (Terminal IntelliSense (Preview))</h2>

<blockquote>
<p>通过 <a href="vscode://settings/terminal.integrated.suggest.enabled"><code>terminal.integrated.suggest.enabled</code></a> 配置项启用。</p>
</blockquote>

<ul>
<li>增强的 Fig 命令完成支持。目前，支持如下命令：

<ul>
<li>基础命令: cat, chmod, chown, cp, curl, df, du, echo, find, grep, head, less, ls, mkdir, more, mv, pwd, rm, rmdir, tail, top, touch, uname</li>
<li>进程命令: kill, killall, ps</li>
<li>包管理: apt, brew</li>
<li>Node.js 生态: node, npm, npx, nvm, pnpm, yarn</li>
<li>版本管理、语言、编辑器: git, nano, python, python3, vim</li>
<li>网络: scp, ssh, wget</li>
</ul></li>

<li><p>支持动态自动完成。</p>

<p><img src="/image/vscode/terminal-git-checkout.png" alt="image" /></p></li>

<li><p>通过 <a href="vscode://settings/terminal.integrated.suggest.quickSuggestions"><code>terminal.integrated.suggest.quickSuggestions</code></a> 设置，可以控制终端的自动完成功能。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;terminal.integrated.suggest.quickSuggestions&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
<span style="color:#f92672">&#34;commands&#34;</span>: <span style="color:#e6db74">&#34;on&#34;</span>,
<span style="color:#f92672">&#34;arguments&#34;</span>: <span style="color:#e6db74">&#34;on&#34;</span>,
<span style="color:#f92672">&#34;unknown&#34;</span>: <span style="color:#e6db74">&#34;off&#34;</span>
}</code></pre></div></li>

<li><p>改进内联建议检测，并默认将内联建议，显示在建议列表的最上方，通过 <a href="vscode://settings/terminal.integrated.suggest.inlineSuggestion"><code>terminal.integrated.suggest.inlineSuggestion</code></a> 可配置对内联建议的处理。</p>

<p><img src="/image/vscode/terminal-fish-inline-suggest.png" alt="image" /></p></li>

<li><p>Bash 和 Zsh 内置命令和 PowerShell 命令的完成将展示命令描述。</p>

<ul>
<li>bash 通过 <code>help &lt;command&gt;</code> 获取</li>
<li>zsh 通过 <code>man zshbuiltins</code> 获取</li>
<li>powershell 通过 <code>Get-Help &lt;command&gt;</code> 获取</li>
</ul>

<p><img src="/image/vscode/terminal-zsh-builtin-completions.png" alt="image" /></p></li>

<li><p>建议列表排序改进。</p>

<ul>
<li><p>对于命令提示：</p>

<ul>
<li>拥有完整的详细信息通常出现在较少详细的完成之上。</li>
<li>内置命令优先于 <code>$PATH</code> 的路径。</li>
</ul>

<p><img src="/image/vscode/terminal-zsh-order.png" alt="image" /></p></li>

<li><p>对于路径提示：</p>

<ul>
<li><code>_</code> 开头文件放到最后。</li>
<li><code>.</code> 开头文件在排序时会去掉开头的 <code>.</code>，再参与排序。</li>
</ul>

<p><img src="/image/vscode/terminal-underscore-punc.png" alt="image" /></p></li>
</ul></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li><p>Task 类别终端标题，添加重新运行按钮。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-the-terminal-rerun-task.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="调试-debug">调试 (Debug)</h2>

<ul>
<li><p>Debug 内联值 Hover 展示全部的值，以方便展示更长的值。</p>

<p><img src="/image/vscode/debug-inline-values-rich-hover.png" alt="image" /></p></li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li>发布 TypeScript 5.8.2 ，更多详见： <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/">TypeScript 5.8 release blog</a>。</li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li>当前版本是对 Linux Legacy 服务器（GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25）支持的最后一个版本，到下一个版本 v1.99 版本后，将不再支持。</li>
</ul>

<p>更多详见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_98.md">官方文档</a></p>

<h2 id="企业支持-enterprise-support">企业支持 (Enterprise support)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_enterprise-support">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python

<ul>
<li>Automatic quotation insertion when breaking long strings.</li>
<li>Pylance 内存消耗改进。</li>
<li>改进 Python Shell 集成，Python Shell 集成可通过 <a href="vscode://settings/python.terminal.shellIntegration.enabled"><code>python.terminal.shellIntegration.enabled</code></a> 配置项开启。</li>
<li>Correct workspace prompt for Windows Git Bash</li>
<li>配置发现扫描文件，可通过 <a href="vscode://settings/python.testing.autoTestDiscoverOnSavePattern"><code>python.testing.autoTestDiscoverOnSavePattern</code></a> 配置设置，默认值为 <code>**/*.py</code>。</li>
<li>Python Test 的调试配置可以同时在 settings.json 和 launch.json 配置，launch.json 优先级更高。</li>
</ul></li>
<li>GitHub authentication，略，详见：<a href="https://code.visualstudio.com/updates/v1_98#_improved-proxy-support-with-electron-fetch-adoption">原文</a>。</li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Authentication API 变更，详见： <a href="https://code.visualstudio.com/updates/v1_98#_authentication">原文</a>。</li>
<li>精简 Snippet API，添加 <code>keepWhitespace</code> 选项，详见： <a href="https://code.visualstudio.com/updates/v1_98#_refined-snippet-api">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>研究文件编码相关 API。</li>
<li>扩展支持读取 Shell 集成终端相关信息（仅在 <a href="vscode://settings/terminal.integrated.shellIntegration.enabled"><code>terminal.integrated.shellIntegration.enabled</code></a> 开启后可用），详见：<a href="https://code.visualstudio.com/updates/v1_98#_shell-environment">原文</a>。</li>
</ul>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>Electron 更新到 34 版本。</li>
<li>MacOS 10.15支持已经结束。</li>
<li>Dev-time tracking of leaked disposables，详见： <a href="https://code.visualstudio.com/updates/v1_98#_devtime-tracking-of-leaked-disposables">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.99 (2025-03) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_99_2025-03/</link><pubDate>Tue, 20 May 2025 11:28:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_99_2025-03/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_99">https://code.visualstudio.com/updates/v1_99</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li>Copilot

<ul>
<li>Agent 模式在 VSCode 稳定版中可用，并支持 mcp。</li>
<li>Ask、Edit、Agent 模式统到的 Chat 视图。</li>
</ul></li>

<li><p>可通过 <a href="vscode://settings/editor.inlineSuggest.syntaxHighlightingEnabled">editor.inlineSuggest.syntaxHighlightingEnabled</a> 可开启内联建议语法高亮。</p>

<p><img src="/image/vscode/inlineSuggestionHighlightingEnabled.png" alt="image" /></p></li>

<li><p><code>&gt;git checkout to...</code> 命令展示更多细节。</p>

<p><img src="/image/vscode/scm-repository-picker.png" alt="image" /></p></li>

<li><p>Linux Legacy Server （GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25） 支持已结束。如果仍想运行，请参考 <a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">FAQ</a> 进行 patch。</p></li>
</ul>

<h2 id="聊天-chat">聊天 (Chat)</h2>

<ul>
<li><p>Agent 模式在 VSCode 稳定版中可用，可通过 <a href="vscode://settings/chat.agent.enabled"><code>chat.agent.enabled</code></a> 设置关闭（默认开启）。可通过如下图使用 Agent 模式。免费订阅也可以使用（<a href="https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot">订阅计划文档</a>）。更多详见： <a href="https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode">agent mode 文档</a>。</p>

<p><img src="/image/vscode/copilot-edits-agent-mode.png" alt="image" /></p>

<p>Agent 模式主要 AI 驱动工具，端到端、自动的实现用户的需求。支持调用如下工具：</p>

<ul>
<li>内建工具：读写文件、运行终端命令。</li>
<li><a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">MCP 工具</a>。</li>
<li><a href="https://code.visualstudio.com/api/extension-guides/tools">通过扩展贡献的工具</a>。</li>
</ul>

<p>相关设置如下：</p>

<ul>
<li><a href="vscode://settings/chat.agent.enabled"><code>chat.agent.enabled</code></a>： 启用 agent 模式，默认为 true。</li>
<li><a href="vscode://settings/chat.agent.maxRequests"><code>chat.agent.maxRequests</code></a>：一次 agent 对话，最大请求模型请求数量（免费用户默认 5， 其他用户默认 15）。</li>
<li><a href="vscode://settings/chat.mcp.discovery.enabled"><code>chat.mcp.discovery.enabled</code></a>：启用 MCP 服务发现，自动发现当前设备上其他 AI 工具配置的 MCP 服务，默认为 true。也可以分应用配置。</li>
</ul>

<p>示例： <code>使用 React 和 Node.js 实现一个 todo list 应用程序。</code>，agent 让 AI 自动运行终端初始化项目，生成相关代码（实测效果不好，没有自动运行服务，且代码存在 bug，启动不起来，让他修复问题也修复不了）。</p></li>

<li><p>Agent 模式支持 mcp server。</p>

<ul>
<li>可通过 <a href="vscode://settings/mcp"><code>mcp</code></a> 或 <code>.vscode/mcp.json</code> 设置配置 mcp server。可通过 <code>${env:API_KEY}</code> 或 <code>${input:ENDPOINT}</code> 引用环境变量或输入变量。</li>

<li><p>可以通过 <code>&gt;MCP: Add Server</code> 快速配置 MCP server，添加完成后可点击聊天输入控件刷新按钮，加载。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-using-a-Github-MCP-tool-in-chat.mp4" type="video/mp4">
</video></li>

<li><p>更多详见： <a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">MCP 文档</a>。</p></li>
</ul></li>

<li><p>新增了如下 Agent mode 内置工具。</p>

<ul>
<li>Thinking 工具 (实验特性)，可通过 <a href="vscode://settings/github.copilot.chat.agent.thinkingTool"><code>github.copilot.chat.agent.thinkingTool</code></a> 设置开启或关闭（默认关闭），开启后将启用深度思考（速度将变慢），详见：<a href="https://code.visualstudio.com/updates/v1_99#_thinking-tool-experimental">原文</a>。</li>
<li>Fetch 工具，可通过 <code>#fetch</code> 引用互联网内容。</li>
<li>Usages 工具，可通过 <code>#usages</code> 调用 IDE 的  &ldquo;Find All References&rdquo;, &ldquo;Find Implementation&rdquo; 能力。</li>
</ul></li>

<li><p>使用 Agent 模式（实验性）创建一个新的工作区，通过 <a href="vscode://settings/github.copilot.chat.newWorkspaceCreation.enabled">github.copilot.chat.newWorkspaceCreation.enabled</a> 设置开启或关闭（默认关闭），详见：<a href="https://code.visualstudio.com/updates/v1_99#_create-a-new-workspace-with-agent-mode-experimental">原文</a>。</p></li>

<li><p>稳定了 <a href="https://code.visualstudio.com/api/extension-guides/tools#create-a-language-model-tool">language model tools API</a>，可以用于 agent 模式，参见： <a href="https://code.visualstudio.com/api/extension-guides/tools#create-a-language-model-tool">LanguageModelTool API 文档</a>。</p></li>

<li><p>代理模式工具批准，可通过 <a href="vscode://settings/chat.tools.autoApprove"><code>chat.tools.autoApprove</code></a> 自动确认。</p></li>

<li><p>Agent 模式在 SWE-bench，使用 Claude 3.7 Sonnet，通过率为 <code>56.0%</code>。</p></li>

<li><p>支持配置自定义模型，详见：<a href="https://code.visualstudio.com/docs/copilot/language-models">官方文档</a>。</p>

<p><img src="/image/vscode/byok.png" alt="image" /></p></li>

<li><p>可重复使用的提示词文件改进，详见：<a href="https://code.visualstudio.com/updates/v1_99#_reusable-prompt-files">原文</a>。</p></li>

<li><p>预览支持的 Copilot Vision （多模态）改进，支持拖拽图片文件发送给模型。</p></li>
</ul>

<h2 id="配置编辑器-configure-the-editor">配置编辑器 (Configure the editor)</h2>

<ul>
<li><p>Ask、Edit、Agent 模式统到的 Chat 视图。</p>

<p><img src="/image/vscode/chat-modes.png" alt="image" /></p></li>

<li><p>使用即时索引的更快的工作区搜索，支持 github 远程索引，详见：<a href="https://code.visualstudio.com/updates/v1_99#_configure-the-editor">文档</a>。</p></li>

<li><p>Copilot 状态菜单，Hover 新增索引状态，状态图标可以查看是否启用了代码完成。</p></li>

<li><p>Copilot 开箱即用的设置，详见：<a href="https://code.visualstudio.com/updates/v1_99#_out-of-the-box-copilot-setup-experimental">原文</a>。</p></li>

<li><p>在稳定版 VSCode 安装了 Copilot chat 预览版，将提示不可用。</p></li>

<li><p>语义文本搜索改进（实验特性），可通过 <a href="vscode://settings/github.copilot.chat.search.semanticTextResults"><code>github.copilot.chat.search.semanticTextResults</code></a> 开启语义化搜索。开启后可在搜索侧边栏按 cmd + i 触发语义化搜索。此外，可以在 chat 输入框中使用 <code>#searchResults</code> 传递搜索结果上下文给 AI。</p></li>

<li><p>优化设置编辑器，搜索算法，略，详见：<a href="https://code.visualstudio.com/updates/v1_99#_settings-editor-search-updates">原文</a>。</p></li>

<li><p>当 <a href="vscode://settings/window.titleBarStyle"><code>window.titleBarStyle</code></a> 为 <code>custom</code> （默认） 时。Windows 和 Linux 新增 <a href="vscode://settings/window.controlsStyle"><code>window.controlsStyle</code></a> 配置项，来配置控制按钮（最大化、最小化、关闭）样式：</p>

<ul>
<li><code>native</code>： 这是默认值，并根据基础平台渲染窗口控制</li>
<li><code>custom</code>： VSCode 风格。</li>
<li><code>hidden</code>： 隐藏标题栏。</li>
</ul></li>
</ul>

<h2 id="代码编辑-code-editing">代码编辑 (Code Editing)</h2>

<ul>
<li><p>下一个编辑建议 （NES） 到达一般可用状态（可通过 <a href="vscode://settings/github.copilot.nextEditSuggestions.enabled">github.copilot.nextEditSuggestions.enabled</a> 开启），并进行了一些改进。</p>

<ul>
<li>使编辑建议更加紧凑，更少干扰周围的代码，并且更易于阅读。</li>
<li>更新指示器，以确保所有建议更容易引起注意。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-NES-suggesting-edits-based-on-the-recent-changes-due-by-the-user.mp4" type="video/mp4">
</video></li>

<li><p>AI 编辑改进。</p>

<ul>
<li>在编辑中重写文件时，将编辑器之外的诊断事件静音。以前，在这种情况下，我们已经禁用了 squiggles。这些更改减少了问题面板中的闪烁，还确保我们不会发布快速修复代码操作的请求。</li>
<li>现在，当您决定保留AI编辑时，我们明确保存文件。</li>
</ul></li>

<li><p>基于工具的编辑模式（可通过 <a href="vscode://settings/chat.edits2.enabled">chat.edits2.enabled</a> 配置项启用），使 Edit 模式和 Agent 模式使用同一工具来编辑代码。</p></li>

<li><p>内联建议语法高亮（可通过 <a href="vscode://settings/editor.inlineSuggest.syntaxHighlightingEnabled">editor.inlineSuggest.syntaxHighlightingEnabled</a> 开启）。</p>

<p>配置开启前</p>

<p><img src="/image/vscode/inlineSuggestionHighlightingDisabled.png" alt="image" /></p>

<p>配置开启后</p>

<p><img src="/image/vscode/inlineSuggestionHighlightingEnabled.png" alt="image" /></p></li>

<li><p>基于 Tree-Sitter 的语法高亮（预览），支持 css 和 正则高亮，通过 <a href="vscode://settings/editor.experimental.preferTreeSitter.css">editor.experimental.preferTreeSitter.css</a> 和 <a href="vscode://settings/editor.experimental.preferTreeSitter.regex">editor.experimental.preferTreeSitter.regex</a> 配置项开启</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>Jupyter notebook document 最小版本升级到 4.5。</li>
<li>AI notebook 编辑改进。详见： <a href="https://code.visualstudio.com/updates/v1_99#_new-notebook-tool">原文</a>。</li>
<li>AI chat 对接 notebook，和常规文本编辑器对齐：

<ul>
<li>通过聊天 AI 对 notebook，创建、编辑、展示 AI diff（<a href="vscode://settings/chat.edits2.enabled">chat.edits2.enabled</a> 启用）。</li>
<li>支持将 notebook 输出作为上下文添加到 chat 输入框。</li>
</ul></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见： <a href="https://code.visualstudio.com/updates/v1_99#_accessibility">原文</a>。</p>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p><code>&gt;git checkout to...</code> 命令，下拉列表，展示更多细节。可通过 <a href="vscode://settings/git.showReferenceDetails"><code>git.showReferenceDetails</code></a> 配置（默认开启）。</p>

<p><img src="/image/vscode/scm-reference-picker.png" alt="image" /></p></li>

<li><p>状态栏添加源代码控制提供商展示。</p>

<p><img src="/image/vscode/scm-repository-picker.png" alt="image" /></p></li>

<li><p>优化尚未提交编辑器装饰，减少其展示。</p></li>

<li><p>提交消息输入框光标样式可以通过 <a href="vscode://settings/editor.cursorStyle"><code>editor.cursorStyle</code></a> 和 <a href="vscode://settings/editor.cursorWidth"><code>editor.cursorWidth</code></a> 配置项配置。</p></li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li>添加终端集成质量，有 rich、 basic、 none 三种（hover 在终端标签上可以查看）。以帮助 ai-agent 运行终端命令的可靠性和兼容性。</li>

<li><p>终端IntelliSense改进（预览）。</p>

<ul>
<li><p>终端输入建议列表对 <code>code</code>、<code>code-insiders</code>、<code>code-tunnel</code> 命令增强</p>

<ul>
<li><p>子命令参数提示。</p>

<p><img src="/image/vscode/terminal-intellisense-code-tunnel.png" alt="image" /></p></li>

<li><p>uninstall 展示安装的插件列表。</p>

<p><img src="/image/vscode/terminal-intellisense-extension.png" alt="image" /></p></li>

<li><p><code>code --locate-shell-integration-path</code> 提示支持的 shell</p>

<p><img src="/image/vscode/terminal-intellisense-locate-shell-integration.png" alt="image" /></p></li>
</ul></li>

<li><p>全局命令的自动刷新。</p></li>

<li><p>对于有参数选项，展示选项值的上下文信息。</p>

<p><img src="/image/vscode/terminal-intellisense-options.png" alt="image" /></p></li>

<li><p>对 fish shell 支持到 rich 质量级别。</p>

<p><img src="/image/vscode/terminal-intellisense-fish.png" alt="image" /></p></li>

<li><p>文件路径建议列表支持展示图标。</p>

<p><img src="/image/vscode/terminal-intellisense-icons.png" alt="image" /></p></li>

<li><p>内联建议（幽灵文本）也添加更多信息。</p>

<p><img src="/image/vscode/terminal-intellisense-consolidated.png" alt="image" /></p></li>
</ul></li>

<li><p>默认简化终端 hover 展示内容。</p>

<p><img src="/image/vscode/terminal-hover-simple.png" alt="image" /></p>

<p>可通过 显示详细信息 按钮展示更多信息。</p>

<p><img src="/image/vscode/terminal-hover-detailed.png" alt="image" /></p></li>

<li><p>签名的 PowerShell shell 集成，详见： <a href="https://code.visualstudio.com/updates/v1_99#_signed-powershell-shell-integration">原文</a>。</p></li>

<li><p>稳定了终端 Shell 类型 API，详见： <a href="https://github.com/microsoft/vscode/blob/99e3ae5586a74ab1c554b6a2a50bb9eb3a4ff7fd/src/vscode-dts/vscode.d.ts#L7740-L7750">源码</a>。</p></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li>Linux Legacy Server （GLIBC &lt; 2.28 或 LIBSTDC++ &lt; 3.4.25） 支持已结束。如果仍想运行，请参考 <a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">FAQ</a> 进行 patch。</li>
</ul>

<h2 id="企业-enterprise">企业 (Enterprise)</h2>

<ul>
<li>支持 macOS 设备管理。</li>
</ul>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li>Python

<ul>
<li>Python 3.13 以上实验性支持 <a href="https://peps.python.org/pep-0660/">PEP 660</a> 描述的可编辑安装。可通过 <a href="vscode://settings/python.analysis.enableEditableInstalls">python.analysis.enableEditableInstalls</a> 配置项配置。</li>
<li>更快，更可靠的  Pylance 诊断经验（实验性），通过 <a href="vscode://settings/python.analysis.usePullDiagnostics">python.analysis.usePullDiagnostics</a> 配置项可配置。</li>
<li>新增 python.analysis.nodeArguments 配置项，可配置传递给 nodejs 语言服务器的参数，如可以配置 <code>--max-old-space-size=8192</code> 限制内存使用。</li>
</ul></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension authoring)</h2>

<ul>
<li>Terminal.shellIntegration tweaks, 详见：<a href="https://code.visualstudio.com/updates/v1_99#_terminalshellintegration-tweaks">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>Task problem matcher status</li>
<li>Send images to LLM</li>
</ul>

<p>更多详见：<a href="https://code.visualstudio.com/updates/v1_99#_proposed-apis">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>使用新的扩展更新检查 API 检查更新。详见：<a href="https://code.visualstudio.com/updates/v1_99#_use-new-latest-api-from-marketplace-to-check-for-extensions-updates">原文</a>。</li>
</ul>
]]></description></item><item><title>VSCode 1.100 (2025-04) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_100_2025-04/</link><pubDate>Sun, 22 Jun 2025 19:12:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_100_2025-04/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_100">https://code.visualstudio.com/updates/v1_100</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>浮动窗口支持紧凑模式和总是保持在最上层。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-floating-window-compact-mode.mp4" type="video/mp4">
</video></li>

<li><p>通过 <a href="vscode://settings/workbench.secondarySideBar.defaultVisibility"><code>workbench.secondarySideBar.defaultVisibility</code></a> 配置项，可配置第二侧边栏默认可见：</p>

<ul>
<li>hidden: 总是隐藏 （默认）。</li>
<li>visibleInWorkspace: 打开新工作区时打开。</li>
<li>visible: 总是打开。</li>
</ul></li>

<li><p>展示 CSS 和 HTML 的浏览器支持情况。</p>

<p><img src="/image/vscode/css-baseline.png" alt="image" /></p></li>

<li><p>添加对 <code>.*.env</code> 文件的语法高亮。</p></li>
</ul>

<h2 id="聊天-chat">聊天 (Chat)</h2>

<ul>
<li><p>提供了自义定 AI 的配置文件：</p>

<ul>
<li><p>说明文件 (Instructions files)，提供了一种描述 Markdown 文件中 AI 模型的通用准则和上下文的方法，例如代码样式规则或要使用的框架。该类型文件以 <code>.instructions.md</code> 为后缀，位于用户数据目录或工作区目录中。也可以 <a href="vscode://settings/chat.instructionsFilesLocations"><code>chat.instructionsFilesLocations</code></a> 配置项说明文件目录。</p>

<ul>
<li>可以通过 Add Context 按钮添加到聊天中。</li>

<li><p>也可以通过 markdown 的 applyTo 在配置文件中指定应用的文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">---

applyTo: &#39;**/*.ts&#39;
---

Place curly braces on separate lines for multi-line blocks:
if (condition)
{
doSomething();
}
else
{
doSomethingElse();
}</code></pre></div></li>

<li><p>在用户数据目录中的说明文件，可以通过设置同步服务自动同步。</p></li>
</ul></li>

<li><p>提示词文件 (Prompt files)，提示文件描述独立的，完整的聊天请求，包括提示文本，聊天模式和使用的工具。提示文件可用于为常见任务创建可重复使用的聊天请求。例如，您可以添加一个提示文件以创建前端组件，或执行安全审核。该类型文件以 <code>.prompt.md</code> 为后缀。位于用户数据目录或工作区目录中。也可以 <a href="vscode://settings/chat.promptFilesLocations"><code>chat.promptFilesLocations</code></a> 配置项提示词文件目录。有如下几种方式使用：</p>

<ul>
<li><p>使用 <code>/</code> 在输出框中引用。</p>

<p><img src="/image/vscode/run-prompt-as-slash-command.png" alt="image" /></p></li>

<li><p>在编辑器中打开提示文件，然后按编辑器工具栏中的 “运行” 按钮。这使您可以快速迭代提示并运行它，而无需切换回聊天视图。</p>

<p><img src="/image/vscode/run-prompt-from-play-button.png" alt="image" /></p></li>

<li><p>在命令面板，运行 <code>Chat: Run Prompt File...</code> 命令。</p></li>
</ul>

<p>可以配置 chat 模式以及使用的工具列表：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">---

mode: &#39;agent&#39;
tools: [&#39;getCurrentMilestone&#39;, &#39;getReleaseFeatures&#39;, &#39;file_search&#39;, &#39;semantic_search&#39;, &#39;read_file&#39;, &#39;insert_edit_into_file&#39;, &#39;create_file&#39;, &#39;replace_string_in_file&#39;, &#39;fetch_webpage&#39;, &#39;vscode_search_extensions_internal&#39;]
---

Generate release notes for the features I worked in the current release and update them in the release notes file. Use [<span style="color:#f92672">release notes writing instructions file</span>](<span style="color:#a6e22e">.github/instructions/release-notes-writing.instructions.md</span>) as a guide.</code></pre></div></li>

<li><p>详见： <a href="https://code.visualstudio.com/docs/copilot/copilot-customization#_prompt-files-experimental">prompt files 官方文档</a></p></li>
</ul></li>

<li><p>更快的 agent 模式的代码编辑，使用 search replace 实现代码编辑。</p></li>

<li><p>将 GPT-4.1 作为默认基础模型。</p></li>

<li><p>添加 <code>#githubRepo</code> 用于搜索任意 github 代码库，也可以在自定义 instructions 中使用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown">---

applyTo: &#39;**&#39;
---

Use the <span style="color:#e6db74">`#githubRepo`</span> tool with <span style="color:#e6db74">`microsoft/vscode`</span> to find relevant code snippets in the VS Code codebase.
Use the <span style="color:#e6db74">`#githubRepo`</span> tool with <span style="color:#e6db74">`microsoft/typescript`</span> to answer questions about how TypeScript is implemented.</code></pre></div>
<p><img src="/image/vscode/github-repo-tool-example.png" alt="image" /></p>

<p>如果想搜索当前代码库，可以使用 <code>#codebase</code> 工具，如果想更多的操纵 github 仓库，可以使用 <a href="https://github.com/github/github-mcp-server?tab=readme-ov-file#github-mcp-server">Github MCP Server</a>。</p></li>

<li><p>可以使用 <code>#extensions</code> 工具搜索 VSCode 扩展。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-using-the-extensions-tool-to-display-popular-Java-extensions.mp4" type="video/mp4">
</video></li>

<li><p>改进 web page fetch 工具：</p>

<ul>
<li>将整个页面作为上下文。</li>
<li>标准化页面格式（Markdown）。</li>
</ul></li>

<li><p>聊天输入改进。</p>

<ul>
<li>附件： <code>#</code> 支持引用提示词文件。</li>
<li>上下文选择器： 简化了上下文选择器，以使选择文件，文件夹和其他附件类型变得更加简单。</li>
<li>完成按钮：  we heard your feedback about the &ldquo;Done&rdquo;-button and we removed it! No more confusion about unexpected session endings. Now, we only start a new session when you create a new chat (⌃L).</li>
</ul></li>

<li><p>聊天模式快捷键。</p>

<ul>
<li><code>⌃⌘i</code> 打开聊天视图</li>
<li><code>⇧⌘I</code> 快捷方式现在打开聊天视图并切换到代理模式。</li>
</ul>

<p>如果想为其他聊天模式设置键盘快捷键，则每个模式都有一个命令：</p>

<ul>
<li><code>workbench.action.chat.openAgent</code></li>
<li><code>workbench.action.chat.openEdit</code></li>
<li><code>workbench.action.chat.openAsk</code></li>
</ul></li>

<li><p>在 agent 模式编辑过程中，支持自动修复问题面板中的问题。可通过 <a href="vscode://settings/github.copilot.chat.agent.autoFix">github.copilot.chat.agent.autoFix</a> 配置项开关。</p></li>

<li><p>在 Agent 模式下，模型可以感知到撤消和用户手动编辑，避免模型困惑。</p></li>

<li><p>对话历史摘要和提示缓存。</p></li>

<li><p>MCP 支持 Streamable HTTP，更多详见： <a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">MCP support in VS Code</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
<span style="color:#f92672">&#34;servers&#34;</span>: {
    <span style="color:#f92672">&#34;my-mcp-server&#34;</span>: {
        <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;http://localhost:3000/mcp&#34;</span>
        }
    }
}</code></pre></div></li>

<li><p>MCP 支持图片输出。</p></li>

<li><p>增强了MCP服务器的输入，输出和进度</p></li>

<li><p>MCP config generation uses inputs： To help keep your secrets secure, AI-assisted configurations generated by the <code>MCP: Add Server</code> command now generate inputs for any secrets, rather than inlining them into the resulting configuration.</p></li>

<li><p>内联聊天 V2，目标是： “将聊天融入代码”，但是在幕后使用与聊天编辑相同的逻辑。</p></li>

<li><p>（实验性） 支持选择和附加 UI 元素（Web 前段的预览页面）到聊天中（添加截图和 CSS、 HTML 代码片段等）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-full-flow-of-the-UI-element-selection-experimental-feature.mp4" type="video/mp4">
</video></li>

<li><p>（实验性） 在 Agent 模式，支持创建并启动一个任务。</p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略，详见： <a href="https://code.visualstudio.com/updates/v1_100#_accessibility">原文</a>。</p>

<h2 id="编辑体验-editor-experience">编辑体验 (Editor Experience)</h2>

<ul>
<li><p>浮动窗口支持紧凑模式和总是保持在最上层。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-floating-window-compact-mode.mp4" type="video/mp4">
</video>

<p>支持将 Chat 框弹出到独立窗口中，并保持在最上层。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-floating-window-always-on-top.mp4" type="video/mp4">
</video>

<p>新增了一些列命令可以绑定键盘快捷键。</p>

<ul>
<li><code>workbench.action.toggleWindowAlwaysOnTop</code>: to toggle always on top mode</li>
<li><code>workbench.action.enableWindowAlwaysOnTop</code>: to set the floating window always on top</li>
<li><code>workbench.action.disableWindowAlwaysOnTop</code>: to set the floating window to normal</li>
<li><code>workbench.action.toggleCompactAuxiliaryWindow</code>: to toggle compact mode</li>
<li><code>workbench.action.enableCompactAuxiliaryWindow</code>: to enable compact mode</li>
<li><code>workbench.action.disableCompactAuxiliaryWindow</code>: to disable compact mode</li>
</ul></li>

<li><p>通过 <a href="vscode://settings/workbench.secondarySideBar.defaultVisibility"><code>workbench.secondarySideBar.defaultVisibility</code></a> 配置项，可配置第二侧边栏默认可见：</p>

<ul>
<li>hidden: 总是隐藏 （默认）。</li>
<li>visibleInWorkspace: 打开新工作区时打开。</li>
<li>visible: 总是打开。</li>
</ul>

<p>这个配置项，只应用首次打开工作区。后续打开工作区，将使用上次状态。</p></li>

<li><p>强制扩展签名验证，详见： <a href="https://code.visualstudio.com/updates/v1_100#_mandatory-extension-signature-verification">原文</a>。</p></li>

<li><p>对于被识别为恶意的扩展，添加了解更多连接，详见： <a href="https://code.visualstudio.com/updates/v1_100#_learn-more-links-for-malicious-extensions">原文</a>。</p></li>

<li><p>避免在 VSCode Stable 中使用 pre-release 版的 Copilot。</p></li>

<li><p>支持打开一个视图但不切换焦点的选项，通过 <code>{ preserveFocus: boolean}</code> 配置。</p></li>

<li><p>（实验性）语义文本搜索，提供关键字建议。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-AI-powered-keyword-suggestions-in-Visual-Studio-Code.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="代码编辑-code-editing">代码编辑 (Code Editing)</h2>

<ul>
<li>新的下一个编辑建议（NES）模型。</li>

<li><p>在 JavaScript 和 TypeScript 语言的 NES 支持自动添加缺失的导入，通过 <a href="vscode://settings/github.copilot.nextEditSuggestions.fixes"><code>github.copilot.nextEditSuggestions.fixes</code></a> 配置项。</p>

<p><img src="/image/vscode/nes-import.png" alt="image" /></p></li>

<li><p>NES 在 VSCode Insiders 已经默认启用。</p></li>

<li><p>支持在 HTML 或 Markdown 生成 alt。</p>

<p><img src="/image/vscode/generate-alt-text.png" alt="image" /></p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>笔记本查找控件现在支持查找和替换输入字段的持久历史记录。</li>

<li><p>支持将 cell 和输出拖拽到聊天视图。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-multiple-cell-outputs-being-attached-as-chat-context-via-drag-and-drop.mp4" type="video/mp4">
</video></li>

<li><p>为 agent 模式添加了一系列 Notebook 工具，详见： <a href="https://code.visualstudio.com/updates/v1_100#_notebook-tools-for-agent-mode">原文</a>。</p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>编辑器快速差异装饰条支持勾选来源。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-managing-staged-changed-from-an-editor-by-using-diff-decorations.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="调试-debugging">调试 (Debugging)</h2>

<ul>
<li>反编译视图新增上下文菜单。</li>
<li>在 Node v22.14.0 以上版本，调试 JavaScript，默认展示调试器网络视图，该功能在 <a href="https://code.visualstudio.com/updates/v1_93#_experimental-network-view">1.93 版本</a>已开始实验。</li>
</ul>

<h2 id="语言-languages">语言 (Languages)</h2>

<ul>
<li><p>展示 CSS 和 HTML 的浏览器支持情况。</p>

<p><img src="/image/vscode/css-baseline.png" alt="image" /></p></li>

<li><p>添加对 <code>.*.env</code> 文件的语法高亮。</p></li>

<li><p>（实验性） JavaScript 和 TypeScript Hover 只是展开和折叠。可通过 <a href="vscode://settings/typescript.experimental.expandableHover"><code>typescript.experimental.expandableHover</code></a> 开启。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-TypeScript-expandable-hover.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<p>略，详见：<a href="https://code.visualstudio.com/updates/v1_100#_remote-development">原文</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python</p>

<ul>
<li><p>支持分支覆盖率。<code>coveragepy</code> 版本必须 &gt;= 7.7 。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-branch-coverage-run-using-the-Test-Explorer-and-branch-coverage-displayed.mp4" type="video/mp4">
</video></li>

<li><p>通过 <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs">Python 环境扩展</a>的 <code>&gt;Python: Create Environment</code> 命令可以快速选择一个环境管理器创建一个 Python 环境。</p>

<p><img src="/image/vscode/python-envs-quick-create.png" alt="image" /></p></li>

<li><p>Python 环境聊天工具，通过 <code>#pythonGetEnvironmentInfo</code> 和 <code>#pythonInstallPackage</code> 可以提供 Python 环境信息并安装 Python 包。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-demoing-the-get-environment-information-tool-call-implicitly-by-the-model-in-agent-mode.mp4" type="video/mp4">
</video>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-demoing-the-get-install-package-tool-call-for-numpy-version-2.mp4" type="video/mp4">
</video></li>

<li><p>Pylance 支持颜色选择。</p>

<p><img src="/image/vscode/pylance-color-picker.png" alt="image" /></p></li>

<li><p>支持使用 AI 转化字符串格式。</p>

<p><img src="/image/vscode/pylance-convert-string.png" alt="image" /></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#e6db74">&#34;python.analysis.aiCodeActions&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {<span style="color:#f92672">&#34;convertFormatString&#34;</span>: <span style="color:#66d9ef">true</span>}</code></pre></div></li>
</ul></li>

<li><p>GitHub Pull Requests and Issues，略，详见： <a href="https://code.visualstudio.com/updates/v1_100#_github-pull-requests-and-issues">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>文本编码 API 已稳定。</li>
<li>NodeJS 扩展支持 ESM。</li>
</ul>

<p>更多，详见： <a href="https://code.visualstudio.com/updates/v1_100#_extension-authoring">原文</a>。</p>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<ul>
<li>Tool calling for images，详见： <a href="https://code.visualstudio.com/updates/v1_100#_tool-calling-for-images">原文</a>。</li>
<li>MCP服务器支持由扩展贡献。</li>
<li>MCP 工具注释。</li>

<li><p>可变的行高。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-variable-line-heights-in-the-editor.mp4" type="video/mp4">
</video></li>
</ul>
]]></description></item><item><title>VSCode 1.101 (2025-05) 更新日志</title><link>https://www.rectcircle.cn/series/vscode/changelog/v1_101_2025-05/</link><pubDate>Sun, 06 Jul 2025 16:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/series/vscode/changelog/v1_101_2025-05/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://code.visualstudio.com/updates/v1_101">https://code.visualstudio.com/updates/v1_101</a></p>
</blockquote>

<h2 id="本次更新看点速览">本次更新看点速览</h2>

<ul>
<li><p>通过 <a href="vscode://settings/editor.find.findOnType"><code>editor.find.findOnType</code></a> 配置项可以控制是否有在输入字符时自动搜索。默认开启，现在可以通过此配置项关闭。</p></li>

<li><p>源代码版本控制图支持变更文件列表/树。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-Source-Control-Graph-view-displaying-the-files-for-a-commit-and-showing-a-diff-editor-of-its-changes.mp4" type="video/mp4">
</video></li>
</ul>

<h2 id="聊天-chat">聊天 (Chat)</h2>

<ul>
<li><p>支持配置在 agent 使用的聊天工具集，可以定义 AI 使用的工具集列表。可选内置工具、MCP 提供的工具、插件提供的工具。通过 <code>&gt;Chat: Configure Tool Sets</code> 命令创建和管理配置后缀为 <code>.toolsets.jsonc</code> 的配置文件，支持用户和工作区维度配置。在聊天中通过 <code>#工具集名</code> 方式引用，详见： <a href="https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_define-tool-sets">官方文档</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;gh-news&#34;</span>: {
    <span style="color:#f92672">&#34;tools&#34;</span>: [<span style="color:#e6db74">&#34;list_notifications&#34;</span>, <span style="color:#e6db74">&#34;dismiss_notification&#34;</span>, <span style="color:#e6db74">&#34;get_notification_details&#34;</span>],
    <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Manage GH notification&#34;</span>,
    <span style="color:#f92672">&#34;icon&#34;</span>: <span style="color:#e6db74">&#34;github-project&#34;</span>
  }
}</code></pre></div>
<p><img src="/image/vscode/tool-set-gh.png" alt="image" /></p></li>

<li><p>支持<a href="https://modelcontextprotocol.io/docs/concepts/prompts">MCP 提示词模板</a> （即将 MCP server 生成的提示词模板给大模型）。在聊天中可通过 <code>/mcp.servername.promptname</code> 方式引用。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/mcp-prompts.mp4" type="video/mp4">
</video></li>

<li><p>支持<a href="https://modelcontextprotocol.io/docs/concepts/resources">MCP 资源</a>（即从 MCP server 读取资源给大模型）。在聊天中可通过 <code>Add Context...</code> 选择添加 MCP 资源给大模型。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/mcp-resources.mp4" type="video/mp4">
</video></li>

<li><p>实验性的支持 <a href="https://modelcontextprotocol.io/docs/concepts/sampling">MCP Sampling</a>（即 MCP server 调用大模型接口）。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/mcp-sampling.mp4" type="video/mp4">
</video></li>

<li><p>支持 MCP Auth （<a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization">2025-3-26 spec</a> 和 <a href="https://modelcontextprotocol.io/specification/draft/basic/authorization">Draft spec</a>），详见： <a href="https://code.visualstudio.com/updates/v1_101#_mcp-support-for-auth">原文</a>。</p></li>

<li><p>支持 MCP 开发者模式，只是 debug 方式启动服务（断点），支持 node 和 python。详见：<a href="https://code.visualstudio.com/updates/v1_101#_mcp-development-mode">原文</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-diff" data-lang="diff">{
&#34;servers&#34;: {
    &#34;gistpad&#34;: {
    &#34;command&#34;: &#34;node&#34;,
    &#34;args&#34;: [&#34;build/index.js&#34;],
<span style="color:#a6e22e">+     &#34;dev&#34;: {
</span><span style="color:#a6e22e">+       &#34;watch&#34;: &#34;build/**/*.js&#34;,
</span><span style="color:#a6e22e">+       &#34;debug&#34;: { &#34;type&#34;: &#34;node&#34; }
</span><span style="color:#a6e22e">+     },
</span></code></pre></div></li>

<li><p>聊天 UX 提升。</p>

<ul>
<li>改进用户消息展示，使之跟容易和 AI 回复区分。</li>
<li>悬浮到用户消息上，展示叉号，可以快速删除这个消息记录（此外也可以单击选中后，可以按 <code>⌘Backspace</code> 快捷键删除）</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/A-video-of-the-new-chat-UI_UX-where-a-request-is-removed-to-undo-edits-since-that-point.mp4" type="video/mp4">
</video></li>

<li><p>更有效的应用编辑：</p>

<ul>
<li>在重写编辑时，禁用自动保存和 squiggles。</li>
<li>保持和回滚按钮，会话快捷键使用 <code>⌘Y</code>、 <code>⌘N</code>；文件中使用 <code>⇧⌘Y</code> 和 <code>⇧⌘N</code>。</li>
</ul></li>

<li><p>隐式文件上下文改进：</p>

<ul>
<li>聊天上下文文件可以通过叉号去除，焦点在聊天框上时，可以通过 <code>shift + tab</code> 切换焦点，按回车删除。</li>
<li>切换编辑器标签后，聊天的隐式上下文提示也会变且逻辑和编辑器类似。</li>
</ul>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/A-video-of-the-current-open-editor-being-suggest-as-implicit-context-and-added-as-an-attachment.mp4" type="video/mp4">
</video></li>

<li><p>修复任务配置错误。配置任务和问题匹配者可能很棘手。在任务配置中存在错误以快速有效地解决时，将使用GitHub Copilot Action进行修复。</p></li>

<li><p>（预览） 自定义聊天模式，支持在 Chat、Edit 和 Agent 之外自定义模式。可以通过 Markdown 文件配置描述，工具集以及提示词。更多详见：<a href="https://code.visualstudio.com/updates/v1_101#_custom-chat-modes-preview">原文</a>。</p></li>

<li><p>当聊天 Agent 执行任务时，它现在知道问题匹配者确定的任何错误或警告。这种诊断上下文使聊天代理可以在出现的问题上更聪明地做出反应。</p></li>

<li><p>终端 pwd 上下文。当代理模式打开终端和外壳集成活动时，聊天代理会知道当前的工作目录（CWD）。这使得更准确，上下文感知的命令支持。</p></li>

<li><p>打开 Chat 浮动窗口会新建一个会话，并支持通过按钮放回主窗口。</p>

<p><img src="/image/vscode/chat-floating.png" alt="image" /></p></li>

<li><p>fetch 工具添加二次确认，以避免潜在提示词注入问题。</p>

<p><img src="/image/vscode/fetch-warning.png" alt="image" /></p></li>

<li><p>Agent 模式支持自由启用禁用工具列表，通过聊天框的设置图标。</p>

<p><img src="/image/vscode/built-in-toolsets.png" alt="image" /></p></li>

<li><p>选择 HTML 元素给聊天。</p>

<p><img src="/image/vscode/live-preview-select-web-elements.png" alt="image" /></p></li>
</ul>

<h2 id="无障碍-accessibility">无障碍 (Accessibility)</h2>

<p>略</p>

<h2 id="编辑体验-editor-experience">编辑体验 (Editor Experience)</h2>

<ul>
<li>通过 <a href="vscode://settings/editor.find.findOnType"><code>editor.find.findOnType</code></a> 配置项可以控制是否有在输入字符时自动搜索。默认开启，现在可以通过此配置项关闭。</li>

<li><p>新增 <a href="vscode://settings/window.menuStyle"><code>window.menuStyle</code></a> 配置项，配置菜单的样式，可选项如下：</p>

<ul>
<li><code>native</code>：（默认值），由操作系统渲染。</li>
<li><code>custom</code>：由 VSCode 渲染。</li>
<li><code>inherit</code>：继承 <a href="vscode://settings/window.titleBarStyle"><code>window.titleBarStyle</code></a>。</li>
</ul></li>

<li><p>Linux 中，点击 VSCode 图标，支持展示 Linux 系统的窗口选项菜单。</p>

<p><img src="/image/vscode/linux-os-title-menu.png" alt="image" /></p></li>

<li><p>进程管理器使用浮动窗口架构，因此 VSCode Web 版也支持展示进程管理器。</p></li>

<li><p>支持 Windows Shell (PowerShell) 环境变量探测。</p></li>

<li><p>插件详情页，展示未在市场发布的扩展警告。</p>

<p><img src="/image/vscode/pulled-extension.png" alt="image" /></p></li>

<li><p>（预览） 设置搜索建议，支持用自然语言，通过 AI 搜索配置项。可通过 <a href="vscode://settings/workbench.settings.showAISearchToggle"><code>workbench.settings.showAISearchToggle</code></a> 配置项开启。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-AI-search-in-the-Settings-editor.mp4" type="video/mp4">
</video></li>

<li><p>（预览） 键盘快捷键 AI 搜索，可通过 <a href="vscode://settings/search.searchView.keywordSuggestions"><code>search.searchView.keywordSuggestions</code></a> 配置项。</p></li>

<li><p>（预览） 通过 <a href="vscode://settings/search.searchView.semanticSearchBehavior"><code>search.searchView.semanticSearchBehavior</code></a> 配置项可以配置何时出发语义化搜索：</p>

<ul>
<li>manual (默认): 仅在手动明确触发时使用语义化搜索 (<code>⌘I</code>)。</li>
<li>runOnEmpty: 仅在常规搜索位空时触发。</li>
<li>auto: 每次都进行语义化搜索。</li>
</ul></li>

<li><p>（实验性） 编辑上下文 API，通过 <a href="vscode://settings/editor.experimentalEditContextEnabled"><code>editor.experimentalEditContextEnabled</code></a> 可启用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/EditContext_API">Web 的 Edit Context API</a>，可以解决很多输入法问题。</p></li>
</ul>

<h2 id="代码编辑-code-editing">代码编辑 (Code Editing)</h2>

<ul>
<li><p>改进了 NES 上个版本引入的包导入的准确性，并添加了 Python 的支持。</p>

<p><img src="/image/vscode/nes-import.png" alt="image" /></p>

<p>stable VSCode 可通过 <a href="vscode://settings/github.copilot.nextEditSuggestions.fixes"><code>github.copilot.nextEditSuggestions.fixes</code></a> 配置项开启，目前在 VS Code Insiders 已默认启用，在 6 月版本将对 stable 版本默认启用</p></li>

<li><p>通过改进的键盘导航，接受下一个编辑建议现在更加无缝。一旦接受建议，只要您还没有再次开始键入，就可以继续使用单个选项卡按下来接受后续建议。一旦开始键入，请按选项卡首先将光标移至下一个建议，然后再接受它。</p></li>
</ul>

<h2 id="笔记本-notebooks">笔记本 (Notebooks)</h2>

<ul>
<li>Agent 执行 Cell 支持 follow 模式。可通过  <a href="vscode://settings/github.copilot.chat.notebook.followCellExecution.enabled"><code>github.copilot.chat.notebook.followCellExecution.enabled</code></a> 配置项启用。</li>

<li><p>Jupyter 扩展为 agent 模式贡献了一些工具。</p>

<ul>
<li>配置笔记本。</li>
<li>Long running agent workflows.</li>
<li>Cell preview in run confirmation.</li>
</ul>

<p>更多详见： <a href="https://code.visualstudio.com/updates/v1_101#_notebook-tools-for-agent-mode">原文</a>。</p></li>
</ul>

<h2 id="源代码版本控制-source-control">源代码版本控制 (Source Control)</h2>

<ul>
<li><p>Github pull request 扩展支持让 Copilot coding agent 处理 PR 和 Issue：</p>

<ul>
<li>Assign to Copilot： 将 PR 和 Issue 分配给 Copilot 处理。</li>
<li>Copilot on My Behalf: 快速查看 Copilot 为你工作的 PR。</li>

<li><p>PR view：查看 Copilot coding agent 编码状态 ，并在浏览器中打开会话详细信息。</p>

<p><img src="/image/vscode/github-pull-request-coding-agent.png" alt="image" /></p></li>
</ul></li>

<li><p>源代码版本控制图支持变更文件列表/树。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-showing-the-Source-Control-Graph-view-displaying-the-files-for-a-commit-and-showing-a-diff-editor-of-its-changes.mp4" type="video/mp4">
</video></li>

<li><p>添加 git 历史到聊天上下文。可通过源代码版本控制图右击选择 Copilot，点击将历史记录项添加到聊天。</p>

<p><img src="/image/vscode/chat-context-source-control-commit.png" alt="image" /></p></li>
</ul>

<h2 id="任务-tasks">任务 (Tasks)</h2>

<ul>
<li>实例策略，详见：<a href="https://code.visualstudio.com/updates/v1_101#_instance-policy">原文</a>。</li>
</ul>

<h2 id="终端-terminal">终端 (Terminal)</h2>

<ul>
<li><p>基于语言服务器的终端建议。现在在终端的 Python REPL 中将提供 LSP 提供的建议。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-that-shows-completions-from-LSP-in-REPL-in-the-terminal.mp4" type="video/mp4">
</video>

<p>需开启如下配置：</p>

<ul>
<li><a href="vscode://settings/terminal.integrated.shellIntegration.enabled"><code>terminal.integrated.shellIntegration.enabled</code></a></li>
<li><a href="vscode://settings/python.terminal.shellIntegration.enabled"><code>python.terminal.shellIntegration.enabled</code></a></li>
<li><a href="vscode://settings/terminal.integrated.suggest.enabled"><code>terminal.integrated.suggest.enabled</code></a></li>
<li><a href="vscode://settings/python.analysis.supportAllPythonDocuments"><code>python.analysis.supportAllPythonDocuments</code></a></li>
</ul></li>
</ul>

<h2 id="远程开发-remote-development">远程开发 (Remote Development)</h2>

<ul>
<li>SSH 预连接脚本</li>
<li>远程资源管理器的改进</li>
</ul>

<p>详见： <a href="https://github.com/microsoft/vscode-docs/blob/main/remote-release-notes/v1_101.md">更新日志</a>。</p>

<h2 id="贡献到扩展-contributions-to-extensions">贡献到扩展 (Contributions to extensions)</h2>

<ul>
<li><p>Python</p>

<ul>
<li><p>Python 给 Copilot Agent 贡献了，“Get information for a Python Environment”, “Get executable information for a Python Environment”, “Install Python Package” and “Configure Python Environment” 工具。可通过 <code>#getPythonEnvironmentInfo</code>、 <code>#installPythonPackage</code> 注入 PE。</p>

<video autoplay="" loop="" muted="" playsinline="" controls="" width="100%">
<source src="/image/vscode/Video-demoing-the-Python-tools-called-implicitly-by-the-model-in-agent-mode.mp4" type="video/mp4">
</video></li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs">Python Environments</a> 支持 从模板创建项目。</p></li>

<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs">Python Environments</a> 支持 PyEnv 和 Poetry。</p></li>
</ul></li>

<li><p>GitHub Pull Requests 略，详见：<a href="https://code.visualstudio.com/updates/v1_101#_github-pull-requests">原文</a>。</p></li>
</ul>

<h2 id="扩展制作-extension-authoring">扩展制作 (Extension Authoring)</h2>

<ul>
<li>MCP extension APIs, 支持通过 VSCode 扩展贡献 MCP Server。</li>
<li>Extension 打包时扫描如 .env 文件的秘钥，避免秘钥泄密。</li>
<li>⚠️ Breaking change ⚠️： extension host 从 v20 升级到 v22。提供了全局的 <a href="https://nodejs.org/docs/latest-v22.x/api/globals.html#navigator_1">navigator 对象</a>。此更改可能会引入破坏的变化，如依靠 navigator 对象的存在来检测是否是 Web 环境。为了兼容性，目前 VSCode 将 navigator 设置为 undefined。详见：<a href="https://code.visualstudio.com/updates/v1_101#_web-environment-detection">原文</a>。</li>
</ul>

<h2 id="api-提案-proposed-apis">API 提案 (Proposed APIs)</h2>

<p>Authentication Providers: Supported Authorization Servers for MCP</p>

<p>详见： <a href="https://code.visualstudio.com/updates/v1_101#_authentication-providers-supported-authorization-servers-for-mcp">原文</a>。</p>

<h2 id="工程-engineering">工程 (Engineering)</h2>

<ul>
<li>更新到 Electron 35,  Chromium 134.0.6998.205 和 Node.js 22.15.1。</li>
<li>Adopting ESM in a real-world extension, 详见： <a href="https://code.visualstudio.com/updates/v1_101#_adopting-esm-in-a-realworld-extension">原文</a>。</li>
</ul>
]]></description></item><item><title>终端详解（五）Shell 集成</title><link>https://www.rectcircle.cn/posts/terminal-detail-5-shell-integration/</link><pubDate>Sat, 27 Dec 2025 16:25:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-detail-5-shell-integration/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文源码: <a href="https://github.com/rectcircle/implement-terminal-from-scratch">rectcircle/implement-terminal-from-scratch</a></p>
</blockquote>

<h2 id="介绍">介绍</h2>

<p>终端（Terminal）本质上是一个简单的字符流处理程序，它只负责显示由 Shell（如 Bash、Zsh）发送过来的文本，并处理基本的 ANSI 转义序列（如设置颜色、移动光标）。然而，终端本身并不知道“语义”层面的信息，例如：</p>

<ul>
<li>哪里是提示符（Prompt）的开始和结束？</li>
<li>用户输入的命令是什么？</li>
<li>刚刚执行的命令是成功还是失败了？</li>
<li>当前的工作目录（CWD）是什么？</li>
</ul>

<p>Shell 集成（Shell Integration）正是为了解决这个问题。它通过让 Shell 在特定的时机（如打印提示符前、执行命令前、命令结束后）向终端发送特殊的、不可见的转义序列，来告知终端当前的上下文状态。</p>

<p>一旦终端“理解”了这些信息，就能提供许多高级功能：</p>

<ul>
<li><strong>命令装饰（Decorations）</strong>：在每一行命令左侧显示原点，蓝色代表执行中，绿色代表成功，红色代表失败。</li>
<li><strong>命令导航</strong>：通过快捷键（如 <code>Cmd/Ctrl + Up/Down</code>）快速在历史命令输出之间跳转。</li>
<li><strong>当前目录跟踪</strong>：新建终端标签页时自动保持在当前目录。</li>
<li><strong>智能选取与重运行</strong>：能够精准地选取某次命令的输出内容，或快速重新运行该命令。</li>
<li><strong>Sticky Scroll</strong>：当命令输出很长时，可以将该命令的提示符行固定在顶部，方便查看。</li>
</ul>

<h2 id="原理-以-vscode-为例">原理 （以 VSCode 为例）</h2>

<p>VSCode 的终端是最早广泛应用 Shell 集成的现代编辑器之一。它主要通过 <strong>脚本注入</strong> 和 <strong>自定义转义序列</strong> 来实现。</p>

<h3 id="1-脚本注入-script-injection">1. 脚本注入 (Script Injection)</h3>

<p>当你在 VSCode 中打开一个终端时，VSCode 并不会直接启动你的 Shell（如 <code>/bin/zsh</code>），而是会通过 <strong>参数劫持</strong> 和 <strong>环境构造</strong> 的方式，让 Shell 在启动时优先加载 VSCode 生成的临时初始化脚本。</p>

<p><strong>加载机制与用户配置兼容：</strong></p>

<p>为了确保用户原本的配置（如 <code>~/.bashrc</code> 或 <code>~/.zshrc</code>）依然生效，VSCode 采用了一种“代理加载”的策略：</p>

<ol>
<li><p><strong>劫持启动入口</strong>：</p>

<ul>
<li><strong>Bash</strong>: VSCode 会使用 <code>--init-file</code> 参数启动 Bash（例如 <code>bash --init-file /tmp/vscode-bash-init</code>），强行指定初始化文件。</li>
<li><strong>Zsh</strong>: VSCode 会设置 <code>ZDOTDIR</code> 环境变量指向一个临时目录（例如 <code>/tmp/vscode-zsh/</code>），该目录下包含一个生成的 <code>.zshrc</code>。</li>
</ul></li>

<li><p><strong>代理脚本 (Chaining)</strong>：
这个被强行加载的临时脚本（Proxy Script）负有双重责任：</p>

<ul>
<li><strong>加载用户配置</strong>：它会首先检查并 <code>source</code> 用户原本的配置文件（如 <code>~/.bashrc</code>, <code>~/.zprofile</code>, <code>~/.zshrc</code> 等），确保用户的别名、环境变量等完全保留。</li>
<li><strong>注入集成逻辑</strong>：在用户配置加载完成后，接着加载 VSCode 的 Shell Integration 脚本。</li>
</ul></li>
</ol>

<p><strong>Shell 钩子注入：</strong></p>

<p>一旦脚本运行起来，它会利用 Shell 提供的钩子机制（Hooks）来监听状态：
* <strong>Bash</strong>: 利用 <code>PROMPT_COMMAND</code> 环境变量（对应 <code>precmd</code>），并通过 <code>trap DEBUG</code> 机制来模拟 <code>preexec</code> 钩子（在 Bash 4.4+ 中也可能利用 <code>PS0</code> 环境变量）。
* <strong>Zsh</strong>: 利用 <code>precmd</code> (提示符前) 和 <code>preexec</code> (执行命令前) 钩子。
* <strong>Fish</strong>: 利用 <code>fish_prompt</code> 和 <code>fish_preexec</code> 事件监听。</p>

<h3 id="2-通信协议-osc-633">2. 通信协议 (OSC 633)</h3>

<p>VSCode 定义了一套私有的转义序列协议，以 <code>OSC 633</code> 开头（即 <code>\x1b]633; ... \x07</code> 或 <code>\x1b]633; ... \x1b\\</code>）。脚本会在不同阶段输出这些序列：</p>

<ul>
<li><strong><code>OSC 633 ; A ST</code> (Prompt Start)</strong>: 标记提示符的开始。脚本会在打印 <code>PS1</code> 之前输出它。</li>
<li><strong><code>OSC 633 ; B ST</code> (Command Start)</strong>: 标记提示符结束，用户开始输入命令的位置。</li>
<li><strong><code>OSC 633 ; C ST</code> (Command Executed)</strong>: 标记用户按下了回车，命令即将开始执行（也是命令输出的起始点）。</li>
<li><strong><code>OSC 633 ; D [; &lt;ExitCode&gt;] ST</code> (Command Finished)</strong>: 标记命令执行结束，并附带退出码（Exit Code）。</li>
<li><strong><code>OSC 633 ; P ; Cwd=&lt;Path&gt; ST</code> (Property - Cwd)</strong>: 告知终端当前的工作目录发生了变化。</li>
</ul>

<h3 id="3-工作流程示例">3. 工作流程示例</h3>

<p><img src="/image/vscode-shell-integration.svg" alt="image" /></p>

<h2 id="shell-集成实现">Shell 集成实现</h2>

<p>本部分将在 <a href="/posts/terminal-detail-3-webshell/">《终端详解（三）实现 WebShell》</a> 基础上，实现 Shell 集成。</p>

<p>源码位于： github 示例仓库的 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/blob/master/project-demo/03-shell-integration"><code>project-demo/03-shell-integration</code></a> 目录。</p>

<h3 id="功能介绍">功能介绍</h3>

<p>相比 WebShell 只“透传字节流”，Shell 集成会让前端“看懂”一次命令执行的边界与元信息：</p>

<ul>
<li>识别一条命令的生命周期：开始输入 → 开始执行 → 执行结束</li>
<li>在不影响终端显示的前提下，额外拿到结构化信息：<code>command</code>、<code>cwd</code>、<code>exitCode</code>、<code>output</code></li>
<li>UI 增强：在页面下方追加 Command History，按条展示每次命令的输出与退出码（成功/失败高亮）</li>
</ul>

<p>核心思路：让 Bash 在关键时刻输出“不可见的标记”，即自定义 OSC 序列 <code>ESC ] 729 ; ... BEL</code>，前端从服务端回传的字节流中识别并剥离这些序列，同时把事件/属性聚合成一条“命令记录”。</p>

<hr />

<h3 id="client">Client</h3>

<h4 id="1-新增页面布局-终端-历史区">1）新增页面布局：终端 + 历史区</h4>

<p><code>project-demo/03-shell-integration/client/index.html</code> 新增了容器与历史列表：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;container&#34;</span>&gt;
  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;app&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;history&#34;</span>&gt;
    &lt;<span style="color:#f92672">h3</span>&gt;Command History&lt;/<span style="color:#f92672">h3</span>&gt;
    &lt;<span style="color:#f92672">ul</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;command-list&#34;</span>&gt;&lt;/<span style="color:#f92672">ul</span>&gt;
  &lt;/<span style="color:#f92672">div</span>&gt;
&lt;/<span style="color:#f92672">div</span>&gt;</code></pre></div>
<p>配套样式在 <code>project-demo/03-shell-integration/client/src/style.css</code>：将页面分成上下两块（上终端、下历史），并为历史条目做了排版（命令、cwd、exit code、输出区域等）。</p>

<h4 id="2-新增-shellintegration-从字节流中-抽取命令记录">2）新增 ShellIntegration：从字节流中“抽取命令记录”</h4>

<p>在 <code>project-demo/03-shell-integration/client/src/main.js</code> 中引入并使用 <code>ShellIntegration</code>，从 WebSocket 收到的数据不再直接 <code>terminal.write(event.data)</code>，而是：</p>

<ul>
<li>先交给 <code>shellIntegration.process()</code> 解析并“吞掉”自定义 OSC 序列</li>
<li>返回给终端显示的只剩“干净的”输出</li>
<li>当识别到“命令结束”事件时，回调里拿到结构化的命令记录并渲染到历史区</li>
</ul>

<p>关键点（节选）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#75715e">// project-demo/03-shell-integration/client/src/main.js
</span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">ShellIntegration</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;./shell-integration.js&#39;</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">shellIntegration</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ShellIntegration</span>((<span style="color:#a6e22e">data</span>) =&gt; {
  <span style="color:#a6e22e">addHistoryItem</span>(<span style="color:#a6e22e">data</span>);
});

<span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">onmessage</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) =&gt; {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cleanData</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">shellIntegration</span>.<span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">data</span>);
  <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">write</span>(<span style="color:#a6e22e">cleanData</span>);
};
</code></pre></div>
<p>历史条目渲染同样在 <code>project-demo/03-shell-integration/client/src/main.js</code> 中新增：每条记录会创建一个“只读的 xterm 实例”来展示该命令的输出（<code>disableStdin: true</code>），并根据 <code>exitCode</code> 显示红/绿状态。</p>

<h4 id="3-shellintegration-的协议与状态机">3）ShellIntegration 的协议与状态机</h4>

<p><code>project-demo/03-shell-integration/client/src/shell-integration.js</code> 是本 Demo 的核心新增文件。</p>

<ul>
<li><strong>协议</strong>：匹配 <code>OSC 729</code>，形如 <code>\x1b]729;&lt;content&gt;\x07</code></li>
<li><strong>content 类型</strong>（由服务端注入的 bash 脚本产生）：

<ul>
<li><code>A</code>：Prompt Start</li>
<li><code>B</code>：Command Start（用户开始输入命令）</li>
<li><code>C</code>：Command Executed（即将开始执行）</li>
<li><code>D;&lt;exitCode&gt;</code>：Command Finished</li>
<li><code>P;Cwd=&lt;pwd&gt;</code>：属性上报（当前工作目录）</li>
</ul></li>
</ul>

<p>解析入口（节选）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#75715e">// project-demo/03-shell-integration/client/src/shell-integration.js
</span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">oscRegex</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/\x1b]729;(.*?)\x07/g</span>;

<span style="color:#66d9ef">while</span> ((<span style="color:#a6e22e">match</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">oscRegex</span>.<span style="color:#a6e22e">exec</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">buffer</span>)) <span style="color:#f92672">!==</span> <span style="color:#66d9ef">null</span>) {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">textBefore</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">buffer</span>.<span style="color:#a6e22e">substring</span>(<span style="color:#a6e22e">lastIndex</span>, <span style="color:#a6e22e">match</span>.<span style="color:#a6e22e">index</span>);
  <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">handleText</span>(<span style="color:#a6e22e">textBefore</span>);
  <span style="color:#a6e22e">outputForTerminal</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">textBefore</span>;

  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">match</span>[<span style="color:#ae81ff">1</span>];
  <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">handleOsc</span>(<span style="color:#a6e22e">content</span>);

  <span style="color:#a6e22e">lastIndex</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">oscRegex</span>.<span style="color:#a6e22e">lastIndex</span>;
}
</code></pre></div>
<ul>
<li><p><code>process()</code> 做两件事：
1) 从输入流中找出完整的 OSC 片段并解析事件<br />
2) 把 OSC 片段从输出中移除，保证真正写入 xterm 的内容不包含这些标记</p></li>

<li><p><code>handleOsc()</code> 内部维护一个简单状态机（<code>PROMPT / INPUT / EXECUTION / UNKNOWN</code>），并在 <code>D</code>（命令结束）时触发 <code>finishCommand()</code>，把 <code>{command, output, exitCode, cwd}</code> 交给 UI。</p></li>
</ul>

<h4 id="4-为什么要-用-xterm-解析命令文本">4）为什么要“用 xterm 解析命令文本”</h4>

<p>用户输入并不等价于最终命令字符串：可能包含退格、左右移动、行编辑等控制序列。为此 <code>ShellIntegration</code> 里用了一个“看不见的 xterm”来还原最终命令文本：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#75715e">// project-demo/03-shell-integration/client/src/shell-integration.js
</span><span style="color:#75715e"></span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">parserTerm</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Terminal</span>({ <span style="color:#a6e22e">rows</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">cols</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1024</span>, <span style="color:#a6e22e">allowProposedApi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span> });

<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">parserTerm</span>.<span style="color:#a6e22e">reset</span>();
<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">parserTerm</span>.<span style="color:#a6e22e">write</span>(<span style="color:#a6e22e">raw</span>, () =&gt; {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">buffer</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">parserTerm</span>.<span style="color:#a6e22e">buffer</span>.<span style="color:#a6e22e">active</span>;
  <span style="color:#75715e">// 从 buffer 里取出最终渲染出的文本作为 command
</span><span style="color:#75715e"></span>});
</code></pre></div>
<p>这样即使用户在终端里编辑过命令，历史区展示的仍然是“执行时的真实命令”。</p>

<hr />

<h3 id="server">Server</h3>

<p>服务端仍然是“pty &lt;-&gt; websocket”桥接，但新增了一个关键能力：<strong>启动 Bash 时注入初始化脚本，用来发出 Shell Integration 的 OSC 事件</strong>。</p>

<h4 id="1-用-init-file-注入-bash-脚本">1）用 <code>--init-file</code> 注入 bash 脚本</h4>

<p><code>project-demo/03-shell-integration/server/main.go</code> 新增 <code>bashInitScript</code>，并在 <code>startShellByPty()</code> 中：</p>

<ul>
<li>写入临时文件</li>

<li><p>用 <code>/bin/bash --init-file &lt;tmp&gt;</code> 启动 shell（替代原来的 <code>bash -il</code>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// project-demo/03-shell-integration/server/main.go
</span><span style="color:#75715e"></span><span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;--init-file&#34;</span>, <span style="color:#a6e22e">tmpFile</span>.<span style="color:#a6e22e">Name</span>())
<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">pty</span>.<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">cmd</span>)</code></pre></div></li>
</ul>

<h4 id="2-在-prompt-执行前-执行后打点">2）在 Prompt / 执行前 / 执行后打点</h4>

<p><code>bashInitScript</code> 里定义了自定义 OSC 通道：</p>

<ul>
<li><code>__rectcircle_shell_integration_demo_osc_start=&quot;\033]729;&quot;</code></li>
<li><code>__rectcircle_shell_integration_demo_osc_end=&quot;\007&quot;</code></li>
</ul>

<p>并在几个关键 hook 上输出事件：</p>

<ul>
<li><code>PROMPT_COMMAND</code>：每次显示 prompt 前调用（这里用来发送 <code>D</code> + <code>P</code> + <code>A</code>）</li>
<li><code>trap '...' DEBUG</code>：每条命令执行前触发（这里发送 <code>C</code>）</li>
<li><code>PS1</code>：在 prompt 字符串中插入不可见序列（这里发送 <code>B</code>，标记“命令输入开始”）</li>
</ul>

<p>关键片段（节选）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># project-demo/03-shell-integration/server/main.go (bashInitScript)</span>
__rectcircle_shell_integration_demo_precmd<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
  local ret<span style="color:#f92672">=</span>$?
  printf <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>start<span style="color:#e6db74">}</span><span style="color:#e6db74">D;</span><span style="color:#e6db74">${</span>ret<span style="color:#e6db74">}${</span>end<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
  printf <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>start<span style="color:#e6db74">}</span><span style="color:#e6db74">P;Cwd=</span><span style="color:#e6db74">${</span>PWD<span style="color:#e6db74">}${</span>end<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
  printf <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>start<span style="color:#e6db74">}</span><span style="color:#e6db74">A</span><span style="color:#e6db74">${</span>end<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
<span style="color:#f92672">}</span>
PROMPT_COMMAND<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;__rectcircle_shell_integration_demo_precmd&#34;</span>

trap <span style="color:#e6db74">&#39;__rectcircle_shell_integration_demo_preexec&#39;</span> DEBUG

PS1<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$PS1<span style="color:#e6db74">\[</span><span style="color:#e6db74">${</span>start<span style="color:#e6db74">}</span><span style="color:#e6db74">B</span><span style="color:#e6db74">${</span>end<span style="color:#e6db74">}</span><span style="color:#e6db74">\]&#34;</span></code></pre></div>
<p>最终效果：Bash 在“同一条字节流”里同时输出给人看的终端内容，以及给前端解析用的“隐形结构化事件”。</p>

<hr />

<h3 id="运行示例">运行示例</h3>

<ul>
<li><p>启动 Server：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/03-shell-integration/server
go run .</code></pre></div></li>

<li><p>启动 Client：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/03-shell-integration/client
npm i
npm run dev</code></pre></div></li>

<li><p>打开页面后在上方终端执行几条命令，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ls -al
false
echo <span style="color:#e6db74">&#34;ok&#34;</span>
echo <span style="color:#e6db74">&#39;abc
</span><span style="color:#e6db74">123&#39;</span></code></pre></div></li>
</ul>

<p>可以在下方 Command History 看到每条命令的：</p>

<ul>
<li>命令文本（已还原行编辑后的最终内容）</li>
<li><code>cwd</code></li>
<li><code>exitCode</code>（成功为 0 绿色，失败非 0 红色）</li>
<li>该命令对应的输出（以只读 xterm 呈现），可以看到颜色效果</li>
<li>多行命令也能正确的识别</li>
</ul>
]]></description></item><item><title>终端详解（四）简单实现一个支持作业控制的 Shell</title><link>https://www.rectcircle.cn/posts/terminal-detail-4-impl-job-control-shell/</link><pubDate>Tue, 14 Oct 2025 21:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-detail-4-impl-job-control-shell/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文源码: <a href="https://github.com/rectcircle/implement-terminal-from-scratch">rectcircle/implement-terminal-from-scratch</a></p>
</blockquote>

<h2 id="前言">前言</h2>

<p>Shell 是我们与操作系统交互的重要桥梁，它不仅能执行命令行程序和解释 Shell 脚本，还具备一项需要与终端设备深度协作的核心功能 —— 作业控制 （Job Control）。</p>

<p>作业控制是 Shell 的高级特性，它让我们能够：</p>

<ul>
<li>在后台运行程序（ <code>command &amp;</code> ）</li>
<li>暂停和恢复进程（ Ctrl+Z 、 fg 、 bg ）</li>
<li>中断正在运行的程序（ Ctrl+C ）</li>
<li>管理多个并发任务</li>
</ul>

<p>本文将通过 Go 语言实现一个简化版的 Shell，深入理解作业控制的工作原理。读完本文，将明白以下常见现象背后的技术原理：</p>

<ul>
<li><code>proc1 | proc2</code> 管道符的实现原理是什么？</li>
<li><code>proc1 &amp;</code> 后台进程，为什么还会输入到屏幕中？</li>
<li><code>ctrl+c</code> 原理是什么？</li>
<li><code>proc1 &amp;</code> ssh 断开后，后台任务为什么退出了？<code>nohup</code> 原理是什么？</li>
</ul>

<h2 id="实现">实现</h2>

<h3 id="初始化项目">初始化项目</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo
mkdir -p <span style="color:#ae81ff">02</span>-shell-demo
cd <span style="color:#ae81ff">02</span>-shell-demo
go mod init github.com/rectcircle/implement-terminal-from-scratch/project-demo/02-shell-demo</code></pre></div>
<h3 id="程序-repl-框架">程序 REPL 框架</h3>

<p><code>project-demo/02-shell-demo/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;bufio&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">reader</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>)
	<span style="color:#a6e22e">jobController</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">JobController</span>{}

	<span style="color:#66d9ef">for</span> {
		<span style="color:#75715e">// 显示提示符
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Print</span>(<span style="color:#e6db74">&#34;shell-demo&gt; &#34;</span>)
		<span style="color:#75715e">// 刷新输出缓冲区
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>.<span style="color:#a6e22e">Sync</span>()

		<span style="color:#75715e">// 读取用户输入
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">ReadString</span>(<span style="color:#e6db74">&#39;\n&#39;</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">EOF</span> {
				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;\nGoodbye!&#34;</span>)
				<span style="color:#66d9ef">break</span>
			}
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>, <span style="color:#e6db74">&#34;Error reading input: %v\n&#34;</span>, <span style="color:#a6e22e">err</span>)
			<span style="color:#66d9ef">continue</span>
		}

		<span style="color:#75715e">// 去除首位换行符和空格
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">input</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">input</span>)

		<span style="color:#75715e">// 如果输入为空，继续下一次循环
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
			<span style="color:#66d9ef">continue</span>
		}

		<span style="color:#75715e">// 如果输入是 exit，退出程序
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;exit&#34;</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Goodbye!&#34;</span>)
			<span style="color:#66d9ef">break</span>
		}

		<span style="color:#75715e">// 解析并执行命令
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">jobController</span>.<span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>, <span style="color:#e6db74">&#34;Error executing command: %v\n&#34;</span>, <span style="color:#a6e22e">err</span>)
		}
	}
}</code></pre></div>
<p>在 <code>project-demo/02-shell-demo/job.go</code> 先实现最简单的命令执行</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JobController</span> <span style="color:#66d9ef">struct</span> {
}

<span style="color:#75715e">// parseAndExecuteCommand 解析并执行命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 分割命令和参数 (不考虑 &#34;&#34; &#39;&#39; 等语法)
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">args</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#75715e">// 第一个参数是命令，其余是参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>:]<span style="color:#f92672">...</span>)

	<span style="color:#75715e">// 设置标准输入、输出、错误输出
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#75715e">// 执行命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
}</code></pre></div>
<h3 id="管道符">管道符</h3>

<p>在 <code>project-demo/02-shell-demo/job.go</code> 中，添加对 Job 的抽象，并实现管道符处理：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JobController</span> <span style="color:#66d9ef">struct</span> {
}

<span style="color:#75715e">// Execute 解析并执行命令，支持管道符
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">job</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewJob</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">// 启动 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>()
}

<span style="color:#75715e">// Job 表示一个作业，包含单个命令或管道命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Job</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">commands</span> []<span style="color:#f92672">*</span><span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Cmd</span>     <span style="color:#75715e">// 命令列表，每个元素是一个 *exec.Cmd
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pgid</span>     <span style="color:#66d9ef">int</span>             <span style="color:#75715e">// 进程组ID
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pipes</span>    []<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadCloser</span> <span style="color:#75715e">// 管道连接
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">exitCode</span> <span style="color:#66d9ef">int</span>           <span style="color:#75715e">// Job 整体退出码（最后一个进程），-1 表示正在运行中
</span><span style="color:#75715e"></span>
}

<span style="color:#75715e">// NewJob 创建一个新的Job，解析命令字符串
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewJob</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#75715e">// 按管道符分割命令
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pipeCommands</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">input</span>, <span style="color:#e6db74">&#34;|&#34;</span>)
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">pipeCommands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Job</span>{}
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>

	<span style="color:#75715e">// 将每个命令构造为 *exec.Cmd
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">cmdStr</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">pipeCommands</span> {
		<span style="color:#a6e22e">cmdStr</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">cmdStr</span>)
		<span style="color:#a6e22e">args</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">cmdStr</span>)
		<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#66d9ef">continue</span>
		}

		<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>:]<span style="color:#f92672">...</span>)
		<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commands</span> = append(<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commands</span>, <span style="color:#a6e22e">cmd</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">job</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Start 启动Job中的所有命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">j</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#a6e22e">Start</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#75715e">// 统一处理所有命令，都创建进程组
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmds</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>

	<span style="color:#75715e">// 设置管道连接
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cmds</span> {
		<span style="color:#75715e">// 设置管道
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#75715e">// 第一个命令从标准输入读取
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#75715e">// 后续命令从前一个命令的输出读取
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">pipes</span>[<span style="color:#a6e22e">i</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]
		}

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> len(<span style="color:#a6e22e">cmds</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
			<span style="color:#75715e">// 最后一个命令输出到标准输出
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#75715e">// 中间命令的输出作为管道
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">stdout</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">StdoutPipe</span>()
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}
			<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">pipes</span> = append(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">pipes</span>, <span style="color:#a6e22e">stdout</span>)
		}

		<span style="color:#75715e">// 所有命令的错误输出都到标准错误
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	}

	<span style="color:#75715e">// 启动所有命令，并设置进程组
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cmds</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#75715e">// 第一个进程作为进程组组长
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
				<span style="color:#a6e22e">Setpgid</span>: <span style="color:#66d9ef">true</span>,
				<span style="color:#a6e22e">Pgid</span>:    <span style="color:#ae81ff">0</span>, <span style="color:#75715e">// 0 表示使用进程自己的PID作为进程组ID
</span><span style="color:#75715e"></span>			}
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#75715e">// 后续进程加入第一个进程的进程组
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
				<span style="color:#a6e22e">Setpgid</span>: <span style="color:#66d9ef">true</span>,
				<span style="color:#a6e22e">Pgid</span>:    <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">pgid</span>,
			}
		}

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#75715e">// 记录第一个进程的进程组ID
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">pgid</span> = <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>
		}
	}

	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Wait 等待Job中的所有进程退出
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">j</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#a6e22e">Wait</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#75715e">// 进程还没有启动
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">continue</span>
		}
		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wstatus</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WaitStatus</span>
		<span style="color:#a6e22e">wpid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Wait4</span>(<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">wstatus</span>, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">nil</span>)
		<span style="color:#75715e">// Wait4 出错，可能是进程不存在或权限问题
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">ECHILD</span> {
				<span style="color:#75715e">// 进程已经不存在了
</span><span style="color:#75715e"></span>				<span style="color:#66d9ef">continue</span>
			}
			<span style="color:#75715e">// 未知错误，直接抛异常
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">wpid</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#75715e">// WNOHANG 且没有子进程状态变化，说明进程还在运行
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">continue</span>
		}
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p>这里参考了 bash，将命令的执行抽象为 Job 概念：</p>

<ul>
<li>单命令，是一个 Job</li>
<li><code>|</code> 管道符连接的多个命令，也是一个 Job。</li>
<li>不管单命令，还是多命令，都会为每个 Job 都会创建一个进程组。</li>
</ul>

<p>以 <code>proc1 | proc2 | proc3</code> 作业为例。</p>

<ul>
<li>三个进程会在同一个进程组内，组长为 <code>proc1</code>，这样方便调用 wait4 系统调用，等待这组进程退出。</li>

<li><p>这三个进程 stdio 连接情况如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+--------------+           +------------------+           +------------------+           +------------------+
|my-shell stdin| --dup2--&gt; |stdin proc1 stdout| --pipe--&gt; |stdin proc2 stdout| --pipe--&gt; |stdin proc3 stdout|
+--------------+           |            stderr|           |            stderr|           |            stderr|
                           +------------------+           +------------------+           +------------------+
                                           |                              |                              |
                                           +------------------------------+-----------------------------+
                                                                          |
                                                                        (dup2)
                                                                          |
                                                                          v
                                                                    my-shell.stderr</pre></div></li>

<li><p>关于进程某个退出后，POSIX 行为如下，以 proc2 退出为例：</p>

<ul>
<li>proc2 进程的 stdio 文件描述符会被关闭。</li>
<li>proc1 写入时会触发 SIGPIPE 信号，默认改程序会退出。</li>
<li>proc3 读取 stdin 会返回 EOF，一般情况下，程序会自行退出。</li>
</ul></li>
</ul>

<blockquote>
<p>另外： Setpgid 的 Go 的实现隐藏了一个细节。如果是 C 的实现需要 fork 后，父子进程都需要调用 Setpgid 以避免静态问题。</p>
</blockquote>

<h3 id="前台进程组">前台进程组</h3>

<p>在上一步，为 Job 创建了进程组。但是没有将这个 Job 设置为前台进程组。因此，这个 shell-demo 和 bash 相比无法正确的处理信号：</p>

<p>比如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/02-shell-demo
go run .
<span style="color:#75715e"># 输入 sleep 100</span> 
<span style="color:#75715e"># 输入 回车</span>
<span style="color:#75715e"># 输入 ctrl+c</span></code></pre></div>
<p>此时 shell-demo 就退出了，这和 bash 行为是不一致的。bash 的行为是将 sleep 100 进程停止， bash 继续等待用户输入。</p>

<p>因为只有前台进程组才能收到终端输入的 ctrl+c 信号，要实现类似 bash 的行为需要：</p>

<ol>
<li>在创建 JobController 的时候，记录当前 shell 所在进程组 ID</li>
<li>在执行 Job 进程组的时候，需要将前台进程组从 Shell 所在进程组切换到 Job 所在进程组。</li>
<li>在 Job 进程组退出后，将前台进程组重新设置为 Shell 所在进程组（必须切换回来，否则 Shell 通过 stdin 读取终端时会触发 SIGTTIN 信号，因为 stdin 只能前台进程组可以读取）。</li>
</ol>

<p><code>project-demo/02-shell-demo/job.go</code> 添加 JobController 构造函数，记录当前 shell 所在进程组 ID：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">import</span> (
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JobController</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 当前 shell 所在的前台进程组 ID
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">shellForegroundPgid</span> <span style="color:#66d9ef">int</span>
}


<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewJobController</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">currentPgid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">Getpgid</span>(<span style="color:#ae81ff">0</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Execute job, get pgid failed: %s&#34;</span>, <span style="color:#a6e22e">err</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">JobController</span>{
		<span style="color:#a6e22e">shellForegroundPgid</span>: <span style="color:#a6e22e">currentPgid</span>,
	}, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p>在 <code>project-demo/02-shell-demo/main.go</code> main 函数中使用构造函数新建 JobController：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">reader</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>)
	<span style="color:#a6e22e">jobController</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewJobController</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Job control not available. Exiting.&#34;</span>)
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
	}
	<span style="color:#75715e">//...
</span><span style="color:#75715e"></span>}</code></pre></div>
<p><code>project-demo/02-shell-demo/job.go</code> Job 的 Start 方法，实现上述第 2 点：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cmds</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#75715e">// 第一个进程作为进程组组长，并将该进程组设置为前台
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
				<span style="color:#a6e22e">Setpgid</span>: <span style="color:#66d9ef">true</span>,
				<span style="color:#a6e22e">Pgid</span>:    <span style="color:#ae81ff">0</span>, <span style="color:#75715e">// 0 表示使用进程自己的PID作为进程组ID
</span><span style="color:#75715e"></span>				<span style="color:#75715e">// 实现原理是：
</span><span style="color:#75715e"></span>				<span style="color:#75715e">// 1. 在 fork 之前，调用 sigprocmask 屏蔽了所有信号 (runtime/proc.go syscall_runtime_BeforeFork)。
</span><span style="color:#75715e"></span>				<span style="color:#75715e">// 2. 在 fork 之后 exec 之前：
</span><span style="color:#75715e"></span>				<span style="color:#75715e">//    a. 调用 TIOCSPGRP 将子进程进程组设置为 session 的前台进程组 (syscall/exec_libc2.go forkAndExecInChild)。
</span><span style="color:#75715e"></span>				<span style="color:#75715e">//    b. 调用 msigrestore 恢复到信号屏蔽集 (runtime/proc.go syscall_runtime_AfterForkInChild)。
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">Foreground</span>: <span style="color:#66d9ef">true</span>, <span style="color:#75715e">// 将当前进程组设置为 session 的前台进程组
</span><span style="color:#75715e"></span>			}
		}
		<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	}
	<span style="color:#f92672">//</span> <span style="color:#f92672">...</span></code></pre></div>
<p><code>project-demo/02-shell-demo/job.go</code> JobController 的 Execute 方法，实现上述第 3 点：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">ForceSetShellForeground</span>() {
	<span style="color:#75715e">// 需要忽略 SIGTTOU 信号，否则会导致前台进程组切换失败，原因如下：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1. Unix 系统为了安全，当调用 TIOCSPGRP 的进程不在前台进程组时，会发送 SIGTTOU 信号，而 SIGTTOU 的默认行为是退出进程。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    因为 TIOCSPGRP 是给 Shell 程序调用的，如果普通程序调用这个函数，会破坏 Shell 的作业管理，因此 Unix 系统才设计了这个机制。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2. 我们实现的这个程序就是一个 Shell，因此就是要调用 TIOCSPGRP 的，因此需要避免 SIGTTOU 信号的影响，有两种办法。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    a. 忽略这个信号，这里采用这个方案。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    b. 通过 sigprocmask 屏蔽这个信号（这里需要说一下，对于其他信号，屏蔽信号只是延后信号的处理，但是对于 SIGTTOU 信号，屏蔽了之后，就不会再产生了） Go 的 syscall.SysProcAttr.Foreground 通过该方案实现。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Ignore</span>(<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTTOU</span>)
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Reset</span>(<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTTOU</span>)
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">IoctlSetPointerInt</span>(int(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>.<span style="color:#a6e22e">Fd</span>()), <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TIOCSPGRP</span>, <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">shellForegroundPgid</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#75715e">// Execute 解析并执行命令，支持管道符
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">job</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewJob</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">// 启动 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">ForceSetShellForeground</span>() <span style="color:#75715e">// 执行结束后强制把 shell 进程设置为前台
</span><span style="color:#75715e"></span>
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>()

}</code></pre></div>
<p>关于 TIOCSPGRP 以及 SIGTTOU 详见 <a href="https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetpgrp.html">文档</a>。</p>

<h3 id="作业控制">作业控制</h3>

<p>前文已经介绍了前台进程组，已经涉及了作业控制的核心部分，即前台/后台进程组，而作业控制就是对多个 Job 的前后台的管理。</p>

<ul>
<li>0 个或 1 个 Job 在前台运行，0 个或多个 Job 在后台运行。</li>
<li>前台 Job 可以切换到后台，后台 Job 可以切换到前台。</li>
<li>前台 Job 可以接收来自终端的的控制信号，后台 Job 不受影响。</li>
<li>交互式 Shell 退出后，所有的 Job 均被 SIGHUP （挂断信号） 终止。</li>
</ul>

<h4 id="是否支持作业控制">是否支持作业控制</h4>

<p>如上可以看出，作业控制和终端密切相关，因此一个 shell 要启用作业控制能力，必须满足如下两个条件：</p>

<ul>
<li>stdin 是否是 tty/pty。</li>
<li>shell 所在进程组必须是当前会话的前台进程组。</li>
</ul>

<p>因此，以 bash 为例：</p>

<ul>
<li>如下条件将可以启用作业控制：

<ul>
<li>使用 ssh/webshell 连接到远端启动的 <code>shell</code>： ssh/webshell server 会配置好会话和 pty。</li>
<li>在一个 shell 交互式终端内执行 <code>bash</code>： 这个 bash 自然的在父 shell 的前台进程组内且 stdin 和 stdout 是 tty/pty。</li>
</ul></li>
<li>这里介绍一下，一些无法启用作业控制例子：

<ul>
<li><code>echo 'ls -al' | bash</code>： 此时 bash 的 stdin 是一个 pipe。</li>
<li><code>bash &amp;</code>： 此时 bash 不在前台进程组。</li>
</ul></li>
</ul>

<p>在 <code>project-demo/02-shell-demo/job.go</code> 实现一个方法，判断当前进程是否可以启用作业控制。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">import</span> (
	<span style="color:#75715e">// ..
</span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>
)

<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// CanEnableJobControl 判断当前进程是否可以启用作业控制
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">CanEnableJobControl</span>() <span style="color:#66d9ef">bool</span> {
	<span style="color:#75715e">// 检查是否有控制终端
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">isatty</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>.<span style="color:#a6e22e">Fd</span>()) {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
	}
	<span style="color:#75715e">// 获取前台进程组ID
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pgrpid</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Getpgrp</span>()

	<span style="color:#75715e">// 如果当前进程组就是前台进程组，则可以启用作业控制
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">shellForegroundPgid</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">pgrpid</span>
}

<span style="color:#75715e">// isatty 检查文件描述符是否是终端
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">isatty</span>(<span style="color:#a6e22e">fd</span> <span style="color:#66d9ef">uintptr</span>) <span style="color:#66d9ef">bool</span> {
	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">IoctlGetTermios</span>(int(<span style="color:#a6e22e">fd</span>), <span style="color:#a6e22e">ioctlReadTermios</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p><code>project-demo/02-shell-demo/main.go</code> 添加检查，如果无法启用作业控制，则直接退出。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">reader</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>)
	<span style="color:#a6e22e">jobController</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">JobController</span>{}

	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">jobController</span>.<span style="color:#a6e22e">CanEnableJobControl</span>() {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Job control not available. Exiting.&#34;</span>)
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
	}

    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>获取终端信息，有一些跨平台问题，因此需要使用条件编译。</p>

<p><code>project-demo/02-shell-demo/term_unix_bsd.go</code> MacOS 等系统：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// https://github.com/golang/term/blob/master/term_unix_bsd.go
</span><span style="color:#75715e"></span>
<span style="color:#75715e">//go:build darwin || dragonfly || freebsd || netbsd || openbsd
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ioctlReadTermios</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TIOCGETA</span>

<span style="color:#f92672">//</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ioctlWriteTermios</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TIOCSETA</span></code></pre></div>
<p><code>project-demo/02-shell-demo/term_unix_other.go</code> Linux 等系统：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// https://github.com/golang/term/blob/master/term_unix_other.go
</span><span style="color:#75715e"></span>
<span style="color:#75715e">//go:build aix || linux || solaris || zos
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ioctlReadTermios</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TCGETS</span>

<span style="color:#f92672">//</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ioctlWriteTermios</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TCSETS</span></code></pre></div>
<p>测试：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/02-shell-demo
<span style="color:#75715e"># 如下将可以正常执行</span>
go run .
<span style="color:#75715e"># 如下将立即退出，打印 Job control not available. Exiting.</span> 
go run . &amp;
<span style="color:#75715e"># 如下将立即退出，打印 Job control not available. Exiting.</span>
echo <span style="color:#e6db74">&#39;ls&#39;</span> | go run .</code></pre></div>
<h4 id="启动后台-job">启动后台 Job</h4>

<p>在 Bash 中，可以通过在命令末尾添加 <code>&amp;</code> 符号，将一个命令置入后台执行，即启用后台 Job。</p>

<p>Bash 后台 Job 的表现如下：</p>

<ul>
<li>后台进程启动后，Bash 不会等待后台进程退出，而是打印 Job ID 和进程组 ID 后，进入下一轮 REPL 循环，等待用户输入新的命令。</li>
<li>后台进程组不会设置为当前会话的前台进程组，也就是说 ctrl + c 等信号不会发送给这个后台进程组。</li>
<li>后台进程的标准输出会输出到当前终端，因此可能出现前后台进程组日志交替输出的现象。</li>
<li>当后台进程组退出后，在 REPL 的下一个命令执行时，会打印这个后台进程组已经退出的相关日志。</li>
</ul>

<p>例如 <code>echo 'sleep 2 &amp;&amp; echo abc' | sh &amp;</code> 输入情况如下：</p>

<ul>
<li>立即打印： <code>[1] 77842</code></li>
<li>立即打印： <code>$PS1</code> 然后等待用户输入</li>
<li>2 秒后打印： <code>abc</code></li>
<li>按回车后打印： <code>[1]+  Done                    echo 'sleep 2 &amp;&amp; echo abc' | sh</code></li>
</ul>

<p>首先把 <code>project-demo/02-shell-demo/main.go</code> 的 main 函数里将用户输入空串的 continue 的逻辑删除掉，因为空串仍然需要后台 Job 逻辑（）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-diff" data-lang="diff"><span style="color:#75715e">@@ -37,11 +41,6 @@ func main() {
</span><span style="color:#75715e"></span>                // 去除首位换行符和空格
                input = strings.TrimSpace(input)
 
<span style="color:#f92672">-               // 如果输入为空，继续下一次循环
</span><span style="color:#f92672">-               if input == &#34;&#34; {
</span><span style="color:#f92672">-                       continue
</span><span style="color:#f92672">-               }
</span><span style="color:#f92672">-
</span></code></pre></div>
<p>然后 <code>project-demo/02-shell-demo/job.go</code> 的 Job 结构体：</p>

<ul>
<li>需要添加 <code>commandStr</code>、<code>exitCode</code>、<code>background</code> 来记录 Job 的命令字符串、退出码以及是否是后台任务，并在 <code>NewJob</code> 构造函数解析 <code>&amp;</code> 符号，并对前面的字段进行正确初始化。</li>
<li>Job 的 Start 函数的 <code>Foreground</code> 字段由 Job 的 <code>background</code> 字段值决定。</li>

<li><p>对 <code>Wait</code> 函数进行扩展改造：</p>

<ul>
<li>支持非阻塞调用，在后台 Job 场景，需要通过非阻塞调用来检测 Job 情况。</li>

<li><p>Job 最后一个命令的退出码需记录到 Job 结构体的 <code>exitCode</code> 字段中。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// Job 表示一个作业，包含单个命令或管道命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Job</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">commandStr</span> <span style="color:#66d9ef">string</span>          <span style="color:#75715e">// 命令字符串
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">exitCode</span>   <span style="color:#66d9ef">int</span>             <span style="color:#75715e">// Job 整体退出码（最后一个进程），-1 表示正在运行中
</span><span style="color:#75715e"></span>
	<span style="color:#a6e22e">background</span> <span style="color:#66d9ef">bool</span> <span style="color:#75715e">// 是否是后台 job
</span><span style="color:#75715e"></span>}


<span style="color:#75715e">// NewJob 创建一个新的Job，解析命令字符串
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewJob</span>(<span style="color:#a6e22e">commandStr</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">commandStr</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">commandStr</span>)
	<span style="color:#a6e22e">background</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">HasSuffix</span>(<span style="color:#a6e22e">commandStr</span>, <span style="color:#e6db74">&#34;&amp;&#34;</span>) {
		<span style="color:#a6e22e">background</span> = <span style="color:#66d9ef">true</span>
		<span style="color:#a6e22e">commandStr</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(<span style="color:#a6e22e">commandStr</span>, <span style="color:#e6db74">&#34;&amp;&#34;</span>)
	}

	<span style="color:#75715e">// 按管道符分割命令
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pipeCommands</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">commandStr</span>, <span style="color:#e6db74">&#34;|&#34;</span>)
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">pipeCommands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Job</span>{}
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span> = <span style="color:#a6e22e">commandStr</span>
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> = <span style="color:#a6e22e">background</span>
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>

	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">// Start 启动Job中的所有命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">j</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#a6e22e">Start</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// ..
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">Foreground</span>: !<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">background</span>, <span style="color:#75715e">// 将当前进程组设置为 session 的前台进程组
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// ..
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">j</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#a6e22e">Wait</span>(<span style="color:#a6e22e">wnohang</span> <span style="color:#66d9ef">bool</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#ae81ff">0</span>
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#75715e">// 调用 wait 命令检查进程状态
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">waitOptions</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">wnohang</span> {
		<span style="color:#a6e22e">waitOptions</span> = <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WNOHANG</span>
	}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span> {
		<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">wpid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Wait4</span>(<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">wstatus</span>, <span style="color:#a6e22e">waitOptions</span>, <span style="color:#66d9ef">nil</span>)
		<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
			<span style="color:#75715e">// 最后一个命令的退出码作为 job 的退出码
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#a6e22e">wstatus</span>.<span style="color:#a6e22e">ExitStatus</span>()
		}
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div></li>
</ul></li>
</ul>

<p>最后 <code>project-demo/02-shell-demo/job.go</code> 的 JobController 结构体：</p>

<ul>
<li>添加 <code>runningJobIds map[int]*Job</code> 字段，记录运行中的所有 Job。</li>
<li>在 <code>NewJobController</code> 构造函数中，对 <code>runningJobIds</code> 进行初始化。</li>

<li><p>在 <code>Execute</code> 函数中，前置检查并打印已退出的 Job，并区分前后台 Job 执行不同的逻辑。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JobController</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 运行中的 job id (从 1 开始)
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">runningJobIds</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">int</span>]<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>

}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewJobController</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#75715e">//...
</span><span style="color:#75715e"></span>
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">JobController</span>{
		<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">runningJobIds</span>:       make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">int</span>]<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>),
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// NewJob 新建一个 Job，返回 JobID
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">AddJob</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">int</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">job</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewJob</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">jobId</span> = <span style="color:#ae81ff">1</span>
	<span style="color:#66d9ef">for</span> ; ; <span style="color:#a6e22e">jobId</span><span style="color:#f92672">++</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]; !<span style="color:#a6e22e">ok</span> {
			<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>] = <span style="color:#a6e22e">job</span>
			<span style="color:#66d9ef">break</span>
		}
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">jobId</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Execute 解析并执行命令，支持管道符
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 前置流程：检查后台进程是否执行完成
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> {
			<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>(<span style="color:#66d9ef">true</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}
			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">statusStr</span> = <span style="color:#e6db74">&#34;&#34;</span>
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
				<span style="color:#75715e">// 进程还在运行
</span><span style="color:#75715e"></span>				<span style="color:#66d9ef">continue</span>
			} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
				<span style="color:#a6e22e">statusStr</span> = <span style="color:#e6db74">&#34;Done&#34;</span>
			} <span style="color:#66d9ef">else</span> {
				<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;Exit %d&#34;</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span>)
			}
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %s                  %s\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">statusStr</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>)
			delete(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>, <span style="color:#a6e22e">jobId</span>)
		}
	}

	<span style="color:#75715e">// 空字符串啥都不做
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#75715e">// 创建 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">AddJob</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
	<span style="color:#75715e">// 启动 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">// 前台执行
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> {
		<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#75715e">// 执行结束后，从 job 列表中删除
</span><span style="color:#75715e"></span>			delete(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>, <span style="color:#a6e22e">jobId</span>)
		}()
		<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">ForceSetShellForeground</span>() <span style="color:#75715e">// 执行结束后强制把 shell 进程设置为前台
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>(<span style="color:#66d9ef">false</span>)
	}
	<span style="color:#75715e">// 后台执行
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %d\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">pgid</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div></li>
</ul>

<h4 id="前后台-job-切换">前后台 Job 切换</h4>

<p>在 Bash 中前后台 Job 的创建和切换操作和原理如下所示：</p>

<ul>
<li>启动后台 Job: 用户在命令最后添加 <code>&amp;</code>，详见上文。</li>
<li>启动前台 Job: 用户直接输入命令，详见上文。</li>
<li>前台 Job -&gt; 后台 (暂停状态) Job: 用户键入 <code>ctrl+z</code>。

<ul>
<li>该 Job 进程组内的所有进程，将接收到 SIGTSTP 信号 (Terminal Stop, 20)，内核将这些进程的调度状态设置为 STOP。</li>
<li>Bash 进程 wait4 系统调用返回，通过 wstatus 可以获取到这些进程处于 stopped 状态（不再能获取到CPU，也就是进程被暂停，不再执行了）。</li>
<li>Bash 进程将该进程设置为后台 Job，并将 Bash 自身设置为前台进程组，打印当前该 Job 的 jobid、处于退出状态、命令字符串如： <code>[1]+  Stopped                 sleep 100</code>，并打印命令提示符，等待下一个命令。</li>
</ul></li>
<li>后台 (暂停状态) Job -&gt; 后台 (运行中) Job：用户在命令提示符输入 <code>bg &lt;jobid&gt;</code>。

<ul>
<li>Bash 向该处于暂停状态的后台 Job 的进程组发送 SIGCONT (Continue, 18) 信号。</li>
<li>内核将该进程组的所有进程加入内核调度队列，进入正常调度。</li>
<li>Bash 打印 jobid 以及命令字符串全文以及 <code>&amp;</code> 后缀，如： <code>[1]+ sleep 100 &amp;</code>。并打印命令提示符，等待下一个命令。</li>
</ul></li>
<li>后台 Job -&gt; 前台 Job： 用户在命令提示符输入  <code>fg &lt;jobid&gt;</code>。

<ul>
<li>打印命令字符串全文。</li>
<li>如果该后台 Job 处于 Stop 状态，则先向该处于暂停状态的后台 Job 的进程组发送 SIGCONT (Continue, 18) 信号。</li>
<li>将 Job 的进程组设置为前台进程组。</li>
<li>阻塞 wait4 该 Job 的进程组退出或暂停。</li>
</ul></li>

<li><p>用户可以通过 <code>jobs</code> 这 bash 内置命令获取到所有后台任务。</p>

<p>如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[1]   Running                 sleep 1000 &amp;
[2]+  Stopped                 sleep 10000
[3]-  Running                 sleep 100000 &amp;</pre></div>
<ul>
<li>第一列 <code>[jobid]±</code> 表示 job id，其中 <code>+</code> 和 <code>-</code> 的含义参见： <a href="https://tldp.org/LDP/abs/html/x9644.html#JOBIDTABLE">文档</a>，本文不多介绍。</li>
<li>第二列 <code>Running|Stopped|Done|Exit x</code> 表示 Job 状态</li>
<li>剩余列 <code>命令字符串</code>，如果是 Running 状态则包含 <code>&amp;</code> 符号。</li>
</ul></li>
</ul>

<p>一个示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">$ sleep 1000&amp;
[1] 58485

$ sleep 10000
^Z
[2]+  Stopped                 sleep 10000

$ sleep 100000
^Z
[3]+  Stopped                 sleep 100000

$ bg 3
[3]+ sleep 100000 &amp;

$ jobs
[1]   Running                 sleep 1000 &amp;
[2]+  Stopped                 sleep 10000
[3]-  Running                 sleep 100000 &amp;</pre></div>
<blockquote>
<p>其他说明： Linux 中暂停一个进程除了 SIGTSTP (Terminal Stop, 20, 可捕获， ctrl+z 或 kill 触发) 外，还有一个 SIGSTOP (Stop, 19, 不可捕获， kill 触发) 信号，两者都能通过 SIGCONT 恢复</p>
</blockquote>

<p>修改 <code>project-demo/02-shell-demo/job.go</code> 添加相关逻辑</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;sort&#34;</span>
	<span style="color:#e6db74">&#34;strconv&#34;</span>
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>)

<span style="color:#75715e">// Job 状态常量
</span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">JobStatusRunning</span> = <span style="color:#e6db74">&#34;Running&#34;</span>
	<span style="color:#a6e22e">JobStatusStopped</span> = <span style="color:#e6db74">&#34;Stopped&#34;</span>
	<span style="color:#a6e22e">JobStatusDone</span>    = <span style="color:#e6db74">&#34;Done&#34;</span>
)

<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// Execute 解析并执行命令，支持管道符
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 前置流程：检查后台进程是否执行完成
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">jobId</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">sortedJobIds</span>() {
		<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> {
			<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>(<span style="color:#66d9ef">true</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}
			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">statusStr</span> = <span style="color:#e6db74">&#34;&#34;</span>
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
				<span style="color:#75715e">// 进程还在运行
</span><span style="color:#75715e"></span>				<span style="color:#66d9ef">continue</span>
			} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
				<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">JobStatusDone</span>
			} <span style="color:#66d9ef">else</span> {
				<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;Exit %d&#34;</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span>)
			}
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %s                  %s\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">statusStr</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>)
			delete(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>, <span style="color:#a6e22e">jobId</span>)
		}
	}

	<span style="color:#75715e">// 空字符串啥都不做
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#75715e">// 尝试执行内建命令
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">isBuiltin</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">tryExecuteBuiltinCommand</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>, <span style="color:#e6db74">&#34;%s\n&#34;</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">isBuiltin</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#75715e">// 创建 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">AddJob</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
	<span style="color:#75715e">// 启动 Job
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		delete(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>, <span style="color:#a6e22e">jobId</span>)
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">// 前台执行
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">waitForegroundJob</span>(<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span>)
	}
	<span style="color:#75715e">// 后台执行
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %d\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">pgid</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}


<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">sortedJobIds</span>() []<span style="color:#66d9ef">int</span> {
	<span style="color:#a6e22e">keys</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">int</span>, <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>))
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">id</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span> {
		<span style="color:#a6e22e">keys</span> = append(<span style="color:#a6e22e">keys</span>, <span style="color:#a6e22e">id</span>)
	}
	<span style="color:#a6e22e">sort</span>.<span style="color:#a6e22e">Ints</span>(<span style="color:#a6e22e">keys</span>) <span style="color:#75715e">// 升序
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">keys</span>
}

<span style="color:#75715e">// tryExecuteBuiltinCommand 处理内置命令，返回是否是内置命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">tryExecuteBuiltinCommand</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">bool</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">args</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">nil</span>
	}

	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>] {
	<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;jobs&#34;</span>:
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">handleJobsCommand</span>()
	<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;bg&#34;</span>:
		<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>) <span style="color:#f92672">!=</span> <span style="color:#ae81ff">2</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bg: usage: bg &lt;jobid&gt;&#34;</span>)
		}
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">handleBgCommand</span>(<span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>])
	<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;fg&#34;</span>:
		<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>) <span style="color:#f92672">!=</span> <span style="color:#ae81ff">2</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fg: usage: fg &lt;jobid&gt;&#34;</span>)
		}
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">handleFgCommand</span>(<span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>])
	<span style="color:#66d9ef">default</span>:
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">nil</span>
	}
}

<span style="color:#75715e">// handleJobsCommand 处理 jobs 命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">handleJobsCommand</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">jobId</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">sortedJobIds</span>() {
		<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">statusStr</span> <span style="color:#66d9ef">string</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
			<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">JobStatusRunning</span>
		} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span> {
			<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">JobStatusStopped</span>
		} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">JobStatusDone</span>
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#a6e22e">statusStr</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;Exit %d&#34;</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span>)
		}

		<span style="color:#a6e22e">commandStr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> {
			<span style="color:#a6e22e">commandStr</span> <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#34; &amp;&#34;</span>
		}

		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %s                  %s\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">statusStr</span>, <span style="color:#a6e22e">commandStr</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// handleBgCommand 处理 bg 命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">handleBgCommand</span>(<span style="color:#a6e22e">jobIdStr</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">jobIdStr</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bg: invalid job id: %s&#34;</span>, <span style="color:#a6e22e">jobIdStr</span>)
	}

	<span style="color:#a6e22e">job</span>, <span style="color:#a6e22e">exists</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">exists</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bg: job %d not found&#34;</span>, <span style="color:#a6e22e">jobId</span>)
	}

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bg: job %d is not stopped&#34;</span>, <span style="color:#a6e22e">jobId</span>)
	}

	<span style="color:#75715e">// 向进程组发送 SIGCONT 信号
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Kill</span>(<span style="color:#f92672">-</span><span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">pgid</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGCONT</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bg: failed to send SIGCONT to job %d: %v&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// 更新 Job 状态
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> = <span style="color:#66d9ef">true</span>

	<span style="color:#75715e">// 打印 Job 信息
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %s &amp;\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// handleFgCommand 处理 fg 命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">handleFgCommand</span>(<span style="color:#a6e22e">jobIdStr</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">jobIdStr</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fg: invalid job id: %s&#34;</span>, <span style="color:#a6e22e">jobIdStr</span>)
	}

	<span style="color:#a6e22e">job</span>, <span style="color:#a6e22e">exists</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>[<span style="color:#a6e22e">jobId</span>]
	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">exists</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fg: job %d not found&#34;</span>, <span style="color:#a6e22e">jobId</span>)
	}

	<span style="color:#75715e">// 打印命令字符串
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;%s\n&#34;</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>)

	<span style="color:#75715e">// 如果 Job 处于 Stop 状态，先发送 SIGCONT 信号
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span> {
		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Kill</span>(<span style="color:#f92672">-</span><span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">pgid</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGCONT</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fg: failed to send SIGCONT to job %d: %v&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span>)
		}
	}

	<span style="color:#75715e">// 将 Job 的进程组设置为前台进程组
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Ignore</span>(<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTTOU</span>)
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Reset</span>(<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTTOU</span>)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">IoctlSetPointerInt</span>(int(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>.<span style="color:#a6e22e">Fd</span>()), <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">TIOCSPGRP</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">pgid</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fg: failed to set job %d as foreground: %v&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// 更新 Job 状态
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">background</span> = <span style="color:#66d9ef">false</span>
	<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>

	<span style="color:#75715e">// TODO: 走统一的 wait 前台进程逻辑，也需要支撑 ctrl + z
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 阻塞等待 Job 退出或暂停
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">waitForegroundJob</span>(<span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">job</span>)
}

<span style="color:#75715e">// waitForegroundJob 等待前台 Job 执行完成，并处理清理逻辑
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">JobController</span>) <span style="color:#a6e22e">waitForegroundJob</span>(<span style="color:#a6e22e">jobId</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">job</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#75715e">// 执行结束后，从 job 列表中删除
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span> {
			delete(<span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">runningJobIds</span>, <span style="color:#a6e22e">jobId</span>)
		}
	}()
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">k</span>.<span style="color:#a6e22e">ForceSetShellForeground</span>() <span style="color:#75715e">// 执行结束后强制把 shell 进程设置为前台
</span><span style="color:#75715e"></span>
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Wait</span>(<span style="color:#66d9ef">false</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}

	<span style="color:#75715e">// 判断是否是 stop 状态
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;[%d] %s                  %s\n&#34;</span>, <span style="color:#a6e22e">jobId</span>, <span style="color:#a6e22e">JobStatusStopped</span>, <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">commandStr</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Job 表示一个作业，包含单个命令或管道命令
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Job</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">commandStr</span> <span style="color:#66d9ef">string</span>          <span style="color:#75715e">// 命令字符串
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">commands</span>   []<span style="color:#f92672">*</span><span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Cmd</span>     <span style="color:#75715e">// 命令列表，每个元素是一个 *exec.Cmd
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pgid</span>       <span style="color:#66d9ef">int</span>             <span style="color:#75715e">// 进程组ID
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pipes</span>      []<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadCloser</span> <span style="color:#75715e">// 管道连接
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">exitCode</span>   <span style="color:#66d9ef">int</span>             <span style="color:#75715e">// Job 整体退出码：-1 运行中，-2 已暂停，其他值为退出码
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">background</span> <span style="color:#66d9ef">bool</span>            <span style="color:#75715e">// 是否是后台 job
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">j</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>) <span style="color:#a6e22e">Wait</span>(<span style="color:#a6e22e">wnohang</span> <span style="color:#66d9ef">bool</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#ae81ff">0</span>
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#75715e">// 调用 wait 命令检查进程状态
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">waitOptions</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WUNTRACED</span> <span style="color:#75715e">// 添加 WUNTRACED
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">wnohang</span> {
		<span style="color:#a6e22e">waitOptions</span> <span style="color:#f92672">|=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WNOHANG</span> <span style="color:#75715e">// 使用位或操作
</span><span style="color:#75715e"></span>	}

	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#75715e">// 进程还没有启动
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">continue</span>
		}
		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wstatus</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WaitStatus</span>
		<span style="color:#a6e22e">wpid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Wait4</span>(<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">wstatus</span>, <span style="color:#a6e22e">waitOptions</span>, <span style="color:#66d9ef">nil</span>)
		<span style="color:#75715e">// Wait4 出错，可能是进程不存在或权限问题
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">ECHILD</span> {
				<span style="color:#75715e">// 进程已经不存在了
</span><span style="color:#75715e"></span>				<span style="color:#66d9ef">continue</span>
			}
			<span style="color:#75715e">// 未知错误，直接抛异常
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">wpid</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
			<span style="color:#75715e">// WNOHANG 且没有子进程状态变化，说明进程还在运行
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">continue</span>
		}
		<span style="color:#75715e">// 检查进程是否被暂停
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">wstatus</span>.<span style="color:#a6e22e">Stopped</span>() {
			<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>
			<span style="color:#75715e">// 对于暂停的进程，不设置 exitCode
</span><span style="color:#75715e"></span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
		}

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">==</span> len(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">commands</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
			<span style="color:#75715e">// 最后一个命令的退出码作为 job 的退出码
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">exitCode</span> = <span style="color:#a6e22e">wstatus</span>.<span style="color:#a6e22e">ExitStatus</span>()
		}
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#f92672">//</span> <span style="color:#f92672">...</span></code></pre></div>
<h4 id="挂断信号">挂断信号</h4>

<p>SIGHUP（Hangup，挂断）是 Unix/POSIX 系统中的一个信号，最初用于表示“控制终端断开”（例如串口/调制解调器挂断）。现代系统中，pty master 被 close 时，内核会发送 SIGHUP （挂断信号） 给会话内的所有进程。</p>

<p>这也是为什么 SSH 断开连接后，正在运行的程序全都退出了。</p>

<p>而 nohup 就是通过忽略 SIGHUP 信号，然后重定向标准输出/标准出错输出到文件（当标准输出/标准出错为终端时）（默认为 nohup.out），标准输入不处理，然后 exec 加载这个程序（nohup 进程就是要执行的进程本身），来解决 shell 退出后，后台任务退出的问题。</p>

<p>常见用法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nohup your_cmd &gt;/var/log/your_cmd.log <span style="color:#ae81ff">2</span>&gt;&amp;<span style="color:#ae81ff">1</span> &lt;/dev/null &amp;</code></pre></div>
<h2 id="总结">总结</h2>

<ul>
<li>将 shell 进程作为会话首进程、会话领导者、控制进程、拥有一个控制终端，不需要 shell 程序自己实现，而是由引导程序通过如下方式设置：

<ul>
<li>fork 出进程，如下操作均在子进程中调用</li>
<li>调用 setsid 创建一个会话（同时切断与之前控制终端的联系）。</li>
<li>紧接着调用 <code>ioctl(slave_fd, TIOCSCTTY, 0)</code> 将当前进程的控制终端设置为指定的终端设备，同时当前进程所在的会话也会和该终端设备关联（<code>Terminal Input/Output Control Set Controlling TTY</code>）。</li>
<li>使用 dup2 系统调用，将当前进程的 stdin stdout stderr 重定向到终端设置文件中。</li>
<li>exec 加载 shell 程序。</li>
</ul></li>
<li>shell 进程如何告知控制终端，前台进程组是哪个？

<ul>
<li>通过 <code>tcsetpgrp</code> 系统调用（Terminal Control SET Process GRouP） 设置前台进程组。</li>
<li>不只是 shell 可以调用这个函数，该会话内的所有进程都有权限调用（bash 里面再起一个 bash 的场景的作业控制也就可以实现了）。</li>
<li>这个系统调用会产生一个 SIGTTOU 信号，需要忽略 SIGTTOU 信号，否则会导致前台进程组切换失败。</li>
</ul></li>
<li>作业控制信号：

<ul>
<li>终端收到如下快捷键，内核会发送信号给关联的会话的前台进程组：

<ul>
<li><code>ctrl+c</code> SIGINT 中断信号</li>
<li><code>ctrl+\</code> SIGQUIT 退出信号</li>
<li><code>ctrl+z</code> SIGSTP 挂起信号 （内核暂停前台进程组的进程，然后父进程 wait4 会返回，然后可以获取这个进程是否处于 stop 状态，然后可以将之切换到后台）</li>
</ul></li>
<li>pty master 被 close 时，内核会发送 SIGHUP （挂断信号） 给会话内的所有进程。

<ul>
<li>因此 <code>&amp;</code> 后，ssh 断开后，进程会挂掉。</li>
<li>此外，如果会话领导者退出了，内核也会发送 SIGHUP 信号。</li>
</ul></li>
</ul></li>
<li>后台进程组

<ul>
<li>读取终端（即读 stdin 时），内核会向该进程发送 SIGTTIN 信号。</li>
<li>后台进程组打印的内容会直接打印到屏幕上（因为这些的程的 stdout 和 stderr 进程了父进程的，指向的都是这个终端设备，是同一个设备文件），因此需要 <code>&gt; output.log 2&gt;&amp;1 &amp;</code>。</li>
</ul></li>
<li>作业和管道：

<ul>
<li>作业是 shell 的概念不是操作系统的概念。</li>
<li>一般由 <code>|</code> 连接起来的命令会组成一个 job，一般 job 和进程组一一对应。</li>
</ul></li>
<li>nohup 的原理：通过忽略 SIGHUP 信号并对标准输出标准出错进行重定向，然后 exec 加载这个程序（nohup 进程就是要执行的进程本身），来解决 shell 退出后，后台任务退出的问题。</li>
<li>观测办法：

<ul>
<li><code>ps -o pid,ppid,pgid,sid,comm</code></li>
</ul></li>
</ul>
]]></description></item><item><title>终端详解（三）实现 WebShell</title><link>https://www.rectcircle.cn/posts/terminal-detail-3-webshell/</link><pubDate>Sun, 03 Aug 2025 16:11:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-detail-3-webshell/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文源码: <a href="https://github.com/rectcircle/implement-terminal-from-scratch">rectcircle/implement-terminal-from-scratch</a></p>
</blockquote>

<h2 id="设计思路">设计思路</h2>

<p>前面系列文章已介绍 terminal 设备 API 和 PTY 的相关机制。基于此，可以按照如下思路实现一个简单的 WebShell 服务：</p>

<ul>
<li>Client (浏览器) &lt;-&gt; Server 使用 WebSocket 进行通讯，收发 ANSI escape 字符流。</li>
<li>Client 使用 <a href="https://xtermjs.org/">xterm.js</a> 库，实现终端能力。</li>
<li>Server 使用 Go 实现：

<ul>
<li>HTTP Server 由使用 Go 的 <code>&quot;net/http&quot;</code> 标准库提供支持。</li>
<li>WebSocket 协议由 <a href="https://github.com/coder/websocket">github.com/coder/websocket</a> 库提供支持。</li>
<li>PTY 创建由 <a href="https://github.com/creack/pty">github.com/creack/pty</a> 库提供支持。</li>
</ul></li>
<li>此外，在 Client 和 Server ANSI escape 字符流收发位置打印传输内容，以便于观测 terminal 相关技术内部实现原理。</li>
</ul>

<h2 id="client">Client</h2>

<p>使用 vite 创建一个 vanilla (不适用任何框架) 前端项目，并添加 xterm.js 依赖。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">npm create vite@latest 
npm install -D @xterm/xterm</code></pre></div>
<p>编写 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/blob/master/project-demo/01-webshell-demo/client/src/main.js">client/src/main.js</a>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#75715e">// 引入项目 css
</span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#39;./style.css&#39;</span>
<span style="color:#75715e">// 引入 xterm 的 css
</span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#39;../node_modules/@xterm/xterm/css/xterm.css&#39;</span>
<span style="color:#75715e">// 引入 xterm.js
</span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Terminal</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@xterm/xterm&#39;</span>


<span style="color:#75715e">// 逻辑
</span><span style="color:#75715e"></span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>() {
  <span style="color:#75715e">// 创建一个终端实例
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">terminal</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Terminal</span>();
  <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">open</span>(document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;#app&#39;</span>));

  <span style="color:#75715e">// 创建 websocket client ， 连接到 server。
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wsConn</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WebSocket</span>(<span style="color:#e6db74">`ws://localhost:8080/`</span>);

  <span style="color:#75715e">// 从 terminal 获取到的用户输入的 ANSI escape 字符流，发送给服务端。
</span><span style="color:#75715e"></span>  <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">onData</span>((<span style="color:#a6e22e">data</span>) =&gt; {
    <span style="color:#75715e">// 打印日志
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#34;terminal-&gt;ws: &#34;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">data</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; [&#34;</span> <span style="color:#f92672">+</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">TextEncoder</span>()).<span style="color:#a6e22e">encode</span>(<span style="color:#a6e22e">data</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;]&#34;</span>);
    <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">send</span>(<span style="color:#a6e22e">data</span>);
  });
  
  <span style="color:#75715e">// 从 websocket 读取服务端返回的 ANSI escape 字符流，写入终端中。
</span><span style="color:#75715e"></span>  <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">onmessage</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) =&gt; {
    <span style="color:#75715e">// 打印日志
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#34;ws-&gt;terminal: &#34;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">data</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; [&#34;</span> <span style="color:#f92672">+</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">TextEncoder</span>()).<span style="color:#a6e22e">encode</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">data</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;]&#34;</span>);
    <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">write</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">data</span>);
  };

  <span style="color:#75715e">// 其他： 略
</span><span style="color:#75715e"></span>  <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">onerror</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) =&gt; {
    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">&#39;WebSocket error: &#39;</span>, <span style="color:#a6e22e">event</span>);
    <span style="color:#75715e">// TODO: 错误处理
</span><span style="color:#75715e"></span>  }
  <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">onclose</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) =&gt; {
    <span style="color:#75715e">// TODO: 关闭处理
</span><span style="color:#75715e"></span>  }

}

<span style="color:#a6e22e">main</span>();
</code></pre></div>
<h2 id="server">Server</h2>

<p>使用 <code>go mod init</code> 创建一个 Go module，然后编写服务端 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/blob/master/project-demo/01-webshell-demo/server/main.go"><code>server/main.go</code></a>：</p>

<p>首先，在 main 函数中注册 http 处理函数并启动 http server：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;log/slog&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 注册处理函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#a6e22e">ptyWsHandler</span>)

	<span style="color:#75715e">// 在 8080 端口启动 http 服务器
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;Starting webshell demo server on :8080...&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ListenAndServe</span>(<span style="color:#e6db74">&#34;:8080&#34;</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;http listen and serve failed &#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
	}
}</code></pre></div>
<p>ptyWsHandler 处理函数， upgrade websocket 请求，创建 pty 和 bash 进程，并将 pty master 和 websocket 流对接。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log/slog&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>

	<span style="color:#e6db74">&#34;github.com/coder/websocket&#34;</span>
)


<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ptyWsHandler</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {

	<span style="color:#75715e">// 允许跨域（仅测试）。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">options</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">AcceptOptions</span>{
		<span style="color:#a6e22e">OriginPatterns</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;*&#34;</span>},
	}

	<span style="color:#75715e">// 接收 websocket upgrade 请求。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">wsConn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Accept</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>, <span style="color:#a6e22e">options</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;websocket accept failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
		<span style="color:#66d9ef">return</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">CloseNow</span>()

	<span style="color:#75715e">// 创建 pty，创建一个 bash 进程，并将 pty slave 和 bash 进程绑定。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 然后，返回 pty master。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ptyFile</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">startShellByPty</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;start shell by pty failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
		<span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">Close</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">StatusInternalError</span>, <span style="color:#e6db74">&#34;start shell by pty failed&#34;</span>)
		<span style="color:#66d9ef">return</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ptyFile</span>.<span style="color:#a6e22e">Close</span>()

	<span style="color:#75715e">// 创建一个新的 ctx。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>()

	<span style="color:#75715e">// 创建两个 channel 接收 websocket 关闭信号。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">clientToPtyCloseCh</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{})
	<span style="color:#a6e22e">ptyToClientCloseCh</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{})

	<span style="color:#75715e">// 从 pty master 读取 -&gt; 写入到 websocket
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">buf</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">byte</span>, <span style="color:#ae81ff">1024</span>)
		<span style="color:#66d9ef">for</span> {
			<span style="color:#75715e">// 从 pty 读取数据
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">n</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ptyFile</span>.<span style="color:#a6e22e">Read</span>(<span style="color:#a6e22e">buf</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#75715e">// TODO: 细化错误处理
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;read from pty failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">Close</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">StatusNormalClosure</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
				close(<span style="color:#a6e22e">clientToPtyCloseCh</span>)
				<span style="color:#66d9ef">return</span>
			}
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">n</span> &gt; <span style="color:#ae81ff">0</span> {
				<span style="color:#75715e">// 打印日志
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">jsonStr</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(string(<span style="color:#a6e22e">buf</span>[:<span style="color:#a6e22e">n</span>]))
				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;pty-&gt;ws: %s, %v\n&#34;</span>, string(<span style="color:#a6e22e">jsonStr</span>), <span style="color:#a6e22e">buf</span>[:<span style="color:#a6e22e">n</span>])
				<span style="color:#75715e">// 读取到数据后，将其写入 WebSocket
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">Write</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">MessageText</span>, <span style="color:#a6e22e">buf</span>[:<span style="color:#a6e22e">n</span>])
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					<span style="color:#75715e">// TODO: 细化错误处理
</span><span style="color:#75715e"></span>					<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;write to websocket failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
					<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">ptyFile</span>.<span style="color:#a6e22e">Close</span>()
					close(<span style="color:#a6e22e">clientToPtyCloseCh</span>)
					<span style="color:#66d9ef">return</span>
				}
			}
		}
	}()

	<span style="color:#75715e">// read from websocket -&gt; write to pty file
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 从 websocket 读取 -&gt; 写入到 pty master
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">for</span> {
			<span style="color:#75715e">// 从 WebSocket 读取数据
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">buf</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">Read</span>(<span style="color:#a6e22e">ctx</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#75715e">// TODO: 细化错误处理
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;read from websocket failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">ptyFile</span>.<span style="color:#a6e22e">Close</span>()
				close(<span style="color:#a6e22e">ptyToClientCloseCh</span>)
				<span style="color:#66d9ef">return</span>
			}

			<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">buf</span>) &gt; <span style="color:#ae81ff">0</span> {
				<span style="color:#75715e">// 打印日志
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">jsonStr</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(string(<span style="color:#a6e22e">buf</span>))
				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ws-&gt;pty: %s, %v\n&#34;</span>, string(<span style="color:#a6e22e">jsonStr</span>), <span style="color:#a6e22e">buf</span>)
				<span style="color:#75715e">// 读取到数据后，将其写入 pty
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ptyFile</span>.<span style="color:#a6e22e">Write</span>(<span style="color:#a6e22e">buf</span>)
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					<span style="color:#75715e">// TODO: 细化错误处理
</span><span style="color:#75715e"></span>					<span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;write to pty failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
					<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">wsConn</span>.<span style="color:#a6e22e">Close</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">StatusNormalClosure</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
					close(<span style="color:#a6e22e">ptyToClientCloseCh</span>)
					<span style="color:#66d9ef">return</span>
				}
			}
		}
	}()

	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">clientToPtyCloseCh</span>:
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">ptyToClientCloseCh</span>:
	}
}</code></pre></div>
<p>startShellByPty 函数，实现非常简单，使用 github.com/creack/pty 库来启动进程即可。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;os/exec&#34;</span>

	<span style="color:#e6db74">&#34;github.com/creack/pty&#34;</span>
)


<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">startShellByPty</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">File</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-il&#34;</span>)
	<span style="color:#75715e">// 使用伪终端启动这个命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">pty</span>.<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">cmd</span>)
}</code></pre></div>
<h2 id="启动服务">启动服务</h2>

<p>启动 server</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/01-webshell-demo/server
go run ./</code></pre></div>
<p>启动 client</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd project-demo/01-webshell-demo/client
npm install
npm run dev</code></pre></div>
<p>打开客户端页面： <a href="http://localhost:5173/">http://localhost:5173/</a> 即获取到一个运行 bash 的终端。</p>

<p>在 server 控制台和客户端页面的开发者工具均可可以观察到 ANSI escape 字符流的详细情况。</p>
]]></description></item><item><title>终端详解（二）PTY 详解</title><link>https://www.rectcircle.cn/posts/terminal-detail-2-pty/</link><pubDate>Mon, 28 Jul 2025 00:36:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-detail-2-pty/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文源码: <a href="https://github.com/rectcircle/implement-terminal-from-scratch">rectcircle/implement-terminal-from-scratch</a></p>
</blockquote>

<h2 id="pty-简述">PTY 简述</h2>

<p>上文，已经介绍了 xterm.js （终端模拟器）的输入输出是一对 ANSI 字符流。在 Unix 系统中，这对 ANSI 字符流并没有直接和应用程序的 stdio 对接。在系统内核中，对终端模拟器设备进行了抽象，提供了一种称为 PTY 的设备类型（硬件终端对应的是 TTY 设备，和 PTY 类似，本文不多介绍）。</p>

<p>每个 PTY 设备会产生两个设备文件描述符，一个（主设备）和终端模拟器连接，一个（从设备）和应用程序的 stdio 连接（一般为 shell 程序），这两个文件描述符中间存在一个被称为行规程（line discipline）的内核程序，来实现通用逻辑。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    应用程序 (Shell: Bash/Zsh 等)
      stdin  stdout/stderr
        ^       |
        |       |
      /dev/pts/xxx (从设备 slave，文件描述符)
        |       |
        |       v
    line discipline (行规程)
        ^       |
        |       |
      /dev/ptmx   (主设备 master，文件描述符)
        |       |
        |       v
    终端模拟器</pre></div>
<h2 id="行规程行为详解">行规程行为详解</h2>

<p>行规程对终端进行一些预处理，实现了一些通用逻辑，会做如下工作：</p>

<ul>
<li>line buffer: 对终端模拟器中的字符输入，进行输入缓冲，直到按 <code>\r</code> （回车符），应用程序才能读到（终端诞生的时代计算机性能羸弱，如果每个字符都直接传递给应用程序，会造成性能问题。当然，现代的应用程序也可以配置这个行规程的默认行为，让行规程立即将字符发送给应用程序）。</li>
<li>line edit: 根据终端模拟器输入的一些特殊字符对行缓冲中的字符序列进行编辑，如退格等。</li>
<li>echo: 回显。在<a href="/posts/terminal-detail-1-device/#终端输入-api">终端输入 API</a>小节中，在终端中输入的内容，终端中并没有显示，而这依赖回显功能实现。即行规程从终端接收到一个可打印字符后，会立即原样发送回终端。</li>
<li>job control: 作业控制，将一些快捷键转换为信号发送给应用程序（如 ctrl+c 等）。</li>
</ul>

<p>下面使用一个 Go 程序验证行规程的行为（源码详见 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/tree/master/experiment/03-pty-demo">github</a>）。</p>

<p>首先，准备一个模拟的应用程序，该程序只做两件事：</p>

<ul>
<li>接收 SIGINT (2) 信号，接收到后，打印日志并退出。</li>
<li>读取 stdin，并将 stdin 转换为字符串，然后用 JSON 格式化一下并打印。</li>
</ul>

<p><code>experiment/03-pty-demo/02-echo-stdin-json-str/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/signal&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 处理 ctrl+c 信号
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ctrlCCh</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">ctrlCCh</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Interrupt</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">ctrlCCh</span>
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#e6db74">&#34;[echo-stdin-json-str][signal]: SIGINT (2)\n&#34;</span>))
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">0</span>)
	}()

	<span style="color:#75715e">// 读并打印 stdin 内容
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">buf</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">byte</span>, <span style="color:#ae81ff">4</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1024</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1024</span>)
	<span style="color:#66d9ef">for</span> {
		<span style="color:#a6e22e">n</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>.<span style="color:#a6e22e">Read</span>(<span style="color:#a6e22e">buf</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">EOF</span> {
				<span style="color:#66d9ef">break</span>
			}
			panic(<span style="color:#a6e22e">err</span>)
		}

		<span style="color:#a6e22e">input</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">buf</span>[:<span style="color:#a6e22e">n</span>])
		<span style="color:#a6e22e">inputJsonStr</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">input</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#e6db74">&#34;[echo-stdin-json-str][stdin]: &#34;</span>))
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>.<span style="color:#a6e22e">Write</span>(<span style="color:#a6e22e">inputJsonStr</span>)
		<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#e6db74">&#34;\n&#34;</span>))
	}

}</code></pre></div>
<p>然后，使用实现一个验证程序，该程序：</p>

<ul>
<li>启动上面的模拟应用程序，并将这个应用程序连接到 pty slave。</li>
<li>从 pty master 读取内容，然后原样打印。</li>
<li>然后发送一些 PTY 序列，观察输出，观测 pty 行规程的应为。</li>
</ul>

<p><code>experiment/03-pty-demo/01-pty-host/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;path/filepath&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/creack/pty&#34;</span>
)

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ASNIEInputSeqDemo</span> = <span style="color:#e6db74">&#34;hello world\r&#34;</span> <span style="color:#f92672">+</span> <span style="color:#75715e">// 第一行: 常规的 ascii 字符，应用程序原样接受
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;中文\r&#34;</span> <span style="color:#f92672">+</span> <span style="color:#75715e">// 第二行：中文字符，行为和第一行一样，应用程序原样接受
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;  对于可打印字符(中英文)\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;    1.在应用程序接受之前已经打印了，这是行规程的回显功能\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;    2.行规程原样透传到应用程序\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;    3.行规程将 \\r 转换为 \\n 传递给应用程序\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;    4.行规程有一个行 buffer 遇到 \\r 才会将 buffer 的内容传递给应用程序\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;测试行编辑(按退格的效果\\u007f): hello world,\u007f!\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;  可以看出，\\u007f 删除了前面的逗号, 应用程序接受到的是 hello world!\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;测试行编辑(按方向键效果): world\u001b[D\u001b[D\u001b[D\u001b[D\u001b[Dhello \r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;  可以看出，方向键不会影响行规程的行编辑\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;* 即将发送 ctrl+c 信号，应用程序将收到 SIGINT(2) 信号\r&#34;</span> <span style="color:#f92672">+</span>
	<span style="color:#e6db74">&#34;\u0003&#34;</span> <span style="color:#75715e">// 最后一行：ctrl+c 信号
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">binPath</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Abs</span>(<span style="color:#e6db74">&#34;./echo-stdin-json-str&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">binPath</span>)

	<span style="color:#a6e22e">ptyMaster</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pty</span>.<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">cmd</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ptyMaster</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>, <span style="color:#a6e22e">ptyMaster</span>)
	}()
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">b</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> []byte(<span style="color:#a6e22e">ASNIEInputSeqDemo</span>) {
		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">ptyMaster</span>.<span style="color:#a6e22e">Write</span>([]<span style="color:#66d9ef">byte</span>{<span style="color:#a6e22e">b</span>})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>)
	}
}</code></pre></div>
<p>运行测试：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd experiment/03-pty-demo
go build -o echo-stdin-json-str ./02-echo-stdin-json-str
go run ./01-pty-host</code></pre></div>
<p>输出如下（Mac 环境输出，Linux 应该也类似）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">hello world
[echo-stdin-json-str][stdin]: &#34;hello world\n&#34;
中文
[echo-stdin-json-str][stdin]: &#34;中文\n&#34;
  对于可打印字符(中英文)
[echo-stdin-json-str][stdin]: &#34;  对于可打印字符(中英文)\n&#34;
    1.在应用程序接受之前已经打印了，这是行规程的回显功能
[echo-stdin-json-str][stdin]: &#34;    1.在应用程序接受之前已经打印了，这是行规程的回显功能\n&#34;
    2.行规程原样透传到应用程序
[echo-stdin-json-str][stdin]: &#34;    2.行规程原样透传到应用程序\n&#34;
    3.行规程将 \r 转换为 \n 传递给应用程序
[echo-stdin-json-str][stdin]: &#34;    3.行规程将 \\r 转换为 \\n 传递给应用程序\n&#34;
    4.行规程有一个行 buffer 遇到 \r 才会将 buffer 的内容传递给应用程序
[echo-stdin-json-str][stdin]: &#34;    4.行规程有一个行 buffer 遇到 \\r 才会将 buffer 的内容传递给应用程序\n&#34;
测试行编辑(按退格的效果\u007f): hello world!
[echo-stdin-json-str][stdin]: &#34;测试行编辑(按退格的效果\\u007f): hello world!\n&#34;
  可以看出，\u007f 删除了前面的逗号, 应用程序接受到的是 hello world!
[echo-stdin-json-str][stdin]: &#34;  可以看出，\\u007f 删除了前面的逗号, 应用程序接受到的是 hello world!\n&#34;
测试行编辑(按方向键效果): world^[[D^[[D^[[D^[[D^[[Dhello 
[echo-stdin-json-str][stdin]: &#34;测试行编辑(按方向键效果): world\u001b[D\u001b[D\u001b[D\u001b[D\u001b[Dhello \n&#34;
  可以看出，方向键不会影响行规程的行编辑
[echo-stdin-json-str][stdin]: &#34;  可以看出，方向键不会影响行规程的行编辑\n&#34;
* 即将发送 ctrl+c ，应用程序将收到 SIGINT(2) 信号
[echo-stdin-json-str][stdin]: &#34;* 即将发送 ctrl+c ，应用程序将收到 SIGINT(2) 信号\n&#34;
^C[echo-stdin-json-str][signal]: SIGINT (2)</pre></div>
<p>上文使用了 Go 的 github.com/creack/pty 库实现了 pty 的创建。该库实现了对各种操作系统 pty 创建过程的封装，屏蔽了各个操作系统的差异和创建和使用过程。</p>

<h2 id="创建-pty">创建 PTY</h2>

<p>为了更好的了解 pty 的创建和使用过程，下面将使用 C 语言重新实现一遍能在 Linux 环境运行的上述 <code>pty-host</code>。</p>

<p><code>experiment/03-pty-demo/03-pty-host-linux-c/main.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">// #define _GNU_SOURCE
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;       // printf, perror, snprintf</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;      // exit</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;      // strlen</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;      // fork, close, read, write, dup2, execl, setsid</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;fcntl.h&gt;       // open, O_RDWR, O_NOCTTY</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;    // waitpid</span><span style="color:#75715e">
</span><span style="color:#75715e"></span><span style="color:#75715e">// #include &lt;pty.h&gt;         // openpty, unlockpt, ptsname_r (POSIX PTY API)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;      // kill, SIGTERM</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;time.h&gt;        // nanosleep, timespec</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/ioctl.h&gt;   // ioctl, TIOCSPTLCK, TIOCGPTN, TIOCSCTTY</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 定义输入序列，与Go版本保持一致
</span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> PTY_INPUT_SEQ_DEMO <span style="color:#f92672">=</span> 
    <span style="color:#e6db74">&#34;hello world</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>  <span style="color:#75715e">// 第一行: 常规的 ascii 字符，应用程序原样接受
</span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#34;中文</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>        <span style="color:#75715e">// 第二行：中文字符，行为和第一行一样，应用程序原样接受
</span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#34;  对于可打印字符(中英文)</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;    1.在应用程序接受之前已经打印了，这是行规程的回显功能</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;    2.行规程原样透传到应用程序</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;    3.行规程将 </span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">r 转换为 </span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">n 传递给应用程序</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;    4.行规程有一个行 buffer 遇到 </span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">r 才会将 buffer 的内容传递给应用程序</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;测试行编辑(按退格的效果</span><span style="color:#ae81ff">\x7f</span><span style="color:#e6db74">): hello world,</span><span style="color:#ae81ff">\x7f</span><span style="color:#e6db74">!</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;  可以看出，</span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">x7f 删除了前面的逗号, 应用程序接受到的是 hello world!</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;测试行编辑(按方向键效果): world</span><span style="color:#ae81ff">\x1b</span><span style="color:#e6db74">[D</span><span style="color:#ae81ff">\x1b</span><span style="color:#e6db74">[D</span><span style="color:#ae81ff">\x1b</span><span style="color:#e6db74">[D</span><span style="color:#ae81ff">\x1b</span><span style="color:#e6db74">[D</span><span style="color:#ae81ff">\x1b</span><span style="color:#e6db74">[Dhello </span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;  可以看出，方向键不会影响行规程的行编辑</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;* 即将发送 ctrl+c 信号，应用程序将收到 SIGINT(2) 信号</span><span style="color:#ae81ff">\r</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\x03</span><span style="color:#e6db74">&#34;</span>;  <span style="color:#75715e">// 最后一行：ctrl+c 信号
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">sleep_ms</span>(<span style="color:#66d9ef">int</span> ms) {
    <span style="color:#66d9ef">struct</span> timespec ts;
    ts.tv_sec <span style="color:#f92672">=</span> ms <span style="color:#f92672">/</span> <span style="color:#ae81ff">1000</span>;
    ts.tv_nsec <span style="color:#f92672">=</span> (ms <span style="color:#f92672">%</span> <span style="color:#ae81ff">1000</span>) <span style="color:#f92672">*</span> <span style="color:#ae81ff">1000000</span>;
    nanosleep(<span style="color:#f92672">&amp;</span>ts, NULL);
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#66d9ef">int</span> master_fd, slave_fd;
    pid_t pid;
    <span style="color:#66d9ef">char</span> slave_name[<span style="color:#ae81ff">256</span>];
    
    <span style="color:#75715e">// 如下是 POSIX PTY API 实现
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// if (openpty(&amp;master_fd, &amp;slave_fd, slave_name, NULL, NULL) == -1) {
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     perror(&#34;openpty failed&#34;);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     exit(1);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// }
</span><span style="color:#75715e"></span>    
    <span style="color:#75715e">// 如下是 Linux 原生方式创建PTY
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 打开 master 端
</span><span style="color:#75715e"></span>    master_fd <span style="color:#f92672">=</span> open(<span style="color:#e6db74">&#34;/dev/ptmx&#34;</span>, O_RDWR <span style="color:#f92672">|</span> O_NOCTTY);
    <span style="color:#66d9ef">if</span> (master_fd <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        perror(<span style="color:#e6db74">&#34;open /dev/ptmx failed&#34;</span>);
        exit(<span style="color:#ae81ff">1</span>);
    }
    
    <span style="color:#75715e">// 解锁 slave 端
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// if (unlockpt(master_fd) == -1) {
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     perror(&#34;unlockpt failed&#34;);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     close(master_fd);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     exit(1);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// }
</span><span style="color:#75715e"></span>    
    <span style="color:#75715e">// 使用 ioctl 解锁 slave 端
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int</span> unlock <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">if</span> (ioctl(master_fd, TIOCSPTLCK, <span style="color:#f92672">&amp;</span>unlock) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        perror(<span style="color:#e6db74">&#34;ioctl TIOCSPTLCK failed&#34;</span>);
        close(master_fd);
        exit(<span style="color:#ae81ff">1</span>);
    }

    <span style="color:#75715e">// 获取 slave 端名称
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// if (ptsname_r(master_fd, slave_name, sizeof(slave_name)) != 0) {
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     perror(&#34;ptsname_r failed&#34;);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     close(master_fd);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//     exit(1);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// }
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">// 使用ioctl获取PTY编号并构造slave名称
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">int</span> pty_num;
    <span style="color:#66d9ef">if</span> (ioctl(master_fd, TIOCGPTN, <span style="color:#f92672">&amp;</span>pty_num) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        perror(<span style="color:#e6db74">&#34;ioctl TIOCGPTN failed&#34;</span>);
        close(master_fd);
        exit(<span style="color:#ae81ff">1</span>);
    }
    snprintf(slave_name, <span style="color:#66d9ef">sizeof</span>(slave_name), <span style="color:#e6db74">&#34;/dev/pts/%u&#34;</span>, pty_num);

    printf(<span style="color:#e6db74">&#34;pty slave path is: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, slave_name);

    <span style="color:#75715e">// 4. 打开slave端
</span><span style="color:#75715e"></span>    slave_fd <span style="color:#f92672">=</span> open(slave_name, O_RDWR <span style="color:#f92672">|</span> O_NOCTTY);
    <span style="color:#66d9ef">if</span> (slave_fd <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        perror(<span style="color:#e6db74">&#34;open slave failed&#34;</span>);
        close(master_fd);
        exit(<span style="color:#ae81ff">1</span>);
    }
    
    printf(<span style="color:#e6db74">&#34;PTY slave file path: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, slave_name);
    
    <span style="color:#75715e">// Fork子进程
</span><span style="color:#75715e"></span>    pid <span style="color:#f92672">=</span> fork();
    <span style="color:#66d9ef">if</span> (pid <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        perror(<span style="color:#e6db74">&#34;fork failed&#34;</span>);
        exit(<span style="color:#ae81ff">1</span>);
    }
    
    <span style="color:#66d9ef">if</span> (pid <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
        <span style="color:#75715e">// 子进程：设置为slave端并执行echo-stdin-json-str
</span><span style="color:#75715e"></span>        close(master_fd);
        
        <span style="color:#75715e">// 创建新会话
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (setsid() <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
            perror(<span style="color:#e6db74">&#34;setsid failed&#34;</span>);
            exit(<span style="color:#ae81ff">1</span>);
        }
        
        <span style="color:#75715e">// 使用ioctl设置slave_fd为控制终端
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (ioctl(slave_fd, TIOCSCTTY, <span style="color:#ae81ff">0</span>) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
            perror(<span style="color:#e6db74">&#34;ioctl TIOCSCTTY failed&#34;</span>);
            exit(<span style="color:#ae81ff">1</span>);
        }

        <span style="color:#75715e">// 重定向标准输入输出到slave
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (dup2(slave_fd, STDIN_FILENO) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">||</span>
            dup2(slave_fd, STDOUT_FILENO) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">||</span>
            dup2(slave_fd, STDERR_FILENO) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
            perror(<span style="color:#e6db74">&#34;dup2 failed&#34;</span>);
            exit(<span style="color:#ae81ff">1</span>);
        }
        
        close(slave_fd);
        
        <span style="color:#75715e">// 执行echo-stdin-json-str程序
</span><span style="color:#75715e"></span>        execl(<span style="color:#e6db74">&#34;./echo-stdin-json-str&#34;</span>, NULL);
        perror(<span style="color:#e6db74">&#34;execl failed&#34;</span>);
        exit(<span style="color:#ae81ff">1</span>);
    } <span style="color:#66d9ef">else</span> {
        <span style="color:#75715e">// 父进程：作为 master 端
</span><span style="color:#75715e"></span>        close(slave_fd);
        
        <span style="color:#75715e">// 创建子进程来读取 PTY 输出并打印到 stdout
</span><span style="color:#75715e"></span>        pid_t reader_pid <span style="color:#f92672">=</span> fork();
        <span style="color:#66d9ef">if</span> (reader_pid <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
            <span style="color:#75715e">// 读取子进程
</span><span style="color:#75715e"></span>            <span style="color:#66d9ef">char</span> buffer[<span style="color:#ae81ff">1024</span>];
            ssize_t bytes_read;
            
            <span style="color:#66d9ef">while</span> ((bytes_read <span style="color:#f92672">=</span> read(master_fd, buffer, <span style="color:#66d9ef">sizeof</span>(buffer))) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) {
                write(STDOUT_FILENO, buffer, bytes_read);
            }
            exit(<span style="color:#ae81ff">0</span>);
        }
        
        <span style="color:#75715e">// 发送输入序列
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> input <span style="color:#f92672">=</span> PTY_INPUT_SEQ_DEMO;
        size_t len <span style="color:#f92672">=</span> strlen(input);
        
        <span style="color:#66d9ef">for</span> (size_t i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> len; i<span style="color:#f92672">++</span>) {
            <span style="color:#66d9ef">if</span> (write(master_fd, <span style="color:#f92672">&amp;</span>input[i], <span style="color:#ae81ff">1</span>) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
                perror(<span style="color:#e6db74">&#34;write to master failed&#34;</span>);
                <span style="color:#66d9ef">break</span>;
            }
            sleep_ms(<span style="color:#ae81ff">10</span>);  <span style="color:#75715e">// 10毫秒延迟，与Go版本一致
</span><span style="color:#75715e"></span>        }
        
        <span style="color:#75715e">// 等待子进程结束
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">int</span> status;
        waitpid(pid, <span style="color:#f92672">&amp;</span>status, <span style="color:#ae81ff">0</span>);
        
        <span style="color:#75715e">// 终止读取进程
</span><span style="color:#75715e"></span>        kill(reader_pid, SIGTERM);
        waitpid(reader_pid, NULL, <span style="color:#ae81ff">0</span>);
        
        close(master_fd);
    }
    
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>运行测试：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd experiment/03-pty-demo
go build -o echo-stdin-json-str ./02-echo-stdin-json-str
go run ./01-pty-host</code></pre></div>
<p>输出略，和上述 Go 版本一致。</p>

<p>在 C 语言中版本示例中，展示了创建 PTY 的更多细节：</p>

<ul>
<li>pty-host 进程：

<ul>
<li>使用 open 系统调用，打开 <code>/dev/ptmx</code> 文件，获取到 pty master 的文件描述符。即，创建了一个 pty master，同步 pty slave 文件也在 <code>/dev/pts/xxx</code> 创建好了。</li>
<li>使用 ioctl 通过 TIOCSPTLCK 操作 pty master 文件描述符吗，对其关联的 pty slave 进行解锁（关于 pty 锁定机制，详见下文）。</li>
<li>使用 ioctl 通过 TIOCGPTN 获取 pty slave 的编号，即可获取到 pty slave 文件的路径。</li>
<li>通过 open 系统调用，打开 <code>/dev/pts/xxx</code> 文件，获取到 pty slave 的文件描述符。</li>
<li>fork 子进程 echo-stdin-json-str（见下文 echo-stdin-json-str 子进程）。</li>
<li>再 fork 一个子进程，读取 pty master，并打印到 stdout 中，观察效果（和 go 逻辑相同）。</li>
<li>将测试的 PTY_INPUT_SEQ_DEMO 写入 pty master（和 go 逻辑相同）。</li>
</ul></li>
<li>echo-stdin-json-str 子进程：

<ul>
<li>关闭无需使用的 pty master 文件描述符 。</li>
<li>通过 setsid 创建一个新会话（同步创建一个进程组）。</li>
<li>通过 ioctl 将这个 pty (pty slave) 设置为这个会话的控制终端，同时这个进程将拥有这个控制终端。设置为控制终端后，这个终端接收到例如 <code>ctrl+c</code> 的 ANSI escape 序列后，pty 行规程才会将对应的信号发送给当前进程（准确的说是拥有控制终端的进程组）（关于控制终端相关，详见后续文章）。</li>
<li>设置当前进程的 stdin、stdout、stdout 为这个 pty slave。</li>
<li>加载执行 echo-stdin-json-str 程序。</li>
</ul></li>
</ul>

<p>关于 PTY 锁定机制（来源 Trae AI）：</p>

<blockquote>
<p>PTY 的锁定机制是一个历史遗留的安全特性，主要有以下几个方面的意义：</p>

<ol>
<li><p>权限和所有权设置的安全窗口保护 <a href="https://unix.stackexchange.com/questions/477247/is-pseudo-terminals-unlockpt-tiocsptlck-a-security-feature">1</a>：</p>

<ul>
<li>在早期系统中，从设备（slave device）的权限和所有权设置需要一定时间</li>
<li>锁定机制确保在权限和所有权正确设置之前，其他进程无法访问从设备</li>
</ul></li>

<li><p>权限控制 <a href="https://man7.org/linux/man-pages/man3/grantpt.3.html">2</a>：</p>

<ul>
<li>从设备的用户 ID 被设置为调用进程的真实 UID</li>
<li>从设备的组 ID 被设置为特定值（例如 tty）</li>
<li>从设备的访问模式被设置为 0620（crw&ndash;w&mdash;-）</li>
</ul></li>

<li><p>初始化保护 <a href="https://man7.org/linux/man-pages/man3/unlockpt.3.html">3</a>：</p>

<ul>
<li><code>unlockpt()</code> 必须在打开从设备之前调用</li>
<li>这确保了从设备在完成所有必要的初始化和权限设置之前不会被访问</li>
</ul></li>
</ol>

<p>在现代 Linux 系统中，特别是使用 devpts 文件系统的系统上，这个锁定机制实际上已经变得不那么重要了。因为 devpts 文件系统可以在创建设备时就确保正确的权限和所有权，不存在权限设置的时间窗口问题。但是为了兼容性，这个机制仍然被保留下来。</p>
</blockquote>
]]></description></item><item><title>终端详解（一）终端硬件设备</title><link>https://www.rectcircle.cn/posts/terminal-detail-1-device/</link><pubDate>Sun, 13 Jul 2025 01:44:36 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-detail-1-device/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文源码: <a href="https://github.com/rectcircle/implement-terminal-from-scratch">rectcircle/implement-terminal-from-scratch</a></p>
</blockquote>

<h2 id="终端设备历史">终端设备历史</h2>

<p>详见： <a href="/posts//terminal-history/">探索终端的历史渊源</a></p>

<h2 id="终端-api-简述">终端 API 简述</h2>

<blockquote>
<p><a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI escape code</a></p>
</blockquote>

<p>在支持图形化交互界面的系统上，软件开发人员可以通过标准的图形 API 来绘制图形。这些标准 API 有： OpenGL、DirectX、Vulkan 等等。</p>

<p>那么类比一下，在命令行交互界面的系统上，软件开发人员通过怎么的 API 来“绘制”命令行呢？也有对应的标准 API 吗？</p>

<p>这就要回到历史悠久的 ASCII 码了，ASCII 定义了英文世界的常用的字母和符号。这些字母和符号只占用了 95 个，这些编码被称之为可显示字符（可打印字符/Printable character），软件开发人员天天可以接触到。</p>

<p>ASCII 码一共有 128 个，那其他的 33 个字符，因为已经进入了图形化交互页面，在大学和工作中很少直接结束。这 33 个编码对应的字符，被称为控制字符（Control code），是 “绘制” 命令行界面的关键。</p>

<p>换个角度来看，ASCII（准确的说是，任何兼容/扩展 ASCII 的编码，即 ANSI 编码） 编码自身就是命令行界面的编程语言，而 ASCII 的控制字符就是命令行界面的 API。而符合一定标准的 ASCII (ANSI) 序列就是命令行界面的绘制程序。</p>

<p>这套渲染命令行界面的标准被称为 <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI escape code</a>，和其他计算机标准演进一样：</p>

<ul>
<li>先有需求和落地产品： 市场上各大终端硬件厂商准寻着不同的各自的私有协议（<a href="https://en.wikipedia.org/wiki/VT52">VT52</a>/<a href="https://en.wikipedia.org/wiki/Hazeltine_1500">Hazeltine 1500</a>），软件开发者不得不分别兼容。</li>
<li>标准化机构（<a href="https://en.wikipedia.org/wiki/American_National_Standards_Institute">ANSI</a>）： 定义了 ECMA-48 标准， 1976 年通过，随后这个标准进行了多次更新，目前使用的是 1991 的第五版，这个标准也被其他标准化机构收录，因此 ECMA-48、 ANSI X3.64、 ISO 6429 是一个东西。</li>
<li>业界事实标准： 因为 1978 年发布的 <a href="https://en.wikipedia.org/wiki/VT100">VT100/VT102</a> 等系列大获成功，因为其实现了 ECMA-48 标准，因此有时这个标准也叫 VT100/VT102 。</li>
</ul>

<p>从层次划分上，ANSI 序列对于终端而言更像是 GPU/CPU 的底层机器码。因为命令行界面和 ANSI 序列对人类是非常友好的，因此不需要再进行高层次的抽象。</p>

<p>需要说明的是。在上个世纪，命令行交互界面时代，终端就是一种真实存在的物理设备。终端作为一个集合了输入和输出的物理设备，ANSI escape code 既是终端输出的协议，也是计算机软件读取用户输入的协议。</p>

<p>在现代计算机的交互已经进化到了图形化交互界面，已经不再需要一个真实的物理终端设备存在。但是在计算机软件开发领域，仍然需要命令行交互界面。因此，在现代，开发人员能接触到的终端设备，指的都是一种对终端设备的仿真软件（模拟器），即： 遵守 ANSI escape code 规范，将 ANSI escape code 序列，通过图形化交互 API 进行渲染的仿真软件。开发人员平常用到了各种终端软件都可以归于此类。</p>

<p>这里，介绍一个 Web 领域，最流行的终端 ANSI 序列渲染库 <a href="https://xtermjs.org/">xterm.js</a>，该库被众多 WebShell 所使用，也是 VSCode 终端的底层渲染库。下文，会通过该库探索 ANSI escape code 标准。</p>

<h2 id="终端输出-api">终端输出 API</h2>

<p>本小结将通过 xterm.js 介绍，如何通过 ANSI escape code 控制终端的输出。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">terminalASNIEscapeSeqDemo</span> <span style="color:#f92672">=</span> 
<span style="color:#e6db74">`这是正常的 ASNI 编码的字符串 (UTF8)，在终端中会被原样渲染</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">r</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">n`</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">`在终端里面必须使用\\r(回车)\\n(换行)进行换行操作</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">r</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">n`</span> <span style="color:#f92672">+</span> 
<span style="color:#e6db74">&#34;终端可以对文字进行修饰，此时就需要使用 escape code，如：\x1B[1;3;31m粗体斜体红色前景色\x1B[0m\r\n&#34;</span> <span style="color:#f92672">+</span> 
<span style="color:#e6db74">&#34;    首先 escape code 是 \\x1B (ESC) 字符告诉终端接下来是一个逃逸指令\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    然后 [ 表示这是一个控制序列 (CSI) 后面需要跟随着参数\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    1;3;31 表示 1 是粗体，3 斜体，31 是 31 号颜色红色前景色\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    m 表示参数结束，告诉终端可以进行渲染了\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    后面可以跟随着任意的 UTF8 编码的字符串，会被渲染为红色前景色\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    最后 \\x1B[0m 也是一个 CSI 指令，0 表示重置所有参数\r\n&#34;</span> <span style="color:#f92672">+</span> 
<span style="color:#e6db74">&#34;    总结来说: \\x1B[数字;数字;...m 用来设置如何渲染接下来的文本\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;除了 CSI 指令，还有很多其他指令，如：\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    \\x1bc 清屏指令，实现类似于 clear 的效果\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    发送特殊字符如 \\x1bD (回车) \\x1bE (换行) 等\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;    光标操作：\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;        \\x1B[1A 上移一行\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;        \\x1B[1B 下移一行\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;        \\x1B[1C 右移一列\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;        \\x1B[1D 左移一列 *\x1B[1B\x1B[1C&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;这段文字应该打印在 * 号的右下角\r\n&#34;</span><span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;更多的指令可以参考 https://en.wikipedia.org/wiki/ANSI_escape_code\r\n&#34;</span> <span style="color:#f92672">+</span>
<span style="color:#e6db74">&#34;&#34;</span>;
</code></pre></div>
<p>将如上 ANSI 序列，发送给 xtermjs 的 terminal 渲染效果如下（下方按字渲染，是因为按照字符发送给 terminal，每发送一个字符 sleep 2 毫秒，可以看出流式处理的特点，源码详见 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/blob/master/experiment/01-xterm-js-ansi-escape/src/main.js#L38">github</a>）：</p>

<script>
function loadStyles(url){
    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    document.head.appendChild(link);
}
loadStyles('https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css')
</script>

<script src="
https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js
"></script>

<div id="terminal"></div>

<script>
const terminalASNIEscapeSeqDemo =
`这是正常的 ASNI 编码的字符串 (UTF8)，在终端中会被原样渲染\r\n` +
`在终端里面必须使用\\r(回车)\\n(换行)进行换行操作\r\n` +
"终端可以对文字进行修饰，此时就需要使用 escape code，如：\x1B[1;3;31m粗体斜体红色前景色\x1B[0m\r\n" +
"    首先 escape code 是 \\x1B (ESC) 字符告诉终端接下来是一个逃逸指令\r\n" +
"    然后 [ 表示这是一个控制序列 (CSI) 后面需要跟随着参数\r\n" +
"    1;3;31 表示 1 是粗体，3 斜体，31 是 31 号颜色红色前景色\r\n" +
"    m 表示参数结束，告诉终端可以进行渲染了\r\n" +
"    后面可以跟随着任意的 UTF8 编码的字符串，会被渲染为红色前景色\r\n" +
"    最后 \\x1B[0m 也是一个 CSI 指令，0 表示重置所有参数\r\n" +
"    总结来说: \\x1B[数字;数字;...m 用来设置如何渲染接下来的文本\r\n" +
"除了 CSI 指令，还有很多其他指令，如：\r\n" +
"    \\x1bc 清屏指令，实现类似于 clear 的效果\r\n" +
"    发送特殊字符如 \\x1bD (回车) \\x1bE (换行) 等\r\n" +
"    光标操作：\r\n" +
"        \\x1B[1A 上移一行\r\n" +
"        \\x1B[1B 下移一行\r\n" +
"        \\x1B[1C 右移一列\r\n" +
"        \\x1B[1D 左移一列 *\x1B[1B\x1B[1C" +
"这段文字应该打印在 * 号的右下角\r\n"+
"更多的指令可以参考 https://en.wikipedia.org/wiki/ANSI_escape_code\r\n" +
"";

async function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
    const terminal = new Terminal();
    terminal.open(document.querySelector('#terminal'));
    // 遍历 terminalASNIEscapeSeqDemo
    for (const char of terminalASNIEscapeSeqDemo) {
        terminal.write(char);
        await sleep(2);
    }
}

main();
</script>

<p>从如上示例可以看出，实现一个终端模拟器还是相对比较简单，即：流式的读取 ANSI 序列，如果是可打印字符，按照终端状态中的属性在光标的下一个位置，按照属性表渲染出这个字符，如果是 escape code 则根据 ANSI escape code 标准，读取指令参数，根据指令标准，进行设置属性、移动光标等操作即可。</p>

<h2 id="终端输入-api">终端输入 API</h2>

<p>本小结将介绍，用户在终端中的键盘输入，终端设备如何处理，会生成怎样的 ANSI escape 序列。示例代码如下：</p>

<p><code>index.html</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#75715e">&lt;!doctype html&gt;</span>
&lt;<span style="color:#f92672">html</span> <span style="color:#a6e22e">lang</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;en&#34;</span>&gt;
  &lt;<span style="color:#f92672">head</span>&gt;
    <span style="color:#75715e">&lt;!-- ... --&gt;</span>
  &lt;<span style="color:#f92672">body</span>&gt;
    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;app&#34;</span>&gt;
      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;terminal&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;xterm_js_on_data_container&#34;</span>&gt;
        &lt;<span style="color:#f92672">pre</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;xterm_js_on_data_pre&#34;</span>&gt;&lt;<span style="color:#f92672">code</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;xterm_js_on_data_code&#34;</span>&gt;&lt;/<span style="color:#f92672">code</span>&gt;&lt;/<span style="color:#f92672">pre</span>&gt;
      &lt;/<span style="color:#f92672">div</span>&gt;
    &lt;/<span style="color:#f92672">div</span>&gt;
    &lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;module&#34;</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/src/main.js&#34;</span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
  &lt;/<span style="color:#f92672">body</span>&gt;
&lt;/<span style="color:#f92672">html</span>&gt;</code></pre></div>
<p><code>src/main.js</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#39;./style.css&#39;</span>
<span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#39;../node_modules/@xterm/xterm/css/xterm.css&#39;</span>
<span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Terminal</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@xterm/xterm&#39;</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">terminalASNIEscapeSeqDemo</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`随意按键盘观察 xterm.js onData 的输出: `</span>;

<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#a6e22e">ms</span>) {
  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Promise(<span style="color:#a6e22e">resolve</span> =&gt; <span style="color:#a6e22e">setTimeout</span>(<span style="color:#a6e22e">resolve</span>, <span style="color:#a6e22e">ms</span>));
}

<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>() {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">terminal</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Terminal</span>();
  <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">open</span>(document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;#terminal&#39;</span>));
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">xtermJsOnDataCode</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;#xterm_js_on_data_code&#39;</span>);

  <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">onData</span>((<span style="color:#a6e22e">data</span>) =&gt; {
    <span style="color:#a6e22e">xtermJsOnDataCode</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">data</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; &#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">charCodeAt</span>(<span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;\n&#34;</span>;
  });

  <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">terminalASNIEscapeSeqDemo</span>) {
    <span style="color:#a6e22e">terminal</span>.<span style="color:#a6e22e">write</span>(<span style="color:#66d9ef">char</span>);
    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">100</span>);
  }

}

<span style="color:#a6e22e">main</span>();
</code></pre></div>
<p>（源码详见 <a href="https://github.com/rectcircle/implement-terminal-from-scratch/tree/master/experiment/02-xterm-js-on-data/src/main.js">github</a>）</p>

<p>在 xterm.js 中，用户在终端中的输入，通过 <code>terminal.onData</code> API 可以获取到，本例中，会将终端用 json 格式化一下（转义一下控制字符，方便观察），然后展示到终端下方页面中。</p>

<p>点击如下终端，获取输入焦点，按键盘任意键即可观察，终端输入 ANSI escape code 协议情况。</p>

<div id="terminal2"></div>
<div id="xterm_js_on_data_container">
<pre id="xterm_js_on_data_pre"><code id="xterm_js_on_data_code"></code></pre>
</div>

<script>
(function(){
    const terminalASNIEscapeSeqDemo = `随意按键盘观察 xterm.js onData 的输出: `;

    async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function main() {
    const terminal = new Terminal();
    terminal.open(document.querySelector('#terminal2'));
    const xtermJsOnDataCode = document.querySelector('#xterm_js_on_data_code');

    terminal.onData((data) => {
        xtermJsOnDataCode.textContent += JSON.stringify(data) + " " + data.charCodeAt(0) + "\n";
    });

    for (const char of terminalASNIEscapeSeqDemo) {
        terminal.write(char);
        await sleep(100);
    }

    }

    main();
})()
</script>

<p>这里介绍一些常见的键盘字符对应的 ANSI escape code（可以自行在上方验证）：</p>

<ul>
<li>可打印字符: 保持原样（英文、中文等均是）。</li>
<li>常见的不可打印字符:

<ul>
<li>ESC 键： <code>&quot;\u001b&quot;</code> （escape code 自身，这是是 json 的 unicode 格式，即上文的 <code>\x1B</code>）</li>
<li>退格键：<code>&quot;\u007f&quot;</code> （由于 js 问题这个转义字符打印不出来，但是从编码可以看出来是这个字符）</li>
<li>方向键：

<ul>
<li>上： <code>&quot;\u001b[A&quot;</code></li>
<li>下： <code>&quot;\u001b[B&quot;</code></li>
<li>右： <code>&quot;\u001b[C&quot;</code></li>
<li>左： <code>&quot;\u001b[D&quot;</code></li>
</ul></li>
<li>F 功能键，F1~F12，分别是: <code>&quot;\u001bOP&quot;, &quot;\u001bOQ&quot;, &quot;\u001bOR&quot;, &quot;\u001bOS&quot;, &quot;\u001b[15~&quot;, &quot;\u001b[17~&quot;, &quot;\u001b[18~&quot;, &quot;\u001b[19~&quot;, &quot;\u001b[20~&quot;, &quot;\u001b[21~&quot;, &quot;\u001b[23~&quot;, &quot;\u001b[24~&quot;</code>。</li>
<li>常见快捷键：

<ul>
<li><code>ctrl+a</code> 行首 (bash)： <code>&quot;\u0001&quot;</code></li>
<li><code>ctrl+b</code> 上一个字符 (bash)： <code>&quot;\u0002&quot;</code></li>
<li><code>ctrl+c</code> 中断运行中程序 (bash)： <code>&quot;\u0003&quot;</code></li>
<li><code>ctrl+d</code> 退出交互式命令自身 (python, node， bash)： <code>&quot;\u0004&quot;</code></li>
<li><code>ctrl+e</code> 行尾 (bash)： <code>&quot;\u0005&quot;</code></li>
<li><code>...</code></li>
</ul></li>
</ul></li>
</ul>

<p>此外，现代的终端设备也支持鼠标，但是由于鼠标是面向图形化交互界面开发的影响，在终端设备中使用较少。本章节将不多介绍。</p>
]]></description></item><item><title>和 Trae AI 协作一周内开发实用 IDE 插件</title><link>https://www.rectcircle.cn/posts/trae-ai-coding-practice-string-converter/</link><pubDate>Fri, 30 May 2025 18:12:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/trae-ai-coding-practice-string-converter/</guid><description type="html"><![CDATA[

<h2 id="showcase">Showcase</h2>

<blockquote>
<p><a href="https://github.com/rectcircle/string-converter-vsc-ext">源码</a></p>
</blockquote>

<p>Hover 在代码字符串上，自动识别解析转换光标位置字符串。</p>

<table>
<thead>
<tr>
<th>功能</th>
<th>示例</th>
</tr>
</thead>

<tbody>
<tr>
<td>字符串去除转义</td>
<td><img src="/image/str-conv-lang-literal.png" alt="str-conv-lang-literal.png" /></td>
</tr>

<tr>
<td>符号命名风格一键转换</td>
<td><img src="/image/str-conv-symbol-style-rename.gif" alt="str-conv-symbol-style-rename.gif" /></td>
</tr>

<tr>
<td>JWT 识别和解析</td>
<td><img src="/image/str-conv-parse-jwt.png" alt="str-conv-parse-jwt.png" /></td>
</tr>

<tr>
<td>时间戳转换</td>
<td><img src="/image/str-conv-parse-timestamp.png" alt="str-conv-parse-timestamp.png" /></td>
</tr>

<tr>
<td>URL 解码</td>
<td><img src="/image/str-conv-parse-url.png" alt="str-conv-parse-url.png" /></td>
</tr>

<tr>
<td>Base64 解码</td>
<td><img src="/image/str-conv-parse-base64-string.png" alt="str-conv-parse-base64-string.png" /> <img src="/image/str-conv-parse-base64-binary.png" alt="str-conv-parse-base64-binary.png" /></td>
</tr>

<tr>
<td>JSON 格式化</td>
<td><img src="/image/str-conv-json-format.png" alt="str-conv-json-format.png" /></td>
</tr>
</tbody>
</table>

<p>可通过如下链接安装体验：</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Rectcircle.str-conv">VSCode 官方市场</a></li>
<li><a href="https://open-vsx.org/extension/Rectcircle/str-conv">Openvsx 社区市场</a></li>
</ul>

<h2 id="缘由">缘由</h2>

<p>最近在开发过程中，经常需要查看 JSON 格式的日志内容，其中日志内容字段是个 JSON 字符串，换行制表符都被转义成 <code>\n</code>、<code>\r</code> 等，肉眼很难观察。</p>

<p>我尝试了一些解决办法，如在线网站、node/python REPL，体验都不好：在线网站还有各种广告，需要跳出 IDE，打断思路。都需要很多步操作，来回复制，很是繁琐。</p>

<p>既然问题存在，且没有看到解决办法。我是否可以写一个工具解决这个问题呢？</p>

<p>作为一个软件开发者，答案当然是可以的，而且这个工具非常适合以 IDE 插件的方式和编码过程深度集成。</p>

<p>有了想法相当于已经完成了 50%，剩下差的就只是将想法落地的时间。当时正好临近五一假期，有了空闲时间。</p>

<p>再加上 Trae AI 的提效（从 5.30 ~ 5.5 总共六天断断续续，算人力最多 3 人天），这个 VSCode 插件得第一个版本在五一假期结束前顺利上线。</p>

<p>本文，将重点介绍如何使用 Trae AI （国内版） 配合 DeepSeek-V3-0324 模型，为真实项目进行提效。</p>

<h2 id="设计">设计</h2>

<p>既然要做，当然不能做只一个玩具项目，要做一个通用的架子，可以满足各种类似需求。回忆这些年来开发过程，类似的痛点还有：</p>

<ul>
<li>时间戳格式化成人类能看懂的格式。</li>
<li>解析 JWT 里面的 json 内容。</li>
<li>URL Query 串中类似于 <code>%xx</code> 格式的URL 编码的内容进行解码。</li>
<li>不同语言/项目有不同的命名风格，很容易一不小心写错了一个变量/函数名的风格，想一键修正。</li>
<li>&hellip;</li>
</ul>

<p>对这些需求抽象，本质上这个插件要做的就是要：识别字符串的类型，将其转换为另一种格式字符串。</p>

<p>既然选择了 IDE 插件形式，那么则需要和 IDE 深度集成，最好的体验是这个能力无感的。</p>

<p>因此，该插件要做的是：</p>

<ul>
<li>用户行为触发 IDE 事件，IDE 插件监听指定事件。</li>
<li>IDE 根据事件信息，获取到原始字符串。然后识别字符串的类型，将其转换为另一种格式字符串。</li>
<li>通过 IDE 的能力将转换后的字符串以合适的方式展示出来，并支持后续的操作。</li>
</ul>

<p>根据如上需求分析，可以看出，该插件可以用 MVC 架构抽象：</p>

<ul>
<li>用户触发 IDE 事件：Controller 层。</li>
<li>字符串转换： Service/Model 层。</li>
<li>结果展示： View 层。</li>
</ul>

<p>先看 IDE 事件的选择，VSCode 系 IDE 为插件提供了很多扩展点（IDE 事件），适合这个需求的有如下两个：</p>

<ul>
<li>HoverProvider: 用户鼠标停留在编辑器代码的位置，将触发该事件，事件中包含文档路径和行列号。回调函数需要返回一个 Markdown 文档，IDE 会把这个 Markdown 文档以浮窗的方式渲染在鼠标附近。</li>
<li>CodeActionProvider: 用户切换了输入光标或选择停留后，触发该事件，事件中包含文档路径和光标或选择起始点的行列号。回调函数需要返回一个操作列表，即待调用的 VSCode 命令和参数列表。IDE 会展示一个小灯泡。用户点击小灯泡（<code>cmd+.</code>）后将展示这个操作列表。</li>
</ul>

<p>再看业务逻辑的抽象。</p>

<p>从上文可以看出 IDE 提供的事件是文档路径和光标或选择起始点的行列号，因此首先需要根据文档路径和行列号信息提取对应位置的完整字符串。有两个要求：</p>

<ul>
<li>在只提供光标位置的情况下，需要准确确认文本边界。</li>
<li>需要获取当前光标位置文本在改编程语言中是什么类型，如果是字符串字面量，还需要处理转义字符串。</li>
</ul>

<p>因此流程上分为两步：</p>

<ul>
<li>利用词法分析器，获取光标位置的文本内容和类型。</li>

<li><p>定义一个接口，每个编程语言均需实现该接口：解析字符串字面量的转义，转化为真正内容。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">StringLiteralParseResult</span> {
    <span style="color:#a6e22e">text</span>: <span style="color:#66d9ef">string</span>;
    <span style="color:#a6e22e">startMarker?</span>: <span style="color:#66d9ef">string</span>,
    <span style="color:#a6e22e">endMarker?</span>: <span style="color:#66d9ef">string</span>,
}

<span style="color:#66d9ef">export</span> <span style="color:#a6e22e">type</span> <span style="color:#a6e22e">StringLiteralParser</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">originText</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">type</span>: <span style="color:#66d9ef">string</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">StringLiteralParseResult</span>;</code></pre></div></li>
</ul>

<p>上面已经获取到了文本内容，后面则需要对字符串的格式进行检测和转换。这个行为可以抽象为 <code>StringConverter</code> 接口，包含如下两个函数：</p>

<ul>
<li><code>match</code> 探测当前字符串是否是指定格式，返回 true 或 false。</li>
<li><code>convert</code> 当 match 返回 true 时，才会调用此函数，将该字符串转换为指定格式。</li>
</ul>

<p>需要识别和转换的类型都需要实现这个接口，如 jwt、 timestamp 等。</p>

<p>至此，已经获取了字符串的转换结果，后面就是对结果进行展示。可以将结果转换为 Markdown 格式，然后通过，如下方式展示：</p>

<ul>
<li>如果是 Hover 触发的，直接将 Markdown 作为 HoverProvider 的返回值。</li>
<li>如果利用 VSCode 内置 Markdown 插件的预览能力，在编辑器中展示结构。</li>
</ul>

<p>针对返回结果，有一些后续 Action ，如 Copy 到剪切板，触发重命名等等。这里和展示方式有关：</p>

<ul>
<li>Hover 浮窗，可以通过 <code>[$(copy)](command:xxx?参数)</code> 触发插件命令，如写入剪切板。</li>
<li>Markdown 插件的预览，可以通过 Uri handler 机制触发命令，<code>[$(copy)](vscode://插件ID/path?参数)</code> 方式触发插件命令，如写入剪切板。</li>
</ul>

<p>总结一下，整体流程如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">IDE 事件 ----&gt; 词法分析提取 Token 和类型 ----&gt; 解决字符串字面量 ----&gt; match&amp;convert ----&gt; Hover/Markdown
                                            (Go JS ...)       (jwt timestamp)            |
                                                                                         |
                                                              执行后续命令（如 copy）  &lt;----+</pre></div>
<p>如上设计在最初仅有一个大概。这个最终结构是和 Trae AI 一起编码过程中，细节才逐步确定的。</p>

<p>下面将摘选一些和 AI 提效的一些示例讲述。</p>

<h2 id="安装配置-trae">安装配置 Trae</h2>

<p>打开 <a href="https://www.trae.com.cn/">官网</a> ，点击下载 IDE 下载安装，并登录注册。</p>

<p>后续使用 DeepSeek-V3-0324 模型。</p>

<h2 id="项目初始化">项目初始化</h2>

<ol>
<li><p>询问 AI: <code>@Builder 在当前工作空间根目录。使用命令初始化一个 vscode extensions 项目</code>。</p>

<p>AI 模型调用 <code>npm -g</code> 安装 yo、generator-code、并执行 <code>npx --yes yo code</code> 初始化 VSCode 扩展。</p>

<p>在终端里面，选择 Web Extension，填写相关信息，最后选择了 esbuild 作为 bundler。</p>

<p>问题: AI 没有在根目录直接创建项目，而是嵌套了一层。</p>

<p><img src="/image/trae-ai-init-project.png" alt="image" /></p></li>

<li><p>按 F5 调试扩展，报错： “错误: problemMatcher 引用无效: $esbuild-watch”。</p></li>

<li><p>询问 AI: <code>#Web vscode 插件 debug， 错误: problemMatcher 引用无效: $esbuild-watch</code>。</p>

<p>AI 进行了网络搜索，建议安装 <code>connor4312.esbuild-problem-matchers</code> 插件。</p>

<p>在 Trae 插件市场，安装此插件后。重新按 F5 启动调试，即可正常拉起 Trae 来调试扩展程序。</p>

<p>输入 cmd+shift+p 输入 &ldquo;&gt;hello world&rdquo; 按回车，即可弹出通知。</p>

<p><img src="/image/trae-ai-init-web-search.png" alt="image" /></p></li>

<li><p>提交项目，打开 Trae 源代码管理， AI 可自动生成提交消息。</p>

<p><img src="/image/trae-ai-init-commit-msg.png" alt="image" /></p></li>
</ol>

<h2 id="代码-token-提取技术调研">代码 Token 提取技术调研</h2>

<h3 id="尝试使用-vscode-原生能力">尝试使用 VSCode 原生能力</h3>

<p>经过和 AI 多轮对话，获取到了如下几个可能 API，经过实验都无法满足需求:</p>

<ul>
<li><code>vscode.provideDocumentSemanticTokens</code> 命令: 语义化 token 需要 LSP 支持，且并不是所有语言都支持的。</li>
<li><code>vscode.executeDocumentHighlights</code> 这个命令获取到的是，<code>菜单-&gt;选择-&gt;选择所有匹配项</code> 那些字符串的 range 列表，并不全。</li>
<li><code>editor.action.smartSelect.expand</code> 这个命令将会从光标位置开始智能扩选，一般比较准确，但是这个命令会操作用户的编辑器，没法后台调用。</li>
</ul>

<h3 id="调研使用开源语法高亮库">调研使用开源语法高亮库</h3>

<p>如下是使用 AI 进行技术调研的过程：</p>

<ul>
<li><p>询问 AI: <code>#Web 帮我调研业界主流的前端语法高亮库，我想使用其将代码文本转换 token 序列或语法树，并且其中带有偏移量或者行号信息。尽量选取主流的支持语言多的库。</code></p>

<p>AI 给了 PrismJS、 Highlight.js、 Monaco Editor、Tree-sitter、Shiki 几种选择。</p>

<p>据我了解 Monaco Editor 就是 VSCode 底层使用的编辑器，在这个场景使用不太合适。</p></li>

<li><p>询问 AI: <code>使用 PrismJS 库如何获取 Token 序列以及偏移量。</code></p>

<p>AI 给出了示例代码，经验证满足需求。</p></li>

<li><p>询问 AI: <code>使用 Highlight.js 库如何获取 Token 序列以及偏移量。</code></p>

<p>AI 给出示例代码，验证满足需求。但是其没有专门的 API，而是在内部变量中。</p></li>

<li><p>询问 AI: <code>#Web 如果我只想用 PrismJS 和 Highlight.js 这两个库，解析 token 序列，请从各个角度分析两者优劣，如 github stars 数，支持语言数目</code>。</p>

<p>AI 只进行了一次网络搜索，给出的结论也不太准确。如： stars 过时了，支持语言数也不准确。</p>

<p><img src="/image/trae-ai-agent-technical-research.png" alt="image" /></p>

<p>结合 AI 建议以及人工搜索，决定使用 PrismJS （性能、包大小、API 使用便捷度、支持语言数）。</p></li>
</ul>

<p>有了方案，在 AI 协助下，很快实现了功能。</p>

<h2 id="解析原始-token-字符串字面量">解析原始 Token 字符串字面量</h2>

<h3 id="实现代码框架和-typescript">实现代码框架和 TypeScript</h3>

<p>上面已经可以获取当前光标位置的 Token 文本和类型了，下面让 AI 实现字符串字面量 Token，在单测生成方案，通过自定义 Rule 可以生成质量很高单测，下面是交互过程：</p>

<ul>
<li>询问 AI: <code>@Builder 设计并实现一套面向不同编程语言的字符串解析机制，输入是 originText、 type、 和 languageId，输出是 text。如果类型不是字符串直接返回 originText。这套机制具有可扩展，每个编程语言都有自己的实现，新增一个编程语言时仅需添加一个文件，并在 index.ts 注册即可。代码在 service 里面新建一个 literalParser 实现包含 index.ts 和 各个编程语言的实现。先实现 typescript/javascript 的解析。</code></li>
<li>询问 AI: <code>@Builder 不要依赖 TokenInfo 类型定义。</code></li>
<li>询问 AI: <code>@Builder 重写： 根据 javascript 字符串、模板字符串语法规范解析字符串，关注性能，不要用查找替换，小心关注各种边界 case。</code></li>
<li>询问 AI: <code>@Builder 给 #file:src/service/literalParser/typescript.ts 在 #file:src/web/test/suite/service/literalParser/typescript.test.ts 中生成单测，所有分支都要覆盖到。</code></li>

<li><p>询问 AI: <code>@Builder 给 #file:sr生成了很好的单测。并测试出了手写的 buc/service/literalParser/index.ts parseLiteral 添加单测</code></p>

<p>AI 并不能很好的理解单测的目录结构，所以生成的单测位置不符合预期，这种场景可以使用 Trae 自定义规则告诉 AI 如何生成单测。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown"><span style="color:#75715e">## 生成单测规则
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">*</span> 该规则仅对 src 目录下的文件生效。
<span style="color:#66d9ef">*</span> 本项目使用了 mocha 单测框架、 assert 断言库。
<span style="color:#66d9ef">*</span> 使用  suite 代替 describe。
<span style="color:#66d9ef">*</span> src/path/to/file.ts 对应的单测文件为 src/web/test/suite/path/to/file.test.ts。
<span style="color:#66d9ef">*</span> 测试文件结构如下：

    ```ts
    suite(&#39;src/path/to/file.ts&#39;, () =&gt; {
        suite(&#39;待测函数...&#39;, () =&gt; {
            test(&#39;测试样例描述...&#39;, () =&gt; {
                assert.equal(1, 1);
            });
        });
    });
    ```</code></pre></div>
<p><img src="/image/trae-ai-agent-custom-rule.png" alt="image" /></p></li>

<li><p>询问 AI: <code>@Builder 修改当前文件所有 Text 的内容为 parseLiteral 函数的返回值。</code></p></li>

<li><p>询问 AI: <code>@Builder 根据 typescript.test.ts 在 testdata/main.ts 中添加更多测试代码</code></p></li>
</ul>

<h3 id="实现更多主流编程语言字符串-token-解析">实现更多主流编程语言字符串 Token 解析</h3>

<ul>
<li><p>以 Rust 为例，各个编程语言的语法都不一样，需要以各个官方语言标准进行解析。因此可以利用 Trae docset 能力让 AI 严格按照语言规范实现。询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 rust.ts 实现 Rust 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<p>AI 基本结构实现正常，但是解析逻辑完全仿写 typescript，没有按照 rust 语法实现。</p>

<p>需要给 AI 更多引导，下载官方手册 <a href="https://github.com/rust-lang-cn/reference-cn/blob/master/src/tokens.md">tokens.md</a>，然后导入到上下文，文档集。</p>

<p><img src="/image/trae-ai-agent-context-local-docset.png" alt="image" /></p>

<p>然后删除之前错误实现，询问 AI: <code>严格按照 @Builder #doc:rust-token 字符串字面量规范，重点关注转义。不要参考其他语言的实现。从光标位置实现解析逻辑</code>。</p>

<p>AI 基本正确实现，人工调整了一下细节。</p>

<p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，提交代码到了 git。</p></li>

<li><p>后续编程语言字符串字面量解析，按照这个提示词 AI 可以很好的实现需求，极大的提效： <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 xxx.ts 实现 Xxx 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<ul>
<li>JSON： <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 json.ts 实现 JSON 解析（直接使用 JSON.parse 实现），然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测，然后在 #folder:testdata 添加对应的人工测试文件。</code></li>
<li>Go： <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 go.ts 实现 Go 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></li>
<li>Java： <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 java.ts 实现 Java 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></li>
<li>Python： <code>@Builder 参考 #file:src/service/literalParser/typescript.ts 风格，根据 Python 语言规范，新建 python.ts 实现 Python 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></li>
</ul></li>
</ul>

<h2 id="可扩展对字符串进行识别和转换的机制">可扩展对字符串进行识别和转换的机制</h2>

<ul>
<li>首先让 AI 生成整体架构，询问 AI： <code>@Builder 定义一个字符串转换器接口，该接口包含如下内容：1. meta 字段，包含当前转换器 id，name，resultLanguageId. 2. match 函数，参数为 tokenInfo 和可选参数 options，返回 boolean。3. convert 函数，参数为 tokenInfo 和可选参数 options，返回字符串。</code></li>
<li>以 JWT 为例，实现第一个转换器，询问 AI： <code>@Builder 在 stringConverter 目录添加一个 jwt StringConverter 的实现。</code>。</li>
<li>后续的转换器的实现按照这个提示词，AI 可以很好的实现需求，极大的提效： <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现 xxx 字符串解析和转换，match 支持 tokenInfo 为 xxx 两种类型。然后并在 #file:src/service/stringConverter.ts 中注册。生成完成后需生成单测。</code>

<ul>
<li>时间戳解析： <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现时间戳（毫秒/秒）解析的支持，match 支持 tokenInfo 为 string 和 number 两种类型。</code></li>
<li>Base64 解析： <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现对 base64 的解析，match 支持 tokenInfo 为 string 类型。</code></li>
<li>JSON 格式化： <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现对 json 格式化的支持，match 支持 tokenInfo 为 string 类型。然后并在 #file:src/service/stringConverter.ts 中注册。生成完成后需生成单测。</code></li>
</ul></li>
</ul>
]]></description></item><item><title>Trae AI 编程实战 —— 开发字符串转换器 （详细记录版本）</title><link>https://www.rectcircle.cn/posts/trae-ai-coding-practice-string-converter-detail/</link><pubDate>Mon, 05 May 2025 23:42:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/trae-ai-coding-practice-string-converter-detail/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<blockquote>
<p>使用 DeepSeek-V3-0324 模型。</p>
</blockquote>

<p>在开发的过程中，我们经常需要将各种字符串格式的数据进行转换：如去除 JSON 转义字符、JSON 格式化，解析 JWT 内容，格式化时间戳等等。</p>

<p>遇到有如上需求的场景，常见的做法，是去各种广告一大堆的在线网站进行解析，非常不便。</p>

<p>针对这个场景，希望可以实现一个 VSCode 扩展，其可以自动探测选中或当前光标所在未知字符串的格式，并提供对该字符串常见操作，并通过 Quick Action (小灯泡) / Hover 供用户选择。</p>

<p>最近国产 AI IDE Trae CN 已经上线，本文将借此项目，对 AI Agent 编程进行深度体验。</p>

<h2 id="安装配置-trae">安装配置 Trae</h2>

<p>打开 <a href="https://www.trae.com.cn/">官网</a> ，点击下载 IDE 下载安装，并登录注册。</p>

<h2 id="初始化项目">初始化项目</h2>

<ol>
<li><p>询问 AI: <code>@Builder 在当前工作空间根目录。使用命令初始化一个 vscode extensions 项目</code>。</p>

<p>AI 模型调用 <code>npm -g</code> 安装 yo、generator-code、并执行 <code>npx --yes yo code</code> 初始化 VSCode 扩展。</p>

<p>在终端里面，选择 Web Extension，填写相关信息，最后选择了 esbuild 作为 bundler。</p>

<p>问题: AI 没有在根目录直接创建项目，而是嵌套了一层。</p>

<p><img src="/image/trae-ai-init-project.png" alt="image" /></p></li>

<li><p>按 F5 调试扩展，报错： “错误: problemMatcher 引用无效: $esbuild-watch”。</p></li>

<li><p>询问 AI: <code>#Web vscode 插件 debug， 错误: problemMatcher 引用无效: $esbuild-watch</code>。</p>

<p>AI 进行了网络搜索，建议安装 <code>connor4312.esbuild-problem-matchers</code> 插件。</p>

<p>在 Trae 插件市场，安装此插件后。重新按 F5 启动调试，即可正常拉起 Trae 来调试扩展程序。</p>

<p>输入 cmd+shift+p 输入 &ldquo;&gt;hello world&rdquo; 按回车，即可弹出通知。</p>

<p><img src="/image/trae-ai-init-web-search.png" alt="image" /></p></li>

<li><p>提交项目，打开 Trae 源代码管理， AI 可自动生成提交消息。</p>

<p><img src="/image/trae-ai-init-commit-msg.png" alt="image" /></p></li>
</ol>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/02a27188546e813dc80b549230d850a257e9991b">chore: 初始化项目基础结构</a>。</p>

<h2 id="最小化可验证-code-action">最小化可验证 Code Action</h2>

<ul>
<li><p>询问 AI: <code>@Builder 这个扩展将在 code action （VSCode 的小灯泡） 菜单添加一项。用户点击这一项将弹窗展示光标处选中的文本，如果未选中则展示光标附近的关键字或者字符串的内容。</code></p>

<p>AI 只实现了 <code>string-converter.showText</code> 命令，并没有实现 Code Action 功能。</p></li>

<li><p>继续询问 AI: <code>@Builder 添加到代码操作菜单中。</code></p>

<p>AI 继续实现了 Code Action 能力。</p></li>

<li><p><code>F5</code> Debug，发现断点命中不了，在运行中的扩展中也找不到。询问 AI： <code>@Builder debug 启动，但是断点好像没有被激活。这个插件在运行中的插件列表中也找不到。</code></p>

<p>AI 修改了 package.json ，但是只在执行命令的时候激活，我想要的是永远激活。手动改为 <code>*</code>。</p></li>

<li><p>让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/eb94420eb04dcf7400e5e67c2ca86a0d68d4ab69">feat(extension): 添加显示选中文本的命令</a>。</p>

<h2 id="实现提取文本">实现提取文本</h2>

<h3 id="重构代码将相关回调抽到单独的函数中">重构代码将相关回调抽到单独的函数中</h3>

<ul>
<li><p>询问 AI: <code>@Builder 重构代码，将命令和 CodeAction 回调抽到 src/handler/strconv.ts 中。</code></p>

<p>AI 将函数调用完全移动到了 strconv.ts 。我想要的是只把回调移动过去，注册逻辑仍然在 extension.ts</p></li>

<li><p>询问 AI: <code>#file:src/handler/strconv.ts 去除 vscode 注册相关的 api，保留回调函数，vscode 注册相关调用在 #file:src/web/extension.ts 中实现，并引用 #file:src/handler/strconv.ts 中的函数。</code> （提示词调试了很多次）。</p>

<p>AI 勉强实现了要求，有很多语法错误，通过 AI Fix 能力可以很好的修复。</p>

<p><img src="/image/trae-ai-refactor-ai-fix.png" alt="image" /></p></li>

<li><p><code>F5</code> Debug，验证无明显问题。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/12234893092f8c43c29516c03f380e280bede8cb">refactor: 将字符串转换功能提取到独立模块</a>。</p>

<h3 id="删除代码模板中的代码">删除代码模板中的代码</h3>

<ul>
<li><p>询问 AI: <code>@Builder 删除 hello world 命令相关的所有代码。</code></p>

<p>注意，关闭所有打开的文件，AI 才能更好的工作，否则会影响上下文召回，导致删除不全。</p>

<p>编辑器打开了 <code>src/handler/strconv.ts</code> 效果：</p>

<p><img src="/image/trae-ai-delete-code-active.png" alt="image" /></p>

<p>编辑器未打开任何文件的效果：</p>

<p><img src="/image/trae-ai-delete-code-clear.png" alt="image" /></p>

<p>最终 AI 还是漏了一个导入代码的删除。使用 AI Fix 可快速解决。</p></li>

<li><p><code>F5</code> Debug，验证无明显问题。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/eea7d11840863e5875227158f4cbd6239c556c5b">refactor: 移除不再使用的helloWorld命令及其相关代码</a>。</p>

<h3 id="尝试使用-vscode-现有能力优化光标处文本提取">尝试使用 VSCode 现有能力优化光标处文本提取</h3>

<p>VSCode 触发 CodeAction 的行为有两种，分别是选中代码和光标变更。在选中场景，很好处理，直接以用户选中的内容当做字符串进行后续处理即可。</p>

<p>但是，在光标变更场景，需要从光标所在位置前后找到这个字符串的终点和起点。然后将这个识别到的字符串进行后续处理。</p>

<ul>
<li><p>询问 AI: <code>通过 vscode api 以及 vscode 内置命令，如何获取从光标所在位置的字符串，如果是符号，则获取到这个符号，如果是字符串字面量。则获取到这个字符串字面量的全部内容（包含引号）。不要手写字符串匹配逻辑，如果有的话，尽量利用 VSCode 语法解析、词法解析的来获取，这样才能更准确。</code></p>

<p>AI 回答提到： <code>'vscode.provideDocumentSemanticTokens'</code>，如果找不到会 fail back 到获取单词。但是据我了解，只有少数语言才支持语义化 Token。</p>

<p>另外，让 AI 生成测试代码，发现 SemanticTokens 只会识别符号，不会识别字符串字面量。</p></li>

<li><p>询问 AI: <code>vscode 高亮原理</code>。</p>

<p>AI 回答： <code>...VS Code 使用 TextMate 语法规则（.tmLanguage 文件）来定义不同语言的语法高亮规则。...</code>。</p></li>

<li><p>询问 AI: <code>VSCode api 获取 TextMate 语法分析结果</code>、<code>vscode api 获取当前光标位置的高亮信息</code>、 <code>vscode api 获取当前光标位置的符号</code> 等。</p>

<p>试了好多次都无法生成符合预期的代码。</p></li>
</ul>

<p>搜索 VSCode 源码找到相关的命令：</p>

<ul>
<li><code>'vscode.executeDocumentHighlights'</code>，这个命令获取到的是，<code>菜单-&gt;选择-&gt;选择所有匹配项</code> 那些字符串的 range 列表，并不全。</li>
<li><code>'editor.action.smartSelect.expand'</code>，这个命令将会从光标位置开始智能扩选，一遍比较准确，但是这个命令会操作用户的编辑器，没法后台调用。</li>
</ul>

<p>先，让 AI 生成代码，优化光标处文本选取的初步实现：</p>

<ul>
<li><p>询问 AI: <code>@Builder showTextCommandCallback 函数中，使用 vscode.executeDocumentHighlights 命令获取当前光标位置的高亮情况，展示到消息中。</code></p>

<p>AI 生成了基本符合预期的代码。</p></li>

<li><p>询问 AI: <code>@Builder 如果  highlights 长度不为 0，获取 highlights[0].range 对应的文本内容。</code></p>

<p>AI 生成了正确的代码。</p></li>

<li><p><code>F5</code> Debug，验证无明显问题。让 AI 生成提交消息，并提交代码到 git。</p></li>

<li><p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/c9bbaea092cbba5867c080657b1c1c991fde6512">feat(handler): 在showTextCommandCallback中添加高亮文本显示</a>。</p></li>
</ul>

<p>然后，利用 CodeAction 机制将 <code>'editor.action.smartSelect.expand'</code> 也加进来（CodeAction 触发后调用），但是这要分为如下几部两步：</p>

<ul>
<li>第一步： 将现有的获取 text 的代码抽到一个单独的函数 <code>quickGetActiveText</code> 里面。

<ul>
<li>询问 AI: <code>@Builder 重构 showTextCommandCallback ，如果用户没有选中内容， text 优先取 vscode.executeDocumentHighlights 内容。</code></li>
<li>询问 AI: <code>@Builder 最后输出消息不需要再展示 highlightInfo 信息了。</code></li>
<li>询问 AI: <code>@Builder 重构 showTextCommandCallback 简化代码逻辑。</code></li>
<li>询问 AI: <code>@Builder 将 showTextCommandCallback 中获取 text 的逻辑提取到单独的函数 quickGetActiveText，这个函数同样无参，返回 string 或 undefined。</code></li>
<li>已经改的差不多了，手动调整下代码风格。</li>
<li>手动将 showTextCommandCallback 添加一个可选参数 text。</li>
</ul></li>
<li>第二步： 重构 getCodeActionProviderCallback：

<ul>
<li>询问 AI: <code>@Builder 重构 getCodeActionProviderCallback provideCodeActions 先调用 quickGetActiveText 尝试获取 text，然后返回里面包含一个 text，在此不需要调用 command 。然后实现一个 resolveCodeAction 函数，在里面消费 text，然后声明对  string-converter.showText 命令的调用，并将 text 作为参数传递过去</code>。</li>
<li>询问 AI: <code>@Builder CodeActionProvider&lt;T extends CodeAction = CodeAction&gt;\nCodeActionProvider 声明如上，代码 data 有类型错误，修复这个问题。</code>。</li>
<li>询问 AI: <code>@Builder 重构 getCodeActionProviderCallback 将重复的类型声明提取出来。</code>。</li>
<li>已经改的差不多了，手动调整下代码风格，以及错误。</li>
</ul></li>
<li>第三步： showTextCommandCallback 函数添加 <code>editor.action.smartSelect.expand</code> 命令调用。然后判断是否以引号开头结尾。

<ul>
<li>询问 AI: <code>@Builder 在 showTextCommandCallback 中 quickGetActiveText 后面，如果 text 还是空。且是当前编辑器语言是 ts，则循环调用 editor.action.smartSelect.expand 命令三次，然后每次都检测首尾是否是几种 ts 的字符串引号，如果是则取选中的文本赋值给 text。最后需要恢复光标位置和状态</code></li>
</ul></li>

<li><p><code>F5</code> Debug，验证无明显问题。让 AI 生成提交消息，并提交代码到 git。</p>

<p><img src="/image/trae-ai-agent-quick-finish.png" alt="image" />
<img src="/image/trae-ai-agent-quick-resolve.png" alt="image" /></p></li>

<li><p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/e48baa53b00718ef270a9aab649b193cdd311551">refactor(handler): 重构字符串处理逻辑以提高效率，支持通过自动扩展选择范围并尝试获取字符串</a>。</p></li>
</ul>

<h3 id="使用开源语法高亮库提取文本">使用开源语法高亮库提取文本</h3>

<p>如上使用 VSCode 现有能力有如下问题：</p>

<ul>
<li>大多数场景在触发 QuickAction 的时候，提取不出文本，无法进行前置分析，体验很差。</li>
<li>使用 VSCode 智能扩选在很多场景拿到的字符串也不准（如 Python 字符串）。</li>
<li>使用 VSCode 智能扩选这个做法很不优雅，很魔法，而且用户会看到光标闪动。</li>
</ul>

<p>因为 VSCode 没有暴露 TextMate 高亮引擎相关 API。因此，需调研开源的语法高亮库是否可以返回解析过的 Token 序列或 ast。</p>

<h4 id="技术调研">技术调研</h4>

<p>如下是使用 AI 进行技术调研的过程：</p>

<ul>
<li><p>询问 AI: <code>#Web 帮我调研业界主流的前端语法高亮库，我想使用其将代码文本转换 token 序列或语法树，并且其中带有偏移量或者行号信息。尽量选取主流的支持语言多的库。</code></p>

<p>AI 给了 PrismJS、 Highlight.js、 Monaco Editor、Tree-sitter、Shiki 几种选择。</p>

<p>据我了解 Monaco Editor 就是 VSCode 底层使用的编辑器，在这个场景使用不太合适。</p></li>

<li><p>询问 AI: <code>使用 PrismJS 库如何获取 Token 序列以及偏移量。</code></p>

<p>AI 给出了示例代码，经验证满足需求。</p></li>

<li><p>询问 AI: <code>使用 Highlight.js 库如何获取 Token 序列以及偏移量。</code></p>

<p>AI 给出可示例代码，经验证满足需求。但是其没有专门的 API 在内部变量中。</p></li>

<li><p>询问 AI: <code>#Web 如果我只想用 PrismJS 和 Highlight.js 这两个库，解析 token 序列，请从各个角度分析两者优劣，如 github stars 数，支持语言数目</code>。</p>

<p>AI 只进行了一次网络搜索，给出的结论也不太准确。如： stars 过时了，支持语言数也不准确。</p>

<p><img src="/image/trae-ai-agent-technical-research.png" alt="image" /></p>

<p>结合 AI 建议以及人工搜索，决定使用 PrismJS （性能、包大小、API 使用边界度、支持语言数）。</p></li>
</ul>

<h4 id="实现文本提取业务逻辑">实现文本提取业务逻辑</h4>

<ul>
<li>询问 AI: <code>@Builder #Folder:src/service 目录添加代码解析文件 。里面实现一个名为提取指定位置代码 Token 的函数。该函数有几个参数，第一个参数为代码内容，类型为 string，第二个参数为代码的语言标识符，第三个参数为光标位置（line 和 character），第四个参数可选为光标结束的位置，如果传递第四个参数说明用户选择了一段代码。这个函数返回一个 TokenInfo 结构体，包含 OriginText、 Text、 Type 字段。</code></li>
<li>询问 AI: <code>@Builder extractCodeToken 函数最后添加一个可选参数 selectionText。</code></li>

<li><p>询问 AI: <code>@Builder 将 extractCodeToken 函数的实现替换为：\n1. 判断 Prism.languages 对应的语言是否存在，如果不存在： a. 如果 selectionText 存在，则返回 OriginText = Text = selectionText， Type = unknown b. 如果 selectionText 不存在，则返回 undefined。\n2. 否则，调用 Prism.tokenize 函数获取 tokens。 a. 如果 endPosition 存在，则从 tokens 中获取过滤出 position 和 endPosition 之间的的文本的 tokens 列表。如果只有一个 token，则返回 OriginText selectionText， Text 为解决转义字符后的字符串， Type = token 的类型。如果选中内容跨 token 了（在多个 token 中），返回  OriginText = Text = selectionText，Type = multi。b. 如果 endPosition 不存在，则从 tokens 中提取，光标位置的 token，OriginText 为 token 原始内容， Text 为解决转义字符后的字符串，Type = token 的类型。</code></p>

<p>AI 实现的大体框架没有问题，但是细节错漏很多， 写提示词也很累，还不如手写。</p>

<p>只保留符合预期的部分，同时修改返回值类型为 <code>TokenInfo[]</code>。</p></li>

<li><p>询问 AI: <code>@Builder 根据注释要求，从光标位置续写。</code>。</p>

<p>注释内容为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    // 过滤 position，endPosition? 范围内的 token。
    // tokens 类型为 (string | Prism.Token)[]， Prism.Token 是一个展平的结构。</pre></div>
<p>思路为，删除有问题的代码，添加注释，让 AI 从光标所在位置续写。 （extra: 这里有个问题，如果忘记保存文件了，会修改文件失败）。</p>

<p>AI 代码写的问题很多，最终还是手写了这个函数。</p></li>

<li><p>询问 AI: <code>@Builder 在 #file:service.test.ts basic 位置添加单测，测试 #file:codeParser.ts 的 extractCodeToken 函数。</code></p>

<p>AI 正确生成了单测。</p></li>

<li><p>询问 AI: <code>@Builder 续写光标位置单测，测试 extractCodeToken，偏移量设置为 tsCode 中几个 hello 后面。</code></p>

<p><img src="/image/trae-ai-agent-ut-gen.png" alt="image" /></p>

<p>AI 生成的单测正确的单测框架，但是断言有问题，需要手动修复。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/967eb8f6db3764b69615695b7b93cfa7dd068cd2">feat(代码解析): 添加基于 PrismJS 的代码解析功能</a>。</p>

<h4 id="quickaction-接入上述实现">QuickAction 接入上述实现</h4>

<ul>
<li><p>询问 AI: <code>@Builder 修改该 provideCodeActions 实现，调用 extractCodeTokens 然后将每一个 token 转换为一个 action 并返回，action 的标题为 token 的 text。</code></p>

<p>问题: 在输入框输入了很多内容，点击新建会话，输入的内容会丢失。</p>

<p>AI 基本实现了需求，但是有一些问题。</p></li>

<li><p>询问 AI: <code>@Builder extractCodeTokens 调用传递的 position 和 endPosition 有问题，需要将 vscode 的 selection 转换为偏移量。</code></p>

<p>AI 实现了需求。</p></li>

<li><p>手动删除一些无用代码。</p></li>

<li><p><code>F5</code> Debug，验证，并修正一些小问题。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/5cca60d1b7631f2cf84f64fff3896cf9f02c0fbc">refactor(handler): 重构字符串处理逻辑以使用新的代码解析器</a>。</p>

<h4 id="根据-token-上下文推测-unknown-类型-低优">根据 token 上下文推测 unknown 类型 （低优）</h4>

<h3 id="解析原始字符串字面量">解析原始字符串字面量</h3>

<blockquote>
<p>根据编程语言，将 string、 template-string 等类型的 OriginText 解析成 Text （去除转义等）。</p>
</blockquote>

<ul>
<li><p>询问 AI: <code>@Builder 设计并实现一套面向不同编程语言的字符串解析机制，输入是 originText、 type、 和 languageId，输出是 text。如果类型不是字符串直接返回 originText。这套机制具有可扩展，每个编程语言都有自己的实现，新增一个编程语言时仅需添加一个文件，并在 index.ts 注册即可。代码在 service 里面新建一个 literalParser 实现包含 index.ts 和 各个编程语言的实现。先实现 typescript/javascript 的解析。</code></p>

<p>AI 目录结构和代码框架实现的较好。</p>

<p>但是具体的 typescript 解析，仅仅去除引号，没有处理转义字符。</p>

<p>另外，代码依赖上层的 TokenInfo 类型定义。</p></li>

<li><p>询问 AI: <code>@Builder 不要依赖 TokenInfo 类型定义。</code></p>

<p>AI 完美的解决了这个问题。</p></li>

<li><p>询问 AI: <code>@Builder 重写： 根据 javascript 字符串、模板字符串语法规范解析字符串，关注性能，不要用查找替换，小心关注各种边界 case。</code></p>

<p>AI 使用了循环逐个字符串处理。但是总是有一些 bug。最终还是手写帮 AI 修 bug。</p></li>

<li><p>询问 AI: <code>@Builder 给 #file:src/service/literalParser/typescript.ts 在 #file:src/web/test/suite/service/literalParser/typescript.test.ts 中生成单测，所有分支都要覆盖到。</code></p>

<p>AI 生成了很好的单测。并测试出了手写的 bug。</p></li>

<li><p>询问 AI: <code>@Builder 给 #file:src/service/literalParser/index.ts parseLiteral 添加单测</code></p>

<p>AI 并不能很好的理解单测的目录结构，所以生成的单测位置不符合预期，这种场景可以使用 Trae 自定义规则告诉 AI 如何生成单测。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-markdown" data-lang="markdown"><span style="color:#75715e">## 生成单测规则
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">*</span> 该规则仅对 src 目录下的文件生效。
<span style="color:#66d9ef">*</span> 本项目使用了 mocha 单测框架、 assert 断言库。
<span style="color:#66d9ef">*</span> 使用  suite 代替 describe。
<span style="color:#66d9ef">*</span> src/path/to/file.ts 对应的单测文件为 src/web/test/suite/path/to/file.test.ts。
<span style="color:#66d9ef">*</span> 测试文件结构如下：

    ```ts
    suite(&#39;src/path/to/file.ts&#39;, () =&gt; {
        suite(&#39;待测函数...&#39;, () =&gt; {
            test(&#39;测试样例描述...&#39;, () =&gt; {
                assert.equal(1, 1);
            });
        });
    });
    ```</code></pre></div>
<p><img src="/image/trae-ai-agent-custom-rule.png" alt="image" /></p></li>

<li><p>询问 AI: <code>@Builder 修改当前文件所有 Text 的内容为 parseLiteral 函数的返回值。</code></p>

<p>打开 src/service/codeParser.ts 文件。</p>

<p>AI 最终按照要求完成了修改，但是又忘记了了导入。</p></li>

<li><p>询问 AI: <code>@Builder 根据 typescript.test.ts 在 testdata/main.ts 中添加更多测试代码</code></p></li>
</ul>

<p>最终 AI 生成更多的人工测试 case。</p>

<ul>
<li><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，并提交代码到 git。</li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/ae860ec43e3dbe07dad92b0aa8ac1948e28e0546">feat: 添加字面量解析器及相关测试</a>。</p>

<h2 id="实现可扩展机制来识别和转换字符串">实现可扩展机制来识别和转换字符串</h2>

<h3 id="以-jwt-为切入点实现整体框架">以 jwt 为切入点实现整体框架</h3>

<ul>
<li><p>询问 AI: <code>@Builder 定义一个字符串转换器接口，该接口包含如下内容：1. meta 字段，包含当前转换器 id，name，resultLanguageId. 2. match 函数，参数为 tokenInfo 和可选参数 options，返回 boolean。3. convert 函数，参数为 tokenInfo 和可选参数 options，返回字符串。</code></p>

<p>AI 按照要求生成了代码。</p></li>

<li><p>询问 AI: <code>@Builder 在 stringConverter 目录添加一个 jwt StringConverter 的实现。</code></p>

<p>AI 基本实现了代码框架，但是逻辑不符合预期。</p>

<p>手动删除不好的代码。</p></li>

<li><p>询问 AI: <code>@Builder 使用最流行的 npm jwt 开源库，来实现 match 和 covert 函数。</code></p>

<p>AI 正确实现了 convert 函数。</p></li>

<li><p>询问 AI: <code>@Builder 实现一个并导出一个 stringConverterManager。支持注册 stringConverter、传递一个 tokenInfo 返回匹配的 stringConverter meta 列表，传递 tokenInfo 和 meta 调用对应的 convert 函数。</code></p>

<p>AI 基本正确实现了需求。</p></li>

<li><p>询问 AI: <code>@Builder 参考 JwtParser 实现一个 DefaultConverter，如果 token originText 不包含 text  则 match 返回 true，convert 永远返回 text。并注册到 manger。</code></p>

<p>AI 基本实现了需求，只需手动处理文案以及一些小问题。</p></li>

<li><p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/9bbf495733ee6b5d14f83ebf1cf5ab588509d305">feat(字符串转换器): 添加JWT解析功能并重构字符串转换器</a>。</p>

<h3 id="探索转换结果的展示">探索转换结果的展示</h3>

<h4 id="技术调研-1">技术调研</h4>

<ul>
<li><p>方案 1: 触发 Quick Action 后，展示 Markdown 浮窗。询问 AI: <code>VSCode API 如何展示一个 markdown 预览编辑器。</code></p>

<p>观察示例代码发现浮窗只支持 Hover 展示，无法动态触发。</p></li>

<li><p>方案 2: 创建一个 Markdown 预览。询问 AI: <code>VSCode API 如何展示一个 markdown 预览编辑器。</code></p>

<p>观察示例代码发现实际上是通过 Webview 实现的。</p></li>
</ul>

<h4 id="尝试使用-webview-展示">尝试使用 Webview 展示</h4>

<ul>
<li><p>询问 AI: <code>@Builder showTextCommandCallback，使用 markdown 预览编辑器。展示 stringConverterManager.convert 结果。</code></p>

<p>AI 基本实现了正确代码。</p></li>

<li><p>询问 AI: <code>@Builder 修改 html convert 结果以代码块方式展示。</code></p>

<p>AI 基本实现了正确代码。</p></li>
</ul>

<p>这个方案没有高亮，经调研如果想要实现代码高亮，需要使用三方高亮库，有点恶心，尝试使用其他方案。</p>

<h4 id="尝试使用-markdown-preview-展示">尝试使用 Markdown Preview 展示</h4>

<ul>
<li><p>询问 AI: <code>@Builder 使用 markdown 预览命令展示结果。先创建 内存 vscode.Uri 并将结果写入这个文档，最后调用 markdown 侧边栏预览命令展示结果。</code></p>

<p>AI 基于 <code>untitled:</code> 实现了通过 markdown 方式展示结果。但是有个问题是，会在编辑器中出现一个 untitled 编辑器。</p>

<p>经搜索发现可以实现自己的文件系统。</p></li>

<li><p>询问 AI: <code>@Builder 在 handler/memfs 实现一个 scheme 为 strconvmemfile 的内存文件系统并在 extensions 中调用 registerTextDocumentContentProvider 注册。</code></p>

<p>AI 很好的实现了这个需求，并解决了之前 <code>untitled:</code> 问题。</p>

<p>可以采用此方案。</p></li>

<li><p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/405dcaebcc946b6d268d7f0cd2a253f90cece089">feat: 添加内存文件系统以支持Markdown预览</a>。</p>

<h4 id="重构-stringconverter-展示更多信息">重构 StringConverter 展示更多信息</h4>

<p>手动修改了 StringConverter 接口的声明，支持返回更多信息，让 AI 修改所有实现和调用点，以适配变更。</p>

<ul>
<li><p>询问 AI: <code>@Builder 我手动修改了 StringConverter 的声明，请修改所有实现和调用点，以适配变更</code>。</p>

<p>AI 完成度较低，只修改了接口实现，调用点没有实现。</p>

<p>后面所有能力手动实现了，并在 JWT 上进行了完整的实现。</p></li>

<li><p><code>F5</code> Debug，人工验证，边验证变修改，无问题后。让 AI 生成提交消息，并提交代码到 git。</p></li>
</ul>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/2362268c787e2da1396d9b092093fcdc5a890d41">feat: 增强 JWT 解析功能并优化字符串转换器</a>。</p>

<h4 id="支持-hover-弹窗展示解析结果">支持 Hover 弹窗展示解析结果</h4>

<p>开发时条件有限，没有网路，全部手写。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/2362268c787e2da1396d9b092093fcdc5a890d41">feat: 重构处理程序模块，提取并优化代码结构，添加 hover 展示结果，并实现复制能力</a></p>

<h2 id="添加主流编程语言字符串-token-解析">添加主流编程语言字符串 Token 解析</h2>

<blockquote>
<ul>
<li>优先支持 JSON、Go、Rust、Java、Python、</li>
<li>后续添加C++、PHP 等 TIOBE - 编程语言排名 / VSCode 官方文档提到的。</li>
<li>添加默认的字符串 Token 解析逻辑。</li>
</ul>
</blockquote>

<ul>
<li><p>添加 JSON 支持，询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 json.ts 实现 JSON 解析（直接使用 JSON.parse 实现），然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测，然后在 #folder:testdata 添加对应的人工测试文件。</code></p>

<p>AI 正确实现了全部需求（提效+1+1+1）。</p>

<p>最后手动修正了一些单测问题。</p>

<p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/9c00c83216668cc118d8bac31e75467dc269db8c">feat(literalParser): 添加JSON字符串解析功能并重构代码</a>。</p></li>

<li><p>添加 Go 支持，询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 go.ts 实现 Go 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<p>AI 正确实现了全部需求（提效+1+1+1）。</p>

<p>最后手动修正了一些单测问题。</p>

<p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/6ad15a39ab8b6bfb4352a6932e88ee0032a8d3c8">feat(literalParser): 添加Go字符串字面量解析功能</a>。</p></li>

<li><p>添加 Rust 支持，询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 rust.ts 实现 Rust 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<p>AI 基本结构实现正常，但是解析逻辑完全仿写 typescript，没有按照 rust 语法实现。</p>

<p>需要给 AI 更多引导，下载官方手册 <a href="https://github.com/rust-lang-cn/reference-cn/blob/master/src/tokens.md">tokens.md</a>，然后导入到上下文，文档集。</p>

<p><img src="/image/trae-ai-agent-context-local-docset.png" alt="image" /></p>

<p>然后删除之前错误实现，询问 AI: <code>严格按照 @Builder #doc:rust-token 字符串字面量规范，重点关注转义。不要参考其他语言的实现。从光标位置实现解析逻辑</code>。</p>

<p>AI 基本正确实现，人工调整了一下细节。</p>

<p><code>F5</code> Debug，人工验证，无问题。让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/f24d4edfd54444f69400003123583bf2bf10b171">feat(literalParser): 添加Rust字符串解析功能</a>。</p></li>

<li><p>添加 Java 支持，，询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts，新建 java.ts 实现 Java 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<p>基本实现了需求，但是缺少了 Text Blocks 以及 8 进制转义序列的支持，</p>

<p>手写并让 AI 继续生成单测。</p>

<p>最后，手动优化字符串解析器的添加首尾标记支持以优化默认转换器识别率。</p>

<p><code>F5</code> Debug，人工验证，修复，无问题后，让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/a743f4b279681349769e1778b2b3352886efc6de">feat(literalParser): 添加Java字符串解析功能，添加字符串解析器的标记支持以优化默认转换器识别率</a>。</p></li>

<li><p>添加 Python 支持，询问 AI: <code>@Builder 参考 #file:src/service/literalParser/typescript.ts 风格，根据 Python 语言规范，新建 python.ts 实现 Python 字符串解析，然后注册到 #file:src/service/literalParser/index.ts ，然后实现单测并在 #file:src/web/test/suite/service/literalParser/index.test.ts 中 import，然后在参考 #file:testdata/main.ts#folder:testdata 添加对应的人工测试文件。</code></p>

<p>AI 实现了大多数功能。但是转义序列处理不全，缺失 raw 字符串处理。</p>

<p>把 <a href="https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences">Python 3 词法分析文档转义序列</a> 部分内容复制发给 AI，让 AI 补充实现。</p>

<p>手动处理 Unicode NameAliases。结合 AI 和 手动处理 raw 字符串。</p>

<p>最后，让 AI 更新单测： <code>@Builder 根据 #file:src/service/literalParser/python.ts 补充 #file:src/web/test/suite/service/literalParser/python.test.ts 自动化测试以及 #file:testdata/main.py 手动测试样例，提高覆盖率。</code></p>

<p><code>F5</code> Debug，人工验证，修复，无问题后，让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/a9c1214f47a557abdb52369773eecf3ef364f8ec">feat(literalParser): 添加Python字符串解析功能</a>。</p></li>
</ul>

<h2 id="添加更多常见类型的字符串转换器">添加更多常见类型的字符串转换器</h2>

<blockquote>
<ul>
<li>优先支持 jwt、时间戳、base64、url、json。</li>
<li>各种在线工具网站提供的能力。</li>
</ul>
</blockquote>

<ul>
<li><p>添加时间戳解析，询问 AI: <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现时间戳（毫秒/秒）解析的支持，match 支持 tokenInfo 为 string 和 number 两种类型。</code></p>

<p>AI 生成了正确的代码，手动和 AI 优化逻辑，生成单测。</p>

<p><code>F5</code> Debug，人工验证，修复，无问题后，让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/2533b1e3c776897fbd1c0121cf1acdd8e44dcf81">feat(stringConverter): 添加时间戳解析器功能</a>。</p></li>

<li><p>添加 Base64 解析，询问 AI: <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现对 base64 的解析，match 支持 tokenInfo 为 string 类型。</code></p>

<p>AI 基本完美实现了代码。但是调研发现 base64 decode 结果可能是二进制数据也可能是任意编码的字符串。因此，考虑拆成如下字符串和二进制两种类型实现。</p>

<p>首先，将当前 base64.ts 修改为 base64 decode 结果是一个字符串的解析器。</p>

<p>经过询问 AI 和调研最终使用如下方案手写：</p>

<ul>
<li>base64 分为两个解析器，字符串和二进制。</li>
<li>针对字符串，使用 chardet 进行字符串编码格式进行推导（特别处理 GB18030 优先级更高）。使用 iconv-lite （配置 esbuild 垫片），进行编码转换。</li>
<li>针对二进制，使用 magic-bytes.js 进行判断，使用 <code>hexy</code> 以类似 xdd 格式展示。</li>
</ul>

<p>最终让 AI 补充单测。</p>

<p><code>F5</code> Debug，人工验证，修复，无问题后，让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/a81cd261d9062b0283eb94496dad28066ad593e9">feat: 添加 Base64 解析器及相关依赖</a>。</p></li>

<li><p>添加 URL 解析，略，代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/a628d34416d534944053e640eaff9b0435d78693">feat(stringConverter): 添加URL解析器以支持URL字符串的解码和解析</a></p></li>

<li><p>添加 json 格式化，询问 AI: <code>@Builder 参考 #file:src/service/stringConverter/jwt.ts ，在 #folder:src/service/stringConverter 目录新建文件，实现对 json 格式化的支持，match 支持 tokenInfo 为 string 类型。然后并在 #file:src/service/stringConverter.ts 中注册。生成完成后需生成单测。</code></p>

<p>AI 生成了基本正确的代码。经过多轮 AI 交互以及人工处理后。<code>F5</code> Debug，人工验证，修复，无问题后，让 AI 生成提交消息，提交代码到了 git。</p>

<p>代码详见： <a href="https://github.com/rectcircle/string-converter-vsc-ext/commit/3ce6a91ca5b3b774dc74427e26a90a2e92af6310">feat: 添加 Base64 解析器及相关依赖</a>。</p></li>
</ul>

<h2 id="扩展体验">扩展体验</h2>

<p>详见： <a href="https://marketplace.visualstudio.com/items?itemName=Rectcircle.str-conv">VSCode 扩展市场 - String Converter</a>。</p>

<h2 id="trae-其他问题">Trae 其他问题</h2>

<ul>
<li>终端进程启动失败: A native exception occurred during launch (posix_spawnp failed.)。</li>
<li>复制输入框内容（包含 <code>#xxx</code> 场景）无法原样粘贴。</li>
</ul>
]]></description></item><item><title>开源 AI 编程工具（一） Continue</title><link>https://www.rectcircle.cn/posts/open-source-ai-code-tool-1-continue/</link><pubDate>Mon, 24 Mar 2025 12:45:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/open-source-ai-code-tool-1-continue/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://www.continue.dev/">官网</a> | <a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue">VSCode 扩展</a> | <a href="https://github.com/continuedev/continue">github</a></p>
</blockquote>

<h2 id="简介">简介</h2>

<p>Continue 是一个开源的 AI 编程工具，以 IDE 插件形式提供给用户，支持 VSCode 和 Jetbrains 系列 IDE。</p>

<p>Continue 的 Slogan 是：“Amplified developers, AI-enhanced development” （赋能开发者，AI 增强开发）。</p>

<p>因此，Continue 是一个利用大模型提升开发者效率的工具（对应自动驾驶 L1、L2），而不是让 AI 自动化的完全替代开发者（对应自动驾驶 L3、L4、L5）。</p>

<p>Continue 是开源的，且不和某个大模型绑定，而是支持和主流的大模型提供商（如 OpenAI、anthropic、Mistral、OpenRouter、SiliconFlow 等）。</p>

<p>Continue 提供了如下能力：</p>

<ul>
<li>Chat - 询问大模型各种编程开发问题，可携带各种项目上下文。</li>
<li>Autocomplete - 利用大模型的 FIM completion 能力（关于 FIM completion，可以参考 <a href="https://medium.com/@SymeCloud/what-is-fim-and-why-does-it-matter-in-llm-based-ai-53f33385585b">What is FIM and why does it matter in LLM-based AI</a>），提供代码自动补全。</li>
<li>Edit - 提供需要编辑的文件列表，并给大模型代码编辑需求，大模型自动生成代码。</li>
<li>Actions - 一些常用操作。如在 Chat 和 Edit 通过 <code>/</code> 附加上下文。在编辑器右键选择一些常用的操作等。</li>
</ul>

<p>本文基于 VSCode 1.0.2 (2025.03.04) 版本，将介绍如何配置云上的免费大模型，使用 Continue 的上述功能。</p>

<p>注意，目前 Continue 仍在高速迭代，后续版本可能会有所变化，本文仅做参考，请以事实为准。</p>

<h2 id="安装">安装</h2>

<p>VSCode 插件市场搜索 <code>Continue</code>，点击安装 <a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue"><code>Continue - Codestral, Claude, and more</code></a>。</p>

<p>安装完成后，建议将 Continue 侧边连拖拽到编辑器右侧，以方便使用。</p>

<p><img src="/image/continue-move-to-right-sidebar.gif" alt="image" /></p>

<h2 id="配置免费模型">配置免费模型</h2>

<p>Continue 有两种使用方式：</p>

<ol>
<li>登录 Continue 官方账号登录，使用官方提供的模型，创建或添加助手。这种方式免费额度有限，超过限制需要付费。</li>
<li>通过本地配置，配置使用模型提供商。支持 OpenAI, Open Router, Anthropic, siliconflow (硅基流动) 等云上 API 提供商，还对接支持 Ollama, llama.cpp 这种本地部署的大模型。</li>
</ol>

<p>本文介绍第二种方式，如何通过本地配置，配置使用云上模型提供商的免费模型。</p>

<p>配置方式：
1. 打开 Continue 侧边栏，点击齿轮图标，打开设置。
2. 选择 <code>Configuration</code> 下面的 <code>Local Config</code>，点击 <code>Open Config File</code>，打开 JSON 配置文件，位于 <code>~/.continue/config.json</code>。
    * 在 <code>models</code> 字段中添加 Chat、 Edit 和 Actions 功能可使用的模型。该字段是个数组可以配置多个。
    * 在 <code>tabAutocompleteModel</code> 字段中配置，自动完成功能使用的模型（自动完成的模型依赖大模型以及其 API 支持 FIM completion）。</p>

<h3 id="mistral-自动完成">mistral （自动完成）</h3>

<p>对于自动完成，<a href="https://docs.continue.dev/autocomplete/model-setup">官方推荐</a> Mistral 的 <a href="https://mistral.ai/news/codestral-2501">Codestral 模型</a>。目前 （25-03-05），Codestral 模型的个人使用仍然是免费的，本小结将介绍如何申请和配置使用 Codestral 模型实现 Continue 的自动完成功能。</p>

<ol>
<li>申请 API Key。打开 <a href="https://mistral.ai/，点击">https://mistral.ai/，点击</a> <code>Sign Up</code>，注册一个账号，注册完成后，点击 <code>Sign In</code>，登录账号，登录完成后。<a href="https://console.mistral.ai/codestral">点此</a>打开控制台的 Codestral 配置页，点击申请早期使用按钮后，即可免费申请一个 API key，复制 API key。</li>

<li><p>配置 <code>~/.continue/config.json</code> 文件的 <code>tabAutocompleteModel</code>，字段，其中 <code>apiKey</code> 替换为第一步获取到的 API key：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;tabAutocompleteModel&#34;</span>: {
        <span style="color:#f92672">&#34;title&#34;</span>: <span style="color:#e6db74">&#34;Codestral&#34;</span>,
        <span style="color:#f92672">&#34;provider&#34;</span>: <span style="color:#e6db74">&#34;mistral&#34;</span>,
        <span style="color:#f92672">&#34;model&#34;</span>: <span style="color:#e6db74">&#34;codestral-latest&#34;</span>,
        <span style="color:#f92672">&#34;apiKey&#34;</span>: <span style="color:#e6db74">&#34;xxx&#34;</span>,
        <span style="color:#f92672">&#34;apiBase&#34;</span>: <span style="color:#e6db74">&#34;https://codestral.mistral.ai/v1&#34;</span>,
    }
}</code></pre></div></li>
</ol>

<p>注意：codestral 模型的 apiBase 是特殊的，如果使用 mistral 的其他模型，无需配置 apiBase。详见： <a href="https://docs.continue.dev/customize/model-providers/mistral">continue 官方文档</a>。</p>

<p>配置完成后，在 VSCode 任意编辑器中编写代码，Continue 将在光标处进行自动完成，按 Tab 键即可接收自动完成的代码。</p>

<h3 id="openrouter">openrouter</h3>

<p>openrouter 是一个大模型 API 中间商，支持多个大模型提供商，可以使用一个 API Key 和统一的 API 调用多个大模型，支持业界主流的开源和闭源大模型。该平台还提供了几十个免费的大模型，可以直接使用。</p>

<ol>
<li>访问 <a href="https://openrouter.ai/">openrouter</a> 官网，注册一个账号，获取 API Key。</li>
<li>打开 <a href="https://openrouter.ai/models">模型页</a>，搜索 free，选择模型，如 <a href="https://openrouter.ai/deepseek/deepseek-r1:free">DeepSeek R1</a>，复制标题下方的模型 ID，如 <code>deepseek/deepseek-r1:free</code>。</li>

<li><p>配置 <code>~/.continue/config.json</code> 文件，在 <code>models</code> 数组中添加以下配置，其中 <code>apiKey</code> 替换为第一步获取到的 API key：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
  &#34;title&#34;: &#34;[openrouter] DeepSeek: R1 (free)&#34;,
  &#34;provider&#34;: &#34;openrouter&#34;,
  &#34;model&#34;: &#34;deepseek/deepseek-r1:free&#34;,
  &#34;apiKey&#34;: &#34;xxx&#34;
},</pre></div></li>
</ol>

<p>配置完成后，Continue Chat 页面即可选择 <code>[openrouter] DeepSeek: R1 (free)</code> 模型，进行交互。</p>

<h3 id="siliconflow-硅基流动">siliconflow (硅基流动)</h3>

<p>siliconflow 和 openrouter 类似，也是一个大模型 API 中间商，中国大陆的的公司，主要提供国产模型，也提供了一些免费模型，可以直接使用。</p>

<ol>
<li>访问 <a href="https://siliconflow.cn/zh-cn/">siliconflow</a> 官网，注册一个账号，获取 API Key。</li>
<li>打开 <a href="https://cloud.siliconflow.cn/models">模型页</a>，选择左侧价格免费选项，选择模型，如 <a href="https://cloud.siliconflow.cn/models?target=deepseek-ai/DeepSeek-R1-Distill-Qwen-7B">DeepSeek-R1-Distill-Qwen-7B (Free)</a>，复制标题模型 ID，如 <code>deepseek-ai/DeepSeek-R1-Distill-Qwen-7B</code>。</li>

<li><p>配置 <code>~/.continue/config.json</code> 文件，在 <code>models</code> 数组中添加以下配置，其中 <code>apiKey</code> 替换为第一步获取到的 API key：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
  &#34;title&#34;: &#34;[siliconflow] DeepSeek-R1-Distill-Qwen-7B (Free)&#34;,
  &#34;provider&#34;: &#34;siliconflow&#34;,
  &#34;model&#34;: &#34;deepseek-ai/DeepSeek-R1-Distill-Qwen-7B&#34;,
  &#34;apiKey&#34;: &#34;xxx&#34;
}</pre></div></li>
</ol>

<p>配置完成后，Continue Chat 页面即可选择 <code>[siliconflow] DeepSeek-R1-Distill-Qwen-7B (Free)</code> 模型，进行交互。</p>

<h2 id="功能">功能</h2>

<h3 id="chat">Chat</h3>

<ul>
<li>如下几种方式打开 Chat 方式：

<ul>
<li>打开 VSCode 侧边栏，点击 continue 图标，点击加号。</li>
<li><code>cmd+l</code> 快捷键（可选中代码将代码上下文发送给模型）。</li>
<li>选中代码，右键 -&gt; continue -&gt; Add Highlighted Code to Chat （<code>cmd+shift+l</code>）。</li>
</ul></li>
<li>在输入框中输入问题，按回车即可发送问题给模型，获取大模型的回答。</li>
<li>除了文字之外，还可以通过 <code>@</code> 提供上下文给模型。

<ul>
<li><code>@File</code> 选择某个文件，将文件作为上下文送给模型。</li>
<li><code>@Codebase</code> 对代码库构建索引，并根据输入内容，查询索引，并将这些上下文发送给模型，原理详见：<a href="https://docs.continue.dev/customize/deep-dives/codebase">官方文档 - @Codebase</a>。</li>
<li><code>@Code</code> 将某个代码符号（函数、类、变量等）作为上下文发送给模型。</li>
<li><code>@Git Diff</code> 将当前文件的 git diff 信息作为上下文发送给模型（如： <code>根据 @Git Diff 生成当前变更的变更内容总结，尽量简短，一句话总结。</code>）。</li>
<li><code>@Terminal</code> 将最后一个终端命令作为上下文发送给模型。</li>
<li><code>@Problems</code> 将当前文件的问题（如 eslint 等）作为上下文发送给模型。</li>
<li><code>@Folder</code> 使用与 <code>@CodeBase</code> 相同的检索机制，但仅在一个文件夹中进行。</li>
<li>更多详见： <a href="https://docs.continue.dev/customize/context-providers">官方文档 - Context providers</a>。</li>
</ul></li>
</ul>

<p>注意：需配置模型，详见 <a href="#配置免费模型">配置免费模型章节</a>。</p>

<h3 id="自动完成">自动完成</h3>

<p>在编辑器中输入任何字符都会触发自动完成，按 Tab 可接受，按 Esc 可取消。按 <code>ctrl + →</code> 可部分接受。</p>

<p>注意：自动完成，基于大模型的 FIM 能力，需配置支持 FIM 的模型模型，详见 <a href="#mistral-自动完成">mistral （自动完成）</a>。</p>

<h3 id="edit">Edit</h3>

<ul>
<li>如下几种方式打开 Eidt 模式：

<ul>
<li>在编辑器中按 <code>cmd+i</code>。</li>
</ul></li>
<li>在 Edit 模式，输入要求，按回车，即可生成代码，并将 diff 展示到编辑器中。

<ul>
<li>按 <code>cmd+opt+y</code> 即可接受单个变更，按 <code>cmd+opt+n</code> 可取消单个变更。</li>
<li>按 <code>cmd+shift+回车</code> 即可接受所有变更，按 <code>cmd+shift+退格</code> 可取消所有变更。</li>
</ul></li>
</ul>

<p>注意：需配置模型，详见 <a href="#配置免费模型">配置免费模型章节</a>。</p>

<h3 id="actions">Actions</h3>

<ul>
<li>斜线命令，在 Chat 模式下，输入 <code>/</code> 可触发，实现下来，在自定义模型下，仅 <code>/cmd</code> 命令基本可用：

<ul>
<li>内建斜线命令，详见： <a href="https://docs.continue.dev/customize/slash-commands">官方文档 - Slash commands</a>。</li>
<li>通过提示词文件自定义斜线命令，详见： <a href="https://docs.continue.dev/customize/deep-dives/prompt-files">官方文档</a></li>
</ul></li>
<li>VSCode 特定的 Actions 集成。

<ul>
<li>Quick actions：通过 VSCode 命令 <code>continue.enableQuickActions</code> 启用。</li>
<li>右击上下文菜单： 选中代码，右击，选择 <code>Continue</code>，即可看到常用的动作（注意：需使用 anthropic 模型）。</li>
<li>Debug action：输入 <code>cmd+shift+r</code>，即可将终端最后一个命令和输出发送到 Chat。</li>
<li>Quick fixes：在代码出现问题的地方（波浪线），可以选择 <code>Ask Continue</code> 来获取帮助。</li>
</ul></li>
</ul>

<h3 id="mcp-工具支持">MCP 工具支持</h3>

<p>需使用 anthropic 模型，本文不多介绍。</p>
]]></description></item><item><title>Nydus 核心二进制使用和性能测评</title><link>https://www.rectcircle.cn/posts/nydus-core-bin-usage-and-perf/</link><pubDate>Tue, 04 Mar 2025 15:54:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nydus-core-bin-usage-and-perf/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://nydus.dev/">官方网站</a> | <a href="https://github.com/dragonflyoss/nydus">Github</a></p>
</blockquote>

<h2 id="简介">简介</h2>

<p>Nydus 采用了一种创新的文件系统设计，将元数据（目录结构）与文件内容分离，从而实现了高效的只读文件系统格式。通过这种设计，用户只需预先下载元数据即可完成文件系统的挂载，而文件内容则在实际使用时按需加载。这种机制显著提升了远程文件系统挂载的速度。</p>

<p>Nydus 的主要应用场景是容器化环境，特别适用于加速 OCI 镜像（如 Docker 镜像）的挂载过程。社区提供了对 Kubernetes、Docker 等主流容器运行时的无缝集成支持，使其在容器生态中具有广泛的适用性。</p>

<p>本文将介绍如何利用 Nydus 提供的核心工具构建 Nydus 镜像，并在 Linux 系统中完成镜像的挂载操作。此外，本文还将浅析 Nydus 的技术原理及其基础性能评估结果。</p>

<p>通过本文，方案设计者可以获取足够的信息来评估是否采用以及何时采用 Nydus 技术。需要注意的是，本文不涉及在 Kubernetes 等容器平台中深度集成 Nydus 的具体实现细节，如需了解相关内容，请参考<a href="https://github.com/dragonflyoss/nydus/tree/v2.3.0?tab=readme-ov-file#supported-platforms">官方文档</a>。</p>

<h2 id="测试环境">测试环境</h2>

<ul>
<li>AWS us-east-1 t2.micro:

<ul>
<li>CPU: 1 vCPU</li>
<li>RAM: 1 GiB</li>
<li>Disk: gp3, 3000 IOPS, 125 MiB/s</li>
</ul></li>
<li>操作系统: Amazon Linux 2023 (fedora)</li>
<li>用户: root</li>
<li>Nydus 版本: v2.3.0</li>
<li>测试目录 <code>/root/nydus-demo</code></li>
</ul>

<h2 id="nydus-核心工具安装">Nydus 核心工具安装</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://github.com/dragonflyoss/nydus/releases/download/v2.3.0/nydus-static-v2.3.0-linux-amd64.tgz
tar -zxvf nydus-static-v2.3.0-linux-amd64.tgz
chown -R <span style="color:#ae81ff">0</span>:0 nydus-static
mv nydus-static/nydus* /usr/local/bin
rm -rf nydus-static nydus-static-v2.3.0-linux-amd64.tgz </code></pre></div>
<h2 id="基本使用">基本使用</h2>

<p>准备 mock 的根目录</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p <span style="color:#ae81ff">01</span>-basic
cd <span style="color:#ae81ff">01</span>-basic
mkdir -p origin-root-dir
mkdir -p origin-root-dir/dir_1 origin-root-dir/dir_2
mkdir -p origin-root-dir/dir_1/subdir_a origin-root-dir/dir_1/subdir_b
echo <span style="color:#e6db74">&#39;file_a&#39;</span> &gt; origin-root-dir/dir_1/subdir_a/file_a
echo <span style="color:#e6db74">&#39;file_b&#39;</span> &gt; origin-root-dir/dir_2/file_b
echo <span style="color:#e6db74">&#39;file_c&#39;</span> &gt; origin-root-dir/file_c
ln -s file_c origin-root-dir/file_c_ln
tree origin-root-dir
<span style="color:#75715e"># origin-root-dir</span>
<span style="color:#75715e"># ├── dir_1</span>
<span style="color:#75715e"># │   ├── subdir_a</span>
<span style="color:#75715e"># │   │   └── file_a</span>
<span style="color:#75715e"># │   └── subdir_b</span>
<span style="color:#75715e"># ├── dir_2</span>
<span style="color:#75715e"># │   └── file_b</span>
<span style="color:#75715e"># ├── file_c</span>
<span style="color:#75715e"># └── file_c_ln -&gt; file_c</span></code></pre></div>
<p>从目录构建 Nydus 镜像</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nydusify --debug build --name image-basic-01 --source-dir origin-root-dir --output-dir image-store/
<span style="color:#75715e"># DEBU[2025-02-24T12:48:02Z]      Command: /usr/local/bin/nydus-image create --bootstrap image-store/image-basic-01.meta --log-level warn --whiteout-spec oci --output-json image-store/output.json --blob image-store/image-basic-01.blob --fs-version 6 --compressor zstd --chunk-size 0x100000 origin-root-dir</span> 
mv image-store/image-basic-01.blob image-store/<span style="color:#66d9ef">$(</span>cat image-store/output.json | jq -r <span style="color:#e6db74">&#39;.blobs[0]&#39;</span><span style="color:#66d9ef">)</span>
du -ah --max-depth <span style="color:#ae81ff">1</span> image-store/
<span style="color:#75715e"># 16K     image-store/image-basic-01.meta</span>
<span style="color:#75715e"># 4.0K    image-store/output.json</span>
<span style="color:#75715e"># 8.0K    image-store/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28</span>
<span style="color:#75715e"># 28K     image-store/</span></code></pre></div>
<p>挂载本地的 Nydus 镜像</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p mnt-local
tee nydusd-config.localfs.json &gt; /dev/null <span style="color:#e6db74">&lt;&lt; EOF
</span><span style="color:#e6db74">{
</span><span style="color:#e6db74">  &#34;device&#34;: {
</span><span style="color:#e6db74">    &#34;backend&#34;: {
</span><span style="color:#e6db74">      &#34;type&#34;: &#34;localfs&#34;,
</span><span style="color:#e6db74">      &#34;config&#34;: {
</span><span style="color:#e6db74">        &#34;dir&#34;: &#34;image-store&#34;
</span><span style="color:#e6db74">      }
</span><span style="color:#e6db74">    },
</span><span style="color:#e6db74">    &#34;cache&#34;: {
</span><span style="color:#e6db74">      &#34;type&#34;: &#34;blobcache&#34;,
</span><span style="color:#e6db74">      &#34;config&#34;: {
</span><span style="color:#e6db74">        &#34;work_dir&#34;: &#34;cache&#34;
</span><span style="color:#e6db74">      }
</span><span style="color:#e6db74">    }
</span><span style="color:#e6db74">  },
</span><span style="color:#e6db74">  &#34;mode&#34;: &#34;direct&#34;,
</span><span style="color:#e6db74">  &#34;digest_validate&#34;: false,
</span><span style="color:#e6db74">  &#34;iostats_files&#34;: false,
</span><span style="color:#e6db74">  &#34;enable_xattr&#34;: true
</span><span style="color:#e6db74">}
</span><span style="color:#e6db74">EOF</span>
nydusd --config nydusd-config.localfs.json --mountpoint mnt-local --bootstrap image-store/image-basic-01.meta --log-level info
<span style="color:#75715e"># 打开新终端，切换到该目录，观察 mnt-local 目录</span>
cd /root/nydus-demo/01-basic
tree mnt-local/
<span style="color:#75715e"># mnt-local/</span>
<span style="color:#75715e"># ├── dir_1</span>
<span style="color:#75715e"># │   ├── subdir_a</span>
<span style="color:#75715e"># │   │   └── file_a</span>
<span style="color:#75715e"># │   └── subdir_b</span>
<span style="color:#75715e"># ├── dir_2</span>
<span style="color:#75715e"># │   └── file_b</span>
<span style="color:#75715e"># ├── file_c</span>
<span style="color:#75715e"># └── file_c_ln -&gt; file_c</span>
du -ah --max-depth <span style="color:#ae81ff">1</span> cache/
<span style="color:#75715e"># 4.0K    cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.data.chunk_map</span>
<span style="color:#75715e"># 0       cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.data</span>
<span style="color:#75715e"># 8.0K    cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.meta</span>
<span style="color:#75715e"># 28K     cache/</span>
cat mnt-local/dir_1/subdir_a/file_a
<span style="color:#75715e"># file_a</span>
cat mnt-local/dir_2/file_b
<span style="color:#75715e"># file_b</span>
cat mnt-local/file_c
<span style="color:#75715e"># file_c</span>
cat mnt-local/file_c_ln
<span style="color:#75715e"># file_c</span>
du -ah --max-depth <span style="color:#ae81ff">1</span> cache/
<span style="color:#75715e"># 8.0K    cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.data.chunk_map</span>
<span style="color:#75715e"># 12K     cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.data</span>
<span style="color:#75715e"># 8.0K    cache/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28.blob.meta</span>
<span style="color:#75715e"># 44K     cache/</span></code></pre></div>
<h2 id="过程分析">过程分析</h2>

<ul>
<li>镜像构建：

<ul>
<li><a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydusify.md"><code>nydusify</code> 工具</a> 指定镜像名、源目录、输出目录。</li>
<li><code>nydusify</code> 工具解析命令行参数，并调用 <a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydus-image.md"><code>nydus-image</code> 工具</a> <code>create</code> 子命令构建 Nydus 镜像，参数如下：

<ul>
<li><code>--bootstrap</code>: 生成的 Nydus 镜像的元数据文件路径。</li>
<li><code>--log-level</code>: 日志级别，可选值： <code>trace</code>, <code>debug</code>, <code>info</code>, <code>warn</code>, <code>error</code>。</li>
<li><code>--whiteout-spec</code>: whiteout 规范，可选值为 <code>oci</code>, <code>overlayfs</code>, <code>none</code>。</li>
<li><code>--output-json</code>: 输出 JSON 文件，包含构建一些元信息，比如 blob 文件 ID。</li>
<li><code>--blob</code>：生成的 Nydus 镜像的 blob 文件路径。</li>
<li><code>--fs-version</code>：Nydus 镜像的文件系统版本，默认值为 6，可选值： <code>5</code>, <code>6</code>。</li>
<li><code>--compressor</code>: 压缩算法，可选值： <code>none</code>, <code>lz4_block</code>, <code>zstd</code>。</li>
<li><code>--chunk-size</code>: 压缩块大小，默认值为 <code>0</code>，可选范围：<code>0</code> 或 <code>0x1000-0x1000000</code>。</li>
<li><code>path/to/source</code>: 源目录路径。</li>
</ul></li>
<li><code>nydus-image</code> 会生成 3 个文件分别是 <code>--bootstrap</code>、<code>--output-json</code> 和 <code>--blob</code>。</li>
<li><code>--bootstrap</code> 文件是 Nydus 镜像的元数据文件，包含了镜像的目录结构和文件信息。</li>
</ul></li>
<li>Nydus 定义了一套元数据（目录结构）与文件内容分离的，文件系统格式 Rafs，大致原理如下：
<img src="/image/rafs-format.png" alt="image" />

<ul>
<li>Image MetaData 对应 <code>--bootstrap</code> 文件。</li>
<li>Share Data Layer 对应 <code>--blob</code> 文件。</li>
<li>更多详见： <a href="https://github.com/dragonflyoss/nydus/blob/master/docs/nydus-design.md">官方文档</a></li>
</ul></li>
<li>镜像挂载：通过 <code>nydusd</code> 工具通过 fuse 挂载 Nydus 镜像。

<ul>
<li><code>--config</code>：指定 Nydus 镜像的 blob 存储位置（支持 oss、s3、本地文件）、 cache 配置、以及文件系统参数。</li>
<li><code>--mountpoint</code>：指定挂载点，须确保目录存在。</li>
<li><code>--bootstrap</code>：指定挂载的 Nydus 镜像的元数据文件路径。</li>
<li><code>--log-level</code>：指定日志级别。</li>
<li><code>--digest-validate</code>：是否开启校验。</li>
<li><code>--iostats-files</code>：是否开启 IO 统计。</li>
<li>除了 FUSE 方式外，还支持 EROFS、VirtioFS 方式挂载，详见下文。</li>
</ul></li>
</ul>

<h2 id="使用场景">使用场景</h2>

<h3 id="单层镜像构建和懒挂载">单层镜像构建和懒挂载</h3>

<p>下面将介绍如何使用 Nydus 构建单层镜像，并将其上传到 s3 上，然后在本地通过 Fuse 方式懒挂载该镜像。</p>

<p><strong>构建单层镜像并上传到 s3</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p <span style="color:#ae81ff">01</span>-basic
cd <span style="color:#ae81ff">01</span>-basic
<span style="color:#75715e"># https://github.com/dragonflyoss/nydus/blob/v2.3.0/contrib/nydusify/pkg/packer/backend.go#L60-L70</span>
tee backend-config.s3.json &gt; /dev/null <span style="color:#e6db74">&lt;&lt; EOF
</span><span style="color:#e6db74">{
</span><span style="color:#e6db74">  &#34;endpoint&#34;: &#34;s3.us-east-1.amazonaws.com&#34;,
</span><span style="color:#e6db74">  &#34;scheme&#34;: &#34;https&#34;,
</span><span style="color:#e6db74">  &#34;access_key_id&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">  &#34;access_key_secret&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">  &#34;bucket_name&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">  &#34;meta_prefix&#34;: &#34;nydus-demo/meta/&#34;,
</span><span style="color:#e6db74">  &#34;blob_prefix&#34;: &#34;nydus-demo/blob/&#34;,
</span><span style="color:#e6db74">  &#34;region&#34;: &#34;us-east-1&#34;
</span><span style="color:#e6db74">}
</span><span style="color:#e6db74">EOF</span>
nydusify --debug build --name image-basic-01-s3 --source-dir origin-root-dir --backend-push --backend-type s3 --backend-config-file backend-config.s3.json
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] found &#39;nydus-image&#39; binary at /usr/local/bin/nydus-image</span> 
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] start to build image from source directory &#34;origin-root-dir&#34;</span> 
<span style="color:#75715e"># DEBU[2025-02-26T08:51:53Z]      Command: /usr/local/bin/nydus-image create --bootstrap .nydus-build-output/image-basic-01-s3.meta --log-level warn --whiteout-spec oci --output-json .nydus-build-output/output.json --blob .nydus-build-output/image-basic-01-s3.blob --fs-version 6 --compressor zstd --chunk-size 0x100000 origin-root-dir</span> 
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] rename blob file into sha256 csum</span>            
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] start to push meta and blob to remote backend</span> 
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] push blob 58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28</span> 
<span style="color:#75715e"># DEBU[2025-02-26T08:51:53Z] uploaded blob nydus-demo/blob/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28 to s3 backend, costs 30.159619ms</span> 
<span style="color:#75715e"># DEBU[2025-02-26T08:51:53Z] uploaded blob nydus-demo/meta/image-basic-01-s3 to s3 backend, costs 60.856919ms</span> 
<span style="color:#75715e"># INFO[2025-02-26T08:51:53Z] successfully built Nydus image (bootstrap:&#39;https://xxx.s3.us-east-1.amazonaws.com/nydus-demo/meta/image-basic-01-s3&#39;, blob:&#39;https://xxx.s3.us-east-1.amazonaws.com/nydus-demo/blob/58048df580011d3a700076d8a1ec9ccaee6881f7feb8352aa298959ee8866f28&#39;)</span> </code></pre></div>
<p><strong>通过 Fuse 懒挂载 S3 的单层镜像</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">tee nydusd-config.s3.json &gt; /dev/null <span style="color:#e6db74">&lt;&lt; EOF
</span><span style="color:#e6db74">{
</span><span style="color:#e6db74">  &#34;device&#34;: {
</span><span style="color:#e6db74">    &#34;backend&#34;: {
</span><span style="color:#e6db74">      &#34;type&#34;: &#34;s3&#34;,
</span><span style="color:#e6db74">      &#34;config&#34;: {
</span><span style="color:#e6db74">        &#34;endpoint&#34;: &#34;s3.us-east-1.amazonaws.com&#34;,
</span><span style="color:#e6db74">        &#34;scheme&#34;: &#34;https&#34;,
</span><span style="color:#e6db74">        &#34;access_key_id&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">        &#34;access_key_secret&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">        &#34;bucket_name&#34;: &#34;xxx&#34;,
</span><span style="color:#e6db74">        &#34;meta_prefix&#34;: &#34;nydus-demo/meta/&#34;,
</span><span style="color:#e6db74">        &#34;blob_prefix&#34;: &#34;nydus-demo/blob/&#34;,
</span><span style="color:#e6db74">        &#34;region&#34;: &#34;us-east-1&#34;
</span><span style="color:#e6db74">      }
</span><span style="color:#e6db74">    },
</span><span style="color:#e6db74">    &#34;cache&#34;: {
</span><span style="color:#e6db74">      &#34;type&#34;: &#34;blobcache&#34;,
</span><span style="color:#e6db74">      &#34;config&#34;: {
</span><span style="color:#e6db74">        &#34;work_dir&#34;: &#34;cache&#34;
</span><span style="color:#e6db74">      }
</span><span style="color:#e6db74">    }
</span><span style="color:#e6db74">  },
</span><span style="color:#e6db74">  &#34;mode&#34;: &#34;direct&#34;,
</span><span style="color:#e6db74">  &#34;digest_validate&#34;: false,
</span><span style="color:#e6db74">  &#34;iostats_files&#34;: false,
</span><span style="color:#e6db74">  &#34;enable_xattr&#34;: true
</span><span style="color:#e6db74">}
</span><span style="color:#e6db74">EOF</span>
<span style="color:#75715e"># 下载元数据</span>
time wget https://xxx.s3.us-east-1.amazonaws.com/nydus-demo/meta/image-basic-01-s3
<span style="color:#75715e"># real    0m0.162s</span>
<span style="color:#75715e"># user    0m0.073s</span>
<span style="color:#75715e"># sys     0m0.042s</span>
<span style="color:#75715e"># 挂载</span>
mkdir -p mnt-s3
nydusd <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --config nydusd-config.s3.json <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --mountpoint mnt-s3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --bootstrap image-basic-01-s3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --log-level debug 
<span style="color:#75715e"># 打开新终端，切换到该目录，观察 mnt-s3 目录</span>
cd /root/nydus-demo/01-basic
tree mnt-s3/
<span style="color:#75715e"># mnt-s3/</span>
<span style="color:#75715e"># ├── dir_1</span>
<span style="color:#75715e"># │   ├── subdir_a</span>
<span style="color:#75715e"># │   │   └── file_a</span>
<span style="color:#75715e"># │   └── subdir_b</span>
<span style="color:#75715e"># ├── dir_2</span>
<span style="color:#75715e"># │   └── file_b</span>
<span style="color:#75715e"># ├── file_c</span>
<span style="color:#75715e"># └── file_c_ln -&gt; file_c</span>
cat mnt-local/dir_1/subdir_a/file_a
<span style="color:#75715e"># file_a</span>
cat mnt-local/dir_2/file_b
<span style="color:#75715e"># file_b</span>
cat mnt-local/file_c
<span style="color:#75715e"># file_c</span>
cat mnt-local/file_c_ln
<span style="color:#75715e"># file_c</span></code></pre></div>
<p><strong>通过基于 fscache 的内核文件系统 EROFS 懒挂载 S3 的单层镜像</strong></p>

<p>详见： <a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydus-fscache.md">Nydus EROFS fscache user guide</a></p>

<h3 id="转化和挂载-oci-镜像-多层">转化和挂载 OCI 镜像（多层）</h3>

<p>在 Docker/K8s 中，使用 Nydus 加速镜像拉取，本质上是：</p>

<ul>
<li>将标准的 OCI 镜像的每一层，通过上述单层镜像的方式，转化为 Nydus Blob 文件和元数据文件，并存储在 OCI Register，创建一个 Nydus 格式的镜像。</li>
<li>在启动容器，从 OCI Register 获取该镜像，并对每一层通过上述单层镜像的方式进行只读挂载，然后，通过 Overlayfs 构造出容器的 rootfs。</li>
</ul>

<p>具体命令本文不再赘述，可以参考：</p>

<ul>
<li><a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydusify.md">Nydusify</a>: 转换、检查、挂载多层 Nydus 镜像。</li>
<li><a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydusd.md">Nydusd</a>：挂载一个多层 Nydus 镜像。</li>
</ul>

<p>此外 Nydus 为了支持标准 OCI，除了标准的 Rafs 格式外，还提供了 Zran 格式以节省 OCI 镜像转换、Push 耗时以及 Register 存储成本，详见： <a href="https://github.com/dragonflyoss/nydus/blob/v2.3.0/docs/nydus-zran.md">Nydus zran artifact user guide</a>。</p>

<h2 id="性能测评">性能测评</h2>

<p>测试单层 Nydus 镜像挂载后文件系统性能。</p>

<p>基本信息如下：</p>

<ul>
<li>镜像信息：

<ul>
<li>大小 2.3G（挂载后 du 统计）</li>
<li>文件数 134567</li>
<li>目录数 67678</li>
</ul></li>
<li>磁盘性能：

<ul>
<li>IOPS 3000</li>
<li>吞吐量 125M</li>
</ul></li>
</ul>

<p>测试结果：</p>

<ul>
<li>元数据文件大小： 140M</li>
<li>数据（blob）文件大小： 811M</li>
<li>挂载速度： 0.029989s （不包含元数据文件下载）</li>
<li>递归 Copy 整个文件系统到本地磁盘（rsync -av &ndash;progress）：

<ul>
<li>localfs 挂载，开启缓存，首次 copy： 2m12.346s</li>
<li>localfs 挂载，开启缓存，二次 copy： 1m52.521s</li>
<li>s3 挂载，开启缓存，首次 copy： 3m13.644s</li>
<li>s3 挂载，开启缓存，二次 copy： 1m49.133s</li>
<li>本地裸目录（基准）： 2m53.854s</li>
</ul></li>
</ul>
]]></description></item><item><title>Linux 动态链接库详解（八） Node.js 语言</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-8-lang-nodejs/</link><pubDate>Sat, 28 Sep 2024 20:59:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-8-lang-nodejs/</guid><description type="html"><![CDATA[

<h2 id="安装">安装</h2>

<blockquote>
<p>v20.17.0</p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 Linux x64 官方版本</span>
mkdir -p ~/.local/share/nodejs
cd ~/.local/share/nodejs
wget https://nodejs.org/dist/v20.17.0/node-v20.17.0-linux-x64.tar.xz -O node-v20.17.0-linux-x64.tar.xz
tar -xf node-v20.17.0-linux-x64.tar.xz
rm -rf node-v20.17.0-linux-x64.tar.xz
<span style="color:#75715e"># 安装非官方版本 Linux x64 glibc 2.17</span>
wget https://unofficial-builds.nodejs.org/download/release/v20.17.0/node-v20.17.0-linux-x64-glibc-217.tar.xz -O node-v20.17.0-linux-x64-glibc-217.tar.xz
tar -xf node-v20.17.0-linux-x64-glibc-217.tar.xz
rm -rf node-v20.17.0-linux-x64-glibc-217.tar.xz
<span style="color:#75715e"># 安装非官方构建版本 Linux x64 musl</span>
wget https://unofficial-builds.nodejs.org/download/release/v20.17.0/node-v20.17.0-linux-x64-musl.tar.xz -O node-v20.17.0-linux-x64-musl.tar.xz
tar -xf node-v20.17.0-linux-x64-musl.tar.xz
rm -rf node-v20.17.0-linux-x64-musl.tar.xz</code></pre></div>
<h2 id="node-命令">node 命令</h2>

<p>验证脚本：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
echo <span style="color:#e6db74">&#39;=== 查看 node Linux x64 情况&#39;</span>
echo <span style="color:#e6db74">&#39;--- 执行 node -v&#39;</span>
~/.local/share/nodejs/node-v20.17.0-linux-x64/bin/node -v
echo <span style="color:#e6db74">&#39;--- lld 查看 node&#39;</span>
ldd ~/.local/share/nodejs/node-v20.17.0-linux-x64/bin/node
echo <span style="color:#e6db74">&#39;--- readelf 查看 node 动态库&#39;</span>
readelf -d ~/.local/share/nodejs/node-v20.17.0-linux-x64/bin/node | grep so
echo <span style="color:#e6db74">&#39;--- readelf 查看 node version&#39;</span>
readelf --version-info ~/.local/share/nodejs/node-v20.17.0-linux-x64/bin/node | grep -A <span style="color:#ae81ff">10000</span> <span style="color:#e6db74">&#39;.gnu.version_r&#39;</span>
echo


echo <span style="color:#e6db74">&#39;=== 查看 node Linux glibc 2.17 情况&#39;</span>
echo <span style="color:#e6db74">&#39;--- 执行 node -v&#39;</span>
~/.local/share/nodejs/node-v20.17.0-linux-x64-glibc-217/bin/node -v
echo <span style="color:#e6db74">&#39;--- lld 查看 node&#39;</span>
ldd ~/.local/share/nodejs/node-v20.17.0-linux-x64-glibc-217/bin/node
echo <span style="color:#e6db74">&#39;--- readelf 查看 node 动态库&#39;</span>
readelf -d ~/.local/share/nodejs/node-v20.17.0-linux-x64-glibc-217/bin/node | grep so
echo <span style="color:#e6db74">&#39;--- readelf 查看 node version&#39;</span>
readelf --version-info ~/.local/share/nodejs/node-v20.17.0-linux-x64-glibc-217/bin/node | grep -A <span style="color:#ae81ff">10000</span> <span style="color:#e6db74">&#39;.gnu.version_r&#39;</span>
echo


echo <span style="color:#e6db74">&#39;=== 查看 node Linux musl 情况&#39;</span>
echo <span style="color:#e6db74">&#39;--- 执行 node -v&#39;</span>
~/.local/share/nodejs/node-v20.17.0-linux-x64-musl/bin/node -v
echo <span style="color:#e6db74">&#39;--- lld 查看 node&#39;</span>
ldd ~/.local/share/nodejs/node-v20.17.0-linux-x64-musl/bin/node
echo <span style="color:#e6db74">&#39;--- readelf 查看 node 动态库&#39;</span>
readelf -d ~/.local/share/nodejs/node-v20.17.0-linux-x64-musl/bin/node | grep so
echo <span style="color:#e6db74">&#39;--- readelf 查看 node version&#39;</span>
readelf --version-info ~/.local/share/nodejs/node-v20.17.0-linux-x64-musl/bin/node | grep -A <span style="color:#ae81ff">10000</span> <span style="color:#e6db74">&#39;.gnu.version_r&#39;</span>
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 查看 node Linux x64 情况
--- 执行 node -v
v20.17.0
--- lld 查看 node
        linux-vdso.so.1 (0x00007ffe88d7a000)
        libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0d08070000)
        libstdc++.so.6 =&gt; /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f0d07e00000)
        libm.so.6 =&gt; /lib/x86_64-linux-gnu/libm.so.6 (0x00007f0d07d21000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f0d08050000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0d0804b000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d07b40000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0d0807d000)
--- readelf 查看 node 动态库
 0x0000000000000001 (NEEDED)             共享库：[libdl.so.2]
 0x0000000000000001 (NEEDED)             共享库：[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库：[libm.so.6]
 0x0000000000000001 (NEEDED)             共享库：[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libpthread.so.0]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
 0x0000000000000001 (NEEDED)             共享库：[ld-linux-x86-64.so.2]
--- readelf 查看 node version
Version needs section &#39;.gnu.version_r&#39; contains 7 entries:
 Addr: 0x0000000000b78048  Offset: 0x00778048  Link: 6 (.dynstr)
  000000: Version: 1  文件：ld-linux-x86-64.so.2  计数：1
  0x0010:   Name: GLIBC_2.2.5  标志：无  版本：38
  0x0020: Version: 1  文件：libgcc_s.so.1  计数：2
  0x0030:   Name: GCC_3.4  标志：无  版本：35
  0x0040:   Name: GCC_3.0  标志：无  版本：26
  0x0050: Version: 1  文件：libdl.so.2  计数：1
  0x0060:   Name: GLIBC_2.2.5  标志：无  版本：7
  0x0070: Version: 1  文件：libm.so.6  计数：2
  0x0080:   Name: GLIBC_2.27  标志：无  版本：28
  0x0090:   Name: GLIBC_2.2.5  标志：无  版本：6
  0x00a0: Version: 1  文件：libstdc++.so.6  计数：12
  0x00b0:   Name: GLIBCXX_3.4.14  标志：无  版本：29
  0x00c0:   Name: GLIBCXX_3.4.18  标志：无  版本：27
  0x00d0:   Name: CXXABI_1.3.5  标志：无  版本：25
  0x00e0:   Name: GLIBCXX_3.4.9  标志：无  版本：24
  0x00f0:   Name: CXXABI_1.3  标志：无  版本：23
  0x0100:   Name: CXXABI_1.3.7  标志：无  版本：20
  0x0110:   Name: GLIBCXX_3.4.15  标志：无  版本：19
  0x0120:   Name: GLIBCXX_3.4.20  标志：无  版本：13
  0x0130:   Name: CXXABI_1.3.9  标志：无  版本：11
  0x0140:   Name: GLIBCXX_3.4.11  标志：无  版本：10
  0x0150:   Name: GLIBCXX_3.4  标志：无  版本：8
  0x0160:   Name: GLIBCXX_3.4.21  标志：无  版本：4
  0x0170: Version: 1  文件：libpthread.so.0  计数：4
  0x0180:   Name: GLIBC_2.3.3  标志：无  版本：36
  0x0190:   Name: GLIBC_2.3.4  标志：无  版本：32
  0x01a0:   Name: GLIBC_2.3.2  标志：无  版本：21
  0x01b0:   Name: GLIBC_2.2.5  标志：无  版本：3
  0x01c0: Version: 1  文件：libc.so.6  计数：15
  0x01d0:   Name: GLIBC_2.12  标志：无  版本：37
  0x01e0:   Name: GLIBC_2.16  标志：无  版本：34
  0x01f0:   Name: GLIBC_2.9  标志：无  版本：33
  0x0200:   Name: GLIBC_2.6  标志：无  版本：31
  0x0210:   Name: GLIBC_2.14  标志：无  版本：30
  0x0220:   Name: GLIBC_2.17  标志：无  版本：22
  0x0230:   Name: GLIBC_2.3.4  标志：无  版本：18
  0x0240:   Name: GLIBC_2.3  标志：无  版本：17
  0x0250:   Name: GLIBC_2.10  标志：无  版本：16
  0x0260:   Name: GLIBC_2.28  标志：无  版本：15
  0x0270:   Name: GLIBC_2.4  标志：无  版本：14
  0x0280:   Name: GLIBC_2.3.2  标志：无  版本：12
  0x0290:   Name: GLIBC_2.7  标志：无  版本：9
  0x02a0:   Name: GLIBC_2.25  标志：无  版本：5
  0x02b0:   Name: GLIBC_2.2.5  标志：无  版本：2

=== 查看 node Linux glibc 2.17 情况
--- 执行 node -v
v20.17.0
--- lld 查看 node
        linux-vdso.so.1 (0x00007ffebbbfc000)
        libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f8e74b64000)
        libstdc++.so.6 =&gt; /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f8e74800000)
        libm.so.6 =&gt; /lib/x86_64-linux-gnu/libm.so.6 (0x00007f8e74a85000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f8e74a65000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f8e74a60000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e7461f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8e74b71000)
--- readelf 查看 node 动态库
 0x0000000000000001 (NEEDED)             共享库：[libdl.so.2]
 0x0000000000000001 (NEEDED)             共享库：[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库：[libm.so.6]
 0x0000000000000001 (NEEDED)             共享库：[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libpthread.so.0]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
 0x0000000000000001 (NEEDED)             共享库：[ld-linux-x86-64.so.2]
--- readelf 查看 node version
Version needs section &#39;.gnu.version_r&#39; contains 7 entries:
 Addr: 0x0000000000ba44c0  Offset: 0x007a44c0  Link: 6 (.dynstr)
  000000: Version: 1  文件：ld-linux-x86-64.so.2  计数：1
  0x0010:   Name: GLIBC_2.2.5  标志：无  版本：31
  0x0020: Version: 1  文件：libgcc_s.so.1  计数：2
  0x0030:   Name: GCC_3.4  标志：无  版本：27
  0x0040:   Name: GCC_3.0  标志：无  版本：26
  0x0050: Version: 1  文件：libm.so.6  计数：1
  0x0060:   Name: GLIBC_2.2.5  标志：无  版本：11
  0x0070: Version: 1  文件：libpthread.so.0  计数：4
  0x0080:   Name: GLIBC_2.3.3  标志：无  版本：28
  0x0090:   Name: GLIBC_2.3.4  标志：无  版本：20
  0x00a0:   Name: GLIBC_2.3.2  标志：无  版本：19
  0x00b0:   Name: GLIBC_2.2.5  标志：无  版本：9
  0x00c0: Version: 1  文件：libc.so.6  计数：13
  0x00d0:   Name: GLIBC_2.12  标志：无  版本：30
  0x00e0:   Name: GLIBC_2.16  标志：无  版本：25
  0x00f0:   Name: GLIBC_2.9  标志：无  版本：24
  0x0100:   Name: GLIBC_2.17  标志：无  版本：22
  0x0110:   Name: GLIBC_2.6  标志：无  版本：17
  0x0120:   Name: GLIBC_2.3.4  标志：无  版本：16
  0x0130:   Name: GLIBC_2.3  标志：无  版本：15
  0x0140:   Name: GLIBC_2.4  标志：无  版本：14
  0x0150:   Name: GLIBC_2.10  标志：无  版本：12
  0x0160:   Name: GLIBC_2.7  标志：无  版本：10
  0x0170:   Name: GLIBC_2.14  标志：无  版本：7
  0x0180:   Name: GLIBC_2.3.2  标志：无  版本：5
  0x0190:   Name: GLIBC_2.2.5  标志：无  版本：4
  0x01a0: Version: 1  文件：libdl.so.2  计数：1
  0x01b0:   Name: GLIBC_2.2.5  标志：无  版本：3
  0x01c0: Version: 1  文件：libstdc++.so.6  计数：9
  0x01d0:   Name: GLIBCXX_3.4.14  标志：无  版本：32
  0x01e0:   Name: GLIBCXX_3.4.18  标志：无  版本：29
  0x01f0:   Name: CXXABI_1.3.5  标志：无  版本：23
  0x0200:   Name: CXXABI_1.3.7  标志：无  版本：21
  0x0210:   Name: GLIBCXX_3.4.15  标志：无  版本：18
  0x0220:   Name: GLIBCXX_3.4.9  标志：无  版本：13
  0x0230:   Name: GLIBCXX_3.4.11  标志：无  版本：8
  0x0240:   Name: CXXABI_1.3  标志：无  版本：6
  0x0250:   Name: GLIBCXX_3.4  标志：无  版本：2

=== 查看 node Linux musl 情况
--- 执行 node -v
04-lang/04-nodejs/02-node-command.sh: 行 25: /home/rectcircle/.local/share/nodejs/node-v20.17.0-linux-x64-musl/bin/node: 无法执行：找不到需要的文
件
--- lld 查看 node
        linux-vdso.so.1 (0x00007ffe39ff2000)
        libstdc++.so.6 =&gt; /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007ffb2e800000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007ffb34151000)
        libc.musl-x86_64.so.1 =&gt; not found
        libm.so.6 =&gt; /lib/x86_64-linux-gnu/libm.so.6 (0x00007ffb34072000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffb2ea1f000)
        /lib/ld-musl-x86_64.so.1 =&gt; /lib64/ld-linux-x86-64.so.2 (0x00007ffb34179000)
--- readelf 查看 node 动态库
 0x0000000000000001 (NEEDED)             共享库：[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库：[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libc.musl-x86_64.so.1]
--- readelf 查看 node version
Version needs section &#39;.gnu.version_r&#39; contains 1 entry:
 Addr: 0x00000000007787c8  Offset: 0x007787c8  Link: 5 (.dynstr)
  000000: Version: 1  文件：libgcc_s.so.1  计数：2
  0x0010:   Name: GCC_3.4  标志：无  版本：3
  0x0020:   Name: GCC_3.0  标志：无  版本：2</pre></div>
<p>node 命令动态库依赖情况如下：</p>

<ul>
<li><p>node Linux x64 官方版：</p>

<ul>
<li>依赖 glibc 2.28 及以上版本 （libm.so.6、libpthread.so.0、libc.so.6、libdl.so.2）。</li>
<li>依赖 gcc 3.4 及以上版本 （libgcc_s.so.1）。</li>
<li>依赖 GLIBCXX_3.4.21、CXXABI_1.3.9（libstdc++.so.6）</li>
<li>更多详见 <a href="https://github.com/nodejs/node/blob/v20.17.0/BUILDING.md">nodejs/node - BUILDING.md</a></li>
</ul></li>

<li><p>node Linux x64 glibc 2.17 版：</p>

<ul>
<li>依赖 glibc 2.17 及以上版本 （libm.so.6、libpthread.so.0、libc.so.6、libdl.so.2）。</li>
<li>依赖 gcc 3.4 及以上版本 （libgcc_s.so.1）。</li>
<li>依赖 GLIBCXX_3.4.18、CXXABI_1.3.7（libstdc++.so.6）</li>
<li>详见 <a href="https://github.com/nodejs/unofficial-builds/">Node.js 非官方构建</a></li>
</ul></li>

<li><p>node Linux x64 musl 版：</p>

<ul>
<li>依赖 （libc.musl-x86_64.so.1）。</li>
<li>依赖 gcc 3.4 及以上版本 （libgcc_s.so.1）。</li>
<li>依赖 （libstdc++.so.6）。</li>
<li>lld 查看包含的 libc.so.6、 libm.so.6 是因为 libgcc_s.so.1、 libstdc++.so.6 时 gnu 版本间接引入的，在使用 musl 作为 libc 的环境中，不会如此。</li>
<li>详见 <a href="https://github.com/nodejs/unofficial-builds/">Node.js 非官方构建</a></li>
</ul></li>
</ul>

<h2 id="node-gyp">node-gyp</h2>

<p><a href="https://github.com/nodejs/node-gyp">node-gyp</a> 是 Node.js 提供用于和 C/C++/Rust 代码编译成 Node.js 插件（addon）的工具。</p>

<p>有很多 npm 包是使用 C/C++/Rust 实现的，如： <a href="https://www.npmjs.com/package/node-pty">node-pty</a>、 <a href="https://www.npmjs.com/package/sqlite3">sqlite3</a> 等。</p>

<p>本部分将介绍，依赖了 C/C++/Rust 实现的 npm 包后，进程动态链接库情况。</p>

<p>创建一个使用 npm 项目，该项目以来 node-pty 包。</p>

<p><code>04-lang/04-nodejs/a01-use-node-gyp/package.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;a01-use-node-gyp&#34;</span>,
  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;1.0.0&#34;</span>,
  <span style="color:#f92672">&#34;main&#34;</span>: <span style="color:#e6db74">&#34;index.js&#34;</span>,
  <span style="color:#f92672">&#34;scripts&#34;</span>: {
    <span style="color:#f92672">&#34;test&#34;</span>: <span style="color:#e6db74">&#34;echo \&#34;Error: no test specified\&#34; &amp;&amp; exit 1&#34;</span>
  },
  <span style="color:#f92672">&#34;author&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
  <span style="color:#f92672">&#34;license&#34;</span>: <span style="color:#e6db74">&#34;ISC&#34;</span>,
  <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
  <span style="color:#f92672">&#34;dependencies&#34;</span>: {
    <span style="color:#f92672">&#34;node-pty&#34;</span>: <span style="color:#e6db74">&#34;^1.0.0&#34;</span>
  }
}</code></pre></div>
<p><code>04-lang/04-nodejs/a01-use-node-gyp/index.js</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">process</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;process&#39;</span>);
<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">pid</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">pid</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">util</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;util&#39;</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">exec</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">util</span>.<span style="color:#a6e22e">promisify</span>(<span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;child_process&#39;</span>).<span style="color:#a6e22e">exec</span>);

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">child_process</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;child_process&#39;</span>);

<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">myOpendDylibs</span>(<span style="color:#a6e22e">params</span>) {

    <span style="color:#66d9ef">try</span> {
        <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">stdout</span>, <span style="color:#a6e22e">stderr</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">exec</span>(<span style="color:#e6db74">`cat /proc/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">pid</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/maps | grep -E &#39;\\.so|\\.node&#39;`</span>);
        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">stdout</span>) {
            <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">stdout</span>);
        }
        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">stderr</span>) {
            <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">stderr</span>);
        }
    } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>) {
        <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#a6e22e">e</span>);
    }
}

<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;=== before require node-pty&#39;</span>)
    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">myOpendDylibs</span>()
    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">pty</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;node-pty&#39;</span>);
    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;=== after require node-pty&#39;</span>)
    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">myOpendDylibs</span>()
}


<span style="color:#a6e22e">main</span>()
</code></pre></div>
<p>验证脚本：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>

cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd a01-use-node-gyp

echo <span style="color:#e6db74">&#39;=== 查看 pty.node&#39;</span>
file node_modules/node-pty/build/Release/pty.node

export PATH<span style="color:#f92672">=</span>$PATH:~/.local/share/nodejs/node-v20.17.0-linux-x64/bin
npm install
node index.js</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 查看 pty.node
node_modules/node-pty/build/Release/pty.node: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9465a142520c0efd2b0400990345067ebfba6117, not stripped

up to date, audited 3 packages in 4s

found 0 vulnerabilities
=== before require node-pty
7f3e4861f000-7f3e48645000 r--p 00000000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f3e48645000-7f3e4879a000 r-xp 00026000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f3e4879a000-7f3e487ed000 r--p 0017b000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f3e487ed000-7f3e487f1000 r--p 001ce000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f3e487f1000-7f3e487f3000 rw-p 001d2000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f3e48800000-7f3e48899000 r--p 00000000 08:01 1702837                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f3e48899000-7f3e4899a000 r-xp 00099000 08:01 1702837                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f3e4899a000-7f3e48a09000 r--p 0019a000 08:01 1702837                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f3e48a09000-7f3e48a14000 r--p 00209000 08:01 1702837                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f3e48a14000-7f3e48a17000 rw-p 00214000 08:01 1702837                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f3e48a1e000-7f3e48a1f000 r--p 00000000 08:01 1740467                    /usr/lib/x86_64-linux-gnu/libpthread.so.0
7f3e48a1f000-7f3e48a20000 r-xp 00001000 08:01 1740467                    /usr/lib/x86_64-linux-gnu/libpthread.so.0
7f3e48a20000-7f3e48a21000 r--p 00002000 08:01 1740467                    /usr/lib/x86_64-linux-gnu/libpthread.so.0
7f3e48a21000-7f3e48a22000 r--p 00002000 08:01 1740467                    /usr/lib/x86_64-linux-gnu/libpthread.so.0
7f3e48a22000-7f3e48a23000 rw-p 00003000 08:01 1740467                    /usr/lib/x86_64-linux-gnu/libpthread.so.0
7f3e48a23000-7f3e48a26000 r--p 00000000 08:01 1700620                    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f3e48a26000-7f3e48a3d000 r-xp 00003000 08:01 1700620                    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f3e48a3d000-7f3e48a41000 r--p 0001a000 08:01 1700620                    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f3e48a41000-7f3e48a42000 r--p 0001d000 08:01 1700620                    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f3e48a42000-7f3e48a43000 rw-p 0001e000 08:01 1700620                    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f3e48a43000-7f3e48a53000 r--p 00000000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f3e48a53000-7f3e48ac6000 r-xp 00010000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f3e48ac6000-7f3e48b20000 r--p 00083000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f3e48b20000-7f3e48b21000 r--p 000dc000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f3e48b21000-7f3e48b22000 rw-p 000dd000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f3e48b22000-7f3e48b23000 r--p 00000000 08:01 1740457                    /usr/lib/x86_64-linux-gnu/libdl.so.2
7f3e48b23000-7f3e48b24000 r-xp 00001000 08:01 1740457                    /usr/lib/x86_64-linux-gnu/libdl.so.2
7f3e48b24000-7f3e48b25000 r--p 00002000 08:01 1740457                    /usr/lib/x86_64-linux-gnu/libdl.so.2
7f3e48b25000-7f3e48b26000 r--p 00002000 08:01 1740457                    /usr/lib/x86_64-linux-gnu/libdl.so.2
7f3e48b26000-7f3e48b27000 rw-p 00003000 08:01 1740457                    /usr/lib/x86_64-linux-gnu/libdl.so.2
7f3e48b2f000-7f3e48b30000 r--p 00000000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f3e48b30000-7f3e48b55000 r-xp 00001000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f3e48b55000-7f3e48b5f000 r--p 00026000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f3e48b5f000-7f3e48b61000 r--p 00030000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f3e48b61000-7f3e48b63000 rw-p 00032000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

=== after require node-pty
7f3e45d63000-7f3e45d66000 r--p 00000000 00:2c 44834996                   /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/04-nodejs/a01-use-node-gyp/node_modules/node-pty/build/Release/pty.node
7f3e45d66000-7f3e45d6a000 r-xp 00003000 00:2c 44834996                   /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/04-nodejs/a01-use-node-gyp/node_modules/node-pty/build/Release/pty.node
7f3e45d6a000-7f3e45d6b000 r--p 00007000 00:2c 44834996                   /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/04-nodejs/a01-use-node-gyp/node_modules/node-pty/build/Release/pty.node
7f3e45d6b000-7f3e45d6c000 r--p 00007000 00:2c 44834996                   /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/04-nodejs/a01-use-node-gyp/node_modules/node-pty/build/Release/pty.node
7f3e45d6c000-7f3e45d6d000 rw-p 00008000 00:2c 44834996                   /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/04-nodejs/a01-use-node-gyp/node_modules/node-pty/build/Release/pty.node
# ... 和之前一致。</pre></div>
<p>可以看出：</p>

<ul>
<li><a href="https://nodejs.org/api/addons.html">Node.JS Addons</a> npm 包本质上，是一个动态链接库，其后缀是 <code>.node</code>。</li>
<li>在 <code>require</code> 引入 Addons 包后，实际上调用了 dlopen 类似的函数，加载了符合 Node.JS Addons 规范的动态链接库。</li>
<li>整体原理和 Python 的 扩展模块原理一致。</li>
</ul>

<p>一些常见的 Node.JS Addons 包示例： <a href="https://github.com/nodejs/node-gyp/blob/main/docs/binding.gyp-files-in-the-wild.md">docs/binding.gyp-files-in-the-wild.md</a>。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://nodejs.org/zh-cn/download/package-manager">Node.js 官方下载页面</a></li>
<li><a href="https://github.com/nodejs/node/blob/v20.17.0/BUILDING.md">nodejs/node - BUILDING.md</a></li>
<li><a href="https://github.com/nodejs/unofficial-builds/">Node.js 非官方构建</a></li>
<li><a href="https://nodejs.org/api/addons.html">Node.JS Addons</a></li>
</ul>
]]></description></item><item><title>Linux 动态链接库详解（七） Python 语言</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-7-lang-python/</link><pubDate>Tue, 17 Sep 2024 01:16:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-7-lang-python/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>在 debian 12 中， 可通过 <code>apt install -y python3</code> 来安装 <a href="https://packages.debian.org/bookworm/python3">Python3</a> （注意：安装后可以使用 <code>python3</code> 命令， python 命令不会导出）。</p>

<p>主要安装了如下目录和文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/bin/
    python3 -&gt; python3.11     # 软链 (python3-minimal)
    python3.11                # python 解释器 (python3.11-minimal)
/usr/lib/
    python3.11/
        encodings/            # (libpython3.11-minimal)
        ssl.py                # (libpython3.11-minimal)
        random.py             # (libpython3.11-minimal)
        ...                   # (libpython3.11-minimal)
        json/                 # (libpython3.11-stdlib)
        ...                   # (libpython3.11-stdlib)
        lib-dynload/
            _hashlib.cpython-311-x86_64-linux-gnu.so   # (libpython3.11-minimal)
            _ssl.cpython-311-x86_64-linux-gnu.so       # (libpython3.11-minimal)
            ...
            _bz2.cpython-311-x86_64-linux-gnu.so       # (libpython3.11-stdlib)
            _json.cpython-311-x86_64-linux-gnu.so      # (libpython3.11-stdlib)
            ...
/lib/x86_64-linux-gnu/
    ld-linux-x86-64.so.2      # (libc6)
    libc.so.6                 # (libc6)
    libm.so.6                 # (libc6)
    ...                       # (libc6)
    libexpat.so.1             # XML 解析 C 库 - 运行时库 (libexpat1)
    libz.so.1                 # 压缩库 - 运行时 (zlib1g)
    libcrypto.so.3            # (libssl3)
    libssl.so.3               # (libssl3)
    # (libbz2-1.0)
    # (libcrypt1)
    # (libdb5.3)
    # (libffi8)
    # (liblzma5)
    # (libncursesw6)
    # (libnsl2)
    # (libreadline8)
    # (libsqlite3-0)
    # (libtinfo6)
    # (libtirpc3)
    # (libuuid1)</pre></div>
<p>debian 12 上安装的 python 版本是 <a href="https://github.com/python/cpython">cpython</a> 3.11，主要分为两个部分： 解释器 和 标准库。</p>

<p>本文将介绍 Python 的解释器和标准库的动态库情况以及第三方 python 包情况。</p>

<h2 id="python-解释器">python 解释器</h2>

<p>验证脚本 <code>04-lang/03-python/01-python-interpreter.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>

echo <span style="color:#e6db74">&#39;=== 观察 python 解释器情况&#39;</span>
echo <span style="color:#e6db74">&#39;--- 使用 ldd 查看&#39;</span>
ldd <span style="color:#66d9ef">$(</span>which python3<span style="color:#66d9ef">)</span></code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 观察 python 解释器情况
--- 使用 ldd 查看
        linux-vdso.so.1 (0x00007ffee66ab000)
        libm.so.6 =&gt; /lib/x86_64-linux-gnu/libm.so.6 (0x00007f451a3c4000)
        libz.so.1 =&gt; /lib/x86_64-linux-gnu/libz.so.1 (0x00007f451a3a5000)
        libexpat.so.1 =&gt; /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f451a37a000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f451a199000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f451a4ab000)</pre></div>
<p>可以得出如下结论，Python 解释本身动态库依赖如下：</p>

<ul>
<li>glibc 的 libc.so.6、libm.so.6。</li>
<li>libexpat1 的 libexpat.so.1。</li>
<li>zlib1g 的 libz.so.1。</li>
</ul>

<h2 id="python-标准库">python 标准库</h2>

<p>验证脚本 <code>04-lang/03-python/01-python-std.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>

echo <span style="color:#e6db74">&#39;=== 观察 python 标准库情况&#39;</span>
python3 -c <span style="color:#e6db74">&#39;import os
</span><span style="color:#e6db74">pid = os.getpid()
</span><span style="color:#e6db74">print(&#34;--- 通过 /proc/%d/maps 查看动态库情况&#34; % pid)
</span><span style="color:#e6db74">os.system(&#34;cat /proc/%d/maps | grep .so&#34; % pid)
</span><span style="color:#e6db74">import ssl
</span><span style="color:#e6db74">import json
</span><span style="color:#e6db74">print(&#34;--- 导入 ssl 和 json 后 通过 /proc/%d/maps 查看动态库情况&#34; % pid)
</span><span style="color:#e6db74">os.system(&#34;cat /proc/%d/maps | grep .so&#34; % pid)
</span><span style="color:#e6db74">&#39;</span>
echo <span style="color:#e6db74">&#39;---  通过 ldd 查看 _ssl.cpython-311-x86_64-linux-gnu.so 情况&#39;</span>
ldd /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 观察 python 标准库情况
--- 通过 /proc/2352681/maps 查看动态库情况
7f96e4fe2000-7f96e5008000 r--p 00000000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e5008000-7f96e515d000 r-xp 00026000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e515d000-7f96e51b0000 r--p 0017b000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51b0000-7f96e51b4000 r--p 001ce000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51b4000-7f96e51b6000 rw-p 001d2000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51c3000-7f96e51c7000 r--p 00000000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51c7000-7f96e51e3000 r-xp 00004000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51e3000-7f96e51eb000 r--p 00020000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51eb000-7f96e51ed000 r--p 00028000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51ed000-7f96e51ee000 rw-p 0002a000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51ee000-7f96e51f1000 r--p 00000000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e51f1000-7f96e5204000 r-xp 00003000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e5204000-7f96e520b000 r--p 00016000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520b000-7f96e520c000 r--p 0001c000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520c000-7f96e520d000 rw-p 0001d000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520d000-7f96e521d000 r--p 00000000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e521d000-7f96e5290000 r-xp 00010000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e5290000-7f96e52ea000 r--p 00083000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52ea000-7f96e52eb000 r--p 000dc000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52eb000-7f96e52ec000 rw-p 000dd000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52f4000-7f96e52f5000 r--p 00000000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e52f5000-7f96e531a000 r-xp 00001000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e531a000-7f96e5324000 r--p 00026000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e5324000-7f96e5326000 r--p 00030000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e5326000-7f96e5328000 rw-p 00032000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffdb8391000-7ffdb8393000 r-xp 00000000 00:00 0                          [vdso]
--- 导入 ssl 和 json 后 通过 /proc/2352681/maps 查看动态库情况
7f96e4400000-7f96e44c5000 r--p 00000000 08:01 1704973                    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7f96e44c5000-7f96e473e000 r-xp 000c5000 08:01 1704973                    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7f96e473e000-7f96e481b000 r--p 0033e000 08:01 1704973                    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7f96e481b000-7f96e487c000 r--p 0041b000 08:01 1704973                    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7f96e487c000-7f96e487f000 rw-p 0047c000 08:01 1704973                    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7f96e4956000-7f96e4975000 r--p 00000000 08:01 1704974                    /usr/lib/x86_64-linux-gnu/libssl.so.3
7f96e4975000-7f96e49d3000 r-xp 0001f000 08:01 1704974                    /usr/lib/x86_64-linux-gnu/libssl.so.3
7f96e49d3000-7f96e49f2000 r--p 0007d000 08:01 1704974                    /usr/lib/x86_64-linux-gnu/libssl.so.3
7f96e49f2000-7f96e49fc000 r--p 0009c000 08:01 1704974                    /usr/lib/x86_64-linux-gnu/libssl.so.3
7f96e49fc000-7f96e4a00000 rw-p 000a6000 08:01 1704974                    /usr/lib/x86_64-linux-gnu/libssl.so.3
7f96e4f35000-7f96e4f37000 r--p 00000000 08:01 1719467                    /usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so
7f96e4f37000-7f96e4f3e000 r-xp 00002000 08:01 1719467                    /usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so
7f96e4f3e000-7f96e4f40000 r--p 00009000 08:01 1719467                    /usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so
7f96e4f40000-7f96e4f41000 r--p 0000a000 08:01 1719467                    /usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so
7f96e4f41000-7f96e4f42000 rw-p 0000b000 08:01 1719467                    /usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so
7f96e4f42000-7f96e4f54000 r--p 00000000 08:01 1718907                    /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
7f96e4f54000-7f96e4f5f000 r-xp 00012000 08:01 1718907                    /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
7f96e4f5f000-7f96e4f6d000 r--p 0001d000 08:01 1718907                    /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
7f96e4f6d000-7f96e4f6e000 r--p 0002a000 08:01 1718907                    /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
7f96e4f6e000-7f96e4f77000 rw-p 0002b000 08:01 1718907                    /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
7f96e4fe2000-7f96e5008000 r--p 00000000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e5008000-7f96e515d000 r-xp 00026000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e515d000-7f96e51b0000 r--p 0017b000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51b0000-7f96e51b4000 r--p 001ce000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51b4000-7f96e51b6000 rw-p 001d2000 08:01 1740455                    /usr/lib/x86_64-linux-gnu/libc.so.6
7f96e51c3000-7f96e51c7000 r--p 00000000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51c7000-7f96e51e3000 r-xp 00004000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51e3000-7f96e51eb000 r--p 00020000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51eb000-7f96e51ed000 r--p 00028000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51ed000-7f96e51ee000 rw-p 0002a000 08:01 1716378                    /usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10
7f96e51ee000-7f96e51f1000 r--p 00000000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e51f1000-7f96e5204000 r-xp 00003000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e5204000-7f96e520b000 r--p 00016000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520b000-7f96e520c000 r--p 0001c000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520c000-7f96e520d000 rw-p 0001d000 08:01 1702832                    /usr/lib/x86_64-linux-gnu/libz.so.1.2.13
7f96e520d000-7f96e521d000 r--p 00000000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e521d000-7f96e5290000 r-xp 00010000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e5290000-7f96e52ea000 r--p 00083000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52ea000-7f96e52eb000 r--p 000dc000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52eb000-7f96e52ec000 rw-p 000dd000 08:01 1740458                    /usr/lib/x86_64-linux-gnu/libm.so.6
7f96e52f4000-7f96e52f5000 r--p 00000000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e52f5000-7f96e531a000 r-xp 00001000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e531a000-7f96e5324000 r--p 00026000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e5324000-7f96e5326000 r--p 00030000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f96e5326000-7f96e5328000 rw-p 00032000 08:01 1740452                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffdb8391000-7ffdb8393000 r-xp 00000000 00:00 0                          [vdso]
---  通过 ldd 查看 _ssl.cpython-311-x86_64-linux-gnu.so 情况
        linux-vdso.so.1 (0x00007ffc653e6000)
        libssl.so.3 =&gt; /lib/x86_64-linux-gnu/libssl.so.3 (0x00007ff53a2b3000)
        libcrypto.so.3 =&gt; /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007ff539e00000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff539c1f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff53a39a000)</pre></div>
<p>可以得出如下结论：</p>

<ul>
<li>python 的部分标准库是用 C 语言实现的，被编译成了动态链接库位于 <code>/usr/lib/python3.11/lib-dynload</code> 目录下。</li>
<li>在执行 import 导入由 C 语言实现的标准库时，python 才会去加载动态链接库（dlopen）。</li>
<li>如上例：

<ul>
<li>json 库的实现为 <code>/usr/lib/python3.11/lib-dynload/_json.cpython-311-x86_64-linux-gnu.so</code>。</li>
<li>ssl 库的实现为 <code>/usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so</code>，并依赖 <code>libcrypto.so.3</code> 和 <code>libssl.so.3</code></li>
</ul></li>
</ul>

<p>这种由 C 语言实现的标准库的包的机制在 cpython 中被称为 &ldquo;扩展模块&rdquo;，在标准库中实现的被称为 &ldquo;内建模块&rdquo;，源码位于： <a href="https://github.com/python/cpython/tree/main/Modules">python/cpython - Modules</a>。</p>

<p>更多关于扩展模块的说明见下文。</p>

<h2 id="第三方包情况">第三方包情况</h2>

<p>Python 作为脚本语言性能羸弱，为了性能，很多 Python 三方包使用 C 语言实现，如 <a href="https://pypi.org/project/numpy/">numpy</a>。</p>

<p>这种使用 C 语言实现的 Python 包，正如上文所说，使用 C 语言实现的 Python 包被称为 &ldquo;扩展模块&rdquo;。</p>

<p>另外，很多软件供应商官方只提供 C 语言实现的动态链接库，如 <a href="https://pypi.org/project/mariadb/">mariadb</a>，这种场景也只能使用 &ldquo;扩展模块&rdquo; 方式提供对应的 Python 包。</p>

<p>Cpython 定义了 C 语言的 ABI 接口和动态库命名规则，如 <code>PyObject *PyInit_modulename(void)</code>，在 C 语言中实现这个函数，然后将其编译成动态链接库，放到 Python Path 中。然后就可以在 Python 中 import 这个模块了。在导入时，Cpython 会使用 dlopen 加载动态链接库，然后调用相关的 ABI 函数进行初始化。之后，在 Python 中调用这个模块的相关函数时，解释器会将 Python 函数参数数据结构转换为 C ABI 约定的 C 的数据结构，然后再调用 C 函数，C 函数返回值后，解释器会将结果转换为 Python 数据结构。</p>

<p>实现一个 &ldquo;扩展模块&rdquo; 包的方式有很多，如：</p>

<ul>
<li>不适用任何三方工具实现。</li>
<li>使用第三方工具，如 <a href="https://cython.org/">cython</a>、 <a href="https://cffi.readthedocs.io/">cffi</a>、<a href="https://www.swig.org/">SWIG</a>、<a href="https://numba.pydata.org/">Numba</a> 。</li>
</ul>

<p>关于扩展模块的 so 动态库查找，规则如下：</p>

<ul>
<li>查找路径为 Python Path （<code>sys.path</code>）。</li>
<li>so 文件命令必须符合 <a href="https://peps.python.org/pep-3149/">PEP 3149</a> （通过 <code>sysconfig.get_config_var('EXT_SUFFIX')</code> 可以查看）。</li>
<li>如果扩展模块的 so 还依赖其他 so，则按照 Linux 常规的动态库查找逻辑查找。</li>
</ul>

<p>更多详见： <a href="https://docs.python.org/zh-cn/3/extending/index.html">官方文档 - 扩展和嵌入 Python 解释器</a>。</p>
]]></description></item><item><title>Linux 动态链接库详解（六） Rust 语言</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-6-lang-rust/</link><pubDate>Mon, 16 Sep 2024 03:29:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-6-lang-rust/</guid><description type="html"><![CDATA[

<h2 id="rust-工具链">rust 工具链</h2>

<p>Rust 使用 rustup 命令来管理 rust 编译工具链，在支持的平台可通过如下命令一键安装 Rust 工具链（详见：<a href="https://www.rust-lang.org/learn/get-started">官网</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl --proto <span style="color:#e6db74">&#39;=https&#39;</span> --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre></div>
<p>如上命令后的默认行为如下（x86_64 Linux 系统为例，rust 1.81）：</p>

<ul>
<li>rust 工具链以及 rustup 元数据将保存到 <code>~/.rustup</code> 目录下。

<ul>
<li>默认将安装最新的 stable 的 default profile 的 工具链（<code>x86_64-unknown-linux-gnu</code>）。</li>
<li>保存到 <code>~/.rustup/toolchains</code> 目录下。</li>
</ul></li>
<li>cargo home 目录配置到 <code>~/.cargo</code>。</li>
<li>rust 工具链的 bin 将安装到 <code>~/.cargo/bin</code> 目录下，包含 <code>rustup</code>、<code>cargo</code>、<code>rustc</code> 等，值得一提的是：这些 bin 文件本质上时同一个文件，通过硬链接设置为不同的名字，这样做的好处是可以最大程度的共享代码，减少工具链体积，更容易管理。</li>
<li>修改当前用户 shell 的 profile 文件，如 <code>~/.bashrc</code>，添加 <code>. &quot;$HOME/.cargo/env&quot;</code>，将 <code>~/.cargo/bin</code> 添加到 <code>PATH</code> 环境变量中。</li>
</ul>

<p>执行如下脚本 <code>04-lang/02-rust/01-rust-toolchain.sh</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>

echo <span style="color:#e6db74">&#39;=== 观察 rustc 情况&#39;</span>
echo <span style="color:#e6db74">&#39;--- 查看 rustc 版本&#39;</span>
rustc -V
echo <span style="color:#e6db74">&#39;--- 查看 rustc 的动态库依赖&#39;</span>
ldd <span style="color:#66d9ef">$(</span>which rustc<span style="color:#66d9ef">)</span>
echo <span style="color:#e6db74">&#39;--- 查看 rustc 的 glibc 依赖情况&#39;</span>
readelf --version-info <span style="color:#66d9ef">$(</span>which rustc<span style="color:#66d9ef">)</span></code></pre></div>
<p>输入出下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 观察 rustc 情况
--- 查看 rustc 版本
rustc 1.81.0 (eeb90cda1 2024-09-04)
--- 查看 rustc 的动态库依赖
        linux-vdso.so.1 (0x00007ffc6d9ce000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f9c0339e000)
        librt.so.1 =&gt; /lib/x86_64-linux-gnu/librt.so.1 (0x00007f9c03399000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9c03394000)
        libm.so.6 =&gt; /lib/x86_64-linux-gnu/libm.so.6 (0x00007f9c032b5000)
        libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9c032b0000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9c0241f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9c033c6000)
--- 查看 rustc 的 glibc 依赖情况

Version symbols section &#39;.gnu.version&#39; contains 330 entries:
 Addr: 0x0000000000003750  Offset: 0x00003750  Link: 4 (.dynsym)
  000:   0 (*本地*)       2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  004:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   4 (GLIBC_2.9)  
  008:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  00c:   5 (GCC_3.0)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   5 (GCC_3.0)    
  010:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  014:   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   5 (GCC_3.0)       0 (*本地*)    
  018:   6 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  01c:   3 (GLIBC_2.2.5)   0 (*本地*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  020:   7 (GLIBC_2.3.2)   0 (*本地*)       3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  024:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   8 (GLIBC_2.7)     2 (GLIBC_2.2.5)
  028:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  02c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  030:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   7 (GLIBC_2.3.2)
  034:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  038:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   9 (GLIBC_2.12) 
  03c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  040:   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       2 (GLIBC_2.2.5)
  044:   5 (GCC_3.0)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  048:   8 (GLIBC_2.7)     2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  04c:   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       3 (GLIBC_2.2.5)
  050:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       2 (GLIBC_2.2.5)
  054:   2 (GLIBC_2.2.5)   6 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   5 (GCC_3.0)    
  058:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   a (GLIBC_2.17) 
  05c:   2 (GLIBC_2.2.5)   0 (*本地*)       b (GLIBC_2.4)     2 (GLIBC_2.2.5)
  060:   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   5 (GCC_3.0)    
  064:   2 (GLIBC_2.2.5)   c (GLIBC_2.3)     2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  068:   c (GLIBC_2.3)     3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  06c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   d (GCC_3.3)    
  070:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   5 (GCC_3.0)    
  074:   d (GCC_3.3)       3 (GLIBC_2.2.5)   4 (GLIBC_2.9)     3 (GLIBC_2.2.5)
  078:   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  07c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   7 (GLIBC_2.3.2)
  080:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  084:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       2 (GLIBC_2.2.5)
  088:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  08c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  090:   e (GLIBC_2.3.4)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  094:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  098:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  09c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0a0:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       f (GCC_4.2.0)  
  0a4:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0a8:   2 (GLIBC_2.2.5)  10 (GLIBC_2.15)    2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0ac:   3 (GLIBC_2.2.5)   d (GCC_3.3)       2 (GLIBC_2.2.5)   7 (GLIBC_2.3.2)
  0b0:   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)    
  0b4:   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0b8:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   e (GLIBC_2.3.4)   2 (GLIBC_2.2.5)
  0bc:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  0c0:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  0c4:   3 (GLIBC_2.2.5)  11 (GLIBC_2.2.5)   5 (GCC_3.0)       2 (GLIBC_2.2.5)
  0c8:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0cc:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   b (GLIBC_2.4)  
  0d0:   2 (GLIBC_2.2.5)   b (GLIBC_2.4)     3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0d4:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0d8:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)       3 (GLIBC_2.2.5)
  0dc:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  0e0:   2 (GLIBC_2.2.5)   5 (GCC_3.0)       2 (GLIBC_2.2.5)   b (GLIBC_2.4)  
  0e4:  12 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0e8:   3 (GLIBC_2.2.5)  13 (GLIBC_2.14)    3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0ec:   3 (GLIBC_2.2.5)   5 (GCC_3.0)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0f0:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  0f4:   2 (GLIBC_2.2.5)   b (GLIBC_2.4)     1 (*全局*)      1 (*全局*)   
  0f8:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  0fc:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  100:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  104:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  108:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  10c:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  110:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  114:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  118:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  11c:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  120:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  124:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  128:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  12c:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  130:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  134:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  138:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  13c:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  140:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  144:   1 (*全局*)      1 (*全局*)      1 (*全局*)      1 (*全局*)   
  148:   1 (*全局*)      1 (*全局*)   

Version needs section &#39;.gnu.version_r&#39; contains 6 entries:
 Addr: 0x00000000000039e8  Offset: 0x000039e8  Link: 5 (.dynstr)
  000000: Version: 1  文件：librt.so.1  计数：1
  0x0010:   Name: GLIBC_2.2.5  标志：无  版本：18
  0x0020: Version: 1  文件：libdl.so.2  计数：1
  0x0030:   Name: GLIBC_2.2.5  标志：无  版本：17
  0x0040: Version: 1  文件：libm.so.6  计数：1
  0x0050:   Name: GLIBC_2.2.5  标志：无  版本：6
  0x0060: Version: 1  文件：libgcc_s.so.1  计数：3
  0x0070:   Name: GCC_4.2.0  标志：无  版本：15
  0x0080:   Name: GCC_3.3  标志：无  版本：13
  0x0090:   Name: GCC_3.0  标志：无  版本：5
  0x00a0: Version: 1  文件：libpthread.so.0  计数：2
  0x00b0:   Name: GLIBC_2.12  标志：无  版本：9
  0x00c0:   Name: GLIBC_2.2.5  标志：无  版本：3
  0x00d0: Version: 1  文件：libc.so.6  计数：10
  0x00e0:   Name: GLIBC_2.14  标志：无  版本：19
  0x00f0:   Name: GLIBC_2.15  标志：无  版本：16
  0x0100:   Name: GLIBC_2.3.4  标志：无  版本：14
  0x0110:   Name: GLIBC_2.3  标志：无  版本：12
  0x0120:   Name: GLIBC_2.4  标志：无  版本：11
  0x0130:   Name: GLIBC_2.17  标志：无  版本：10
  0x0140:   Name: GLIBC_2.7  标志：无  版本：8
  0x0150:   Name: GLIBC_2.3.2  标志：无  版本：7
  0x0160:   Name: GLIBC_2.9  标志：无  版本：4
  0x0170:   Name: GLIBC_2.2.5  标志：无  版本：2</pre></div>
<p>可以看出 rust 的 <code>stable-x86_64-unknown-linux-gnu</code> (<code>1.81.0</code>) 工具链：</p>

<ul>
<li>依赖 2.17 及以上版本的 glibc。</li>
<li>依赖 4.2.0 及以上版本的 gcc。</li>
</ul>

<p>这些信息在官方文档 <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">rustc - 支持的平台</a> 中 <code>Tier xxx with Host Tools</code> 部分。</p>

<h2 id="rust-target">rust target</h2>

<p>上一小节介绍的是 rust 工具链自身的动态链接库，实际上 rust 工具链是支持交叉编译的（例如：在 Linux 平台可以编译出 Windows 平台的产物）。在官方文档 <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">rustc - 支持的平台</a> 中 <code>Tier xxx</code> 部分有关于支持的平台的详细介绍。</p>

<p>本文主要探索 rust target 为 <code>x86_64-unknown-linux-gnu</code> 以及 <code>x86_64-unknown-linux-musl</code> 这两种情况。</p>

<p><code>x86_64-unknown-linux-gnu</code> 已默认安装，<code>x86_64-unknown-linux-musl</code> 通过如下命令安装：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">rustup target add x86_64-unknown-linux-musl</code></pre></div>
<p>从如上命令的输出来看， <code>rustup target add</code> 命令主要安装的是 <code>rust-std</code> 库，安装到了 <code>~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/</code> 路径下。</p>

<p>通过另外通过 <code>rustc --print target-list</code> 可以查看所有可用的内置 target （<a href="https://doc.rust-lang.org/rustc/targets/built-in.html">rustc - 内建的目标</a>）。</p>

<p>这里最重要的是链接器的配置，可以通过如下命令查看（<a href="https://doc.rust-lang.org/rustc/targets/custom.html">rustc - 自定义目标</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># rustup toolchain install nightly</span>
rustc +nightly -Z unstable-options --target<span style="color:#f92672">=</span>x86_64-unknown-linux-gnu --print target-spec-json | grep linker-flavor
<span style="color:#75715e">#   &#34;linker-flavor&#34;: &#34;gnu-lld-cc&#34;,</span>
rustc +nightly -Z unstable-options --target<span style="color:#f92672">=</span>x86_64-unknown-linux-musl --print target-spec-json | grep linker-flavor
<span style="color:#75715e">#   &#34;linker-flavor&#34;: &#34;gnu-cc&#34;,</span></code></pre></div>
<h2 id="示例">示例</h2>

<h3 id="示例项目">示例项目</h3>

<p>使用 cargo new 命令创建一个示例项目：</p>

<p><code>04-lang/02-rust/a01-hello/Cargo.toml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">package</span>]
<span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;a01-hello&#34;</span>
<span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.1.0&#34;</span>
<span style="color:#a6e22e">edition</span> = <span style="color:#e6db74">&#34;2021&#34;</span>

[<span style="color:#a6e22e">dependencies</span>]</code></pre></div>
<p><code>04-lang/02-rust/a01-hello/Cargo.lock</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#75715e"># This file is automatically @generated by Cargo.</span>
<span style="color:#75715e"># It is not intended for manual editing.</span>
<span style="color:#a6e22e">version</span> = <span style="color:#ae81ff">3</span>

[[<span style="color:#a6e22e">package</span>]]
<span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;a01-hello&#34;</span>
<span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.1.0&#34;</span></code></pre></div>
<p><code>04-lang/02-rust/a01-hello/src/main.rs</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rs" data-lang="rs"><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() {
    println<span style="color:#f92672">!</span>(<span style="color:#e6db74">&#34;Hello, world!&#34;</span>);
}
</code></pre></div>
<h3 id="使用默认参数构建">使用默认参数构建</h3>

<p>根据 cargo 官方文档（<a href="https://doc.rust-lang.org/cargo/reference/config.html">The Cargo Book - Configuration</a>）可知，默认情况下 cargo build 的：</p>

<ul>
<li>默认 target 是当前机器的架构，即 rustup 安装的工具链默认的架构，在本示例中，就是 <code>x86_64-unknown-linux-gnu</code>。</li>
<li>默认 profile 是 dev。</li>
<li>默认的链接器为 gcc</li>
</ul>

<p>验证脚本 <code>04-lang/02-rust/02-rust-build-hello-default.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd a01-hello
rm -rf target


echo <span style="color:#e6db74">&#39;=== 构建 hello&#39;</span>
echo <span style="color:#e6db74">&#39;--- 构建 dev 产物&#39;</span>
cargo build
<span style="color:#75715e"># cargo build --profile dev</span>
echo <span style="color:#e6db74">&#39;执行产物&#39;</span>
target/debug/a01-hello
echo <span style="color:#e6db74">&#39;lld 查看产物&#39;</span>
ldd target/debug/a01-hello
echo <span style="color:#e6db74">&#39;readelf 查看产物版本&#39;</span>
readelf --version-info target/debug/a01-hello
echo <span style="color:#e6db74">&#39;readelf 查看产物的 RUNPATH&#39;</span>
readelf -d target/debug/a01-hello | grep RUNPATH

echo <span style="color:#e6db74">&#39;--- 构建 release 产物&#39;</span>
cargo build --release
echo <span style="color:#e6db74">&#39;执行产物&#39;</span>
target/release/a01-hello
echo <span style="color:#e6db74">&#39;lld 查看产物&#39;</span>
ldd target/release/a01-hello
echo <span style="color:#e6db74">&#39;readelf 查看产物版本&#39;</span>
readelf --version-info target/release/a01-hello
echo <span style="color:#e6db74">&#39;readelf 查看产物的 RUNPATH&#39;</span>
readelf -d target/release/a01-hello | grep RUNPATH
echo</code></pre></div>
<p>输出如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 构建 hello
--- 构建 dev 产物
   Compiling a01-hello v0.1.0 (/home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/
04-lang/02-rust/a01-hello)
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.22s
执行产物
Hello, world!
lld 查看产物
        linux-vdso.so.1 (0x00007ffe52f8e000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f161a202000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f161a021000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f161a280000)
readelf 查看产物版本

Version symbols section &#39;.gnu.version&#39; contains 67 entries:
 Addr: 0x0000000000000e56  Offset: 0x00000e56  Link: 6 (.dynsym)
  000:   0 (*本地*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  004:   3 (GLIBC_2.34)    2 (GLIBC_2.2.5)   4 (GCC_3.3)       2 (GLIBC_2.2.5)
  008:   1 (*全局*)      2 (GLIBC_2.2.5)   5 (GLIBC_2.32)    2 (GLIBC_2.2.5)
  00c:   6 (GLIBC_2.18)    7 (GLIBC_2.3.4)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  010:   8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)       8 (GCC_3.0)    
  014:   2 (GLIBC_2.2.5)   9 (GLIBC_2.33)    3 (GLIBC_2.34)    2 (GLIBC_2.2.5)
  018:   2 (GLIBC_2.2.5)   a (GCC_4.2.0)     2 (GLIBC_2.2.5)   8 (GCC_3.0)    
  01c:   3 (GLIBC_2.34)    2 (GLIBC_2.2.5)   b (GLIBC_2.3)     2 (GLIBC_2.2.5)
  020:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.34)    1 (*全局*)   
  024:   c (GLIBC_2.3)     d (GLIBC_2.14)    8 (GCC_3.0)       3 (GLIBC_2.34) 
  028:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  02c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   e (GLIBC_2.16)    f (GLIBC_2.28) 
  030:   8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)       2 (GLIBC_2.2.5)
  034:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  038:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   9 (GLIBC_2.33)    2 (GLIBC_2.2.5)
  03c:   1 (*全局*)      8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)    
  040:   3 (GLIBC_2.34)    8 (GCC_3.0)       2 (GLIBC_2.2.5)

Version needs section &#39;.gnu.version_r&#39; contains 3 entries:
 Addr: 0x0000000000000ee0  Offset: 0x00000ee0  Link: 7 (.dynstr)
  000000: Version: 1  文件：ld-linux-x86-64.so.2  计数：1
  0x0010:   Name: GLIBC_2.3  标志：无  版本：11
  0x0020: Version: 1  文件：libgcc_s.so.1  计数：3
  0x0030:   Name: GCC_4.2.0  标志：无  版本：10
  0x0040:   Name: GCC_3.0  标志：无  版本：8
  0x0050:   Name: GCC_3.3  标志：无  版本：4
  0x0060: Version: 1  文件：libc.so.6  计数：10
  0x0070:   Name: GLIBC_2.28  标志：无  版本：15
  0x0080:   Name: GLIBC_2.16  标志：无  版本：14
  0x0090:   Name: GLIBC_2.14  标志：无  版本：13
  0x00a0:   Name: GLIBC_2.3  标志：无  版本：12
  0x00b0:   Name: GLIBC_2.33  标志：无  版本：9
  0x00c0:   Name: GLIBC_2.3.4  标志：无  版本：7
  0x00d0:   Name: GLIBC_2.18  标志：无  版本：6
  0x00e0:   Name: GLIBC_2.32  标志：无  版本：5
  0x00f0:   Name: GLIBC_2.34  标志：无  版本：3
  0x0100:   Name: GLIBC_2.2.5  标志：无  版本：2
readelf 查看产物的 RUNPATH
--- 构建 release 产物
   Compiling a01-hello v0.1.0 (/home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/
04-lang/02-rust/a01-hello)   
   Finished `release` profile [optimized] target(s) in 5.68s
执行产物
Hello, world!
lld 查看产物
        linux-vdso.so.1 (0x00007fffd8faa000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fc0bee97000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc0becb6000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc0bef15000)
readelf 查看产物版本

Version symbols section &#39;.gnu.version&#39; contains 67 entries:
 Addr: 0x0000000000000e56  Offset: 0x00000e56  Link: 6 (.dynsym)
  000:   0 (*本地*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  004:   3 (GLIBC_2.34)    2 (GLIBC_2.2.5)   4 (GCC_3.3)       2 (GLIBC_2.2.5)
  008:   1 (*全局*)      2 (GLIBC_2.2.5)   5 (GLIBC_2.32)    2 (GLIBC_2.2.5)
  00c:   6 (GLIBC_2.18)    7 (GLIBC_2.3.4)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  010:   8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)       8 (GCC_3.0)    
  014:   2 (GLIBC_2.2.5)   9 (GLIBC_2.33)    3 (GLIBC_2.34)    2 (GLIBC_2.2.5)
  018:   2 (GLIBC_2.2.5)   a (GCC_4.2.0)     2 (GLIBC_2.2.5)   8 (GCC_3.0)    
  01c:   3 (GLIBC_2.34)    2 (GLIBC_2.2.5)   b (GLIBC_2.3)     2 (GLIBC_2.2.5)
  020:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.34)    1 (*全局*)   
  024:   c (GLIBC_2.3)     d (GLIBC_2.14)    8 (GCC_3.0)       3 (GLIBC_2.34) 
  028:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  02c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   e (GLIBC_2.16)    f (GLIBC_2.28) 
  030:   8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)       2 (GLIBC_2.2.5)
  034:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  038:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   9 (GLIBC_2.33)    2 (GLIBC_2.2.5)
  03c:   1 (*全局*)      8 (GCC_3.0)       2 (GLIBC_2.2.5)   8 (GCC_3.0)    
  040:   3 (GLIBC_2.34)    8 (GCC_3.0)       2 (GLIBC_2.2.5)

Version needs section &#39;.gnu.version_r&#39; contains 3 entries:
 Addr: 0x0000000000000ee0  Offset: 0x00000ee0  Link: 7 (.dynstr)
  000000: Version: 1  文件：ld-linux-x86-64.so.2  计数：1
  0x0010:   Name: GLIBC_2.3  标志：无  版本：11
  0x0020: Version: 1  文件：libgcc_s.so.1  计数：3
  0x0030:   Name: GCC_4.2.0  标志：无  版本：10
  0x0040:   Name: GCC_3.0  标志：无  版本：8
  0x0050:   Name: GCC_3.3  标志：无  版本：4
  0x0060: Version: 1  文件：libc.so.6  计数：10
  0x0070:   Name: GLIBC_2.28  标志：无  版本：15
  0x0080:   Name: GLIBC_2.16  标志：无  版本：14
  0x0090:   Name: GLIBC_2.14  标志：无  版本：13
  0x00a0:   Name: GLIBC_2.3  标志：无  版本：12
  0x00b0:   Name: GLIBC_2.33  标志：无  版本：9
  0x00c0:   Name: GLIBC_2.3.4  标志：无  版本：7
  0x00d0:   Name: GLIBC_2.18  标志：无  版本：6
  0x00e0:   Name: GLIBC_2.32  标志：无  版本：5
  0x00f0:   Name: GLIBC_2.34  标志：无  版本：3
  0x0100:   Name: GLIBC_2.2.5  标志：无  版本：2
readelf 查看产物的 RUNPATH</pre></div>
<p>说明： 该可执行文件只能在 glibc 版本大于等于 2.34 的 Linux 系统中运行。</p>

<h3 id="使用-musl-构建">使用 musl 构建</h3>

<p>若使用 musl 构建，需要做如下事情：</p>

<ul>
<li>使用 rustup 安装 musl 的 <code>rust-std</code> 组件，命令为： <code>rustup target add x86_64-unknown-linux-musl</code>。</li>

<li><p>下载 musl 交叉编译库，这里可以前往 <a href="https://musl.cc">https://musl.cc</a> 下载预编译好的包，命令如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://musl.cc/x86_64-linux-musl-cross.tgz -O x86_64-linux-musl-cross.tgz
mkdir -p ~/.local/share/musl
tar -xzf x86_64-linux-musl-cross.tgz -C ~/.local/share/musl
~/.local/share/musl/x86_64-linux-musl-cross/bin/x86_64-linux-musl-ld
rm -rf x86_64-linux-musl-cross.tgz</code></pre></div></li>
</ul>

<p>验证脚本 <code>04-lang/02-rust/03-rust-build-hello-musl.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd a01-hello
rm -rf target


echo <span style="color:#e6db74">&#39;=== 构建 hello target x86_64-unknown-linux-musl&#39;</span>
<span style="color:#75715e"># CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=$HOME/.local/share/musl/x86_64-linux-musl-cross/bin/x86_64-linux-musl-ld cargo build --target x86_64-unknown-linux-musl</span>
cargo build --target x86_64-unknown-linux-musl --config <span style="color:#e6db74">&#34;target.x86_64-unknown-linux-musl.linker=&#39;</span>$HOME<span style="color:#e6db74">/.local/share/musl/x86_64-linux-musl-cross/bin/x86_64-linux-musl-ld&#39;&#34;</span>
echo <span style="color:#e6db74">&#39;--- 执行产物&#39;</span>
target/x86_64-unknown-linux-musl/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- ldd 查看产物&#39;</span>
ldd target/x86_64-unknown-linux-musl/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- readelf 查看产物版本&#39;</span>
readelf --version-info target/x86_64-unknown-linux-musl/debug/a01-hello</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 构建 hello target x86_64-unknown-linux-musl
   Compiling a01-hello v0.1.0 (/home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/
04-lang/02-rust/a01-hello)                                                                                 Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.82s
--- 执行产物
Hello, world!
--- ldd 查看产物
        statically linked
--- readelf 查看产物版本

No version information found in this file.</pre></div>
<p>可以看出使用 musl 构建可以实现 rust 的静态编译。</p>

<h3 id="使用-glibc-2-31-构建">使用 glibc 2.31 构建</h3>

<p>上文 《使用默认参数构建》 中，使用默认参数构建，会使用系统默认的 glibc 版本（验证系统为 Debian 12 glibc 版本为 2.36）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 实测 gcc12 无法编译成功，需在 gcc10 环境中编译</span>
<span style="color:#75715e"># 这里使用 nix 安装 gcc10</span>
nix-env -iA nixpkgs.gcc10
export GLIBC_VERSION<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>.31
rm -rf /tmp/glibc-work <span style="color:#f92672">&amp;&amp;</span> mkdir -p /tmp/glibc-work
cd /tmp/glibc-work
wget https://ftp.gnu.org/gnu/glibc/glibc-$GLIBC_VERSION.tar.gz -O glibc-$GLIBC_VERSION.tar.gz
tar -xzf glibc-$GLIBC_VERSION.tar.gz
rm -rf glibc-$GLIBC_VERSION.tar.gz
mkdir build
cd build
mkdir -p $HOME/.local/share/glibc/glibc-$GLIBC_VERSION
../glibc-$GLIBC_VERSION/configure <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --prefix<span style="color:#f92672">=</span>$HOME/.local/share/glibc/glibc-$GLIBC_VERSION <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --host<span style="color:#f92672">=</span>x86_64-linux-gnu <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --build<span style="color:#f92672">=</span>x86_64-linux-gnu <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --disable-werror <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  CC<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gcc -m64&#34;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  CXX<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;g++ -m64&#34;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  CFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-O2&#34;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  CXXFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-O2&#34;</span>
make
make install
rm -rf /tmp/glibc-work</code></pre></div>
<p>也可以前往 <a href="https://toolchains.bootlin.com/">toolchains.bootlin.com</a> 下载预编译版本（x86-64 只有 2.34 之后的版本）（未验证）。</p>

<p>安装 patchelf 工具，用于修改动态库的 RUNPATH。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.patchelf</code></pre></div>
<p>验证脚本 <code>04-lang/02-rust/04-rust-build-hello-glibc2.31.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd a01-hello
rm -rf target


echo <span style="color:#e6db74">&#39;=== 构建 hello use glibc 2.31&#39;</span>

<span style="color:#75715e"># cargo build --target x86_64-unknown-linux-gnu --config &#34;target.x86_64-unknown-linux-gnu.rustflags=&#39;-L native=$HOME/.local/share/glibc/glibc-2.31/lib&#39;&#34;</span>
cargo build --config <span style="color:#e6db74">&#34;build.rustflags=&#39;-L native=</span>$HOME<span style="color:#e6db74">/.local/share/glibc/glibc-2.31/lib&#39;&#34;</span>
echo <span style="color:#e6db74">&#39;--- 执行产物&#39;</span>
target/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- ldd 查看产物&#39;</span>
ldd target/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- readelf 查看产物的 RUNPATH&#39;</span>
readelf -d target/debug/a01-hello | grep RUNPATH
echo <span style="color:#e6db74">&#39;--- readelf 查看产物版本&#39;</span>
readelf --version-info target/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- patchelf 移除 RUNPATH 设置并设置动态链接器&#39;</span>
patchelf --remove-rpath target/debug/a01-hello
patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 target/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- ldd 查看产物&#39;</span>
ldd target/debug/a01-hello
echo <span style="color:#e6db74">&#39;--- 执行产物&#39;</span>
target/debug/a01-hello</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 构建 hello use glibc 2.31
   Compiling a01-hello v0.1.0 (/home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/02-rust/a01-hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.72s
--- 执行产物
target/debug/a01-hello: /home/rectcircle/.local/share/glibc/glibc-2.31/lib/libc.so.6: version `GLIBC_2.34&#39; not found (required by /nix/store/6h8sqf7pgrrkgp2rwsh51w915dhxs8z2-gcc-10.5.0-lib/lib/libgcc_s.so.1)
--- ldd 查看产物
target/debug/a01-hello: /home/rectcircle/.local/share/glibc/glibc-2.31/lib/libc.so.6: version `GLIBC_2.34&#39; not found (required by /nix/store/6h8sqf7pgrrkgp2rwsh51w915dhxs8z2-gcc-10.5.0-lib/lib/libgcc_s.so.1)
        linux-vdso.so.1 (0x00007ffed1f50000)
        libgcc_s.so.1 =&gt; /nix/store/6h8sqf7pgrrkgp2rwsh51w915dhxs8z2-gcc-10.5.0-lib/lib/libgcc_s.so.1 (0x00007f6e6f072000)
        libpthread.so.0 =&gt; /home/rectcircle/.local/share/glibc/glibc-2.31/lib/libpthread.so.0 (0x00007f6e6f051000)
        libdl.so.2 =&gt; /home/rectcircle/.local/share/glibc/glibc-2.31/lib/libdl.so.2 (0x00007f6e6f04c000)
        libc.so.6 =&gt; /home/rectcircle/.local/share/glibc/glibc-2.31/lib/libc.so.6 (0x00007f6e6ee8f000)
        /nix/store/3dyw8dzj9ab4m8hv5dpyx7zii8d0w6fi-glibc-2.39-52/lib/ld-linux-x86-64.so.2 =&gt; /lib64/ld-linux-x86-64.so.2 (0x00007f6e6f0e5000)
--- readelf 查看产物的 RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [/home/rectcircle/.local/share/glibc/glibc-2.31/lib:/nix/store/3dyw8dzj9ab4m8hv5dpyx7zii8d0w6fi-glibc-2.39-52/lib:/nix/store/6h8sqf7pgrrkgp2rwsh51w915dhxs8z2-gcc-10.5.0-lib/lib]
--- readelf 查看产物版本

Version symbols section &#39;.gnu.version&#39; contains 67 entries:
 Addr: 0x00000000000010c4  Offset: 0x000010c4  Link: 6 (.dynsym)
  000:   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  004:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   4 (GCC_3.3)       3 (GLIBC_2.2.5)
  008:   3 (GLIBC_2.2.5)   1 (*global*)      2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  00c:   5 (GLIBC_2.18)    6 (GLIBC_2.3.4)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  010:   7 (GCC_3.0)       3 (GLIBC_2.2.5)   7 (GCC_3.0)       7 (GCC_3.0)    
  014:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  018:   2 (GLIBC_2.2.5)   8 (GCC_4.2.0)     3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  01c:   7 (GCC_3.0)       3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   9 (GLIBC_2.3)  
  020:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  024:   1 (*global*)      a (GLIBC_2.3)     b (GLIBC_2.14)    7 (GCC_3.0)    
  028:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  02c:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   c (GLIBC_2.16)    d (GLIBC_2.28) 
  030:   3 (GLIBC_2.2.5)   7 (GCC_3.0)       2 (GLIBC_2.2.5)   7 (GCC_3.0)    
  034:   3 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)
  038:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  03c:   2 (GLIBC_2.2.5)   1 (*global*)      7 (GCC_3.0)       2 (GLIBC_2.2.5)
  040:   7 (GCC_3.0)       7 (GCC_3.0)       2 (GLIBC_2.2.5)

Version needs section &#39;.gnu.version_r&#39; contains 4 entries:
 Addr: 0x0000000000001150  Offset: 0x00001150  Link: 7 (.dynstr)
  000000: Version: 1  File: ld-linux-x86-64.so.2  Cnt: 1
  0x0010:   Name: GLIBC_2.3  Flags: none  Version: 9
  0x0020: Version: 1  File: libgcc_s.so.1  Cnt: 3
  0x0030:   Name: GCC_4.2.0  Flags: none  Version: 8
  0x0040:   Name: GCC_3.0  Flags: none  Version: 7
  0x0050:   Name: GCC_3.3  Flags: none  Version: 4
  0x0060: Version: 1  File: libpthread.so.0  Cnt: 1
  0x0070:   Name: GLIBC_2.2.5  Flags: none  Version: 3
  0x0080: Version: 1  File: libc.so.6  Cnt: 7
  0x0090:   Name: GLIBC_2.28  Flags: none  Version: 13
  0x00a0:   Name: GLIBC_2.16  Flags: none  Version: 12
  0x00b0:   Name: GLIBC_2.14  Flags: none  Version: 11
  0x00c0:   Name: GLIBC_2.3  Flags: none  Version: 10
  0x00d0:   Name: GLIBC_2.3.4  Flags: none  Version: 6
  0x00e0:   Name: GLIBC_2.18  Flags: none  Version: 5
  0x00f0:   Name: GLIBC_2.2.5  Flags: none  Version: 2
--- patchelf 移除 RUNPATH 设置并设置动态链接器
--- ldd 查看产物
        linux-vdso.so.1 (0x00007ffc401f5000)
        libgcc_s.so.1 =&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f0c9d00d000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0c9d008000)
        libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0c9d003000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0c9ce22000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0c9d3c2000)
--- 执行产物
Hello, world!</pre></div>
<p>分析说明：</p>

<ul>
<li>这里使用 nix 安装了 gcc10，目前 nix 的 gcc10 是使用 glibc 2.39 编译的，因此 <code>libgcc_s.so.1</code> 最小 glibc 依赖是 glibc 2.34。</li>
<li>然后使用 nix 的 gcc10 编译了 glibc 2.31。</li>
<li>然后使用 cargo 编译 rust 项目，此时配置如下：

<ul>
<li>使用 PATH 环境变量中 nix 的 gcc10 作为链接器。</li>
<li>通过 <code>&quot;build.rustflags='-L native=$HOME/.local/share/glibc/glibc-2.31/lib'&quot;</code> 参数指定了 glibc 。</li>
</ul></li>
<li>因为使用了 nix 的 gcc10 作为链接器，nix 的 gcc 会在编译产物中配置 rpath，动态库都能正确找到。</li>
<li>因此 rust 项目的编译产物中动态库依赖情况是：

<ul>
<li><code>libgcc_s.so.1</code> 是 nix 的 gcc10 中的。</li>
<li>其他动态链接库都是 glibc 2.31 中。</li>
</ul></li>
<li>执行时，因为 <code>libgcc_s.so.1</code> 依赖更新版本的 glibc 2.34，但是加载的是 2.31，所以运行不起来。</li>
<li>通过 <code>pathelf</code> 去除 rpath，此时，可执行文件将会 debian 12 系统路径中找 <code>libgcc_s.so.1</code> 和 glibc，此时版本是满足约束的，因此可以正常执行。</li>
<li>最后，通过此方式编译出的可执行文件可以在 glibc 版本 &gt;= 2.28 以上的 Linux 平台中运行，而之前使用构建参数构建的可执行文件只能在 glibc 版本大于等于 2.34 的 Linux 系统中运行。</li>
</ul>

<p>最后，使用 <code>nix-env --uninstall gcc-wrapper-10.5.0</code> 清理现场。</p>

<h2 id="与动态库有关配置总结">与动态库有关配置总结</h2>

<p><code>~.cargo/config.toml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">build</span>]
<span style="color:#a6e22e">target</span> = <span style="color:#e6db74">&#34;triple&#34;</span>             <span style="color:#75715e"># 构建目标架构，默认为当前操作系统的架构如 x86_64-unknown-linux-gnu，可以配置为 x86_64-unknown-linux-musl 实现静态链接。也可以通过 cargo build --target 或 `CARGO_BUILD_TARGE` 环境变量指定。</span>
<span style="color:#a6e22e">rustflags</span> = [<span style="color:#e6db74">&#34;…&#34;</span>, <span style="color:#e6db74">&#34;…&#34;</span>]        <span style="color:#75715e"># 传递给 rustc 的参数可以是字符串或字符串数组，可以 &#34;-L native=/path/to/lib&#34; 指定动态库查找路径。也可以通过 cargo build --config &#34;build.rustflags=&#39;xxx&#39;&#34; 或环境变量 `CARGO_BUILD_RUSTFLAGS`、`CARGO_ENCODED_RUSTFLAGS`、`RUSTFLAGS` 指定。</span>


[<span style="color:#a6e22e">target</span>.<span style="color:#960050;background-color:#1e0010">&lt;</span><span style="color:#a6e22e">triple</span><span style="color:#960050;background-color:#1e0010">&gt;</span>]         <span style="color:#75715e"># 构建目标架构的详细配置，如 x86_64-unknown-linux-musl。</span>
<span style="color:#a6e22e">linker</span> = <span style="color:#e6db74">&#34;…&#34;</span>              <span style="color:#75715e"># 链接器的路径，一般情况下默认为 PATH 中的 gcc，配置 musl 时，可以前往 musl.cc 下载交叉编译工具链，并将 x86_64-linux-musl-ld 的绝对路径配置到这里。也可以通过 cargo build --config &#34;target.x86_64-unknown-linux-musl.linker=&#39;xxx&#39;&#34; 或环境变量 `CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER` 指定。</span>
<span style="color:#a6e22e">rustflags</span> = [<span style="color:#e6db74">&#34;…&#34;</span>, <span style="color:#e6db74">&#34;…&#34;</span>]    <span style="color:#75715e"># 用途和 build.rustflags 一样。也可以通过 cargo build --config &#34;target.x86_64-unknown-linux-musl.rustflags=&#39;xxx&#39;&#34; 或环境变量 `CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS` 指定。</span></code></pre></div>
<h2 id="参考">参考</h2>

<ul>
<li><a href="https://www.rust-lang.org/learn/get-started">官网 - Get Started</a></li>
<li><a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">rustc - 支持的平台</a></li>
<li><a href="https://doc.rust-lang.org/rustc/targets/built-in.html">rustc - 内建的目标</a></li>
<li><a href="https://doc.rust-lang.org/rustc/targets/custom.html">rustc - 自定义目标</a></li>
<li><a href="https://rust-lang.github.io/rustup-components-history/x86_64-unknown-linux-gnu.html">rustup-components-history</a></li>
<li><a href="https://doc.rust-lang.org/cargo/reference/config.html">The Cargo Book - Configuration</a></li>
<li><a href="https://doc.rust-lang.org/rustc/codegen-options/index.html#linker">The rustc book - Codegen Options - linker</a></li>
<li><a href="https://github.com/jueve/build-glibc">Build glibc from source</a></li>
<li><a href="https://toolchains.bootlin.com/">toolchains.bootlin.com</a></li>
<li><a href="https://ftp.gnu.org/gnu/glibc/">glibc ftp</a></li>
<li><a href="https://github.com/NixOS/patchelf">pathelf</a></li>
<li><a href="https://www.cnblogs.com/turingguo/p/17406999.html">Rust交叉编译arm64 linux环境设置</a></li>
</ul>
]]></description></item><item><title>Linux 动态链接库详解（五） Golang 语言</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-5-lang-go/</link><pubDate>Sat, 14 Sep 2024 11:20:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-5-lang-go/</guid><description type="html"><![CDATA[

<h2 id="标准库中的动态库">标准库中的动态库</h2>

<h3 id="默认行为">默认行为</h3>

<p>在之前的文章 <a href="/posts/go-static-compile-and-cgo/">《Go 静态编译 和 CGO》</a> 介绍过，Go 标准库的 <code>os/user</code> 和 <code>net</code> 包有部分函数的实现有 C 和纯 Go 两个版本，在构建时，编译期选择那个实现的默认行为如下：</p>

<ul>
<li>当项目的标准库中没有引入这两个包时，且项目不包含任何 CGO 代码时，默认将静态编译，此时 lld 查看产物将看不到任何动态链接库信息。</li>
<li>当我们的项目的引入了如上两个包时，且当前环境包含 gcc 时，将会使用 C 的实现，此时 ldd 查看产物将看到存在动态链接库的依赖。</li>
</ul>

<h3 id="示例">示例</h3>

<h4 id="验证代码">验证代码</h4>

<p>下面是示例代码：</p>

<p><code>04-lang/01-go/01-std-nonecgo/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Hello, World&#34;</span>)
}</code></pre></div>
<p><code>04-lang/01-go/02-std-cgo/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">u</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Lookup</span>(<span style="color:#e6db74">&#34;root&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;root uid: %s\n&#34;</span>, <span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">Uid</span>)
	<span style="color:#a6e22e">addrs</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">LookupHost</span>(<span style="color:#e6db74">&#34;localhost&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;localhost addrs: %v\n&#34;</span>, <span style="color:#a6e22e">addrs</span>)
}</code></pre></div>
<h4 id="验证脚本">验证脚本</h4>

<p>验证脚本 <code>04-lang/01-go/01-build-dep-std.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>

echo <span style="color:#e6db74">&#39;=== 编译 01-std-nonecgo&#39;</span>
cd ./01-std-nonecgo
go build -o main ./
echo <span style="color:#e6db74">&#39;--- ldd 输出如下&#39;</span>
ldd ./main
cd ../
echo

echo <span style="color:#e6db74">&#39;=== 编译 02-std-cgo&#39;</span>
cd ./02-std-cgo
go clean -cache <span style="color:#f92672">&amp;&amp;</span> go build -o main ./
echo <span style="color:#e6db74">&#39;--- ldd 输出如下&#39;</span>
ldd ./main
echo <span style="color:#e6db74">&#39;--- readelf -r 输出如下&#39;</span>
readelf -r ./main
cd ../
echo</code></pre></div>
<h4 id="输出">输出</h4>

<ul>
<li><p>在 <code>go1.23.1</code>、<code>gcc12</code>、<code>glibc2.36</code>、<code>debian12</code> 环境下上述脚本，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 编译 01-std-nonecgo
--- ldd 输出如下
        不是动态可执行文件

=== 编译 02-std-cgo
--- ldd 输出如下
        linux-vdso.so.1 (0x00007fffe5f25000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7f48fa9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7f49192000)
--- readelf -r 输出如下

重定位节 &#39;.rela&#39; at offset 0x145288 contains 1 entry:
偏移量          信息           类型           符号值        符号名称 + 加数
0000005eb4e8  000e00000006 R_X86_64_GLOB_DAT 0000000000000000 stderr@GLIBC_2.2.5 + 0

重定位节 &#39;.rela.plt&#39; at offset 0x1452a0 contains 42 entries:
偏移量          信息           类型           符号值        符号名称 + 加数
0000005eb398  000400000007 R_X86_64_JUMP_SLO 0000000000000000 __errno_location@GLIBC_2.2.5 + 0
0000005eb3a0  000500000007 R_X86_64_JUMP_SLO 0000000000000000 getaddrinfo@GLIBC_2.2.5 + 0
0000005eb3a8  000600000007 R_X86_64_JUMP_SLO 0000000000000000 free@GLIBC_2.2.5 + 0
0000005eb3b0  000700000007 R_X86_64_JUMP_SLO 0000000000000000 freeaddrinfo@GLIBC_2.2.5 + 0
0000005eb3b8  000800000007 R_X86_64_JUMP_SLO 0000000000000000 gai_strerror@GLIBC_2.2.5 + 0
0000005eb3c0  000900000007 R_X86_64_JUMP_SLO 0000000000000000 getgrgid_r@GLIBC_2.2.5 + 0
0000005eb3c8  000a00000007 R_X86_64_JUMP_SLO 0000000000000000 getgrnam_r@GLIBC_2.2.5 + 0
0000005eb3d0  000b00000007 R_X86_64_JUMP_SLO 0000000000000000 getpwnam_r@GLIBC_2.2.5 + 0
0000005eb3d8  000c00000007 R_X86_64_JUMP_SLO 0000000000000000 getpwuid_r@GLIBC_2.2.5 + 0
0000005eb3e0  000d00000007 R_X86_64_JUMP_SLO 0000000000000000 sysconf@GLIBC_2.2.5 + 0
0000005eb3e8  000f00000007 R_X86_64_JUMP_SLO 0000000000000000 fwrite@GLIBC_2.2.5 + 0
0000005eb3f0  001000000007 R_X86_64_JUMP_SLO 0000000000000000 vfprintf@GLIBC_2.2.5 + 0
0000005eb3f8  001100000007 R_X86_64_JUMP_SLO 0000000000000000 fputc@GLIBC_2.2.5 + 0
0000005eb400  001200000007 R_X86_64_JUMP_SLO 0000000000000000 abort@GLIBC_2.2.5 + 0
0000005eb408  001300000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_mutex_lock@GLIBC_2.2.5 + 0
0000005eb410  001400000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_cond_wait@GLIBC_2.3.2 + 0
0000005eb418  001500000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_mutex_unlock@GLIBC_2.2.5 + 0
0000005eb420  001600000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_key_create@GLIBC_2.34 + 0
0000005eb428  001700000007 R_X86_64_JUMP_SLO 0000000000000000 fprintf@GLIBC_2.2.5 + 0
0000005eb430  001800000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_setspecific@GLIBC_2.34 + 0
0000005eb438  001900000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_cond_broadcast@GLIBC_2.3.2 + 0
0000005eb440  001a00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_create@GLIBC_2.34 + 0
0000005eb448  001b00000007 R_X86_64_JUMP_SLO 0000000000000000 nanosleep@GLIBC_2.2.5 + 0
0000005eb450  001c00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_detach@GLIBC_2.34 + 0
0000005eb458  001d00000007 R_X86_64_JUMP_SLO 0000000000000000 strerror@GLIBC_2.2.5 + 0
0000005eb460  001e00000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0
0000005eb468  001f00000007 R_X86_64_JUMP_SLO 0000000000000000 sigfillset@GLIBC_2.2.5 + 0
0000005eb470  002000000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_sigmask@GLIBC_2.32 + 0
0000005eb478  002100000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_init@GLIBC_2.2.5 + 0
0000005eb480  002200000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_gets[...]@GLIBC_2.34 + 0
0000005eb488  002300000007 R_X86_64_JUMP_SLO 0000000000000000 mmap@GLIBC_2.2.5 + 0
0000005eb490  002400000007 R_X86_64_JUMP_SLO 0000000000000000 munmap@GLIBC_2.2.5 + 0
0000005eb498  002500000007 R_X86_64_JUMP_SLO 0000000000000000 setenv@GLIBC_2.2.5 + 0
0000005eb4a0  002600000007 R_X86_64_JUMP_SLO 0000000000000000 unsetenv@GLIBC_2.2.5 + 0
0000005eb4a8  002700000007 R_X86_64_JUMP_SLO 0000000000000000 sigemptyset@GLIBC_2.2.5 + 0
0000005eb4b0  002800000007 R_X86_64_JUMP_SLO 0000000000000000 sigaddset@GLIBC_2.2.5 + 0
0000005eb4b8  002900000007 R_X86_64_JUMP_SLO 0000000000000000 sigaction@GLIBC_2.2.5 + 0
0000005eb4c0  002a00000007 R_X86_64_JUMP_SLO 0000000000000000 sigismember@GLIBC_2.2.5 + 0
0000005eb4c8  002b00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_self@GLIBC_2.2.5 + 0
0000005eb4d0  002c00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_getattr_np@GLIBC_2.32 + 0
0000005eb4d8  002d00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_getstack@GLIBC_2.34 + 0
0000005eb4e0  002e00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_destroy@GLIBC_2.2.5 + 0</pre></div></li>

<li><p>在 <code>go1.23.1</code>、<code>gcc10</code>、<code>glibc2.31</code>、<code>debian11</code> 环境下上述脚本，输出和上述区别如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 编译 02-std-cgo
--- ldd 输出如下
        linux-vdso.so.1 (0x00007ffeedd67000)
        libresolv.so.2 =&gt; /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f65b8cca000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f65b8ca8000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f65b8ad4000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f65b8ced000)
--- readelf -r 输出如下

# ...
0000005c83c0  001a00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_create@GLIBC_2.2.5 + 0
#...
0000005c8400  002200000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_gets[...]@GLIBC_2.2.5 + 0
#...
0000005c8450  002c00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_getattr_np@GLIBC_2.2.5 + 0
0000005c8458  002d00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_getstack@GLIBC_2.2.5 + 0
0000005c8460  002e00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_attr_destroy@GLIBC_2.2.5 + 0</pre></div></li>
</ul>

<h4 id="分析">分析</h4>

<ul>
<li>go 编译器对标准库的默认行为的和上文一致。</li>
<li>标准库的 cgo 依赖 <code>resolv</code>、 <code>pthread</code> 库相关函数。</li>
<li>启用标准库 cgo 后，go 的 glibc 2.31 和 2.36 的产物有如下区别：

<ul>
<li>pthread 相关函数的默认实现在 2.32 和 2.34 发生了变化。</li>
<li>2.36 版本产物不再依赖 <code>libresolv.so.2</code> 和 <code>libpthread.so.0</code>。</li>
<li>原因详见： <a href="/posts/linux-dylib-detail-2-version/#glibc-情况">《Linux 动态链接库详解（二）版本管理 - glibc 情况》</a></li>
</ul></li>
<li>可以得出如下结论：依赖 go 标准库 cgo 实现的产物的 glibc 向前兼容性（使用新版本 glibc 编译，在旧版本的 glibc 环境下是否可以运行）如下：

<ul>
<li><code>2.3.2</code> ~ <code>2.31</code></li>
<li><code>2.32</code> ~ <code>2.33</code></li>
<li><code>2.34</code> ~ ???</li>
</ul></li>
</ul>

<h2 id="go-构建过程探索">Go 构建过程探索</h2>

<p>验证代码 <code>04-lang/01-go/02-build-detail.sh</code> （使用 <code>-x</code> 打印详细信息）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>

cd ./02-std-cgo
go clean -cache <span style="color:#f92672">&amp;&amp;</span> CGO_LDFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;-Wl,--verbose&#39;</span> go build -x -o main ./</code></pre></div>
<p>核心输出示意如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 将编译纯 go 包 internal/goarch 包为 .a 文件（静态链接库）。</span>
echo <span style="color:#e6db74">&#39;# import config&#39;</span> &gt; $WORK/b006/importcfg <span style="color:#75715e"># internal</span>
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/compile -o $WORK/b006/_pkg_.a -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b006=&gt;&#34;</span> -p internal/goarch -lang<span style="color:#f92672">=</span>go1.23 -std -complete -buildid _I81RMeLWXI9j1YyfN8b/_I81RMeLWXI9j1YyfN8b -goversion go1.23.1 -c<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> -nolocalimports -importcfg $WORK/b006/importcfg -pack /home/rectcircle/.gvm/gos/go1.23.1/src/internal/goarch/goarch.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/goarch/goarch_amd64.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/goarch/zgoarch_amd64.go
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/buildid -w $WORK/b006/_pkg_.a <span style="color:#75715e"># internal</span>
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># 将编译有依赖的纯 go 包  internal/abi</span>
cat &gt;/tmp/go-build1051277818/b005/importcfg <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39; # internal
</span><span style="color:#e6db74"># import config
</span><span style="color:#e6db74">packagefile internal/goarch=/tmp/go-build1051277818/b006/_pkg_.a
</span><span style="color:#e6db74">EOF</span>
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/compile -o $WORK/b005/_pkg_.a -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b005=&gt;&#34;</span> -p internal/abi -lang<span style="color:#f92672">=</span>go1.23 -std -buildid Nb65lMcIZuXoor9TVLLA/Nb65lMcIZuXoor9TVLLA -goversion go1.23.1 -symabis $WORK/b005/symabis -c<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> -nolocalimports -importcfg $WORK/b005/importcfg -pack -asmhdr $WORK/b005/go_asm.h /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/abi.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/abi_amd64.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/compiletype.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/escape.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/funcpc.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/iface.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/map.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/rangefuncconsts.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/runtime.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/stack.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/switch.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/symtab.go /home/rectcircle/.gvm/gos/go1.23.1/src/internal/abi/type.go
<span style="color:#75715e"># 编译 go 汇编的 internal/cpu 包</span>
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/asm -p internal/cpu -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b011=&gt;&#34;</span> -I $WORK/b011/ -I /home/rectcircle/.gvm/gos/go1.23.1/pkg/include -D GOOS_linux -D GOARCH_amd64 -D GOAMD64_v1 -o $WORK/b011/cpu.o ./cpu.s
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/asm -p internal/cpu -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b011=&gt;&#34;</span> -I $WORK/b011/ -I /home/rectcircle/.gvm/gos/go1.23.1/pkg/include -D GOOS_linux -D GOARCH_amd64 -D GOAMD64_v1 -o $WORK/b011/cpu_x86.o ./cpu_x86.s
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/pack r $WORK/b011/_pkg_.a $WORK/b011/cpu.o $WORK/b011/cpu_x86.o <span style="color:#75715e"># internal</span>
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/buildid -w $WORK/b011/_pkg_.a <span style="color:#75715e"># internal</span>
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># 编译包含 cgo 源码的 os/user 包</span>
mkdir -p $WORK/b067/
cd /home/rectcircle/.gvm/gos/go1.23.1/src/os/user
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> CGO_LDFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;&#39;</span> /home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/cgo -objdir $WORK/b067/ -importpath os/user <span style="color:#e6db74">&#34;-ldflags=\&#34;-Wl,--verbose\&#34;&#34;</span> -- -I $WORK/b067/ -O2 -g -fno-stack-protector ./cgo_lookup_cgo.go ./getgrouplist_unix.go
cd $WORK/b067
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I /home/rectcircle/.gvm/gos/go1.23.1/src/os/user -fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -ffile-prefix-map<span style="color:#f92672">=</span>$WORK/b067<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -I $WORK/b067/ -O2 -g -fno-stack-protector -ffile-prefix-map<span style="color:#f92672">=</span>/home/rectcircle/.gvm/gos/go1.23.1<span style="color:#f92672">=</span>/_/GOROOT -frandom-seed<span style="color:#f92672">=</span>K0OFSSy7CbgIZxgL3TAR -o $WORK/b067/_x001.o -c _cgo_export.c
cd /home/rectcircle/omv/00-Important/Workspace/rectcircle/linux-dylib-demo/04-lang/01-go/02-std-cgo
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I /home/rectcircle/.gvm/gos/go1.23.1/src/os/user -fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -ffile-prefix-map<span style="color:#f92672">=</span>$WORK/b067<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -o $WORK/b067/_cgo_.o $WORK/b067/_cgo_main.o $WORK/b067/_x001.o $WORK/b067/_x002.o $WORK/b067/_x003.o -Wl,--verbose
GNU ld <span style="color:#f92672">(</span>GNU Binutils <span style="color:#66d9ef">for</span> Debian<span style="color:#f92672">)</span> <span style="color:#ae81ff">2</span>.40 <span style="color:#75715e"># 发生了链接</span>
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/compile -o $WORK/b067/_pkg_.a -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b067=&gt;&#34;</span> -p os/user -lang<span style="color:#f92672">=</span>go1.23 -std -buildid K0OFSSy7CbgIZxgL3TAR/K0OFSSy7CbgIZxgL3TAR -goversion go1.23.1 -c<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> -nolocalimports -importcfg $WORK/b067/importcfg -pack /home/rectcircle/.gvm/gos/go1.23.1/src/os/user/cgo_listgroups_unix.go /home/rectcircle/.gvm/gos/go1.23.1/src/os/user/cgo_lookup_unix.go /home/rectcircle/.gvm/gos/go1.23.1/src/os/user/lookup.go /home/rectcircle/.gvm/gos/go1.23.1/src/os/user/us
/home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/pack r $WORK/b067/_pkg_.a $WORK/b067/_x001.o $WORK/b067/_x002.o $WORK/b067/_x003.o <span style="color:#75715e"># internal</span>
<span style="color:#75715e"># 链接为可执行文件</span>
GOROOT<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;/home/rectcircle/.gvm/gos/go1.23.1&#39;</span> /home/rectcircle/.gvm/gos/go1.23.1/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode<span style="color:#f92672">=</span>exe -buildid<span style="color:#f92672">=</span>IkkB1ptOifj-RaHtd9ta/OhOaSc7x9zXjEXRMwq10/ADNTTNgYXWDRapJgS5mC/IkkB1ptOifj-RaHtd9ta -extld<span style="color:#f92672">=</span>gcc $WORK/b001/_pkg_.a</code></pre></div>
<p>过程如下（默认的 <a href="https://pkg.go.dev/cmd/go#hdr-Build_modes">buildmode</a>）：</p>

<ul>
<li><p>从 main 包开始，按照深度优先遍历包的依赖树，从叶子节点依次编译包。</p>

<ul>
<li><p>如果是纯 go 源码，构建命令为 <code>$GOROOT/pkg/tool/linux_amd64/compile</code>（如 <code>internal/goarch</code>）。参数说明（详见： <a href="https://pkg.go.dev/cmd/compile">go cmd compile</a>）：</p>

<ul>
<li><code>-p</code> 指定包名。</li>
<li><code>-pack</code> 指定 go 源代码文件。</li>

<li><p><code>-importcfg</code> 指定依赖的其他包，该参数是一个文件，格式如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># import config
packagefile internal/goarch=/tmp/go-build1051277818/b006/_pkg_.a</pre></div></li>
</ul></li>

<li><p>如果包中包含 <code>.s</code> go 汇编源码（如：<code>internal/cpu</code>）：</p>

<ul>
<li>先使用 <code>$GOROOT/pkg/tool/linux_amd64/asm</code> 命令将汇编文件编译为 <code>.o</code> 文件（详见： <a href="https://pkg.go.dev/cmd/asm">go cmd asm</a>）。</li>
<li>再使用 <code>$GOROOT/pkg/tool/linux_amd64/pack</code> 命令将 <code>.o</code> 打包为 <code>.a</code> 文件（详见： <a href="https://pkg.go.dev/cmd/pack">go cmd pack</a>）。</li>
</ul></li>

<li><p>如果包中包含 <code>cgo</code> 源码（如： <code>os/user</code>）：</p>

<ul>
<li><p>先使用 <code>$GOROOT/pkg/tool/linux_amd64/cgo</code> 命令生成展开注释生成 <code>_cgo_export.c</code>、<code>_cgo_main.c</code>、<code>xxx.cgo2.c</code> 等代码文件（详见： <a href="https://pkg.go.dev/cmd/cgo">go cmd cgo</a>），（<code>CGO_LDFLAGS</code> 作为 <code>cgo</code> 命令的 <code>-ldflags</code> 参数传递给 cgo）。</p>

<ul>
<li>使用 <code>gcc</code> 编译包中的 <code>.c</code>、<code>.S</code> 源码为 <code>.o</code> 文件。</li>
<li>使用 <code>gcc</code> 编译 cgo 生成的代码为 <code>.o</code> 文件。</li>

<li><p>最后使用 <code>gcc</code> 编译将所有的 <code>.o</code> 生成 <code>_cgo_.o</code>，这里的 <code>-ldflags</code> 将传递给该命令，在此阶段发生了链接，原因可能是是 <code>_cgo_main.c</code> 里面生成了 c 语言的 <code>main</code> 函数：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stddef.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() { <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>; }
<span style="color:#f92672">//</span> ...</code></pre></div></li>
</ul></li>

<li><p>再使用 <code>$GOROOT/pkg/tool/linux_amd64/cgo</code> 使用 <code>_cgo_.o</code> 生成 <code>_cgo_import.go</code>。</p></li>

<li><p>然后使用 <code>$GOROOT/pkg/tool/linux_amd64/compile</code> 编译纯 go 的源码（<code>_cgo_import.go</code> 也作为参数），生成 <code>.o</code>。</p></li>

<li><p>最后使用 <code>$GOROOT/pkg/tool/linux_amd64/pack</code> 命令将 <code>_cgo_.o</code> 以及 <code>compile</code> 生成的代码，打包为 <code>.a</code> 文件。</p></li>
</ul></li>
</ul></li>

<li><p>最后一步将所有 <code>.a</code> 文件链接为可执行文件，这里涉及到 <code>$GOROOT/pkg/tool/linux_amd64/link</code>（详见： <a href="https://pkg.go.dev/cmd/link">go cmd link</a>） 命令的 <code>-linkmode</code> 参数：</p>

<ul>
<li>其默认值 <code>auto</code> （参考：<a href="https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/cmd/cgo/doc.go;l=542">cmd/cgo/doc.go Implementation details</a>）：

<ul>
<li>如果是未启用 cgo 或者只使用了标准库的 CGO，则使用 <code>internal</code> 模式。</li>
<li>否则为 <code>external</code> 模式。</li>
</ul></li>
<li><code>internal</code>： 使用 go 实现的原生链接器进行链接，因为上述的 cgo 过程已经进行过链接了，因此动态库的信息已经知晓了，因此在此阶段不需要再进行动态库查找了。直接生成 <code>a.out</code> 即可。</li>
<li><code>external</code>： 使用外部的链接器进行链接，一般是 <code>gcc</code>。

<ul>
<li>先将 <code>.a</code> 转换为链接器可识别的 <code>.o</code> 文件。</li>
<li>使用 <code>gcc</code> 进行链接生成 <code>a.out</code>。</li>
</ul></li>
</ul></li>
</ul>

<h2 id="和动态库有关构建参数">和动态库有关构建参数</h2>

<p>根据上文的分析，可以总结出和动态链接库有关的命令行参数和环境变量在 go build 细节，以及这些参数透传过程，如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">CC<span style="color:#f92672">=</span>$cc CGO_LDFLAGS<span style="color:#f92672">=</span>$cgo_ldflags CGO_ENABLED<span style="color:#f92672">=</span>$cgo_enabled go build -ldflags <span style="color:#e6db74">&#39;-linkmode=$linkmode -extld=$extld -extldflags=$extldflags&#39;</span> -o $main ./
    <span style="color:#75715e"># 项目以及依赖中包含 cgo 代码且 $cgo_enabled 不存在或非零</span>
    go tool cgo -ldflags<span style="color:#f92672">=</span>$cgo_ldflags ...
        $cc -o xxx.o xxx.c
        $cc -o _cgo_.o xxx.o xxx.o xxx.o $cgo_ldflags
            ld 处理后的$cgo_ldflags
    go tool link -linkmode $linkmode -extld $extld -extldflags $extldflags ...
        <span style="color:#75715e"># 项目中包含 cgo 代码且启用了 $cgo_enabled=1</span>
        $extld -o a.out xxx.o xxx.o xxx.o xxx.o $cgo_ldflags $extldflags
            ld 处理后的$cgo_ldflags 处理后的$extldflags
    cp a.out $main</code></pre></div>
<ul>
<li><code>CGO_ENABLED=0</code> 时，如下场景将报错：

<ul>
<li><code>-ldflags</code> 配置为 <code>-linkmode=external</code>，将报错 <code>-linkmode=external requires external (cgo) linking, but cgo is not enabled</code>。</li>
<li>项目中只有 CGO 的实现时。</li>
</ul></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><strong><a href="https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/cmd/cgo/doc.go;l=542">cmd/cgo/doc.go Implementation details</a></strong></li>
<li><a href="https://pkg.go.dev/cmd/go">go cmd go</a></li>
<li><a href="https://pkg.go.dev/cmd/compile">go cmd compile</a></li>
<li><a href="https://pkg.go.dev/cmd/cgo">go cmd cgo</a></li>
<li><a href="https://pkg.go.dev/cmd/link">go cmd link</a></li>
<li><a href="https://pkg.go.dev/cmd/pack">go cmd pack</a></li>
<li><a href="https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-05-internal.html">Go语言高级编程 2.5 内部机制</a></li>
</ul>
]]></description></item><item><title>Linux 动态链接库详解（四） C/C&#43;&#43; 语言</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-4-lang-c/</link><pubDate>Sat, 14 Sep 2024 11:11:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-4-lang-c/</guid><description type="html"><![CDATA[

<h2 id="前言">前言</h2>

<p>前文关于动态库的介绍都是基于 C / C++ 编程语言直接使用 gcc 或者 makefile 进行构建的。</p>

<p>这里介绍一下一些其他常见构建工具对动态库的处理情况。</p>

<h2 id="pkg-config">pkg-config</h2>

<p>真实的 C/C++ 项目的动态库依赖是十分复杂的，在调用 gcc 或编写 makefile 时，手动指定 <code>-L</code> 和 <code>-l</code> 是很比较麻烦的。</p>

<p>pkg-config 就可以解决这个问题，其通过 <code>.pc</code> 格式的文件能自动生成 <code>-L</code> 和 <code>-l</code> 参数。</p>

<p>一般的使用流程如下：</p>

<ul>
<li>库开发者：发布库时会提供一个 <code>.pc</code> 文件，这个文件中包含了库的元信息（开源届主流的 C/C++ 库，如 <a href="https://github.com/curl/curl/blob/master/libcurl.pc.in">libcurl</a>、<a href="https://github.com/madler/zlib/blob/develop/zlib.pc.in">zlib</a>、<a href="https://github.com/libevent/libevent/blob/master/libevent.pc.in">libevent</a> 等都有）。</li>

<li><p>项目开发者：使用 <code>pkg-config --cflags --libs xxx</code> 命令，生成 gcc 的 <code>-L</code> 或 <code>-l</code> 参数（可以与 gcc、makefile 或 Autotools、CMake 等集成）。例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gcc -o example example.c <span style="color:#66d9ef">$(</span>pkg-config --cflags --libs gtk+-3.0<span style="color:#66d9ef">)</span></code></pre></div></li>

<li><p>项目使用者：使用包管理工具将项目发型的包、依赖的库都安装到系统中，按照上篇文章介绍的运行时查找方式来查找动态库。</p></li>
</ul>

<h2 id="主流的构建工具">主流的构建工具</h2>

<p>真实的 C/C++ 项目，不会手动使用 gcc 或 makefile 来构建项目，而是使用一些项目管理工具/构建工具，如：</p>

<ul>
<li>CMake</li>
<li>Autotools</li>
<li>Ninja</li>
<li>Meson</li>
<li>Bazel</li>
</ul>

<p>这些项目最终也是使用 pkg-config 或者配置 <code>-L</code>、 <code>-l</code> 来管理动态链接库的，在此次不多赘述。</p>
]]></description></item><item><title>Nix 高级话题之 动态链接库的处理</title><link>https://www.rectcircle.cn/posts/nix-advanced-dylib/</link><pubDate>Sat, 31 Aug 2024 19:57:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-advanced-dylib/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.22.1</p>
</blockquote>

<h2 id="背景知识">背景知识</h2>

<p>详见：</p>

<ul>
<li><a href="/posts/linux-dylib-detail-1-sample/">Linux 动态链接库详解（一）简单示例</a></li>
<li><a href="/posts/linux-dylib-detail-2-version/">Linux 动态链接库详解（二）版本管理</a></li>
<li><a href="/posts/linux-dylib-detail-3-search/">Linux 动态链接库详解（三）动态库查找</a></li>
</ul>

<h2 id="动态库组织">动态库组织</h2>

<p>在 Linux 软件包可基本上可以分为两种：开发包（<code>-dev</code>）和运行包。</p>

<p>开发包主要包含头文件、动态链接库等，运行包主要包含可执行文件、文档、配置文件等。</p>

<p>常规的 Linux 发行版，这些软件包的文件会按照 FHS 约定被安装到系统的不同目录下。也就是说，软件包在文件系统的组织是先按照文件类型划分目录，然后这些目录里面包含各个软件包的部分文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/bin
    gcc      # gcc 包的可执行文件
    python3  # python 包的可执行文件
/usr/lib
    libgcc_s.so.1  # gcc 包的动态链接库
    python3.12/    # python 包的库</pre></div>
<p>和常规 Linux 发行版不同，nix 管理的所有包，首先都存储在 <code>/nix/store/&lt;包名&gt;</code> 目录下。也就是说，软件包在文件系统的组织是先按照包名划分目录，然后这些目录里面包含各个软件包的部分目录：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/xxx-gcc-x.y.z/  # gcc 包
    /bin/gcc
    /lib/libgcc_s.so.1
/nix/store/xxx-python-x.y.z/  # python 包
    /bin/python3
    /lib/python3.12/</pre></div>
<p>因此，Nix 在构建时，以及其构建出的可执行文件的无法像常规的 Linux 发型版那样到几个指定的路径查找动态链接库。</p>

<p>Nix 解决办法是，在构建（链接阶段）过程，通过 <code>-L</code> 参数指定链接过程中的库查找，通过 <code>-rpath</code> 参数，将改可执行文件依赖的所有动态链接库的包路径写入到可执行文件中。这样，在安装时根据依赖关系将依赖的包安装到 <code>/nix/store/</code> 目录下，可执行文件在这些指定的路径中查找动态链接库即可。</p>

<p>整体流程如下：</p>

<ul>
<li>包定义和构建： nix 将世界上所有主流的软件包维护在名为 <a href="https://github.com/NixOS/nixpkgs"><code>nixpkgs</code></a> 的 github 仓库中。这个仓库使用 nix dsl 语言编写，提供了方便的构建函数 <code>stdenv.mkDerivation</code>，包发布者可以使用这些函数声明包的依赖以及构建过程，在这个函数内就实现了上述注入 <code>-rpath</code> 参数的过程，而无需包发布者手动指定。</li>
<li>包安装： 使用 nix 相关命令将这个包以及该包的依赖安装到 <code>/nix/store/</code> 下。</li>
<li>包执行： 在执行过程中 Linux 内核将读取可执行文件 ELF <code>.dynamic</code> 段的 <code>DT_RPATH</code> 或 <code>DT_RUNPATH</code> 在 <code>/nix/store/</code> 找到正确的动态链接库并加载。</li>
</ul>

<p><code>stdenv.mkDerivation</code> 特别说明：</p>

<ul>
<li>在构建过程中，使用 gcc 等命令式经过一个 wrap 脚本，这个脚本会识别 <code>NIX_CFLAGS_COMPILE</code>、<code>NIX_LDFLAGS</code> 相关命令，将这些参数传递给编译器和链接器。另外可以通过 <code>NIX_DEBUG=1</code> 打印更多日志。</li>

<li><p><code>buildInputs = [ zlib ]</code> 参数会注入 <code>NIX_CFLAGS_COMPILE</code> 和 <code>NIX_LDFLAGS</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat &gt; shell.nix <span style="color:#e6db74">&lt;&lt;EOF ;nix-shell
</span><span style="color:#e6db74">with import &lt;nixpkgs&gt; {};
</span><span style="color:#e6db74">stdenv.mkDerivation {
</span><span style="color:#e6db74">name = &#34;myenv&#34;;
</span><span style="color:#e6db74">buildInputs = [ zlib ];
</span><span style="color:#e6db74">}
</span><span style="color:#e6db74">EOF</span>
<span style="color:#f92672">[</span>nix-shell:~<span style="color:#f92672">]</span> $ echo $NIX_CFLAGS_COMPILE
-isystem /nix/store/bjl5kk674rmdzzpmcsvmw73hvf35jwh8-zlib-1.2.11-dev/include -isystem /nix/store/bjl5kk674rmdzzpmcsvmw73hvf35jwh8-zlib-1.2.11-dev/include
<span style="color:#f92672">[</span>nix-shell:~<span style="color:#f92672">]</span> $ echo $NIX_LDFLAGS
-rpath /nix/store/d5dzr90q2wy2nlw0z7s0pgxkjfjv1jrj-myenv/lib64 -rpath /nix/store/d5dzr90q2wy2nlw0z7s0pgxkjfjv1jrj-myenv/lib -L/nix/store/5dphwv1xs46n0qbhynny2lbhmx4xh1fc-zlib-1.2.11/lib -L/nix/store/5dphwv1xs46n0qbhynny2lbhmx4xh1fc-zlib-1.2.11/lib</code></pre></div></li>

<li><p>除了上述机制，改函数还提供了对其他主流 C 构建工具的支持：</p>

<ul>
<li><code>pkg-config</code></li>
<li><code>cmake</code></li>
</ul></li>
</ul>

<p>更多详见： <a href="https://nixos.wiki/wiki/C">NixOS Wiki - C</a></p>
]]></description></item><item><title>Linux 动态链接库详解（三）动态库查找</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-3-search/</link><pubDate>Wed, 28 Aug 2024 23:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-3-search/</guid><description type="html"><![CDATA[

<blockquote>
<p>示例代码： <a href="https://github.com/rectcircle/linux-dylib-demo/tree/master/03-symbolversion">rectcircle/linux-dylib-demo</a></p>
</blockquote>

<p>本篇将介绍在构建时（链接阶段）以及运行时的动态库的查找过程。</p>

<h2 id="构建时查找">构建时查找</h2>

<p>在构建过程中，动态库的查找发生在链接阶段，即由 gcc 或 clang 等命令间接调用 <code>ld</code> 命令进行链接时，搜索动态链接库。</p>

<p>使用 <a href="https://gcc.gnu.org/onlinedocs/gcc/Environment-Variables.html#index-LIBRARY_005fPATH">gcc</a> 或 clang 构建依赖动态链接库的代码时，可以通过如下，如下方式设置额外的搜索路径：</p>

<ul>
<li><code>LIBRARY_PATH</code> 环境变量（注意不是 <code>LD_LIBRARY_PATH</code>，<code>LD_LIBRARY_PATH</code> 不识别），多个以 <code>:</code> 分割。</li>
<li><code>-L</code> 命令行参数，可以配置多，如 <code>gcc ... -L /path/to/dir-a -L /path/to/dir-b</code>。</li>
</ul>

<p>除了以上自定义的搜索路径外，还会搜索 linker script 配置的默认搜索路径，以 debian 11 为例，通过 <code>ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012</code> 可以查看，其值为：</p>

<ul>
<li><code>/usr/local/lib/x86_64-linux-gnu</code></li>
<li><code>/lib/x86_64-linux-gnu</code></li>
<li><code>/usr/lib/x86_64-linux-gnu</code></li>
<li><code>/usr/local/lib64</code></li>
<li><code>/lib64</code></li>
<li><code>/usr/lib64</code></li>
<li><code>/usr/local/lib</code></li>
<li><code>/lib</code></li>
<li><code>/usr/lib</code></li>
<li><code>/usr/x86_64-linux-gnu/lib64</code></li>
<li><code>/usr/x86_64-linux-gnu/lib</code></li>
</ul>

<p>注意 <code>/etc/ld.so.conf</code> 配置的路径不会在构建（链接阶段）搜索（该配置文件影响运行阶段的搜索，详见下文）。</p>

<p>如上配置的搜索优先级顺序为：</p>

<ul>
<li>命令行 <code>-L</code> 参数指定的路径（包含用户自定义的以及 gcc 添加的）。</li>
<li>环境变量 <code>LIBRARY_PATH</code> 指定的路径。</li>
<li>链接器 linker script 配置的默认搜索路径。</li>
</ul>

<p>最后，在编译真实的项目时，一般使用 Makefile 来定义编译过程，Makefile 识别如下环境变量，可以用来配置自定义搜索路径：</p>

<ul>
<li><a href="https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html#index-LDFLAGS"><code>LDFLAGS</code></a> 环境变量，可以用来配置 <code>-L</code> 参数。</li>
<li><a href="https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html#index-LDLIBS"><code>LDLIBS</code></a> 环境变量，可以用来配置 <code>-l</code> 参数。</li>
</ul>

<p>验证如下：<code>01-sample/03-build-stage-search.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
rm -rf build/bin <span style="color:#f92672">&amp;&amp;</span> mkdir -p build/bin


echo <span style="color:#e6db74">&#39;=== 方式 1: 找不到库情况&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./main.c <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;构建成功&#39;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#39;构建失败&#39;</span>
echo

echo <span style="color:#e6db74">&#39;=== 方式 2: 使用 LD_LIBRARY_PATH 指定查找路径&#39;</span>
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib gcc -I ./build/include -o ./build/bin/main ./main.c -l sample <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;构建成功&#39;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#39;构建失败&#39;</span>
echo

echo <span style="color:#e6db74">&#39;=== 方式 3: 使用 -L 指定查找路径&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./main.c -L ./build/lib -l sample <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;构建成功&#39;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#39;构建失败&#39;</span>
echo

echo <span style="color:#e6db74">&#39;=== 方式 4: 使用 LIBRARY_PATH 指定查找路径&#39;</span>
LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib gcc -I ./build/include -o ./build/bin/main ./main.c -l sample <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;构建成功&#39;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#39;构建失败&#39;</span>
echo

echo <span style="color:#e6db74">&#39;=== 方式 5: 使用 -L LIBRARY_PATH 指定错误的路径观察查找路径&#39;</span>
mkdir -p /tmp/by_LIBRARY_PATH /tmp/by_-l
LIBRARY_PATH<span style="color:#f92672">=</span>/tmp/by_LIBRARY_PATH gcc -I ./build/include -o ./build/bin/main ./main.c -L /tmp/by_-l -l sample -Wl,-verbose <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;构建成功&#39;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#39;构建失败&#39;</span>  
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">bash ./01-sample/03-build-stage-search.sh
=== 方式 1: 找不到库情况
/usr/bin/ld: /tmp/ccLFcL27.o: in function `main&#39;:
main.c:(.text+0xa): undefined reference to `print_hello&#39;
collect2: error: ld returned 1 exit status
构建失败

=== 方式 2: 使用 LD_LIBRARY_PATH 指定查找路径
/usr/bin/ld: 找不到 -lsample: 没有那个文件或目录
collect2: error: ld returned 1 exit status
构建失败

=== 方式 3: 使用 -L 指定查找路径
构建成功

=== 方式 4: 使用 LIBRARY_PATH 指定查找路径
构建成功

=== 方式 5: 使用 -L LIBRARY_PATH 指定错误的路径观察查找路径
...
试图打开 /tmp/by_-l/libsample.so 失败
试图打开 /tmp/by_-l/libsample.a 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/libsample.so 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/libsample.a 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/libsample.so 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/libsample.a 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib/libsample.so 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib/libsample.a 失败
试图打开 /lib/x86_64-linux-gnu/libsample.so 失败
试图打开 /lib/x86_64-linux-gnu/libsample.a 失败
试图打开 /lib/../lib/libsample.so 失败
试图打开 /lib/../lib/libsample.a 失败
试图打开 /usr/lib/x86_64-linux-gnu/libsample.so 失败
试图打开 /usr/lib/x86_64-linux-gnu/libsample.a 失败
试图打开 /usr/lib/../lib/libsample.so 失败
试图打开 /usr/lib/../lib/libsample.a 失败
试图打开 /tmp/by_LIBRARY_PATH/libsample.so 失败
试图打开 /tmp/by_LIBRARY_PATH/libsample.a 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../libsample.so 失败
试图打开 /usr/lib/gcc/x86_64-linux-gnu/12/../../../libsample.a 失败
试图打开 /usr/local/lib/x86_64-linux-gnu/libsample.so 失败
试图打开 /usr/local/lib/x86_64-linux-gnu/libsample.a 失败
试图打开 /lib/x86_64-linux-gnu/libsample.so 失败
试图打开 /lib/x86_64-linux-gnu/libsample.a 失败
试图打开 /usr/lib/x86_64-linux-gnu/libsample.so 失败
试图打开 /usr/lib/x86_64-linux-gnu/libsample.a 失败
试图打开 /usr/lib/x86_64-linux-gnu64/libsample.so 失败
试图打开 /usr/lib/x86_64-linux-gnu64/libsample.a 失败
试图打开 /usr/local/lib64/libsample.so 失败
试图打开 /usr/local/lib64/libsample.a 失败
试图打开 /lib64/libsample.so 失败
试图打开 /lib64/libsample.a 失败
试图打开 /usr/lib64/libsample.so 失败
试图打开 /usr/lib64/libsample.a 失败
试图打开 /usr/local/lib/libsample.so 失败
试图打开 /usr/local/lib/libsample.a 失败
试图打开 /lib/libsample.so 失败
试图打开 /lib/libsample.a 失败
试图打开 /usr/lib/libsample.so 失败
试图打开 /usr/lib/libsample.a 失败
试图打开 /usr/x86_64-linux-gnu/lib64/libsample.so 失败
试图打开 /usr/x86_64-linux-gnu/lib64/libsample.a 失败
试图打开 /usr/x86_64-linux-gnu/lib/libsample.so 失败
试图打开 /usr/x86_64-linux-gnu/lib/libsample.a 失败
/usr/bin/ld: 找不到 -lsample: 没有那个文件或目录
...
collect2: error: ld returned 1 exit status
构建失败</pre></div>
<h2 id="运行时查找">运行时查找</h2>

<p>在可执行文件执行过程中，动态库的查找发生在程序装载阶段，由操作系统内核调用 <code>ld.so</code> （<code>/lib64/ld-linux-x86-64.so.2</code>） 这个 so 内的代码实现。</p>

<p>默认情况下， <code>ld.so</code> 查找 <code>/etc/ld.so.conf</code> （引用了 <code>/etc/ld.so.conf.d/*.conf</code>） 配置的默认搜索路径，以 debian 11 为例，其默认值为：</p>

<ul>
<li><code>/usr/local/lib</code></li>
<li><code>/usr/local/lib/x86_64-linux-gnu</code></li>
<li><code>/lib/x86_64-linux-gnu</code></li>
<li><code>/usr/lib/x86_64-linux-gnu</code></li>
</ul>

<p>可以通过修改 <code>/etc/ld.so.conf</code> （并执行 <code>sudo ldconfig</code> 来更新缓存 <code>/etc/ld.so.cache</code>，通过 <code>ldconfig -p</code> 可查看发现的所有库文件） 配置文件来修改系统的运行动态库搜索路径。</p>

<p>除了上述方式外，还可以通过如下方式添加动态库搜索路径：</p>

<ul>
<li><code>LD_LIBRARY_PATH</code> 环境变量。</li>
<li>在构建（链接阶段）时，通过 <code>-rpath</code> 参数指定动态库搜索路径（写入到了可执行文件 ELF <code>.dynamic</code> 段的 <code>DT_RPATH</code> 或 <code>DT_RUNPATH</code>，通过 <code>readelf -d</code> 可查询）。</li>
<li><code>LD_AUDIT</code> 环境变量指定一个 <code>xxx.so</code> 文件，可以使用 C 语言实现更加智能灵活的动态库搜索。</li>
</ul>

<p>如上配置的搜索优先级顺序为：</p>

<ul>
<li>环境变量 <code>LD_LIBRARY_PATH</code> 指定的路径。</li>
<li>可执行文件中 ELF <code>.dynamic</code> 段 <code>DT_RPATH</code> 或 <code>DT_RUNPATH</code> 指定的路径。</li>
<li><code>/etc/ld.so.conf</code> 配置的哪些。</li>
</ul>

<p>验证如下：<code>01-sample/04-runtime-stage-search.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
rm -rf build/bin <span style="color:#f92672">&amp;&amp;</span> mkdir -p build/bin


echo <span style="color:#e6db74">&#39;=== 准备: 指定 -rpath&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./main.c -L ./build/lib -l sample -Wl,-rpath,/tmp/by_-rpath
echo


echo <span style="color:#e6db74">&#39;=== 验证: 观察查找路径&#39;</span>
mkdir -p /tmp/by_LD_LIBRARY_PATH /tmp/by_-rpath
LD_DEBUG<span style="color:#f92672">=</span>libs LD_LIBRARY_PATH<span style="color:#f92672">=</span>/tmp/by_LD_LIBRARY_PATH ./build/bin/main
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 准备: 指定 -rpath

=== 验证: 观察查找路径
    655097:     find library=libsample.so [0]; searching
    655097:      search path=/tmp/by_LD_LIBRARY_PATH/glibc-hwcaps/x86-64-v2:/tmp/by_LD_LIBRARY_PATH/tls/x86_64/x86_64:/tmp/by_LD_LIBRARY_PATH/tls/x86_64:/tmp/by_LD_LIBRARY_PATH/tls/x86_64:/tmp/by_LD_LIBRARY_PATH/tls:/tmp/by_LD_LIBRARY_PATH/x86_64/x86_64:/tmp/by_LD_LIBRARY_PATH/x86_64:/tmp/by_LD_LIBRARY_PATH/x86_64:/tmp/by_LD_LIBRARY_PATH         (LD_LIBRARY_PATH)
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/tls/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/tls/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/tls/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/x86_64/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/x86_64/libsample.so
    655097:       trying file=/tmp/by_LD_LIBRARY_PATH/libsample.so
    655097:      search path=/tmp/by_-rpath/glibc-hwcaps/x86-64-v2:/tmp/by_-rpath/tls/x86_64/x86_64:/tmp/by_-rpath/tls/x86_64:/tmp/by_-rpath/tls/x86_64:/tmp/by_-rpath/tls:/tmp/by_-rpath/x86_64/x86_64:/tmp/by_-rpath/x86_64:/tmp/by_-rpath/x86_64:/tmp/by_-rpath (RUNPATH from file ./build/bin/main)
    655097:       trying file=/tmp/by_-rpath/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/tmp/by_-rpath/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/tls/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/tls/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/tls/libsample.so
    655097:       trying file=/tmp/by_-rpath/x86_64/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/x86_64/libsample.so
    655097:       trying file=/tmp/by_-rpath/libsample.so
    655097:      search cache=/etc/ld.so.cache
    655097:      search path=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2:/lib/x86_64-linux-gnu/tls/x86_64/x86_64:/lib/x86_64-linux-gnu/tls/x86_64:/lib/x86_64-linux-gnu/tls/x86_64:/lib/x86_64-linux-gnu/tls:/lib/x86_64-linux-gnu/x86_64/x86_64:/lib/x86_64-linux-gnu/x86_64:/lib/x86_64-linux-gnu/x86_64:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2:/usr/lib/x86_64-linux-gnu/tls/x86_64/x86_64:/usr/lib/x86_64-linux-gnu/tls/x86_64:/usr/lib/x86_64-linux-gnu/tls/x86_64:/usr/lib/x86_64-linux-gnu/tls:/usr/lib/x86_64-linux-gnu/x86_64/x86_64:/usr/lib/x86_64-linux-gnu/x86_64:/usr/lib/x86_64-linux-gnu/x86_64:/usr/lib/x86_64-linux-gnu:/lib/glibc-hwcaps/x86-64-v2:/lib/tls/x86_64/x86_64:/lib/tls/x86_64:/lib/tls/x86_64:/lib/tls:/lib/x86_64/x86_64:/lib/x86_64:/lib/x86_64:/lib:/usr/lib/glibc-hwcaps/x86-64-v2:/usr/lib/tls/x86_64/x86_64:/usr/lib/tls/x86_64:/usr/lib/tls/x86_64:/usr/lib/tls:/usr/lib/x86_64/x86_64:/usr/lib/x86_64:/usr/lib/x86_64:/usr/lib                (system search path)
    655097:       trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/tls/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/tls/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/tls/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/x86_64/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/x86_64/libsample.so
    655097:       trying file=/lib/x86_64-linux-gnu/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/tls/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/tls/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/tls/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/x86_64/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64-linux-gnu/libsample.so
    655097:       trying file=/lib/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/lib/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/lib/tls/x86_64/libsample.so
    655097:       trying file=/lib/tls/x86_64/libsample.so
    655097:       trying file=/lib/tls/libsample.so
    655097:       trying file=/lib/x86_64/x86_64/libsample.so
    655097:       trying file=/lib/x86_64/libsample.so
    655097:       trying file=/lib/x86_64/libsample.so
    655097:       trying file=/lib/libsample.so
    655097:       trying file=/usr/lib/glibc-hwcaps/x86-64-v2/libsample.so
    655097:       trying file=/usr/lib/tls/x86_64/x86_64/libsample.so
    655097:       trying file=/usr/lib/tls/x86_64/libsample.so
    655097:       trying file=/usr/lib/tls/x86_64/libsample.so
    655097:       trying file=/usr/lib/tls/libsample.so
    655097:       trying file=/usr/lib/x86_64/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64/libsample.so
    655097:       trying file=/usr/lib/x86_64/libsample.so
    655097:       trying file=/usr/lib/libsample.so
    655097:
./build/bin/main: error while loading shared libraries: libsample.so: cannot open shared object file: No such file or directory</pre></div>]]></description></item><item><title>Linux 动态链接库详解（二）版本管理</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-2-version/</link><pubDate>Mon, 26 Aug 2024 02:06:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-2-version/</guid><description type="html"><![CDATA[

<blockquote>
<p>示例代码： <a href="https://github.com/rectcircle/linux-dylib-demo/tree/master/03-symbolversion">rectcircle/linux-dylib-demo</a></p>
</blockquote>

<h2 id="语义化版本">语义化版本</h2>

<p>动态链接库作为可执行文件的依赖，必然带来版本管理的问题，因此对于动态链接库的版本管理的实现基本上是符合<a href="https://semver.org/">“语义化版本规范”</a> （动态链接库的出现远早于“语义化版本”的概念，可以合理推测<a href="https://semver.org/">“语义化版本规范”</a>是在动态链接库版本管理的基础上发展出来的）。</p>

<p>安装动态库的时候，我们会看到如下三种文件：</p>

<ul>
<li><p><code>libxxx.so.x.y.z</code> 这里的 <code>x</code> 是主版本号，<code>y</code> 是次版本号，<code>z</code> 是发布号（修订号），业界约定规则如下：</p>

<ul>
<li>主版本号变更，表示库有的重大升级，不同主版本号的库之间是不兼容的，依赖于旧的主版本号的程序需要改动相应的部分，并且重新编译，才可以在新版的共享库中运行；或者，系统必须保留旧版的共享库，使得那些依赖于旧版共享库的程序能够正常运行。</li>
<li>次版本号表示库的增量升级，即增加一些新的接口符号，且保持原来的符号不变。在主版本号相同的情况下，高的次版本号的库向后兼容低的次版本号的库。一个依赖于旧的次版本号共享库的程序，可以在新的次版本号共享库中运行，因为新版中保留了原来所有的接口，并且不改变它们的定义和含义。比如系统中有个共享库为 libfoo.so.1.2.x，后来在升级过程中添加了一个函数，版本号变成了 1.3.x 。因为 1.2.x 的所有接口都被保留到 1.3.x 中了，所以 那些依赖于 1.1.x 或 1.2.x 的程序都可以在 1.3.x 中正常运行而无需重新编译。</li>
<li>布版本号表示库的一些错误的修正、性能的改进等，并不添加任何新的接口，也不对接口进行更改。相同主版本号、次版本号的共享库，不同的发布版本号之间完全兼容，依赖 于某个发布版本号的程序可以在任何一个其他发布版本号中正常运行，而无须做任何修改。</li>
</ul></li>

<li><p><code>libxxx.so.x -&gt; libxxx.so.x.y.z</code>， <code>libxxx.so.x</code> 详见下文 soname。</p></li>

<li><p><code>libxxx.so -&gt; libxxx.so.x.y.z</code>，编译时依赖通过 <code>-lxxx</code> 指定查找的文件，详见上文说明。</p></li>
</ul>

<p>下文将介绍 Linux 环境下，动态库版本管理的细节。</p>

<h2 id="so-name">SO-NAME</h2>

<h3 id="原理">原理</h3>

<p>根据上面的概念，可以看出对于主版本号不变的库是先后兼容的（使用旧版本库编译的可执行文件，可以和新版本的动态链接库一起工作而不会有问题）。</p>

<p>因此 Linux 通过 SO-NAME 机制来实现这一点：</p>

<ul>
<li>gcc 编译一个动态链接库时，可以通过指定 <code>-Wl,-soname</code> 参数指定一个库的 SO-NAME，这个 SO-NAME 会写入 <code>.so</code> 文件（elf 的 <code>.dynamic</code> 的 <code>DT_SONAME</code>，通过 <code>readelf -d</code> 查看）。如 <code>-Wl,-soname,libfoo.so.1 -o libfoo.1.0.0</code>，在 <code>libfoo.1.0.0</code> 将看到 <code>libfoo.so.1</code> 的符号。</li>
<li>安装一个库到系统指定的运行时查找路径时，安装脚本会调用 <code>ldconfig</code> 会扫描具体版本的动态库文件，查找 SO-NAME 符号，生成或更新一个名为 SO-NAME 符号值的软链指向该文件。如扫描 <code>libfoo.so.1.0.0</code> 将生成 <code>libfoo.so.1 -&gt; libfoo.so.1.0.0</code> 的软链。</li>
<li>gcc 编译一个可执行文件时，使用 <code>-L</code> 和 <code>-l</code> 指定依赖一个动态库时，如果该动态库包含 SO-NAME 符号，会将 SO-NAME 作为该动态库的运行时查找库的名字，而非文件名。如： <code>gcc ... -L ... -l foo</code> 在构建（链接阶段）时，解析到 <code>libfoo.so</code> 文件包含 SO-NAME 为 <code>libfoo.so.1</code> 则在执行文件中使用 <code>libfoo.so.1</code> 作为运行时查找的名字而非 <code>libfoo.so</code>。</li>
<li>执行可执行文件时，会按照运行时查找规则查找名字为 SO-NAME 的动态链接库文件进行查找。如上例中，查找的是 <code>libfoo.so.1</code> 而非 <code>libfoo.so</code>。</li>
</ul>

<h3 id="示例">示例</h3>

<h4 id="头文件">头文件</h4>

<p>lib 头文件 <code>02-soname/include/foo.h</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-h" data-lang="h"><span style="color:#75715e">#ifndef _FOO_H
</span><span style="color:#75715e">#define _FOO_H 1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_foo</span>();
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_foo1_1</span>();
<span style="color:#75715e">#endif</span></code></pre></div>
<h4 id="编译动态链接库">编译动态链接库</h4>

<p>libfoo 的 1.0.0 版本源文件 <code>02-soname/1.0.0/foo.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_foo</span>(){
    printf(<span style="color:#e6db74">&#34;libfoo1.0.0</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}</code></pre></div>
<p>libfoo 的 1.0.0 版本编译脚本 <code>02-soname/1.0.0/build-lib.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd ../
mkdir -p build/lib
rm -rf build/include

cp -rf ./include ./build
gcc -Wl,-soname,libfoo.so.1 -I ./build/include -shared -fPIC -o ./build/lib/libfoo.so.1.0.0 ./1.0.0/foo.c
echo <span style="color:#e6db74">&#39;--- 查看 so 符号&#39;</span>
readelf -d ./build/lib/libfoo.so.1.0.0 | grep .so</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">--- 查看 so 符号
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libfoo.so.1]</pre></div>
<p>libfoo 的 1.1.0 版本源文件 <code>02-soname/1.1.0/foo.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_foo1_1</span>(){
    printf(<span style="color:#e6db74">&#34;libfoo1.1.0</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}


<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_foo</span>(){
    print_foo1_1();
}</code></pre></div>
<p>libfoo 的 1.1.0 版本编译脚本 <code>02-soname/1.1.0/build-lib.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
cd ../
mkdir -p build/lib
rm -rf build/include

cp -rf ./include ./build
gcc -Wl,-soname,libfoo.so.1 -I ./build/include -shared -fPIC -o ./build/lib/libfoo.so.1.1.0 ./1.1.0/foo.c
echo <span style="color:#e6db74">&#39;--- 查看 so 符号&#39;</span>
readelf -d ./build/lib/libfoo.so.1.1.0 | grep .so</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">--- 查看 so 符号
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libfoo.so.1]</pre></div>
<h4 id="编译运行可执行文件">编译运行可执行文件</h4>

<p>依赖 <code>1.0.0</code> 的可执行文件的源文件 <code>02-soname/main1_0.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;foo.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
    print_foo();
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>编译 <code>1.0.0</code> 的可执行文件的编译脚本 <code>02-soname/use-lib1_0.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
mkdir -p build/bin


echo <span style="color:#e6db74">&#39;=== 步骤 1: 使用 1.0.0 版本编译&#39;</span>
echo <span style="color:#e6db74">&#39;--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so
gcc -I ./build/include -o ./build/bin/main1_0 ./main1_0.c -L ./build/lib -l foo
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main1_0
echo <span style="color:#e6db74">&#39;--- readelf -d 输出&#39;</span>
readelf -d ./build/bin/main1_0 | grep .so
echo


echo <span style="color:#e6db74">&#39;=== 步骤 2: 运行&#39;</span>
echo <span style="color:#e6db74">&#39;--- 直接运行&#39;</span>
./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.1.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0
echo


echo <span style="color:#e6db74">&#39;=== 步骤 3: 使用 1.1.0 版本编译&#39;</span>
echo <span style="color:#e6db74">&#39;--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.1.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.1.0 ./build/lib/libfoo.so
gcc -I ./build/include -o ./build/bin/main1_0 ./main1_0.c -L ./build/lib -l foo
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main1_0
echo <span style="color:#e6db74">&#39;--- readelf -d 输出&#39;</span>
readelf -d ./build/bin/main1_0 | grep .so
echo

echo <span style="color:#e6db74">&#39;=== 步骤 4: 运行&#39;</span>
echo <span style="color:#e6db74">&#39;--- 直接运行&#39;</span>
./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.1.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_0</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 步骤 1: 使用 1.0.0 版本编译
--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.0.0
--- ldd 输出
        linux-vdso.so.1 (0x00007fff3a1f7000)
        libfoo.so.1 =&gt; not found
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3e66d38000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3e66f26000)
--- readelf -d 输出
 0x0000000000000001 (NEEDED)             共享库：[libfoo.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]

=== 步骤 2: 运行
--- 直接运行
./build/bin/main1_0: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so -&gt; libfoo.so.1.0.0
./build/bin/main1_0: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0
libfoo1.0.0
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0
libfoo1.1.0

=== 步骤 3: 使用 1.1.0 版本编译
--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.1.0
--- ldd 输出
        linux-vdso.so.1 (0x00007ffe1599b000)
        libfoo.so.1 =&gt; not found
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f865d698000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f865d886000)
--- readelf -d 输出
 0x0000000000000001 (NEEDED)             共享库：[libfoo.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]

=== 步骤 4: 运行
--- 直接运行
./build/bin/main1_0: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so -&gt; libfoo.so.1.0.0
./build/bin/main1_0: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0
libfoo1.0.0
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0
libfoo1.1.0</pre></div>
<p>依赖 <code>1.1.0</code> 的可执行文件的源文件 <code>02-soname/main1_1.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;foo.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
    printf(<span style="color:#e6db74">&#34;Hello from main1_1.c</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    print_foo1_1();
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>编译 <code>1.1.0</code> 的可执行文件的编译脚本 <code>02-soname/use-lib1_1.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
mkdir -p build/bin


echo <span style="color:#e6db74">&#39;=== 步骤 1: 使用 1.0.0 版本编译&#39;</span>
echo <span style="color:#e6db74">&#39;--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so
gcc -I ./build/include -o ./build/bin/main1_1 ./main1_1.c -L ./build/lib -l foo
echo


echo <span style="color:#e6db74">&#39;=== 步骤 2: 使用 1.1.0 版本编译&#39;</span>
echo <span style="color:#e6db74">&#39;--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.1.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.1.0 ./build/lib/libfoo.so
gcc -I ./build/include -o ./build/bin/main1_1 ./main1_1.c -L ./build/lib -l foo
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main1_1
echo <span style="color:#e6db74">&#39;--- readelf -d 输出&#39;</span>
readelf -d ./build/bin/main1_1 | grep .so
echo


echo <span style="color:#e6db74">&#39;=== 步骤 3: 运行&#39;</span>
echo <span style="color:#e6db74">&#39;--- 直接运行&#39;</span>
./build/bin/main1_1

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.0.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_1

echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0&#39;</span>
rm -rf ./build/lib/libfoo.so ./build/lib/libfoo.so.1
ln -s libfoo.so.1.1.0 ./build/lib/libfoo.so.1
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main1_1</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 步骤 1: 使用 1.0.0 版本编译
--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.0.0
/usr/bin/ld: /tmp/ccDIoZpM.o: in function `main&#39;:
main1_1.c:(.text+0x19): undefined reference to `print_foo1_1&#39;
collect2: error: ld returned 1 exit status

=== 步骤 2: 使用 1.1.0 版本编译
--- 创建软链 ./build/lib/libfoo.so -&gt; libfoo.so.1.1.0
--- ldd 输出
        linux-vdso.so.1 (0x00007fffba884000)
        libfoo.so.1 =&gt; not found
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007feb5820f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007feb583fd000)
--- readelf -d 输出
 0x0000000000000001 (NEEDED)             共享库：[libfoo.so.1]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]

=== 步骤 3: 运行
--- 直接运行
./build/bin/main1_1: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.0.0
Hello from main1_1.c
./build/bin/main1_1: symbol lookup error: ./build/bin/main1_1: undefined symbol: print_foo1_1
--- 指定 LD_LIBRARY_PATH 只包含 libfoo.so.1 -&gt; libfoo.so.1.1.0
Hello from main1_1.c
libfoo1.1.0</pre></div>
<h4 id="结论">结论</h4>

<ul>
<li><p>只依赖 1.0.0 版本库中符号的可执行文件源码。</p>

<table>
<thead>
<tr>
<th></th>
<th>能否编译成功</th>
<th>使用 1.0.0 运行</th>
<th>使用 1.1.0 运行</th>
</tr>
</thead>

<tbody>
<tr>
<td>使用 1.0.0 编译</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>使用 1.1.0 编译</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
</tbody>
</table></li>

<li><p>依赖了只在 1.1.0 有而 1.0.0 中没有的符号的库的可执行文件源码。</p>

<table>
<thead>
<tr>
<th></th>
<th>能否编译成功</th>
<th>使用 1.0.0 运行</th>
<th>使用 1.1.0 运行</th>
</tr>
</thead>

<tbody>
<tr>
<td>使用 1.0.0 编译</td>
<td>❌</td>
<td>-</td>
<td>-</td>
</tr>

<tr>
<td>使用 1.1.0 编译</td>
<td>✅</td>
<td>❌ (<strong>运行时</strong>报错)</td>
<td>✅</td>
</tr>
</tbody>
</table></li>
</ul>

<h3 id="问题">问题</h3>

<p>可以看出，在有了 SO-NAME 机制的情况下：</p>

<ul>
<li>如果可执行文件依赖了某个库，那么后续，该库的次版本号的升级将不会破坏任何东西。</li>
<li>但是如果可执行文件依赖了较新的次版本库中符号，那么后续，在运行时，如果不小心使用了同一个主版本号的较旧的次版本号，那么操作系统将不会拒绝这个程序的运行，而是运行到调用时才能发现这个符号不存，在运行时直接崩溃。要解决这个问题有如下几个办法：

<ul>
<li>库使用者：始终更新库到当前主版本号的最新的次版本。</li>
<li>库开发者：使用 Linux 提供的符号版本机制，将报错提前到加载这个程序的阶段，详见下文。</li>
</ul></li>
</ul>

<h2 id="符号版本">符号版本</h2>

<h3 id="ld-version-scripts">ld version scripts</h3>

<p>正如上文介绍的， SO-NAME 实现了可执行文件依赖某个主版本号的库，如果该库的主版本号不匹配则将在程序加载阶段报错。</p>

<p>但是，版本管理对于次版本的规定：只保证向后兼容（使用旧版本的库编译，可以在新版本的库运行），不保证向前兼容（不保证使用新版本的库编译，使用旧版本的库可以运行）。而只有 SO-NAME 情况下，在使用新版本的库编译，运行时使用旧版本的库的情况下，操作系统无法再程序加载阶段报错，而是在运行依赖这个符号时直接崩溃。</p>

<p>为了解决这个问题，Linux 提供了 ld version scripts 语法，可以通过编写一个脚本，这个脚本声明版本，每个版本中包含了在这个版本引入的符号。然后：</p>

<ul>
<li>在使用 gcc 编译库时，通过 <code>-Wl,--version-script,xxx.map</code> 指定这个脚本，如下信息将编译到库中：

<ul>
<li>每个符号版本，如 <code>print_bar_d@@BAR_1.1</code>（在 elf 的 <code>.dynsym</code> 段，通过 <code>readelf --dyn-syms</code> 查看）。</li>
<li>声明的所有版本，如 <code>BAR_1.1</code>、<code>BAR_1.0</code>（在 elf 的 <code>.gnu.version_d</code> 段，通过 <code>readelf --version-info</code> 查看）。</li>
</ul></li>
<li>在使用 gcc 构建（链接阶段）可执行文件时，收集调用该库中所有符号的版本列表，去重编译到库中，如，仅调用了 <code>print_bar_d</code> 函数，则将获取到依赖的版本列表为 <code>BAR_1.1</code>（在 elf 的 <code>.gnu.version_r</code> 段，通过 <code>readelf --version-info</code> 查看）。</li>
<li>在运行该可执行文件的加载阶段，会使用可执行文件中的 <code>.gnu.version_r</code> 和库中的 <code>.gnu.version_d</code>，进行匹配，如果发现找不到的符号，则直接加载失败。</li>
</ul>

<p>示例如下：</p>

<p>1.0.0 版本的 <code>libbar.map</code> 如下（<code>...</code> 为省略）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">BAR_1.0 {
    ...
};

BARprivate {
    ...
};</pre></div>
<p>1.1.0 版本的 <code>libbar.map</code> 如下（<code>...</code> 为省略）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">BAR_1.0 {
    ...
};

BARprivate {
    ...
};

BAR_1.1 {
    global:
    print_bar_d;
} BAR_1.0;</pre></div>
<p>编译 <code>main_d.c</code> 使用了 1.1.0 版本的库，运行时使用 1.0.0 的库时将报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">./build/bin/main_d: ./build/lib/libbar.so.1: version `BAR_1.1&#39; not found (required by ./build/bin/main_d)</pre></div>
<p>因此：依赖了只在 1.1.0 有而 1.0.0 中没有的符号的库的可执行文件源码。</p>

<table>
<thead>
<tr>
<th></th>
<th>能否编译成功</th>
<th>使用 1.0.0 运行</th>
<th>使用 1.1.0 运行</th>
</tr>
</thead>

<tbody>
<tr>
<td>使用 1.0.0 编译</td>
<td>❌</td>
<td>-</td>
<td>-</td>
</tr>

<tr>
<td>使用 1.1.0 编译</td>
<td>✅</td>
<td>❌ (<strong>加载时</strong>报错)</td>
<td>✅</td>
</tr>
</tbody>
</table>

<p>从而解决了该问题。</p>

<p>这里简单介绍一下 ld version scripts 的语法：</p>

<ul>
<li>有多个 <code>NAME_X.Y {};</code> 或 <code>NAMEprivate {};</code> 块组成，声明多个版本，其中 <code>NAMEprivate</code> 表示这些符号时私有的，不保证未来是否会被删除，外部不应该依赖。</li>
<li>对于新的版本一般要继承上一个版本如 <code>BAR_1.1 {} BAR_1.0;</code>，表示 <code>BAR_1.1</code> 继承了 <code>BAR_1.0</code> 中的所有符号。</li>
<li>每个版本块 <code>{}</code> 内可以声明导出的符号：

<ul>
<li><code>global:</code> 表示导出的全局符号列表。</li>
<li><code>local:</code> 表述局部符号。</li>
<li><code>:</code> 后面用来声明符号，每个符号使用 <code>;</code> 结尾。</li>
<li>可以使用 <code>*</code> 通配符声明表示所有的符号。</li>
</ul></li>
</ul>

<h3 id="符号重载">符号重载</h3>

<p>假设一个库函数 <code>print_bar_b</code> 在 1.0.0 版本有一个实现，但是 1.1.0 版本，库开发者想改变这个函数的语义，导致不兼容，按照语义化版本，这种场景需要升级大版本好到 2.0.0，但是如果仅仅为了这个小小的改动就升级大版本，有点小题大做。因此希望能做如下场景：</p>

<ul>
<li>在 1.0.0 版本的库中，存在一个实现 <code>print_bar_b</code>。</li>
<li>在 1.1.0 版本的库中，存在一个新的实现 <code>__print_bar_b_1_1</code>，同时 1.0.0 的实现 <code>print_bar_b</code> 变为 <code>__print_bar_b_1_0</code> 仍然存在。</li>
<li>可执行文件调用了 <code>print_bar_b</code> 函数。</li>
<li>可执行文件是使用 1.0.0 版本的库编译的，在运行时：

<ul>
<li>使用的 1.0.0 版本的库时，实际调用的是 <code>print_bar_b</code>。</li>
<li>使用的 1.1.0 版本的库时，实际调用的是 <code>__print_bar_b_1_0</code> （即 1.0.0 的实现）。</li>
</ul></li>
<li>可执行文件是使用 1.1.0 版本的库编译的，在运行时：

<ul>
<li>使用的 1.0.0 版本的库时，将报错，因为没有 1.1.0 的实现。</li>
<li>使用的 1.1.0 版本的库时，将调用 <code>__print_bar_b_1_1</code> （即 1.1.0 的实现）。</li>
</ul></li>
</ul>

<p>能实现如上场景的机制被称为符号多版本重载， Linux 通过 <code>asm</code> 指定实现了该机制，示例如下：</p>

<ul>
<li><p>在使用该特性的情况下，必须要声明 ld version scripts，如下（<code>...</code> 为省略）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">BAR_1.0 {
    global:
    ...
    print_bar_b;
    ...
};

BARprivate {
    global:
    ...
    __print_bar_b_1_0;
    __print_bar_b_1_1;
    ...
};
...</pre></div></li>

<li><p>在 1.0.0 版本的库中，相关代码如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_bar_b</span>() {
    printf(<span style="color:#e6db74">&#34;libbar1.0.0 b</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}</code></pre></div>
<ul>
<li>完成编译后，通过 <code>readelf --dyn-syms</code> 查看，可以看到 <code>print_bar_b@@BAR_1.0</code>。</li>
</ul></li>

<li><p>在 1.1.0 版本的库中，相关代码如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#66d9ef">asm</span>(<span style="color:#e6db74">&#34;.symver __print_bar_b_1_0,print_bar_b@BAR_1.0&#34;</span>);
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">__print_bar_b_1_0</span>() {
    printf(<span style="color:#e6db74">&#34;libbar1.0.0 b</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}

<span style="color:#66d9ef">asm</span>(<span style="color:#e6db74">&#34;.symver __print_bar_b_1_1,print_bar_b@@BAR_1.1&#34;</span>);
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">__print_bar_b_1_1</span>() {
    printf(<span style="color:#e6db74">&#34;libbar1.0.0 b</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}</code></pre></div>
<ul>
<li>完成编译后，通过 <code>readelf --dyn-syms</code> 查看，可以看到 <code>print_bar_b@@BAR_1.1</code> 和 <code>print_bar_b@BAR_1.0</code>。</li>
<li><code>asm(&quot;.symver 实现的符号名,导出的符号名@版本号&quot;)</code> 导出一个带版本的符号，指向某个实现。</li>
<li>上述的 <code>@@</code> 表示该导出符号的默认的实现，在编译时将使用该版本（编译到可执行文件中，可通过 <code>readelf --dyn-syms</code> 查看）。</li>
</ul></li>
</ul>

<h3 id="示例代码">示例代码</h3>

<p>详见： <a href="https://github.com/rectcircle/linux-dylib-demo/tree/master/03-symbolversion">rectcircle/linux-dylib-demo/03-symbolversion</a></p>

<h2 id="glibc-情况">glibc 情况</h2>

<p>glibc 主要使用了上述符号版本机制，如果遇到可执行文件报各种关于 glibc 的错误，通过了解上述机制，应该可以快速的解决问题。</p>

<ul>
<li><a href="https://github.com/bminor/glibc/blob/master/string/Versions">glibc 的 ld version scripts 示例</a> 可以查看 glibc 符号的版本。</li>
<li>可以使用 <code>ldd</code>、 <code>readelf --version-info</code>、 <code>readelf --dyn-syms</code> 等参数查看可执行文件依赖的动态库，找到其中最大的版本。则这个版本就是该可执行文件依赖的 glibc 的最小版本号。</li>
<li>通过 <a href="https://abi-laboratory.pro/index.php?view=timeline&amp;l=glibc">ABI Laboratory</a> 可以查询 glibc 的向前兼容情况（这个站点叫做向后兼容性，个人理解应该是向前兼容，即：使用新版 glibc 编译在旧版 glibc 环境下仍能运行的符号比例）。</li>
<li>这里重点介绍下 <a href="https://lists.gnu.org/archive/html/info-gnu/2021-08/msg00001.html">glibc 2.34</a> 的一个重大变化，即：<code>-lpthread</code>, <code>-ldl</code>, <code>-lutil</code>, <code>-lanl</code>, <code>-lresolv</code> 等的符号，已经被移动到 <code>libc.so.6</code> 中。因此，在使用这些库函数的项目编译时，在 2.34 之后，通过 ldd 将只能看到 <code>libc.so.6</code> 的依赖。</li>
</ul>

<h2 id="优缺点和使用场景">优缺点和使用场景</h2>

<p>动态链接库的本质是对通用逻辑的复用。因此，动态链接库有如下优点：</p>

<ul>
<li>节省磁盘和内存资源：将可以复用的代码编译成动态链接库，那么这些代码在一台设备中的磁盘中只需要保存一份，在运行时只需要将这些代码加载一份到内存中，从而节省磁盘和内存资源。</li>
<li>可执行文件和库的发布解耦：如果采用静态编译的方式，当库存在问题需要更新时，需要通知所有可执行文件开发者重新编译可执行文件。而采用动态链接库的方式，库开发者只需在满足兼容性的条件下，更新库即可，而无需通知可执行文件开发者。</li>
</ul>

<p>基于以上优势，动态链接库主要的应用场景如下：</p>

<ul>
<li>操作系统系统调用封装的函数库：如 libc 库的 POSIX 部分。</li>
<li>通用函数库：如 openssl、libz 等。</li>
</ul>

<p>软件工程没有银弹，所有技术都是有代价的，动态链接库也存在很多问题：</p>

<ul>
<li>隐式依赖： 一个可执行文件的能否运行隐式的依赖了某些动态链接库，这带来了运行环境搭建的成本。</li>
<li>依赖地狱（Dependency Hell）： 不同的应用程序可能需要同一个库的不同版本，某些极端场景无法协调这些版本，可能造成：

<ul>
<li>某些可执行文件只能安装旧版本的而无法升级。</li>
<li>需要维护多个版本的动态库，运行环境的维护会变得异常复杂。</li>
</ul></li>
<li>故障半径大： 为某个可执行文件升级动态链接库可能导致其他可执行文件的崩溃。</li>
</ul>

<p>为了解决如上问题，业界又引入很多复杂的技术，如：</p>

<ul>
<li>容器化： 将可执行文件和其依赖的动态链接库打包到镜像中，将依赖固化下来，实现可重现的运行。</li>
<li>Nix： 采用可寻址的包管理机制，支持一个操作系统系统中安装多个版本的动态链接库而互不干扰，可通过声明式的方式安装各个版本的包。</li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://book.douban.com/subject/3652388/">《程序员的自我修养&ndash;链接、装载与库：第八章》</a></li>
<li><a href="https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html">LD Version Scripts 官方文档</a></li>
<li><a href="https://blog.csdn.net/rubikchen/article/details/130218838">博客： 符号别名，编译指定版本，链接指定版本</a>。</li>
<li><a href="https://github.com/chenpengcong/blog/issues/16">共享库及符号的版本控制实践</a></li>
</ul>
]]></description></item><item><title>Linux 动态链接库详解（一）简单示例</title><link>https://www.rectcircle.cn/posts/linux-dylib-detail-1-sample/</link><pubDate>Mon, 26 Aug 2024 02:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-dylib-detail-1-sample/</guid><description type="html"><![CDATA[

<blockquote>
<p>示例代码： <a href="https://github.com/rectcircle/linux-dylib-demo/tree/master/03-symbolversion">rectcircle/linux-dylib-demo</a></p>
</blockquote>

<h2 id="构建库">构建库</h2>

<p>lib 头文件 <code>01-sample/include/sample.h</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-h" data-lang="h"><span style="color:#75715e">#ifndef _SAMPLE_H
</span><span style="color:#75715e">#define _SAMPLE_H 1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_hello</span>();
<span style="color:#75715e">#endif</span></code></pre></div>
<p>lib 源代码 <code>01-sample/sample.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_hello</span>(){
    printf(<span style="color:#e6db74">&#34;Hello World!</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
}</code></pre></div>
<p>lib 构建脚本 <code>01-sample/01-build-lib.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
rm -rf build/lib <span style="color:#f92672">&amp;&amp;</span> mkdir -p build/lib
rm -rf build/include

cp -rf ./include ./build
gcc -I ./build/include -shared -fPIC -o ./build/lib/libsample.so.1.0.0 ./sample.c
ln -s libsample.so.1.0.0 ./build/lib/libsample.so
echo <span style="color:#e6db74">&#39;--- 查看 build/lib 目录&#39;</span>
ls -al ./build/lib
echo <span style="color:#e6db74">&#39;--- 查看 so 符号&#39;</span>
readelf -d ./build/lib/libsample.so.1.0.0 | grep .so</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">--- 查看 build/lib 目录
libsample.so -&gt; libsample.so.1.0.0
libsample.so.1.0.0
--- 查看 so 符号
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]</pre></div>
<h2 id="使用库">使用库</h2>

<p>可执行文件源代码 <code>01-sample/main.c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sample.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(){
    print_hello();
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>编译运行可执行文件的脚本 <code>01-sample/02-use-lib.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f $0<span style="color:#66d9ef">))</span>
rm -rf build/bin <span style="color:#f92672">&amp;&amp;</span> mkdir -p build/bin


echo <span style="color:#e6db74">&#39;=== 方式 1: 直接指定 so (绝对路径)&#39;</span>
<span style="color:#75715e"># 写法 1.1: 一步完成</span>
gcc -I ./build/include -o ./build/bin/main ./main.c ./build/lib/libsample.so
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main
echo <span style="color:#e6db74">&#39;--- readelf -d 输出&#39;</span>
readelf -d ./build/bin/main | grep .so
./build/bin/main
<span style="color:#75715e"># 写法 1.2: 分步骤编译</span>
gcc -c -I ./build/include -o ./build/main.o ./main.c
gcc -o ./build/bin/main ./build/main.o ./build/lib/libsample.so
<span style="color:#75715e"># ldd ./build/bin/main</span>
<span style="color:#75715e"># ./build/bin/main</span>
echo

echo <span style="color:#e6db74">&#39;=== 方式 2: 使用 -L 和 -l 指定动态库&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./main.c -L ./build/lib -l sample
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main
echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH ldd 输出&#39;</span>
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ldd ./build/bin/main
echo <span style="color:#e6db74">&#39;--- readelf -d 输出&#39;</span>
readelf -d ./build/bin/main | grep .so
echo <span style="color:#e6db74">&#39;--- 直接执行&#39;</span>
./build/bin/main
echo <span style="color:#e6db74">&#39;--- 指定 LD_LIBRARY_PATH 执行&#39;</span>
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./build/lib ./build/bin/main
echo


<span style="color:#75715e"># 写法 3: 使用 -rpath 将动态库绝对路径写入链接器</span>
echo <span style="color:#e6db74">&#39;=== 方式 3: 使用 -L 和 -l 指定动态库，使用 -Wl,-rpath 配置运行时查找路径。&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./main.c -L ./build/lib -l sample -Wl,-rpath,./build/lib
echo <span style="color:#e6db74">&#39;--- ldd 输出&#39;</span>
ldd ./build/bin/main
echo <span style="color:#e6db74">&#39;--- cd 到 build 目录 ldd 输出&#39;</span>
cd build <span style="color:#f92672">&amp;&amp;</span> ldd ./bin/main
echo <span style="color:#e6db74">&#39;--- cd 到 build 目录，指定 LD_LIBRARY_PATH 后 ldd 输出&#39;</span>
LD_LIBRARY_PATH<span style="color:#f92672">=</span>./lib ldd ./bin/main
cd ../
echo

echo <span style="color:#e6db74">&#39;=== 有问题的写法: so 在 main.o 或 main.c 前面&#39;</span>
gcc -I ./build/include -o ./build/bin/main ./build/lib/libsample.so ./main.c
gcc -I ./build/include -L ./build/lib -lsample -o ./build/bin/main ./main.c
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== 方式 1: 直接指定 so (绝对路径)
--- ldd 输出
        linux-vdso.so.1 (0x00007ffecf4f7000)
        ./build/lib/libsample.so (0x00007f61a64be000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61a62d7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f61a64ca000)
--- readelf -d 输出
 0x0000000000000001 (NEEDED)             共享库：[./build/lib/libsample.so]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
Hello World!

=== 方式 2: 使用 -L 和 -l 指定动态库
--- ldd 输出
        linux-vdso.so.1 (0x00007fff9e7ad000)
        libsample.so =&gt; not found
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4c7adee000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4c7afdc000)
--- readelf -d 输出
 0x0000000000000001 (NEEDED)             共享库：[libsample.so]
 0x0000000000000001 (NEEDED)             共享库：[libc.so.6]
--- 直接执行
./build/bin/main: error while loading shared libraries: libsample.so: cannot open shared object file: No such file or directory
--- 指定 LD_LIBRARY_PATH 执行
Hello World!

=== 有问题的写法: so 在 main.o 或 main.c 前面
/usr/bin/ld: /tmp/cc9RC5kK.o: in function `main&#39;:
main.c:(.text+0xa): undefined reference to `print_hello&#39;
collect2: error: ld returned 1 exit status
/usr/bin/ld: /tmp/ccAH6tZo.o: in function `main&#39;:
main.c:(.text+0xa): undefined reference to `print_hello&#39;
collect2: error: ld returned 1 exit status</pre></div>
<h2 id="分析">分析</h2>

<ul>
<li>使用 <code>gcc</code> 构建构建一个动态链接库时，和构建可执行文件不同点在于需添加如下两个参数：

<ul>
<li><code>-shared</code> 生成动态链接库。</li>
<li><code>-fPIC</code> 生成位置无关代码。</li>
</ul></li>
<li>使用 <code>ldd</code> 命令可以查看某个可执行文件运行过程查找的动态链接库的路径，可用于定位运行时动态链接库查找问题。</li>

<li><p>使用 <code>gcc</code> 构建一个可执行文件要依赖一个动态链接库时，有如下几种写法说明如下：</p>

<table>
<thead>
<tr>
<th>方式</th>
<th>编译命令</th>
<th>运行时库查找说明</th>
<th>ldd 输出</th>
</tr>
</thead>

<tbody>
<tr>
<td>方式 1 （不推荐）</td>
<td><code>gcc ... main.c ./build/lib/libsample.so</code></td>
<td>直接加载 <code>./build/lib/libsample.so</code> 找不到立即报错</td>
<td><code>./build/lib/libsample.so</code></td>
</tr>

<tr>
<td>方式 2 （推荐）</td>
<td><code>gcc ... main.c -L ./build/lib -l sample</code></td>
<td>按照运行时查找规则查找（详见后续文章）</td>
<td><li>未指定 LD_LIBRARY_PATH 输出为： <code>libsample.so =&gt; not found</code></li><li> 指定了正确的 <code>LD_LIBRARY_PATH</code> 输出为： <code>libsample.so =&gt; ./build/lib/libsample.so</code></li></td>
</tr>

<tr>
<td>方式 3 （推荐）</td>
<td><code>gcc ... main.c -L ./build/lib -l sample -Wl,-rpath,./build/lib</code></td>
<td>先按照 <code>-rpath</code> 指定的路径查找，然后按照运行时查找规则查找</td>
<td><li>运行时所在路径和编译时一样时，输出为：<code>libsample.so =&gt; ./build/lib/libsample.so</code>。</li><li>运行时所在路径和编译时不一样时，输出为：<code>libsample.so =&gt; not found</code>。</li><li>指定了正确的 LD_LIBRARY_PATH 时，输出为：<code>libsample.so =&gt; ./lib/libsample.so</code>。</li></td>
</tr>
</tbody>
</table></li>

<li><p><code>gcc</code> 的 <code>-L &lt;search-dir&gt;</code> 参数用于指定编译时动态链接库查找路径，详见后续文章。</p></li>

<li><p><code>gcc</code> 的 <code>-l &lt;link-name&gt;</code> 参数用于指定动态链接库的名称，在构建（链接阶段）过程中，会在编译时动态链接库路径中查找：<code>lib&lt;link-name&gt;.so</code> 文件。</p></li>

<li><p>在 gcc 命令参数的顺序是有意义的，<code>-L</code> <code>-l</code> <code>-Wl</code> 相关参数会按照顺序传递给链接器 ld。链接器按照命令行中列出的顺序处理输入文件。当链接器遇到一个文件，它会在当前文件中尝试解析以前的所有未定义的引用。然后它会继续处理下一个文件。如果一个符号在它第一次被引用之前没有被定义（例如，库被列在源文件之前），该符号将保持未定义状态。因此，<strong><code>-L</code>、 <code>-l</code>、 <code>-Wl</code>、 <code>xxx.so</code> 一定要在 <code>xxx.c</code> 参数的后面</strong></p></li>
</ul>
]]></description></item><item><title>Bash 测试框架</title><link>https://www.rectcircle.cn/posts/bash-test-framework/</link><pubDate>Thu, 15 Aug 2024 17:43:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/bash-test-framework/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>在使用各种语言编写的程序时，我们经常需要对程序的功能进行验证。这时候就需要使用测试框架，如 Java 的 JUnit、Python 的 pytest、Go 的 testing 模块等。</p>

<p>Bash 作为 Shell 脚本语言，一般不会写一些复杂的逻辑，因此很少有使用测试框架的需求。但是，当我们想执行一些复杂端到端测试或验证一个操作系统环境时（如虚拟机镜像的测试、Docker 镜像的测试），Bash 来编写测试脚本就非常方便了。</p>

<p>本文将以此为例，介绍如何使用 Bash 编写一个简单的测试框架，以及如何使用这个测试框架，验证面向各个开发语言的编译环境。</p>

<p>最后，将介绍主流的开源 Bash 测试框架 Bats。</p>

<h2 id="实现简单的-bash-测试框架">实现简单的 Bash 测试框架</h2>

<h3 id="loginfo-函数">loginfo 函数</h3>

<p>包含时间日期的格式打印日志： <code>loginfo &lt;msg&gt;</code>。</p>

<p>实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">loginfo<span style="color:#f92672">(){</span>
    echo <span style="color:#e6db74">&#34;[</span><span style="color:#66d9ef">$(</span>date -u +<span style="color:#e6db74">&#34;%Y-%m-%dT%H:%M:%S.%3NZ&#34;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">] </span>$@<span style="color:#e6db74">&#34;</span> 
<span style="color:#f92672">}</span></code></pre></div>
<p>示例</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">loginfo hello
<span style="color:#75715e"># [2024-08-15T07:32:51.000Z] hello</span></code></pre></div>
<h3 id="assert-函数">assert 函数</h3>

<p><code>assert</code> 函数，传递一个子命令，如果子命令执行成功，则测试通过；如果子命令执行失败，则测试失败。用法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">assert &lt;msg&gt; -- &lt;cmd&gt; &lt;args...&gt;
assert &lt;cmd&gt; &lt;args...&gt;</pre></div>
<p>实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert<span style="color:#f92672">(){</span>
    set +e
    msg_arr<span style="color:#f92672">=()</span>
    sub_cmd<span style="color:#f92672">=</span>
    sub_args<span style="color:#f92672">=()</span>

    meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">for</span> arg in <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>; <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
            <span style="color:#75715e"># 没有遇到分割符号 --</span>
            <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;--&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
                meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
            <span style="color:#66d9ef">else</span>
                msg_arr<span style="color:#f92672">+=(</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
            <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
            <span style="color:#75715e"># 遇到过分割符后的第一个参数</span>
            sub_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span>
            meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
        <span style="color:#66d9ef">else</span>
            sub_args<span style="color:#f92672">+=(</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
        <span style="color:#66d9ef">fi</span>
    <span style="color:#66d9ef">done</span>
    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">${#</span>msg_arr[@]<span style="color:#e6db74">}</span> -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
        sub_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>msg_arr[0]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
        sub_args<span style="color:#f92672">=(</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>msg_arr[@]:1<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
        msg_arr<span style="color:#f92672">=()</span>
    <span style="color:#66d9ef">fi</span>
    unset meet_delimiter arg
    <span style="color:#75715e"># echo msg=&#34;${msg_arr[@]}&#34; sub_cmd=&#34;$sub_cmd&#34; sub_args=&#34;${sub_args[@]}&#34;</span>

    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$sub_cmd<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
        echo <span style="color:#e6db74">&#34;error: cmd param not found&#34;</span>
        echo <span style="color:#e6db74">&#34;usage: assert &lt;msg&gt; -- &lt;cmd&gt; &lt;args...&gt;&#34;</span>
        echo <span style="color:#e6db74">&#34;       assert &lt;cmd&gt; &lt;args...&gt;&#34;</span>
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
    <span style="color:#66d9ef">fi</span>
    loginfo <span style="color:#e6db74">&#34;--- will assert </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
    eval <span style="color:#e6db74">&#34;</span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $? -eq <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
        loginfo <span style="color:#e6db74">&#34;--- assert ok </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
        echo
    <span style="color:#66d9ef">else</span>
        loginfo <span style="color:#e6db74">&#34;--- assert fail </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
        echo
        exit <span style="color:#ae81ff">1</span>
    <span style="color:#66d9ef">fi</span>
<span style="color:#f92672">}</span></code></pre></div>
<ul>
<li>首先解析参数，真正实现应该采用 getopt getopts 解析。</li>
<li>打印执行子命令之前的的日志。</li>
<li>使用 eval 执行子命令，原因是有些场景子命令需要执行 <code>|</code> 管道符，直接执行无法处理。</li>
<li>判断子命令执行结果，如果执行成功，则打印日志，测试通过；如果执行失败，则打印日志，测试失败直接退出，退出码为 1。</li>
<li>失败场景使用 <code>exit 1</code> 原因是，希望失败可以直接中断后续的测试。另外，实测 <code>return 1</code> 调用方使用 <code>set -e</code> 无效。</li>
</ul>

<p>使用示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert
<span style="color:#75715e"># error: cmd param not found</span>
<span style="color:#75715e"># usage: assert &lt;msg&gt; -- &lt;cmd&gt; &lt;args...&gt;</span>
<span style="color:#75715e">#        assert &lt;cmd&gt; &lt;args...&gt;</span>
assert pwd
<span style="color:#75715e"># [2024-08-15T07:50:19.099Z] --- will assert : pwd</span> 
<span style="color:#75715e"># /home/rectcircle</span>
<span style="color:#75715e"># [2024-08-15T07:50:19.105Z] --- assert ok : pwd</span> 
assert -- pwd
<span style="color:#75715e"># [2024-08-15T07:50:19.099Z] --- will assert : pwd</span> 
<span style="color:#75715e"># /home/rectcircle</span>
<span style="color:#75715e"># [2024-08-15T07:50:19.105Z] --- assert ok : pwd</span> 
assert <span style="color:#e6db74">&#39;test pwd&#39;</span> -- pwd
<span style="color:#75715e"># [2024-08-15T07:51:28.083Z] --- will assert test pwd: pwd</span> 
<span style="color:#75715e"># /home/rectcircle</span>
<span style="color:#75715e"># [2024-08-15T07:51:28.084Z] --- assert ok test pwd: pwd</span> 
assert <span style="color:#e6db74">&#39;test false&#39;</span> -- false
<span style="color:#75715e"># [2024-08-15T07:52:26.675Z] --- will assert test false: false</span> 
<span style="color:#75715e"># [2024-08-15T07:52:26.676Z] --- assert fail test false: false</span> 
<span style="color:#75715e"># 退出码 1</span></code></pre></div>
<h3 id="assert-regex-函数">assert_regex 函数</h3>

<p><code>assert_regex</code> 函数，判断子命令的输出是否匹配正则表达式。用法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">assert_regex &lt;grep-pattern&gt; &lt;msg&gt; -- &lt;cmd&gt; &lt;args...&gt;
assert_regex &lt;grep-pattern&gt; &lt;cmd&gt; &lt;args...&gt;</pre></div>
<p>实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert_regex<span style="color:#f92672">(){</span>
    grep_pattern<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
    shift
    assert <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;|&#34;</span> grep -E <span style="color:#e6db74">&#34;</span>$grep_pattern<span style="color:#e6db74">&#34;</span>
<span style="color:#f92672">}</span></code></pre></div>
<ul>
<li>这里实现非常简单，直接调用 <code>assert</code> 函数，并使用管道符合 grep 进行输出匹配。</li>
</ul>

<p>使用示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert_regex <span style="color:#e6db74">&#34;abc&#34;</span> echo <span style="color:#e6db74">&#39;abc&#39;</span>
<span style="color:#75715e"># [2024-08-15T08:04:00.441Z] --- will assert : echo abc | grep -E abc</span>
<span style="color:#75715e"># abc</span>
<span style="color:#75715e"># [2024-08-15T08:04:00.443Z] --- assert ok : echo abc | grep -E abc</span>
assert_regex <span style="color:#e6db74">&#34;123&#34;</span> echo <span style="color:#e6db74">&#39;abc&#39;</span>
<span style="color:#75715e"># [2024-08-15T08:04:05.941Z] --- will assert : echo abc | grep -E 123</span>
<span style="color:#75715e"># [2024-08-15T08:04:05.943Z] --- assert fail : echo abc | grep -E 123</span>
<span style="color:#75715e"># 退出码 1</span></code></pre></div>
<h3 id="assert-http-函数">assert_http 函数</h3>

<p><code>assert_http</code> 函数，后台启动子命令，并发起 http 请求，并判断 http 响应码是否符合预期。用法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">assert_http &lt;url&gt; &lt;status-code&gt; &lt;msg&gt; -- &lt;server-start-cmd&gt; &lt;args...&gt;
assert_http &lt;url&gt; &lt;status-code&gt; &lt;server-start-cmd&gt; &lt;args...&gt;</pre></div>
<p>实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert_http<span style="color:#f92672">(){</span>
    set +e
    url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
    status_code<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$2<span style="color:#e6db74">&#34;</span>
    shift
    shift

    msg_arr<span style="color:#f92672">=()</span>
    sub_cmd<span style="color:#f92672">=</span>
    sub_args<span style="color:#f92672">=()</span>

    meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">for</span> arg in <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>; <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
            <span style="color:#75715e"># 没有遇到分割符号 --</span>
            <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;--&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
                meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
            <span style="color:#66d9ef">else</span>
                msg_arr<span style="color:#f92672">+=(</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
            <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
            <span style="color:#75715e"># 遇到过分割符后的第一个参数</span>
            sub_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span>
            meet_delimiter<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
        <span style="color:#66d9ef">else</span>
            sub_args<span style="color:#f92672">+=(</span><span style="color:#e6db74">&#34;</span>$arg<span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
        <span style="color:#66d9ef">fi</span>
    <span style="color:#66d9ef">done</span>
    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$meet_delimiter<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">${#</span>msg_arr[@]<span style="color:#e6db74">}</span> -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
        sub_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>msg_arr[0]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
        sub_args<span style="color:#f92672">=(</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>msg_arr[@]:1<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
        msg_arr<span style="color:#f92672">=()</span>
    <span style="color:#66d9ef">fi</span>
    unset meet_delimiter arg
    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$sub_cmd<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span> ;<span style="color:#66d9ef">then</span>
        echo <span style="color:#e6db74">&#34;error: cmd param not found&#34;</span>
        echo <span style="color:#e6db74">&#34;usage: assert_http &lt;url&gt; &lt;status-code&gt; &lt;msg&gt; -- &lt;server-start-cmd&gt; &lt;args...&gt;&#34;</span>
        echo <span style="color:#e6db74">&#34;       assert_http &lt;url&gt; &lt;status-code&gt; &lt;server-start-cmd&gt; &lt;args...&gt;&#34;</span>
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
    <span style="color:#66d9ef">fi</span>
    loginfo <span style="color:#e6db74">&#34;--- will assert http </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: url is </span>$url<span style="color:#e6db74">, want status code is </span>$status_code<span style="color:#e6db74">, server start command is </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
    loginfo <span style="color:#e6db74">&#34;will start server&#34;</span>
    set -m
    eval <span style="color:#e6db74">&#34;</span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> &amp;
    job_pid_id<span style="color:#f92672">=</span>$!
    set +m
    echo <span style="color:#e6db74">&#34;server pid is </span>$job_pid_id<span style="color:#e6db74">&#34;</span>
    <span style="color:#75715e"># sleep 1000</span>
    max_retries<span style="color:#f92672">=</span><span style="color:#ae81ff">30</span>
    trap <span style="color:#e6db74">&#34;kill -9 -</span>$job_pid_id<span style="color:#e6db74">&#34;</span> RETURN SIGINT SIGTERM
    retries<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">while</span> <span style="color:#f92672">[</span> $retries -lt $max_retries <span style="color:#f92672">]</span>; <span style="color:#66d9ef">do</span>
        loginfo <span style="color:#e6db74">&#34;will exec: curl --max-time 1 -s -o /dev/null -w \&#34;%{http_code}\&#34; \&#34;</span>$url<span style="color:#e6db74">\&#34;&#34;</span>
        got_status_code<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl --max-time <span style="color:#ae81ff">1</span> -s -o /dev/null -w <span style="color:#e6db74">&#34;%{http_code}&#34;</span> <span style="color:#e6db74">&#34;</span>$url<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$got_status_code<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$status_code<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
            loginfo <span style="color:#e6db74">&#34;retry </span>$retries<span style="color:#e6db74"> success: status code is equal to </span>$status_code<span style="color:#e6db74">&#34;</span>
            loginfo <span style="color:#e6db74">&#34;--- http assert ok </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: url is </span>$url<span style="color:#e6db74">, want status code is </span>$status_code<span style="color:#e6db74">, server start command is </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
            <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
        <span style="color:#66d9ef">else</span>
            loginfo <span style="color:#e6db74">&#34;retry </span>$retries<span style="color:#e6db74"> not success: status code is </span>$got_status_code<span style="color:#e6db74"> not equal to </span>$status_code<span style="color:#e6db74">&#34;</span>
            retries<span style="color:#f92672">=</span><span style="color:#66d9ef">$((</span>retries <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#66d9ef">))</span>
            sleep <span style="color:#ae81ff">1</span>
        <span style="color:#66d9ef">fi</span>
    <span style="color:#66d9ef">done</span>
    loginfo <span style="color:#e6db74">&#34;--- http assert failed </span><span style="color:#e6db74">${</span>msg_arr[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">: url is </span>$url<span style="color:#e6db74">, want status code is </span>$status_code<span style="color:#e6db74">, server start command is </span>$sub_cmd<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>sub_args[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
    exit <span style="color:#ae81ff">1</span>
<span style="color:#f92672">}</span></code></pre></div>
<ul>
<li>首先解析参数，真正实现应该采用 getopt getopts 解析。</li>
<li>打印执行子命令之前的的日志。</li>
<li>使用 <code>set -m</code> 启用作业控制（确保为后续执行的子命令创建进程组） 使用 eval 执行子命令，并通过 <code>&amp;</code> 在后台执行。</li>
<li>使用 curl 轮询判断状态码是否符合预期，如果不符合预期在指定次数后，判定测试失败。</li>
<li>执行结束后，使用 <code>kill -9</code> 杀死整个进程组（<code>-</code> 表示进程组）。</li>
</ul>

<p>使用示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">assert_http http://localhost:8080 <span style="color:#ae81ff">200</span> <span style="color:#e6db74">&#39;echo -e &#34;HTTP/1.1 200 OK\n\nok&#34; | nc -l -k -p 8080 -q 1&#39;</span>
<span style="color:#75715e"># [2024-08-15T09:50:40.245Z] --- will assert http : url is http://localhost:8080, want status code is 200, server start command is echo -e &#34;HTTP/1.1 200 OK\n\nok&#34; | nc -l -k -p 8080 -q 1</span> 
<span style="color:#75715e"># [2024-08-15T09:50:40.245Z] will start server</span>
<span style="color:#75715e"># server pid is 926101</span>
<span style="color:#75715e"># [2024-08-15T09:50:40.246Z] will exec: curl --max-time 1 -s -o /dev/null -w &#34;%{http_code}&#34; &#34;http://localhost:8080&#34;</span>
<span style="color:#75715e"># GET / HTTP/1.1</span>
<span style="color:#75715e"># Host: localhost:8080</span>
<span style="color:#75715e"># User-Agent: curl/7.88.1</span>
<span style="color:#75715e"># Accept: */*</span>

<span style="color:#75715e"># [2024-08-15T09:50:40.252Z] retry 0 success: status code is equal to 200</span>
<span style="color:#75715e"># [2024-08-15T09:50:40.253Z] --- http assert ok : url is http://localhost:8080, want status code is 200, server start command is echo -e &#34;HTTP/1.1 200 OK\n\nok&#34; | nc -l -k -p 8080 -q 1</span> 
<span style="color:#75715e"># ./util.sh: 第 144 行：kill: (-926101) - 没有那个进程</span></code></pre></div>
<h3 id="register-clear-函数">register_clear 函数</h3>

<p><code>register_clear</code> 函数，注册一个清理函数，在一个脚本结束且退出码为非 0 时执行。用法为 <code>registe_clear &lt;cmd-or-func&gt;</code>。</p>

<p>实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">registe_clear<span style="color:#f92672">(){</span>
    trap <span style="color:#e6db74">&#34;[ \&#34;\$?\&#34; -eq 0 ] &amp;&amp; </span>$1<span style="color:#e6db74"> || true&#34;</span> EXIT
<span style="color:#f92672">}</span></code></pre></div>
<ul>
<li>这里使用了 trap 命令，注册一个信号处理函数，即：当脚本退出时，判断退出码是否为 0，如果为 0 则执行清理函数，否则不执行。</li>
</ul>

<p>使用示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># test.sh</span>
<span style="color:#75715e"># registe_clear &#39;echo clear&#39;</span>
<span style="color:#75715e"># exit 1</span>
bash test.sh
<span style="color:#75715e"># 无输出</span>

<span style="color:#75715e"># test.sh</span>
<span style="color:#75715e"># registe_clear &#39;echo clear&#39;</span>
<span style="color:#75715e"># exit 0</span>
bash test.sh
<span style="color:#75715e"># clear</span></code></pre></div>
<h2 id="示例-测试各个编程语言编译环境">示例：测试各个编程语言编译环境</h2>

<ul>
<li>假设上述函数定义在 <code>util.sh</code>。</li>
<li>如下每个语言的测试脚本位于 <code>test/&lt;lang&gt;/test.sh</code>。</li>
<li><code>data/&lt;lang&gt;</code> 目录下有各个语言的测试项目。</li>
</ul>

<h3 id="基础环境">基础环境</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e

script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh

loginfo <span style="color:#e6db74">&#34;=== start test _default_ ===&#34;</span>


loginfo <span style="color:#e6db74">&#34;=== test file system ===&#34;</span>
assert touch /tmp/.abc
assert test -e /etc/command-not-found.sh
assert ! touch /.abc
assert <span style="color:#e6db74">&#39;arr=(/path/to/xxx-xxx-*) &amp;&amp; [ ${#arr[@]} -ne 0 ]&#39;</span>

loginfo <span style="color:#e6db74">&#34;=== test command ===&#34;</span>
assert which bash
assert which sh
assert which wget
assert which curl
assert which git
assert which sudo
assert which netstat
assert which nc
assert which zsh
assert which socat
assert which man
assert which tmux
assert which rsync
assert which sshpass
assert which unzip
assert which locale
assert which ip
assert which ping
assert which less
assert which vim
assert which nc
assert which xz
assert which fzy
assert which jq
assert which nix
assert which nix-build
assert which nix-channel
assert which nix-channel-index
assert which nix-collect-garbage
assert which nix-copy-closure
assert which nix-daemon
assert which nix-env
assert which nix-hash
assert which nix-index
assert which nix-instantiate
assert which nix-locate
assert which nix-prefetch-url
assert which nix-shell
assert which nix-store
assert which python python3 python3.12 pip node gcc openssl gdb make pkg-config gettext</code></pre></div>
<h3 id="c">C++</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e


script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh


clear<span style="color:#f92672">(){</span>
    loginfo <span style="color:#e6db74">&#34;will clear test data&#34;</span>
    rm -rf $script_path/../../data/cpp/hello/c-main
    rm -rf $script_path/../../data/cpp/hello/cpp-main
    rm -rf $script_path/../../data/cpp/cmake/build
    rm -rf /tmp/test/cpp
<span style="color:#f92672">}</span>
register_clear clear
clear


loginfo <span style="color:#e6db74">&#34;=== start test basic env ===&#34;</span>
assert which clang-apply-replacements clangd clang-format clang-linker-wrapper clang-offload-packager clang-refactor clang-repl clang-change-namespace clang-doc clang-include-cleaner clang-move clang-pseudo clang-rename clang-scan-deps clang-check clang-extdef-mapping clang-include-fixer clang-offload-bundler clang-query clang-reorder-fields clang-tidy
assert which cmake cpack ctest
assert which autoconf  autoheader  autom4te  autoreconf  autoscan  autoupdate  ifnames
assert which aclocal automake
assert which m4
assert which libtool  libtoolize
assert_regex share/aclocal <span style="color:#e6db74">&#39;echo $ACLOCAL_PATH&#39;</span> 


loginfo <span style="color:#e6db74">&#34;=== start test hello project&#34;</span>
cd $script_path/../../data/cpp/hello
assert g++ main.cpp -o cpp-main
assert_regex <span style="color:#e6db74">&#34;Hello&#34;</span> ./cpp-main
assert gcc main.c -o c-main
assert_regex <span style="color:#e6db74">&#34;Hello&#34;</span> ./c-main


loginfo <span style="color:#e6db74">&#34;=== start test cmake project&#34;</span>
cd $script_path/../../data/cpp/cmake
assert cmake -S./ -B./build
assert cmake --build ./build
assert_regex <span style="color:#e6db74">&#34;Hello&#34;</span> ./build/main


loginfo <span style="color:#e6db74">&#34;=== start test compile dropbear&#34;</span>
mkdir -p /tmp/test/cpp
cd /tmp/test/cpp
git clone https://github.com/mkj/dropbear.git
cd dropbear
git checkout DROPBEAR_2022.83
assert nix-env -iA nixpkgs.zlib nixpkgs.libxcrypt
assert ./configure --prefix<span style="color:#f92672">=</span>$HOME/.local
assert <span style="color:#e6db74">&#39;source ~/.bashrc &amp;&amp; make PROGRAMS=&#34;dropbear dbclient dropbearkey dropbearconvert scp&#34;&#39;</span>
assert make install
assert ~/.local/sbin/dropbear -h


loginfo <span style="color:#e6db74">&#34;=== start test compile leveldb&#34;</span>
mkdir -p /tmp/test/cpp
cd /tmp/test/cpp
git clone https://github.com/google/leveldb
cd leveldb
git submodule update --init --recursive
mkdir -p build <span style="color:#f92672">&amp;&amp;</span> cd build
assert cmake -DCMAKE_BUILD_TYPE<span style="color:#f92672">=</span>Release ..
assert cmake --build .
assert make test


loginfo <span style="color:#e6db74">&#34;=== start test compile curl with nghttp2&#34;</span>
mkdir -p /tmp/test/cpp
cd /tmp/test/cpp
git clone https://github.com/nghttp2/nghttp2
cd nghttp2
git checkout d97bc7d8745ded136efa6e9e747f2310406893dd
git submodule update --init
assert autoreconf -i
assert automake
assert autoconf
assert ./configure --prefix<span style="color:#f92672">=</span>$HOME/.local
assert make
assert make install
assert_regex libnghttp2.a ls -al ~/.local/lib 
assert_regex libnghttp2.so ls -al ~/.local/lib 

cd ../
git clone https://github.com/curl/curl
cd curl
git checkout ba235ab269080dc66e35835c829f7ac4290dbc1d
assert autoreconf -fi
assert nix-env -iA nixpkgs.libpsl
assert ./configure --prefix<span style="color:#f92672">=</span>$HOME/.local --with-nghttp2<span style="color:#f92672">=</span>$HOME/.local --with-ssl
assert make
assert make install
assert_regex <span style="color:#e6db74">&#39;nghttp2&#39;</span> ~/.local/bin/curl --version
assert_regex <span style="color:#e6db74">&#39;libnghttp2.so&#39;</span> ldd ~/.local/bin/curl
assert ~/.local/bin/curl --http2 -I nghttp2.org
assert_regex <span style="color:#e6db74">&#39;HTTP/2&#39;</span> ~/.local/bin/curl --http2 -I nghttp2.org</code></pre></div>
<h3 id="golang">Golang</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e

script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh

loginfo <span style="color:#e6db74">&#34;=== start test golang ===&#34;</span>


loginfo <span style="color:#e6db74">&#34;=== test basic env ===&#34;</span>
assert_regex <span style="color:#e6db74">&#34;GOPROXY.*https.*goproxy.cn.*direct&#34;</span> go env
assert which go
assert which dlv
assert which gofmt
assert which gopls
assert which staticcheck
assert_regex <span style="color:#e6db74">&#34;GOROOT=.*nix&#34;</span> go env
assert_regex <span style="color:#e6db74">&#34;GOPATH=.*home/.*/go&#34;</span> go env


loginfo <span style="color:#e6db74">&#34;=== test hello project ===&#34;</span>
cd $script_path/../../data/golang/hello
assert test -e go.mod
assert go get github.com/gin-gonic/gin
assert go mod tidy 
assert go build
assert ./main
assert go run ./
assert go test


loginfo <span style="color:#e6db74">&#34;=== test sample cgo project ===&#34;</span>
cd $script_path/../../data/golang/samplecgo
assert test -e go.mod
assert go build
assert_regex <span style="color:#e6db74">&#34;/nix/store/.*/ld-linux-x86-64.so.2&#34;</span> <span style="color:#e6db74">&#34;ldd ./main | grep ld-linux-x86-64.so.2&#34;</span>
assert ./main
assert <span style="color:#e6db74">&#34;CGO_LDFLAGS=&#39;-Wl,-rpath=/lib/x86_64-linux-gnu -Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2&#39; go build&#34;</span>
assert <span style="color:#e6db74">&#39;arr=(/nix/store/.*/ld-linux-x86-64.so.2) &amp;&amp; [ ${#arr[@]} -eq 0 ]&#39;</span>


loginfo <span style="color:#e6db74">&#34;=== test kubernetes project ===&#34;</span>
cd /tmp <span style="color:#f92672">&amp;&amp;</span> rm -rf gin
assert git clone --depth <span style="color:#ae81ff">1</span> https://github.com/gin-gonic/gin.git
cd gin
assert go build
assert go test
rm -rf /tmp/gin</code></pre></div>
<h3 id="java">Java</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e


script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh


clear<span style="color:#f92672">(){</span>
    loginfo <span style="color:#e6db74">&#34;will clear test data&#34;</span>
    rm -rf $script_path/../../data/java/hello/Main.class
    rm -rf /tmp/test/java
<span style="color:#f92672">}</span>
register_clear clear
clear


loginfo <span style="color:#e6db74">&#34;=== start test basic env ===&#34;</span>
assert which java
assert which mvn
assert which gradle
assert_regex <span style="color:#e6db74">&#34;maven.aliyun.com&#34;</span> cat ~/.m2/settings.xml
assert_regex <span style="color:#e6db74">&#34;&#39;version.*17.*&#39;&#34;</span> <span style="color:#e6db74">&#39;java -version 2&gt;&amp;1&#39;</span>
assert_regex <span style="color:#e6db74">&#34;&#39;javac.*17.*&#39;&#34;</span> <span style="color:#e6db74">&#39;javac -version 2&gt;&amp;1&#39;</span>
assert mvn -v
assert gradle -v

loginfo <span style="color:#e6db74">&#34;=== start test hello ===&#34;</span>
cd $script_path/../../data/java/hello
javac Main.java
java Main


loginfo <span style="color:#e6db74">&#34;=== start test maven and gradle project&#34;</span>
mkdir -p /tmp/test/java
cd /tmp/test/java
wget https://repo.huaweicloud.com/repository/maven/org/springframework/boot/spring-boot-cli/3.3.2/spring-boot-cli-3.3.2-bin.zip
<span style="color:#75715e"># wget https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-cli/3.3.2/spring-boot-cli-3.3.2-bin.zip</span>
unzip -o spring-boot-cli-3.3.2-bin.zip
spring_cmd<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/spring-3.3.2/bin/spring
<span style="color:#75715e"># $spring_cmd init --list</span>
mkdir -p spring-web-maven
cd spring-web-maven
assert $spring_cmd init --type maven-project --dependencies web --extract
assert mvn package
assert_http localhost:8080 <span style="color:#ae81ff">404</span> mvn spring-boot:run
cd /tmp/test/java
mkdir -p spring-web-gradle
cd spring-web-gradle
assert $spring_cmd init --type gradle-project --dependencies web --extract
<span style="color:#75715e"># fixme: https://docs.gradle.org/current/userguide/toolchains.html#sec:custom_loc</span>
<span style="color:#75715e"># gradle -q javaToolchains</span>
mkdir -p ~/.gradle
export JAVA_HOME<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>which java<span style="color:#66d9ef">))))</span>
export GRADLE_USER_HOME<span style="color:#f92672">=</span>$HOME/.gradle
echo <span style="color:#e6db74">&#39;org.gradle.java.installations.fromEnv=JAVA_HOME&#39;</span> &gt; ~/.gradle/gradle.properties <span style="color:#75715e"># 不工作，原因是 cat $(which gradle) -P 覆盖了。</span>
echo <span style="color:#e6db74">&#34;org.gradle.java.installations.paths=</span>$JAVA_HOME<span style="color:#e6db74">&#34;</span> &gt; ~/.gradle/gradle.properties
<span style="color:#75715e"># fixme end</span>
<span style="color:#75715e"># ./gradlew -q javaToolchains</span>
<span style="color:#75715e"># gradle -q javaToolchains</span>
<span style="color:#75715e"># gradle -Porg.gradle.java.installations.fromEnv=JAVA_HOME -q javaToolchains</span>
<span style="color:#75715e"># gradle -Porg.gradle.java.installations.paths=/nix/store/zmj3m7wrgqf340vqd4v90w8dw371vhjg-openjdk-17.0.7+7/lib/openjdk -q javaToolchains</span>
assert gradle clean bootJar
assert_http localhost:8080 <span style="color:#ae81ff">404</span> gradle clean bootRun
rm -rf /tmp/test/java


loginfo <span style="color:#e6db74">&#34;=== start test spring-projects/spring-petclinic&#34;</span>
mkdir -p /tmp/test/java
cd /tmp/test/java
git clone https://gitee.com/rectcircle/spring-petclinic.git
<span style="color:#75715e"># git clone https://github.com/spring-projects/spring-petclinic</span>
cd spring-petclinic <span style="color:#f92672">&amp;&amp;</span> git checkout 383edc1656e305f8151c258b6925df00f7b53655
assert mvn install -Dmaven.test.skip<span style="color:#f92672">=</span>true
assert_http localhost:8080 <span style="color:#ae81ff">200</span> java -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar
rm -rf /tmp/test/java</code></pre></div>
<h3 id="node-js">Node.js</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e


script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh


clear<span style="color:#f92672">(){</span>
    loginfo <span style="color:#e6db74">&#34;will clear test data&#34;</span>
    rm -rf $script_path/../../data/nodejs/koa/app
    rm -rf $script_path/../../data/nodejs/koa/node_modules
    cd $script_path/../../data/nodejs/koa
    npm remove sass bcrypt sqlite3 krb5
    rm -rf ~/.nvm/alias ~/.nvm/versions
    <span style="color:#75715e"># rm -rf ~/.nvm/.cache</span>
<span style="color:#f92672">}</span>
register_clear clear
clear



loginfo <span style="color:#e6db74">&#34;=== start test basic env ===&#34;</span>
assert_regex <span style="color:#e6db74">&#34;npmmirror.com&#34;</span> <span style="color:#e6db74">&#39;echo $FNM_NODE_DIST_MIRROR&#39;</span>
assert_regex <span style="color:#e6db74">&#34;npmmirror.com&#34;</span> <span style="color:#e6db74">&#39;echo $NVM_NODEJS_ORG_MIRROR&#39;</span>
assert ls -al ~/.npm/lib
assert_regex <span style="color:#e6db74">&#39;yarn&#39;</span> cat ~/.nvm/default-packages
assert_regex <span style="color:#e6db74">&#39;pnpm&#39;</span> cat ~/.nvm/default-packages
assert_regex <span style="color:#e6db74">&#39;node_modules/.bin&#39;</span> <span style="color:#e6db74">&#39;echo $PATH&#39;</span>
assert_regex <span style="color:#e6db74">${</span>PNPM_HOME<span style="color:#e6db74">}</span> <span style="color:#e6db74">&#39;echo $PATH&#39;</span>
assert_regex .npm/bin <span style="color:#e6db74">&#39;echo $PATH&#39;</span>
assert ! test -z $FNM_NODE_DIST_MIRROR
assert_regex $HOME/.nvm <span style="color:#e6db74">&#39;echo $NVM_DIR&#39;</span>
assert_regex $HOME/.local/share/pnpm <span style="color:#e6db74">&#39;echo $PNPM_HOME&#39;</span>
assert_regex $HOME/.npm <span style="color:#e6db74">&#39;echo $npm_config_prefix&#39;</span>
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; type fnm&#39;&#34;</span>
assert which node
assert which npm
assert which npx
assert which nvm.sh
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; type nvm&#39;&#34;</span>
assert which pnpm
assert which pnpx
assert which yarn
assert which yarnpkg


loginfo <span style="color:#e6db74">&#34;=== start test package manager ===&#34;</span>
cd $script_path/../../data/nodejs/koa
export ELECTRON_MIRROR<span style="color:#f92672">=</span>http://npmmirror.com/mirrors/electron/
assert pnpm --version
assert pnpm install
assert_http http://localhost:3000 <span style="color:#ae81ff">200</span> pnpm run start
assert pnpm i -g @electron-forge/cli
assert which electron-forge
assert <span style="color:#e6db74">&#39;electron-forge init --template=webpack app&#39;</span>
assert pnpm add tree-sitter tree-sitter-javascript
assert pnpm rebuild
assert pnpm remove tree-sitter tree-sitter-javascript
assert pnpm rebuild
rm -rf node_modules
rm -rf $script_path/../../data/nodejs/koa/app

assert npm install
assert_http http://localhost:3000 <span style="color:#ae81ff">200</span> npm run start
assert npm i -g @electron-forge/cli
assert which electron-forge
assert <span style="color:#e6db74">&#39;electron-forge init --template=webpack app&#39;</span>
assert npm install tree-sitter tree-sitter-javascript
assert npm rebuild
assert npm remove tree-sitter tree-sitter-javascript
assert npm rebuild
rm -rf node_modules
rm -rf $script_path/../../data/nodejs/koa/app

assert yarn install
assert_http http://localhost:3000 <span style="color:#ae81ff">200</span> yarn run start
assert yarn global add @electron-forge/cli
assert which electron-forge
assert <span style="color:#e6db74">&#39;electron-forge init --template=webpack app&#39;</span>
assert yarn add tree-sitter tree-sitter-javascript
assert yarn install --force
assert yarn remove tree-sitter tree-sitter-javascript
assert yarn install --force
rm -rf node_modules
rm -rf $script_path/../../data/nodejs/koa/app


loginfo <span style="color:#e6db74">&#34;=== start test nvm ===&#34;</span>
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; nvm install 16&#39;&#34;</span>
assert_regex <span style="color:#ae81ff">16</span> <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; node -v&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#34;</span>$HOME<span style="color:#e6db74">/.nvm/versions/node/&#34;</span>  <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which npm&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#34;</span>$HOME<span style="color:#e6db74">/.nvm/versions/node/&#34;</span>  <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which pnpm&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#34;</span>$HOME<span style="color:#e6db74">/.nvm/versions/node/&#34;</span>  <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which yarn&#39;&#34;</span>
rm -rf ~/.nvm/alias ~/.nvm/versions
<span style="color:#75715e"># rm -rf ~/.nvm/.cache</span>


loginfo <span style="color:#e6db74">&#34;=== start test fnm ===&#34;</span>
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; fnm install 18&#39;&#34;</span>
assert_regex <span style="color:#ae81ff">18</span> <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; node -v&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#39;fnm_multishells/.*/bin/npm&#39;</span> <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which npm&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#39;fnm_multishells/.*/bin/pnpm&#39;</span> <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which pnpm&#39;&#34;</span>
assert_regex <span style="color:#e6db74">&#39;fnm_multishells/.*/bin/yarn&#39;</span> <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; which yarn&#39;&#34;</span>


loginfo <span style="color:#e6db74">&#34;=== start test node gyp ===&#34;</span>
cd $script_path/../../data/nodejs/koa
assert npm i sass bcrypt sqlite3
assert <span style="color:#e6db74">&#39;nix-env -iA nixpkgs.libkrb5 &amp;&amp; pip install setuptools &amp;&amp; CC=$(which gcc) npm i krb5&#39;</span>
npm remove sass bcrypt sqlite3 krb5</code></pre></div>
<h3 id="python">Python</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e


script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh


clear<span style="color:#f92672">(){</span>
    loginfo <span style="color:#e6db74">&#34;will clear test data&#34;</span>
    rm -rf ~/.local/bin/pip ~/.local/bin/pip3 ~/.local/bin/pip3.12 
    rm -rf ~/.local/lib/python3.12/site-packages
    rm -rf /tmp/test/venv
    rm -rf /tmp/test/poerty ~/.local/bin/poetry
    rm -rf ~/.local/lib/python3.11/site-packages ~/.local/bin/pip3.11
    <span style="color:#66d9ef">if</span> test -e ~/.bashrc_bak ;<span style="color:#66d9ef">then</span>
        rm -rf ~/.bashrc
        mv ~/.bashrc_bak ~/.bashrc
    <span style="color:#66d9ef">fi</span>
    rm -rf ~/.conda
    <span style="color:#75715e"># rm -rf ~/.cache</span>
    <span style="color:#75715e"># mkdir -p ~/.cache</span>
<span style="color:#f92672">}</span>
register_clear clear
clear

loginfo <span style="color:#e6db74">&#34;=== start basic env ===&#34;</span>
assert_regex <span style="color:#e6db74">&#39;.conda/bin&#39;</span> <span style="color:#e6db74">&#39;echo $PATH&#39;</span>
assert which python
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; type conda&#39;&#34;</span>
assert which pip
assert which poetry


loginfo <span style="color:#e6db74">&#34;=== test hello project ===&#34;</span>
cd $script_path/../../data/python/hello
assert_regex <span style="color:#e6db74">&#39;&#34;hello world&#34;&#39;</span> python main.py


loginfo <span style="color:#e6db74">&#34;=== test pip ===&#34;</span>
assert <span style="color:#e6db74">&#39;测试 upgrade pip 后 pip 能否正常工作&#39;</span> -- <span style="color:#e6db74">&#34;pip install --upgrade pip &amp;&amp; pip install -y requests&#34;</span>
assert <span style="color:#e6db74">&#39;测试 mysqlclient 安装&#39;</span> -- <span style="color:#e6db74">&#34;nix-env -iA nixpkgs.libmysqlclient &amp;&amp; pip install mariadb &amp;&amp; python -c &#39;import mariadb&#39;&#34;</span>
<span style="color:#75715e">#  boto3 太慢，先去掉</span>
assert <span style="color:#e6db74">&#39;测试 top100 库安装&#39;</span> -- pip install urllib3 botocore requests certifi typing-extensions idna charset-normalizer python-dateutil setuptools packaging s3transfer aiobotocore wheel pyyaml six grpcio-status pip numpy s3fs fsspec cryptography cffi google-api-core pycparser pandas importlib-metadata pyasn1 rsa zipp click pydantic attrs protobuf jmespath platformdirs pytz jinja2 awscli colorama markupsafe pyjwt tomli googleapis-common-protos wrapt filelock cachetools google-auth pluggy requests-oauthlib virtualenv pytest oauthlib pyarrow docutils exceptiongroup pyasn1-modules jsonschema iniconfig scipy pyparsing aiohttp isodate soupsieve sqlalchemy beautifulsoup4 psutil pydantic-core pygments multidict pyopenssl yarl decorator tzdata async-timeout tqdm grpcio frozenlist pillow aiosignal greenlet openpyxl et-xmlfile requests-toolbelt annotated-types lxml tomlkit werkzeug proto-plus pynacl deprecated azure-core asn1crypto distlib importlib-resources coverage more-itertools google-cloud-storage websocket-client
<span style="color:#75715e"># import boto3; 太慢，先去掉</span>
assert <span style="color:#e6db74">&#39;测试 top100 库导入&#39;</span> -- <span style="color:#e6db74">&#34;python -c &#39;import urllib3; import botocore; import requests; import certifi; import typing_extensions; import idna; import charset_normalizer; import dateutil; import packaging; import s3transfer; import aiobotocore; import yaml; import six; import numpy; import s3fs; import fsspec; import cryptography; import cffi; import pycparser; import pandas; import importlib_metadata; import pyasn1; import rsa; import zipp; import click; import pydantic; import attrs; import jmespath; import platformdirs; import pytz; import jinja2; import awscli; import colorama; import markupsafe; import jwt; import tomli; import wrapt; import filelock; import cachetools; import pluggy; import requests_oauthlib; import virtualenv; import pytest; import oauthlib; import pyarrow; import docutils; import exceptiongroup; import pyasn1_modules; import jsonschema; import iniconfig; import scipy; import pyparsing; import aiohttp; import isodate; import soupsieve; import sqlalchemy; import bs4; import psutil; import pydantic_core; import pygments; import multidict; import OpenSSL; import yarl; import decorator; import tzdata; import async_timeout; import tqdm; import frozenlist; import PIL; import aiosignal; import greenlet; import openpyxl; import et_xmlfile; import requests_toolbelt; import annotated_types; import lxml; import tomlkit; import werkzeug; import nacl; import deprecated; import azure; import asn1crypto; import distlib; import importlib_resources; import coverage; import more_itertools; import websocket;&#39;&#34;</span>


loginfo <span style="color:#e6db74">&#34;=== test venv ===&#34;</span>
rm -rf /tmp/test/venv <span style="color:#f92672">&amp;&amp;</span> mkdir -p /tmp/test/venv
cd /tmp/test/venv
assert <span style="color:#e6db74">&#39;venv 测试前置准备确保 requests 已安装到系统 python 中&#39;</span> -- pip install requests
assert python -m venv defaultvenv
source ./defaultvenv/bin/activate
assert <span style="color:#e6db74">&#39;venv 和系统库隔离 requests 找不到&#39;</span> -- <span style="color:#e6db74">&#34;! python -c &#39;import requests&#39;&#34;</span>
assert_regex /tmp/test/venv/defaultvenv/bin/pip which pip
assert_regex /tmp/test/venv/defaultvenv/bin/python which python
assert_regex <span style="color:#e6db74">&#39;&#34;hello world&#34;&#39;</span> python $script_path/../../data/python/hello/main.py
assert <span style="color:#e6db74">&#39;测试 venv pip 简单安装&#39;</span> -- pip install requests 
assert <span style="color:#e6db74">&#39;测试 venv mysqlclient 安装&#39;</span> -- <span style="color:#e6db74">&#34;nix-env -iA nixpkgs.libmysqlclient &amp;&amp; pip install mariadb &amp;&amp; python -c &#39;import mariadb&#39;&#34;</span>
deactivate
cd $script_path/../..


loginfo <span style="color:#e6db74">&#34;=== test poerty project ===&#34;</span>
rm -rf /tmp/test/poerty <span style="color:#f92672">&amp;&amp;</span> mkdir -p /tmp/test/poerty
cd /tmp/test/poerty
assert poetry new myproject312
cd myproject312
assert poetry config virtualenvs.in-project true
assert poetry install
assert_regex <span style="color:#e6db74">&#39;3.12&#39;</span> ./.venv/bin/python -V
assert <span style="color:#e6db74">&#39;poerty 测试前置准备确保 requests 已安装到系统 python 中&#39;</span> -- pip install requests
source ./.venv/bin/activate
assert <span style="color:#e6db74">&#39;poerty venv 和系统库隔离 requests 找不到&#39;</span> -- <span style="color:#e6db74">&#34;! python -c &#39;import requests&#39;&#34;</span>
assert_regex /tmp/test/poerty/myproject312/.venv/bin/pip which pip
assert_regex /tmp/test/poerty/myproject312/.venv/bin/python which python
assert_regex <span style="color:#e6db74">&#39;&#34;hello world&#34;&#39;</span> python $script_path/../../data/python/hello/main.py
assert <span style="color:#e6db74">&#39;测试 poerty venv pip 简单安装&#39;</span> -- pip install requests 
assert <span style="color:#e6db74">&#39;测试 poerty venv mysqlclient 安装&#39;</span> -- <span style="color:#e6db74">&#34;nix-env -iA nixpkgs.libmysqlclient &amp;&amp; pip install mariadb &amp;&amp; python -c &#39;import mariadb&#39;&#34;</span>
deactivate
cd $script_path/../..


loginfo <span style="color:#e6db74">&#34;=== test use nix switch python version ===&#34;</span>
assert nix-env -iA nixpkgs.python311
assert_regex <span style="color:#e6db74">&#39;3.11&#39;</span> python -V
assert <span style="color:#e6db74">&#39;检查 python3.11 pip&#39;</span> -- <span style="color:#e6db74">&#34;bash -lc &#39;pip -V&#39;&#34;</span>
assert <span style="color:#e6db74">&#39;测试 python3.11 pip 简单安装&#39;</span> -- pip install requests 
assert <span style="color:#e6db74">&#39;测试 python3.11 mysqlclient 安装&#39;</span> -- <span style="color:#e6db74">&#34;nix-env -iA nixpkgs.libmysqlclient &amp;&amp; pip install mariadb &amp;&amp; python -c &#39;import mariadb&#39;&#34;</span>
assert nix-env --uninstall python3
assert_regex <span style="color:#e6db74">&#39;3.12&#39;</span>  <span style="color:#e6db74">&#39;py=$(which python) &amp;&amp; $py -V&#39;</span>


<span style="color:#75715e"># loginfo &#34;=== test conda ===&#34;</span>
cp -rf ~/.bashrc ~/.bashrc_bak
assert <span style="color:#e6db74">&#34;bash -c &#39;source ~/.bashrc &amp;&amp; conda init bash&#39;&#34;</span>
source ~/.bashrc
assert_regex <span style="color:#e6db74">&#39;.conda/bin/python&#39;</span> which python
<span style="color:#75715e"># fixme: 内存不够，会被 kill</span>
<span style="color:#75715e"># assert &#39;source ~/.bashrc &amp;&amp; nix-env --uninstall mariadb-connector-c &amp;&amp; conda install -y conda-forge::mariadb-connector-c&#39;</span>
<span style="color:#75715e"># assert pip install mariadb</span>
<span style="color:#75715e"># assert &#34;python -c &#39;import mariadb&#39;&#34;</span></code></pre></div>
<h3 id="rust">Rust</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>set -e


script_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">))</span>
source $script_path/../../util.sh


clear<span style="color:#f92672">(){</span>
    loginfo <span style="color:#e6db74">&#34;will clear test data&#34;</span>
    rm -rf /tmp/test/rust
    rm -rf ~/.rustup <span style="color:#f92672">&amp;&amp;</span> mkdir -p ~/.rustup
<span style="color:#f92672">}</span>
register_clear clear
clear



loginfo <span style="color:#e6db74">&#34;=== start test basic env ===&#34;</span>
assert_regex <span style="color:#e6db74">&#34;rsproxy-sparse&#34;</span> <span style="color:#e6db74">&#39;cat ~/.cargo/config&#39;</span>
assert_regex <span style="color:#e6db74">&#34;rsproxy.cn&#34;</span> <span style="color:#e6db74">&#39;cat ~/.cargo/config&#39;</span>
assert which rustc cargo rust-analyzer rustc rustdoc rust-gdb rust-gdbgui rust-lldb rustup
assert_regex <span style="color:#e6db74">&#39;.cargo/bin&#39;</span> <span style="color:#e6db74">&#39;echo $PATH&#39;</span>
assert_regex <span style="color:#e6db74">&#34;&#39;git-fetch-with-cli = true&#39;&#34;</span> <span style="color:#e6db74">&#39;cat ~/.cargo/config&#39;</span>
assert_regex <span style="color:#e6db74">&#39;rust-analyzer.debug.engine.*vadimcn.vscode-lldb&#39;</span> <span style="color:#e6db74">&#39;cat /cloudide/workspace/.cloudide/data/Machine/settings.json&#39;</span>


loginfo <span style="color:#e6db74">&#34;=== start test cargo rust hello project ===&#34;</span>
mkdir -p /tmp/test/rust
cd /tmp/test/rust
assert cargo new hello
cd hello
assert_regex <span style="color:#e6db74">&#34;&#39;Hello, world!&#39;&#34;</span> cargo run
assert cargo build --release
assert_regex <span style="color:#e6db74">&#34;&#39;Hello, world!&#39;&#34;</span> ./target/release/hello


loginfo <span style="color:#e6db74">&#34;=== start test rustup&#34;</span>
<span style="color:#75715e"># fixme: rustup 源配置</span>
export RUSTUP_DIST_SERVER<span style="color:#f92672">=</span>https://rsproxy.cn
export RUSTUP_UPDATE_ROOT<span style="color:#f92672">=</span>https://rsproxy.cn/rustup
assert rustup install <span style="color:#ae81ff">1</span>.80.1
assert_regex <span style="color:#e6db74">&#39;1.80.1&#39;</span> rustc -V</code></pre></div>
<h2 id="开源-bash-测试框架-bats-简介">开源 Bash 测试框架 bats 简介</h2>

<p>上面只实现了一个测试框架最最基础的能力，是对 bash 测试框架原理的一种探索。</p>

<p>当然在开源有很多成熟的开源测试框架，比如（stars 截止 20240815）：</p>

<ul>
<li><a href="https://github.com/bats-core/bats-core">bats-core</a> 4.8k stars。</li>
<li><a href="https://github.com/kward/shunit2">shunit2</a> 1.6k stars。</li>
<li><a href="https://github.com/shellspec/shellspec">shellspec</a> 1.1k stars。</li>
<li><a href="https://github.com/pgrange/bash_unit">bash_unit</a> 590 stars。</li>
<li><a href="https://github.com/bach-sh/bach">bach</a> 549 stars。</li>
<li><a href="https://github.com/lehmannro/assert.sh">assert.sh</a> 487 stars。</li>
<li><a href="https://github.com/rylnd/shpec">shpec</a> 377 stars</li>
</ul>

<p>目前 github starts 数量最多的 Bash 测试框架 bits，本部分将介绍其基本用法。</p>

<p>安装：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local</code></pre></div>
<p>bats 有一套自己 dsl 语法，写一个测试脚本 <code>test.bats</code>，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bats" data-lang="bats">#!/usr/bin/env bats

@test &#34;true&#34; {
  # 这里写 bash 脚本，如果相当于 set -e 执行，如果有任意一条命令失败（退出码非零），该测试失败。
  true
}

@test &#34;false&#34; {
  false
}</code></pre></div>
<p>运行测试 <code>bats test.bats</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"> ✓ true
 ✗ false
   (in test file test.bats, line 8)
     `false&#39; failed

2 tests, 1 failure</pre></div>
<p>VSCode 扩展： <a href="https://marketplace.visualstudio.com/items?itemName=jetmartin.bats">jetmartin.bats</a>。</p>

<p>更多详见： <a href="https://bats-core.readthedocs.io/en/stable/index.html">官网</a>。</p>
]]></description></item><item><title>Nix 高级话题之 channel</title><link>https://www.rectcircle.cn/posts/nix-advanced-channel/</link><pubDate>Sun, 21 Jul 2024 19:31:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-advanced-channel/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.22.1</p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Nix channel 类似于 apt 的 source，即软件源。</p>

<p>Nix 可以配置多个 channel，每个 channel 都是一个 <code>&lt;channel-name&gt;</code> 和 <code>&lt;channel-url&gt;</code> 的映射。</p>

<p>Nix 通过 <code>nix-channel</code> 命令管理 channel 的配置，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-channel {--add url [name] | --remove name | --list | --update [names…] | --list-generations | --rollback [generation] }</pre></div>
<p>配置完成 nix-channel 后，可以通过 <code>nix-env -iA &lt;channel-name&gt;.xxx</code>，安装软件包。</p>

<h2 id="配置文件">配置文件</h2>

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.22/command-ref/files/channels">Nix 参考手册 - 8.6.3 Channels</a></p>
</blockquote>

<p>Nix channel 的配置文件位于 <code>~/.nix-channels</code>，其格式为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;url&gt; &lt;name&gt;
...</pre></div>
<p>可以手动修改该配置文件，也可以通过，如下命令修改：</p>

<ul>
<li><code>nix-channel --add url [name]</code> 添加有个 channel， 如果 name 为空，name 默认为 url 的最后一个 <code>/</code> 后面的字符串，并去除后缀 <code>-stable</code> 或 <code>-unstable</code>。</li>
<li><code>nix-channel --remove name</code> 删除一个 channel。</li>
<li><code>nix-channel --list</code> 列出 channel。</li>
</ul>

<h2 id="更新-channel">更新 channel</h2>

<p>上述步骤更新了配置文件，还是需要手动执行 <code>nix-channel --update  [names…]</code> 更新 channel （这里的 <code>[name…] 是选填的</code> ）。</p>

<p>通过如下过程观察 <code>nix-channel</code> 执行过程：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 卸载 nix</span>
sudo rm -rf /nix ~/.nix-* ~/.local/state/nix ~/.config/nix
<span style="color:#75715e"># 安装 nix</span>
sh &lt;<span style="color:#f92672">(</span>curl -L https://nixos.org/nix/install<span style="color:#f92672">)</span> --no-daemon --no-channel-add
. /home/rectcircle/.nix-profile/etc/profile.d/nix.sh
<span style="color:#75715e"># 添加 channel 将配置添加到  ~/.nix-channels</span>
nix-channel --add https://channels.nixos.org/nixpkgs-unstable nixpkgs
sudo apt update
sudo apt install -y cpulimit
<span style="color:#75715e"># 限制 cpu 执行更新 channel</span> 
cpulimit -l <span style="color:#ae81ff">1</span> -- nix-channel --update -vvv
<span style="color:#75715e"># did not find cache entry for &#39;file:{&#34;name&#34;:&#34;nixpkgs-unstable&#34;,&#34;store&#34;:&#34;/nix/store&#34;,&#34;url&#34;:&#34;https://channels.nixos.org/nixpkgs-unstable&#34;}&#39;</span>
<span style="color:#75715e"># downloading &#39;https://channels.nixos.org/nixpkgs-unstable&#39;...</span>
<span style="color:#75715e"># starting download of https://channels.nixos.org/nixpkgs-unstable</span>
<span style="color:#75715e"># finished download of &#39;https://channels.nixos.org/nixpkgs-unstable&#39;; curl status = 0, HTTP status = 200, body = 1856 bytes</span>
<span style="color:#75715e"># acquiring write lock on &#39;/nix/var/nix/temproots/7531&#39;</span>
<span style="color:#75715e"># locking path &#39;/nix/store/bb5kbdycl7k6fdg5ain3w4jxxh887jl7-nixpkgs-unstable&#39;</span>
<span style="color:#75715e"># lock acquired on &#39;/nix/store/bb5kbdycl7k6fdg5ain3w4jxxh887jl7-nixpkgs-unstable.lock&#39;</span>
<span style="color:#75715e"># lock released on &#39;/nix/store/bb5kbdycl7k6fdg5ain3w4jxxh887jl7-nixpkgs-unstable.lock&#39;</span>
<span style="color:#75715e"># did not find cache entry for &#39;file:{&#34;name&#34;:&#34;nixexprs.tar.xz&#34;,&#34;store&#34;:&#34;/nix/store&#34;,&#34;url&#34;:&#34;https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre642175.90338afd6177/nixexprs.tar.xz&#34;}&#39;</span>
<span style="color:#75715e"># downloading &#39;https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre642175.90338afd6177/nixexprs.tar.xz&#39;...</span>
<span style="color:#75715e"># starting download of https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre642175.90338afd6177/nixexprs.tar.xz</span>
<span style="color:#75715e"># finished download of &#39;https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre642175.90338afd6177/nixexprs.tar.xz&#39;; curl status = 0, HTTP status = 200, body = 29240136 bytes</span>
<span style="color:#75715e"># locking path &#39;/nix/store/s7c3bxnz9j8r2n17vwzg8ssgl6wl10pn-nixexprs.tar.xz&#39;</span>
<span style="color:#75715e"># lock acquired on &#39;/nix/store/s7c3bxnz9j8r2n17vwzg8ssgl6wl10pn-nixexprs.tar.xz.lock&#39;</span>
<span style="color:#75715e"># lock released on &#39;/nix/store/s7c3bxnz9j8r2n17vwzg8ssgl6wl10pn-nixexprs.tar.xz.lock&#39;</span>
<span style="color:#75715e"># unpacking 1 channels...</span>
<span style="color:#75715e"># download thread shutting down</span>

<span style="color:#75715e"># 在上一步执行过程中，在另一个终端执行</span>
ps -ef -w w | grep nix
<span style="color:#75715e"># /nix/store/4z2dxs69kmhhkynyygxc4h5g28axhxjm-nix-2.23.0/bin/nix-env --profile /home/rectcircle/.local/state/nix/profiles/channels --file /tmp/nix.zaPRJ1 --install --remove-all --from-expression &#39;f: f { name = &#34;nixpkgs&#34;; channelName = &#34;nixpkgs&#34;; src = builtins.storePath &#34;/nix/store/s7c3bxnz9j8r2n17vwzg8ssgl6wl10pn-nixexprs.tar.xz&#34;;  }&#39; --quiet</span>
<span style="color:#75715e"># 在上一步执行过程中，在另一个终端执行</span> 
<span style="color:#75715e"># https://github.com/NixOS/nix/blob/2.22.1/src/libstore/unix/build/local-derivation-goal.cc#L2171</span>
<span style="color:#75715e"># https://github.com/NixOS/nix/blob/2.22.1/src/libstore/unix/builtins/unpack-channel.cc</span>
cat /tmp/nix.*
#
<span style="color:#75715e"># { name, channelName, src }:</span>
#
<span style="color:#75715e"># derivation {</span>
<span style="color:#75715e">#   builder = &#34;builtin:unpack-channel&#34;;</span>
#
<span style="color:#75715e">#   system = &#34;builtin&#34;;</span>
#
<span style="color:#75715e">#   inherit name channelName src;</span>
#
<span style="color:#75715e">#   # No point in doing this remotely.</span>
<span style="color:#75715e">#   preferLocalBuild = true;</span>
<span style="color:#75715e"># }</span>
ls -al ~/.nix-defexpr
<span style="color:#75715e"># channels -&gt; /home/rectcircle/.local/state/nix/profiles/channels</span>
<span style="color:#75715e"># channels_root -&gt; /nix/var/nix/profiles/per-user/root/channels</span>
ls -al /home/rectcircle/.local/state/nix/profiles/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels -&gt; channels-1-link</span>
ls -al /home/rectcircle/.local/state/nix/profiles/channels-1-link
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels-1-link -&gt; /nix/store/9lryg21ajp4yx4d8qavg3jwy0crbp80f-user-environment</span>
ls -al /nix/store/9lryg21ajp4yx4d8qavg3jwy0crbp80f-user-environment
<span style="color:#75715e"># manifest.nix -&gt; /nix/store/zzgjar0372zvbc1xazyf5p35aw92pspd-env-manifest.nix</span>
<span style="color:#75715e"># nixpkgs -&gt; /nix/store/j13ha7fdr5n95kgk5q7nnn89h2bq6mr2-nixpkgs/nixpkgs</span>
cat /nix/store/zzgjar0372zvbc1xazyf5p35aw92pspd-env-manifest.nix
<span style="color:#75715e"># [ { meta = { }; name = &#34;nixpkgs&#34;; out = { outPath = &#34;/nix/store/j13ha7fdr5n95kgk5q7nnn89h2bq6mr2-nixpkgs&#34;; }; outPath = &#34;/nix/store/j13ha7fdr5n95kgk5q7nnn89h2bq6mr2-nixpkgs&#34;; outputs = [ &#34;out&#34; ]; system = &#34;builtin&#34;; type = &#34;derivation&#34;; } ]</span>
ls -al /nix/store/j13ha7fdr5n95kgk5q7nnn89h2bq6mr2-nixpkgs/nixpkgs
<span style="color:#75715e"># CONTRIBUTING.md</span>
<span style="color:#75715e"># COPYING</span>
<span style="color:#75715e"># default.nix</span>
<span style="color:#75715e"># doc</span>
<span style="color:#75715e"># .editorconfig</span>
<span style="color:#75715e"># flake.nix</span>
<span style="color:#75715e"># .gitattributes</span>
<span style="color:#75715e"># .git-blame-ignore-revs</span>
<span style="color:#75715e"># .github</span>
<span style="color:#75715e"># .gitignore</span>
<span style="color:#75715e"># .git-revision</span>
<span style="color:#75715e"># lib</span>
<span style="color:#75715e"># .mailmap</span>
<span style="color:#75715e"># maintainers</span>
<span style="color:#75715e"># nixos</span>
<span style="color:#75715e"># pkgs</span>
<span style="color:#75715e"># README.md</span>
<span style="color:#75715e"># .version -&gt; lib/.version</span>
<span style="color:#75715e"># .version-suffix</span>
<span style="color:#f92672">(</span>p<span style="color:#f92672">=</span>/nix/store/j13ha7fdr5n95kgk5q7nnn89h2bq6mr2-nixpkgs; find $p -type f; find $p -type d<span style="color:#f92672">)</span> | wc -l
<span style="color:#75715e"># 70552</span></code></pre></div>
<p>因此总结一下，<code>nix-channel --update</code> 执行过程如下：</p>

<ul>
<li>读取 nix channel 的配置文件  <code>~/.nix-channels</code> 获取到 channel 列表，名依次执行如下操作。</li>
<li>尝试请求 <code>url</code>，如果是重定向，则获取到重定向后的地址，然后拼接 <code>/nixexprs.tar.xz</code>，并下载该到 <code>/nix/store/xxx-nixexprs.tar.xz</code> 中。</li>
<li>解压 <code>/nix/store/xxx-nixexprs.tar.xz</code> 到 <code>/nix/store/xxx-&lt;channel-name&gt;</code> 中。</li>

<li><p>构造一个 <code>/nix/store/xxx-user-environment</code>，包含如下文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/xxx-user-environment
├── manifest.nix -&gt; /nix/store/xxx-env-manifest.nix       # 源信息
├── nixpkgs -&gt; /nix/store/xxx-nixpkgs/nixpkgs             # 解压的 channel 的 nixexprs.tar.xz
└── ...                                                   # ...</pre></div>
<p><code>/nix/store/xxx-env-manifest.nix</code> 是一个 <code>[]derivation</code>，示例内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[ { meta = { }; name = &#34;nixpkgs&#34;; out = { outPath = &#34;/nix/store/alvsn668jlsllk18p29b6jqadg9qsa44-nixpkgs&#34;; }; outPath = &#34;/nix/store/alvsn668jlsllk18p29b6jqadg9qsa44-nixpkgs&#34;; outputs = [ &#34;out&#34; ]; system = &#34;builtin&#34;; type = &#34;derivation&#34;; } ... ]</pre></div></li>

<li><p>读取 <code>~/.nix-defexpr/channels</code> 软链的指向的软链（<code>~/.local/state/nix/profiles/channels</code>）所在目录（即 <code>~/.local/state/nix/profiles</code>）创建一个 <code>channels-x-link</code> 软链，指向 <code>/nix/store/xxx-user-environment</code>。然后更新 <code>~/.nix-defexpr/channels</code> 软链的指向的软链（<code>~/.local/state/nix/profiles/channels</code>）到 <code>channels-x-link</code> （和 nix-profile 类似，但是无法自定义，详见下文）。</p></li>
</ul>

<h2 id="其他说明">其他说明</h2>

<h3 id="国内源使用固定版本问题">国内源使用固定版本问题</h3>

<p>通过 <a href="https://channels.nixos.org/">Nix channels</a> 站点可以查询到所有固定版本的 channels，但是这些版本会随着时间而变化，而更新，如：</p>

<ul>
<li><code>https://channels.nixos.org/nixos-24.05</code> 在当前时刻会重定向到 <code>https://releases.nixos.org/nixos/24.05/nixos-24.05.3103.0c53b6b8c2a3</code>，随着时间的推移，重定向的目标会变化。</li>
</ul>

<p>因此如果真的想强保证，使用一个固定的版本，应该使用真是的 URL。比如我们项使用 nixos-24.05 的 <code>24.05.3103.0c53b6b8c2a3</code> 子版本，则应该重定向后的 url。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># https://releases.nixos.org/nixos/24.05/nixos-24.05.675.805a384895c6</span>
nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixos-24.05@nixos-24.05.675.805a384895c6 nixpkgs_24_05
nix-channel --update
<span style="color:#75715e"># error: path &#39;bvdwdkhfgid51xir705v9rh4gg8hcbbn-nixos-24.05@nixos-24.05.675.805a384895c6&#39; is not a valid store path: name &#39;nixos-24.05@nixos-24.05.675.805a384895c6&#39; contains illegal character &#39;@&#39;</span>
nix-channel --remove nixpkgs_24_05
nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixos-24.05%40nixos-24.05.675.805a384895c6 nixpkgs_24_05
nix-channel --update
<span style="color:#75715e"># error: path &#39;sciazysgcwzs52ig7afsa59fbx6dqw6n-nixos-24.05%40nixos-24.05.675.805a384895c6&#39; is not a valid store path: name &#39;nixos-24.05%40nixos-24.05.675.805a384895c6&#39; contains illegal character &#39;%&#39;</span>
nix-channel --remove nixpkgs_24_05</code></pre></div>
<p>因为国内源的最后一段使用了 @ 特殊字符，导致 <code>nix-channel --update</code> 失败，目前官方仍未有任何回应，相关 issue 如下：</p>

<ul>
<li><a href="https://github.com/tuna/issues/issues/1976">清华源 issue</a>。</li>
<li><a href="https://github.com/NixOS/nix/issues/10831">Nix issue</a>。</li>
</ul>

<p>目前仍未得到解决，临时的解决办法是使用短链接服务（如 <a href="https://bitly.com">bitly</a>），做一个转换。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-channel --add https://bit.ly/3Y80c55 nixpkgs_24_05
nix-channel --update</code></pre></div>
<h3 id="无法像切换-profile-存储位置">无法像切换 profile 存储位置</h3>

<p>有些场景，希望将 <code>nix-channel --update</code> 的结果的引用存储到一个地方，实现随时切换（类似 <code>nix-env --switch-profile</code> 那样的需求）。</p>

<p>但是，目前 nix 不支持，验证如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p /tmp/test-channels
readlink ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels</span>
rm -rf ~/.nix-defexpr/channels
ln -s /tmp/test-channels/channels ~/.nix-defexpr/channels
ls -al ~/.nix-defexpr/channels
<span style="color:#75715e">#  /home/rectcircle/.nix-defexpr/channels -&gt; /tmp/test-channels/channels</span>
nix-channel --update
ls -al ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.nix-defexpr/channels -&gt; /home/rectcircle/.local/state/nix/profiles/channels</span></code></pre></div>
<p>可以看出，手动配置 ~/.nix-defexpr/channels 软链是无效的。下面尝试通过 nix-env 切换 profile，观察 channel 的配置是否发生变化。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>readlink -f <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">))</span>
<span style="color:#75715e"># /nix/store/xxx-nix-xxx/bin/nix</span>
readlink ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels</span>
mkdir -p /tmp/test-profiles
nix-env --switch-profile /tmp/test-profiles/profile
readlink ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels</span>
<span style="color:#e6db74">${</span>nix_path<span style="color:#e6db74">}</span>-channel --update
readlink ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels</span>
rm -rf ~/.nix-defexpr/channels
ln -s /tmp/test-channels/channels ~/.nix-defexpr/channels
<span style="color:#e6db74">${</span>nix_path<span style="color:#e6db74">}</span>-channel --update
readlink ~/.nix-defexpr/channels
<span style="color:#75715e"># /home/rectcircle/.local/state/nix/profiles/channels</span>
<span style="color:#e6db74">${</span>nix_path<span style="color:#e6db74">}</span>-env --switch-profile ~/.local/state/nix/profiles/profile <span style="color:#75715e"># 恢复</span></code></pre></div>
<p>虽然 <code>nix-channel --update</code> 的存储位置和 profile 类似，但是在目前版本，存储位置是强制写死到了 <code>~/.local/state/nix/profiles/channels</code>，无法通过任何办法自定义。</p>

<p>如果项实现类似  <code>nix-env --switch-profile</code> 的能力，只能通过手动维护 <code>~/.local/state/nix/profiles/channels-x-link</code> 的元信息，在需要的时候，手动修改 <code>~/.local/state/nix/profiles/channels</code> 软链指向特定的 <code>~/.local/state/nix/profiles/channels-x-link</code> 来实现。</p>
]]></description></item><item><title>Nix 高级话题之 profile</title><link>https://www.rectcircle.cn/posts/nix-advanced-profile/</link><pubDate>Wed, 17 Jul 2024 21:48:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-advanced-profile/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.23.2</p>
</blockquote>

<h2 id="简述">简述</h2>

<p>Nix profile （用户环境， user environments） 是 Nix 实现不同用户拥有不同的环境，实现环境回滚的底层机制。</p>

<p>和 Nix profile 有关命令的主要由 nix-env、 nix-collect-garbage、 nix-channel。</p>

<p>本文将介绍 Nix profile 的原理以及 nix-env、 nix-collect-garbage 详细用法和示例。</p>

<p>（nix-channel 本文不做介绍，下一篇专门讨论）</p>

<h2 id="原理">原理</h2>

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.23/package-management/profiles">Nix 参考手册 - 6.1 Profiles</a></p>
</blockquote>

<h3 id="基本结构和相关命令">基本结构和相关命令</h3>

<p><img src="/image/nix-user-environments.png" alt="image" /></p>

<ul>
<li><p>nix 在使用 <code>sh &lt;(curl -L https://nixos.org/nix/install) --no-daemon</code> 安装 Nix 时，会在用户的 shell profile （<code>~/.profile</code>） 中注入类似如下语句。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -e ~/.nix-profile/etc/profile.d/nix.sh <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> . ~/.nix-profile/etc/profile.d/nix.sh; <span style="color:#66d9ef">fi</span> <span style="color:#75715e"># added by Nix installer</span></code></pre></div>
<p>这个脚本主要设置了 <code>PATH</code>、<code>MANPATH</code>、<code>XDG_DATA_DIRS</code> 环境变量，让命令，man 可以识别 nix 安装的包。</p></li>

<li><p>执行 <code>nix-env -iA nixpkgs.hello</code> 安装一个包后，观察情况。</p></li>

<li><p>其中 <code>~/.nix-profile</code> 是一个软链，单用户模式，该软链指向 <code>~/.local/state/nix/profiles/profile</code>，而  <code>~/.local/state/nix/profiles/profile</code> 也是一个软链，指向同目录的 <code>profile-1-link</code>，而最终 <code>~/.local/state/nix/profiles/profile-1-link</code> 指向 nix store 中的一个 user-environments 目录，如 <code>/nix/store/197xfcwzc2xk6wkjyblc37grnpc3k4xk-user-environment</code> 。示意如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">~/.nix-profile
  -&gt; ~/.local/state/nix/profiles/profile
  -&gt; ~/.local/state/nix/profiles/profile-2-link
  -&gt; /nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment</pre></div>
<ul>
<li><code>~/.nix-profile</code> 是整个 nix profile 的入口文件，<code>nix-env --switch-profile</code> 命令的职责就是改变这个软链的指向，详见下文。</li>

<li><p><code>~/.local/state/nix/profiles</code> 是单用户模式默认的 user profiles 的存储目录，包含一堆软链， <code>nix-env --install|--remove|--rollback|--switch-generation</code>、 <code>nix-channel</code> 等命令均会操作该目录，示意如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">channels -&gt; channels-1-link
channels-1-link -&gt; /nix/store/197xfcwzc2xk6wkjyblc37grnpc3k4xk-user-environment
profile -&gt; profile-2-link
profile-1-link -&gt; /nix/store/mndsg7lyka8k7bsh3dxmrpk8rzcbkbr1-user-environment
profile-2-link -&gt; /nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment</pre></div>
<ul>
<li><code>nix-env --install|--remove</code> 完成安装卸载后，会生成一个新的 <code>/nix/store/xxx-user-environment</code>，然后创建一个 <code>~/.local/state/nix/profiles/profile-x-link</code> 软链，然后更新 <code>~/.local/state/nix/profiles/profile</code> 软链的指向。</li>
<li><code>nix-env --rollback|--switch-generation</code> 执行后，将更新 <code>~/.local/state/nix/profiles/profile</code> 的指向。</li>
<li><code>nix-channel</code> 命令和 <code>nix-env</code> 类似，会更新 <code>~/.local/state/nix/profiles/channels</code> 相关的文件，详见下一篇文章。</li>
</ul></li>
</ul></li>

<li><p>最终 <code>~/.nix-profile</code> 将指向 <code>/nix/store/xxx-user-environment</code>，该目录就是 nix 存储用户环境的路径。其目录和常规 Linux 的 <code>/usr/local</code> 结构类似，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment
├── bin
│   ├── hello -&gt; /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/bin/hello
│   ├── nix -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix
│   ├── nix-build -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-build
│   ├── nix-channel -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-channel
│   ├── nix-collect-garbage -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-collect-garbage
│   ├── nix-copy-closure -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-copy-closure
│   ├── nix-daemon -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-daemon
│   ├── nix-env -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-env
│   ├── nix-hash -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-hash
│   ├── nix-instantiate -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-instantiate
│   ├── nix-prefetch-url -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-prefetch-url
│   ├── nix-shell -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-shell
│   └── nix-store -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-store
├── etc -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/etc
├── lib -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib
├── libexec -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/libexec
├── manifest.nix -&gt; /nix/store/kbhbbkqbyq1ii3y5hclzsbzir82v87js-env-manifest.nix
└── share
    ├── bash-completion -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/bash-completion
    ├── fish -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/fish
    ├── info -&gt; /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/info
    ├── locale -&gt; /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/locale
    ├── man
    └── zsh -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/zsh</pre></div>
<ul>
<li>安装的 hello 可执行文件在 <code>bin/</code> 下存在一个软链。</li>
<li><code>~/.profile</code> 中 source 的 <code>~/.nix-profile/etc/profile.d/nix.sh</code> 最终指向的就是该目录的 <code>etc/profile.d/nix.sh</code> 下。</li>

<li><p><a href="https://nix.dev/manual/nix/2.23/command-ref/files/manifest.nix"><code>manifest.nix</code></a> 是一个 nix 代码文件，数据结构为 <code>derivation[]</code> （derivation 数组）， 记录了这个 profile 装的包列表的一些元信息，<code>nix-env --query --installed</code> 数据来源就是该文件，通过 <code>nix-instantiate --strict --eval  manifest.nix --json</code> 可以以 json 格式，获取到安装的包存储路径，从而可以获取更多信息，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -q --installed --json --out-path --drv-path --description --meta
nix-instantiate --strict --eval  ~/.nix-profile/manifest.nix --json
<span style="color:#75715e"># [&#34;/nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1&#34;,&#34;/nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2&#34;]</span>
nix-store --query --deriver /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1
<span style="color:#75715e"># /nix/store/c9v5d926q8d2cdb0jq6k3ybnxqdl3nb2-hello-2.12.1.drv</span> 
nix derivation show /nix/store/c9v5d926q8d2cdb0jq6k3ybnxqdl3nb2-hello-2.12.1.drv --extra-experimental-features nix-command
nix derivation show /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1 --extra-experimental-features nix-command
<span style="color:#75715e"># 略</span></code></pre></div></li>
</ul></li>
</ul>

<h3 id="derivation-outputs-和-profile">derivation outputs 和 profile</h3>

<blockquote>
<p><a href="https://ryantm.github.io/nixpkgs/stdenv/multiple-output/">Multiple-output packages</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 gcc 标准编译器（nix wrapper）</span>
nix-env -iA  nixpkgs.gcc13
<span style="color:#75715e"># 观察 profiles 情况</span>
tree -L <span style="color:#ae81ff">2</span>  ~/.nix-profile/
<span style="color:#75715e"># /home/rectcircle/.nix-profile/</span>
<span style="color:#75715e"># ├── bin</span>
<span style="color:#75715e"># │   ├── addr2line -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/addr2line</span>
<span style="color:#75715e"># │   ├── ar -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/ar</span>
<span style="color:#75715e"># │   ├── as -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/as</span>
<span style="color:#75715e"># │   ├── c++ -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/c++</span>
<span style="color:#75715e"># │   ├── cc -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/cc</span>
<span style="color:#75715e"># │   ├── c++filt -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/c++filt</span>
<span style="color:#75715e"># │   ├── cpp -&gt; /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/cpp</span>
<span style="color:#75715e"># │   ├── ...</span>
<span style="color:#75715e"># │   └── ...</span>
<span style="color:#75715e"># ├── etc -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/etc</span>
<span style="color:#75715e"># ├── lib -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib</span>
<span style="color:#75715e"># ├── libexec -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/libexec</span>
<span style="color:#75715e"># ├── manifest.nix -&gt; /nix/store/0p5qf6srlcxp30vcyr9j7jcj4qhpjh9g-env-manifest.nix</span>
<span style="color:#75715e"># └── share</span>
<span style="color:#75715e">#     ├── bash-completion -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/bash-completion</span>
<span style="color:#75715e">#     ├── fish -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/fish</span>
<span style="color:#75715e">#     ├── info -&gt; /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/info</span>
<span style="color:#75715e">#     ├── locale -&gt; /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/locale</span>
<span style="color:#75715e">#     ├── man</span>
<span style="color:#75715e">#     └── zsh -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/zsh</span>
nix-env -q --installed --out-path
<span style="color:#75715e"># gcc-wrapper-13.3.0  man=/nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man;/nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0</span>
<span style="color:#75715e"># hello-2.12.1        /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1</span>
<span style="color:#75715e"># nix-2.23.2          /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2</span>
tree -L <span style="color:#ae81ff">3</span> /nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man
<span style="color:#75715e"># /nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man</span>
<span style="color:#75715e"># └── share</span>
<span style="color:#75715e">#     └── man</span>
<span style="color:#75715e">#         ├── man1</span>
<span style="color:#75715e">#         └── man7</span></code></pre></div>
<p>可以看出，derivation outputs 目录 <code>out</code> 和 <code>man</code> 的 <code>bin/</code>、<code>share/man</code>  都被正确的安装到了 <code>~/.nix-profile/</code> 目录（默认是否安装到 profiles 中，是由 <code>meta.outputsToInstall</code> 属性控制的，默认应该是 <code>out</code>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.libgcc
tree -L <span style="color:#ae81ff">2</span>  ~/.nix-profile/
<span style="color:#75715e"># 输出关键点如下</span>
<span style="color:#75715e"># /home/rectcircle/.nix-profile/</span>
<span style="color:#75715e"># ├── bin</span>
<span style="color:#75715e"># ├── ...</span>
<span style="color:#75715e"># ├── lib</span>
<span style="color:#75715e"># │   ├── libboost_context.so -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libboost_context.so</span>
<span style="color:#75715e"># │   ├── ...</span>
<span style="color:#75715e"># │   ├── libgcc_s.so -&gt; /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so</span>
<span style="color:#75715e"># │   ├── libgcc_s.so.1 -&gt; /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so.1</span>
<span style="color:#75715e"># │   ├── libnixcmd.so -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libnixcmd.so</span>
<span style="color:#75715e"># │   └── ...</span>
<span style="color:#75715e"># └── ...</span></code></pre></div>
<p>可以看出，lib 目录变成了 nix 的 lib 以及 libgcc 的 lib 的聚合体。</p>

<p>最后，安装一下其他常见的 lib 库。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.libz
nix-env -iA nixpkgs.libxcrypt
tree -L <span style="color:#ae81ff">2</span>  ~/.nix-profile/
<span style="color:#75715e"># 输出关键点如下</span>
<span style="color:#75715e"># /home/rectcircle/.nix-profile/</span>
<span style="color:#75715e"># ├── bin</span>
<span style="color:#75715e"># ├── ...</span>
<span style="color:#75715e"># ├── include</span>
<span style="color:#75715e"># │   ├── c++ -&gt; /nix/store/zc0nsv23pakbafngjy32kvhfzb16as43-gcc-13.3.0/include/c++</span>
<span style="color:#75715e"># │   └── crypt.h -&gt; /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/include/crypt.h</span>
<span style="color:#75715e"># ├── lib</span>
<span style="color:#75715e"># │   ├── ...</span>
<span style="color:#75715e"># │   ├── libcrypt.so -&gt; /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so</span>
<span style="color:#75715e"># │   ├── libcrypt.so.2 -&gt; /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so.2</span>
<span style="color:#75715e"># │   ├── libcrypt.so.2.0.0 -&gt; /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so.2.0.0</span>
<span style="color:#75715e"># │   ├── ...</span>
<span style="color:#75715e"># │   ├── libz.so -&gt; /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so</span>
<span style="color:#75715e"># │   ├── libz.so.1 -&gt; /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so.1</span>
<span style="color:#75715e"># │   ├── libz.so.1.2.8 -&gt; /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so.1.2.8</span>
<span style="color:#75715e"># │   └── ...</span>
<span style="color:#75715e"># └── ...</span></code></pre></div>
<p>可以看出，这些包的 include、lib 都安装到了正确的位置。</p>

<p>再验证一下已经安装了 gcc 的情况下再安装 clang。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.clang_16
<span style="color:#75715e"># 报错</span>
<span style="color:#75715e"># error: Unable to build profile. There is a conflict for the following files:</span>

<span style="color:#75715e">#          /nix/store/g2f50c20wy9ca6nd46d449v5gbzx4rwy-clang-wrapper-16.0.6/bin/addr2line</span>
<span style="color:#75715e">#          /nix/store/piz0jc0js7xnnka355n2yw07zj7p2hgq-gcc-wrapper-14.1.0/bin/addr2line</span>
<span style="color:#75715e"># error: builder for &#39;/nix/store/1wqinfagy9lxqlanszck2fh5rsbkpxy4-user-environment.drv&#39; failed with exit code 1</span></code></pre></div>
<p>可以看出，如果两个包存在同名的二进制，将提示冲突。</p>

<p>最后再安装 libmysqlclient，来观察一下 outputs 包含 dev 的场景：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.libmysqlclient
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># copying path &#39;/nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5&#39; from &#39;https://cache.nixos.org&#39; ...</span>
<span style="color:#75715e"># copying path &#39;/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev&#39; from &#39;https://cache.nixos.org&#39; ...</span>
nix-env -q --installed --out-path
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># mariadb-connector-c-3.3.5  /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5</span>
<span style="color:#75715e"># ...</span>
ls -al ~/.nix-profile/lib/
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># mariadb -&gt; /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5/lib/mariadb</span>
<span style="color:#75715e"># ...</span>
which mariadb_config
<span style="color:#75715e"># mariadb_config not found</span>
ls -al /nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin
<span style="color:#75715e"># mariadb_config</span>
<span style="color:#75715e"># mysql_config -&gt; mariadb_config</span>
ls -al ~/.nix-profile/bin/mariadb_config
<span style="color:#75715e"># ls: cannot access &#39;$HOME/.nix-profile/bin/mariadb_config&#39;: No such file or directory</span>
cat ~/.nix-profile/manifest.nix
<span style="color:#75715e"># [ { meta = { ...; outputsToInstall = [ &#34;out&#34; ]; ... }; name = &#34;mariadb-connector-c-3.3.5&#34;; ...; outputs = [ &#34;out&#34; ]; ... } ...]</span>
nix-shell --pure -p libmysqlclient --run env | grep PATH
<span style="color:#75715e"># stdenv=/nix/store/d3dzfy4amjl826fb8j00qp1d9887h7hm-stdenv-linux</span>
<span style="color:#75715e"># buildInputs=/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev</span>
<span style="color:#75715e"># PATH=...:/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin:...</span>
nix derivation show nixpkgs#libmysqlclient --extra-experimental-features <span style="color:#e6db74">&#39;nix-command flakes&#39;</span>
<span style="color:#75715e"># {</span>
<span style="color:#75715e">#   &#34;/nix/store/jnwpkqz0qx9cx7ljirsks5s4b5lxhmz7-mariadb-connector-c-3.3.5.drv&#34;: {</span>
<span style="color:#75715e">#     //...</span>
<span style="color:#75715e">#    &#34;name&#34;: &#34;mariadb-connector-c-3.3.5&#34;,</span>
<span style="color:#75715e">#    &#34;inputDrvs&#34;: {</span>
<span style="color:#75715e">#      //...</span>
<span style="color:#75715e">#      &#34;/nix/store/kjniqf3ladgc55nh4h41vrcwp3z7426b-zlib-1.3.1.drv&#34;: {</span>
<span style="color:#75715e">#        &#34;dynamicOutputs&#34;: {},</span>
<span style="color:#75715e">#        &#34;outputs&#34;: [</span>
<span style="color:#75715e">#          &#34;dev&#34;</span>
<span style="color:#75715e">#        ]</span>
<span style="color:#75715e">#      },</span>
<span style="color:#75715e">#      //...</span>
<span style="color:#75715e">#    }</span>
<span style="color:#75715e">#     &#34;outputs&#34;: {</span>
<span style="color:#75715e">#       &#34;dev&#34;: {</span>
<span style="color:#75715e">#         &#34;path&#34;: &#34;/nix/store/lzjh7kfbwhcslywmas0288w1k5k8zh93-mariadb-connector-c-3.3.5-dev&#34;</span>
<span style="color:#75715e">#       },</span>
<span style="color:#75715e">#       &#34;out&#34;: {</span>
<span style="color:#75715e">#         &#34;path&#34;: &#34;/nix/store/118ayny4nv1d687bgi4js46b40wg4md2-mariadb-connector-c-3.3.5&#34;</span>
<span style="color:#75715e">#       }</span>
<span style="color:#75715e">#     },</span>
<span style="color:#75715e">#     //...</span>
<span style="color:#75715e">#   }</span>
<span style="color:#75715e"># }</span>
nix-instantiate --eval --expr <span style="color:#e6db74">&#39;let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.libmysqlclient.outputs&#39;</span>
<span style="color:#75715e"># [ &#34;out&#34; &#34;dev&#34; ]</span>
nix-instantiate --eval --expr <span style="color:#e6db74">&#39;let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.zlib.outputs&#39;</span>
<span style="color:#75715e"># [ &#34;out&#34; &#34;dev&#34; &#34;static&#34; ]</span></code></pre></div>
<p>可以发现 libmysqlclient derivation outputs 是 <code>[ &quot;out&quot; &quot;dev&quot; ]</code>，但 <code>nix-env --install</code> 的 dev 目录的 <code>mariadb_config</code> 可执行文件并没有安装到 profiles 里面，也就是说安装的 <code>out</code> 输出。</p>

<p>libmysqlclient 这个包配置的 outputs 是 <code>[ &quot;out&quot; &quot;dev&quot; ]</code>，当执行 <code>nix-shell</code> shell 是，配置到 PATH 里的是 <code>/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin</code>，说明 <code>nix-shell</code> 引用的是 <code>dev</code> 输出。</p>

<p>如果想安装 dev 目录到 profile 中，需要强制指定 <code>nix-env -iA nixpkgs.libmysqlclient.dev nixpkgs.libmysqlclient.out</code> 都安装（特别提醒： <strong>nix 似乎有 bug 一旦下面命令执行， profile 就损坏了！因此建议直接使用 nix-shell</strong>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.libmysqlclient.dev nixpkgs.libmysqlclient.out
nix-env -q --installed --out-path
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># mariadb-connector-c-3.3.5  dev=/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev</span>
<span style="color:#75715e"># mariadb-connector-c-3.3.5  /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5</span>
<span style="color:#75715e"># ...</span>
ls -al ~/.nix-profile/lib/
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># mariadb -&gt; /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5/lib/mariadb</span>
<span style="color:#75715e"># ...</span>
which mariadb_config
<span style="color:#75715e"># /home/cloudide/.nix-profile/bin/mariadb_config</span>
ls -al ~/.nix-profile/bin/mariadb_config
<span style="color:#75715e"># $HOME/.nix-profile/bin/mariadb_config -&gt; /nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin/mariadb_config</span>
nix-env -iA nixpkgs.zlib
<span style="color:#75715e"># 报错</span>
<span style="color:#75715e"># error: this derivation has bad &#39;meta.outputsToInstall&#39;</span>
cat ~/.nix-profile/manifest.nix
<span style="color:#75715e"># [ { meta = { ...; outputsToInstall = [ &#34;dev&#34; ]; ... }; name = &#34;mariadb-connector-c-3.3.5&#34;; ...; outputs = [ &#34;out&#34; ]; ... } ...]</span></code></pre></div>
<p>总结，在执行 <code>nix-env --install</code> 时：</p>

<ul>
<li>nixpkgs 声明的 derivation 都有一个 <code>meta.outputsToInstall</code> 属性（一般情况下为 <code>out</code> 或 <code>bin</code>），会将其指向的子目录都软链到 ~/.nix-profile/ 中。如果裸使用 <code>derivation</code>，没有配置 <code>meta.outputsToInstall</code>，nix-env 会安装所有的 outputs。</li>

<li><p>多个包的 outputs 的子目录会进行合并，合并是递归的进行：如果安装的包的 outputs 的子目录没有没有重复的，则直接创建一个软链指向到这个子目录。如果存在存在重复的，则在中创建这个目录，然后创建软链。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装了 nix，只有 nix 的 output 目录有 lib 目录，此时 lib 为：</span>
lib -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib

<span style="color:#75715e"># 安装了 nix 和 libgcc，这两个目录都有 lib 目录</span>
lib
├── libboost_context.so -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libboost_context.so
├── ...
├── libgcc_s.so -&gt; /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so
├── libgcc_s.so.1 -&gt; /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so.1
├── libnixcmd.so -&gt; /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libnixcmd.so
└── ...</code></pre></div></li>

<li><p>如果最终存在冲突（比如：gcc 和 clang 都需要安装 bin/addr2line），同时安装，将报错。有两点说明如下：</p>

<ul>
<li>默认情况， nix-env 安装的 pname 相同包时，旧的 pname 的包将被删除，并安装这个新的 pname 包（未找到相关文档，实测如此），执行 <code>nix-env --install</code> 时，可使用 <code>--preserve-installed</code> 阻止该行为，行为切换为报错冲突。</li>
<li>derivation 有一个 <code>meta.priority</code> 属性（<a href="https://nix.dev/manual/nix/2.22/command-ref/nix-env/install#description">文档</a>），如果两个包的优先级相同，nix-env 安装存在冲突时，就会报错，如果安装一个优先级更高的包存在冲突时，这个包会覆盖之前安装的优先级地的包。<code>meta.priority</code> 的默认值为 <code>5</code>。另外，也可以通过 <code>nix-env --set-flag priority 数字</code> 调整已安装的包的优先级。</li>
</ul></li>

<li><p><code>nix-env --install</code> 支持指定安装特定的 outputs，格式形如 <code>nixpkgs.libmysqlclient.dev</code>，但是，这样会破坏掉 profile ，导致后续安装任何的包都报错。原因是生成的 <code>manifest.nix</code> 中 <code>meta.outputsToInstall</code> 属性的值不包含在 <code>outputs</code> 属性中。</p></li>

<li><p>由于 nix 的包都是 nixpkgs 维护的，而关于 outputs 目录， nixpkgs 有如下如下约定：</p>

<ul>
<li>如果 outputs 有多个输出，<code>out</code> 目录一般放到最前面，例如 <code>[ &quot;out&quot; &quot;dev&quot; ]</code>。</li>
<li><code>meta.outputsToInstall</code> 默认值规则为：如果 outputs 存在 bin 目录，则添加 bin；如果存在 out 目录，则添加 out；否则添加 outputs 的第一个。最后，如果存在 man，一定会 append man （详见：<a href="https://github.com/NixOS/nixpkgs/blob/4c68bf5473a8e87ffd94322cc3e79a449311325b/pkgs/stdenv/generic/check-meta.nix#L474">源码</a>） 。</li>
<li>nixpkgs 的包维护者，可以按需选择 <code>outputs</code> 中的目录添加到 <code>meta.outputsToInstall</code> 中，一般情况下 dev 目录一般不会加到这个属性中。</li>
<li>使用 nix-shell 或 nix-build 包的依赖是通过 <code>nixpkgs.lib.stdenv.mkDerivation</code> 的 buildInputs 声明时，如果这个依赖 outputs 包含 dev 时，实际依赖的是 dev 而非 out 目录。源码详见：<a href="https://github.com/NixOS/nixpkgs/blob/d2f01055afe920f3eb496dbc167b4918ebedfa21/pkgs/stdenv/generic/make-derivation.nix#L310">make-derivation.nix</a> 和 <a href="https://github.com/NixOS/nixpkgs/blob/master/lib/attrsets.nix#L1888">attrsets.nix</a>。</li>
<li>关于 outputs 更多参见： <a href="https://nixos.org/manual/nixpkgs/stable/#chap-multiple-output">Nixpkgs Reference Manual - Multiple-output packages</a> ，<a href="https://ianthehenry.com/posts/how-to-learn-nix/derivations-in-detail/">博客 How to Learn Nix, Part 29: Derivations in detail</a>，<a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh">setenv.sh</a>。</li>
</ul></li>
</ul>

<h3 id="c-库-和-profile">C 库 和 profile</h3>

<blockquote>
<p><a href="https://nixos.wiki/wiki/C">Nix Wiki - C</a></p>
</blockquote>

<p>从上文可以看出，lib 和 include 已经正确的设置到 profile 中了，但是和 bin 不同。如果使用的不是 NixOS，而是在传统 Linux （如 debian） 中使用 Nix。上面的 profile 中的 lib include 将不会设置到系统中，因为如果设置了，会和系统的 lib 冲突，造成严重问题。</p>

<p>因此，如果想用 nix 管理 C/C++ 项目的依赖（即 include 头文件 和 lib 动态链接库 so），需要使用 Nix shell 声明依赖，并启动一个 shell，这个 shell 里面会设置 <code>NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu=1</code> 等 <code>NIX_CFLAGS_COMPILE</code> 以及 <code>NIX_LDFLAGS</code> （详见源码： <a href="https://github.com/NixOS/nixpkgs/blob/63fb5880a4f67415f7baf4e2e789d326aa87bd95/pkgs/stdenv/generic/setup.sh">setup.sh</a>，<a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/setup-hook.sh">setup-hook.sh</a>。</p>

<p>然后使用 wrapper 的 C/C++ 编译器（如 <code>nixpkgs.gcc13</code>、 <code>nixpkgs.clang_16</code>），这样 C/C++ 编译器才能使用正确的识别 Nix 安装依赖。</p>

<p>详见： <a href="https://nixos.wiki/wiki/C">Nix Wiki - C</a>。</p>

<h2 id="nix-env-命令详解">nix-env 命令详解</h2>

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.22/command-ref/nix-env">Nix 参考手册 - 8.3.4 nix-env</a></p>
</blockquote>

<p>nix 通过 nix-env 命令来实现对 profile 的管理，本部分将详细介绍该命令的各种能力和细节。</p>

<h3 id="nix-env-delete-generations"><code>nix-env --delete-generations</code></h3>

<p>删除 profile 的历史版本，示例如下：</p>

<ul>
<li><code>nix-env delete-generations 1 2 3</code> 删除 profile 的 1、2、3 版本</li>
<li><code>nix-env --delete-generations old</code> 删除除了当前版本之外的所有的版本。</li>
<li><code>nix-env delete-generations 30d</code> 删除 30 天之前的版本。</li>
<li><code>nix-env delete-generations 5+</code> 保留当前版本之前的 5 个版本以及大于当前版本的版本，删除其他的版本。</li>
</ul>

<h3 id="nix-env-install"><code>nix-env --install</code></h3>

<p>安装一个或多个包 (derivation) 到 profile 中，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--install | -i} args… [{--prebuilt-only | -b}] [{--attr | -A}] [--from-expression] [-E] [--from-profile path] [--preserve-installed | -P] [--remove-all | -r]</pre></div>
<p>安装包的各种写法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 最常见的写法（通过属性名安装 nixpkgs 的包）</span>
nix-env -iA nixpkgs.python312
nix-env --install --attr nixpkgs.python312

<span style="color:#75715e"># 根据 derivation 的 name 安装（这种方式需要评估所有 nixpkgs 的包，性能很差，不推荐用在 nixpkgs 的包的安装）</span>
nix-env --install python3-3.12.3

<span style="color:#75715e"># -A 和 -f 结合 channel</span>
nix-env -iA nixpkgs.python312 -f ~/.nix-defexpr/
nix-env -iA nixpkgs.python312 -f ~/.nix-defexpr/channels
nix-env -iA python312 -f ~/.nix-defexpr/channels/nixpkgs

<span style="color:#75715e"># -f 自定义 nix 表达式</span>
<span style="color:#75715e"># -f 文件，最终是一个 derivation</span>
cat &gt; /tmp/single-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.python312
</span><span style="color:#e6db74">EOF</span>
nix-env -i -f /tmp/single-python.nix 
<span style="color:#75715e"># -f 文件，最终是一个 derivation 列表</span>
cat &gt; /tmp/list-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">let pkgs = import &lt;nixpkgs&gt; {}; in [pkgs.python312]
</span><span style="color:#e6db74">EOF</span>
nix-env -i -f /tmp/list-python.nix
<span style="color:#75715e"># -f 文件，最终是一个属性集</span>
cat &gt; /tmp/attrset-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">let pkgs = import &lt;nixpkgs&gt; {}; in { python312 = pkgs.python312; }
</span><span style="color:#e6db74">EOF</span>
nix-env -iA python312 -f /tmp/attrset-python.nix
<span style="color:#75715e"># -f 文件最终函数声明为 a: derivation ，这里的 a 是一个属性集。</span>
cat &gt; /tmp/func-single-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">{ pkgs ? import &lt;nixpkgs&gt; {} }: pkgs.python312
</span><span style="color:#e6db74">EOF</span>
<span style="color:#75715e"># 实测，改为如下是不行的</span>
<span style="color:#75715e"># _: let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.python312</span>
nix-env -i -f /tmp/func-single-python.nix
<span style="color:#75715e"># -f 文件最终函数声明为 a: derivation列表，这里的 a 是一个属性集。</span>
cat &gt; /tmp/func-list-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">{ pkgs ? import &lt;nixpkgs&gt; {} }: pkgs.python312
</span><span style="color:#e6db74">EOF</span>
nix-env -i -f /tmp/func-list-python.nix
<span style="color:#75715e"># -f 文件最终函数声明为 a: {} 这里的 a 是一个属性集。 （和 nixpkgs 原理相同）</span>
cat &gt; /tmp/func-attrset-python.nix <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">{ pkgs ? import &lt;nixpkgs&gt; {} }: { python312 = pkgs.python312; }
</span><span style="color:#e6db74">EOF</span>
nix-env -i -f /tmp/func-attrset-python.nix <span style="color:#e6db74">&#39;.*&#39;</span> <span style="color:#75715e"># 方式 0：可以通过 .* 安装属性集中的所有包。</span>
nix-env -iA python312 -f /tmp/func-attrset-python.nix  <span style="color:#75715e"># 方式 1</span>
nix-env -iA python312 --arg pkgs <span style="color:#e6db74">&#39;import &lt;nixpkgs&gt; {}&#39;</span> -f /tmp/func-attrset-python.nix  <span style="color:#75715e"># 方式 2: 验证覆盖函数参数</span>
nix-env -i -E <span style="color:#e6db74">&#39;a: let mypkgs = a{}; in mypkgs.python312&#39;</span> -f /tmp/func-attrset-python.nix <span style="color:#75715e"># 方式 3: 使用表达式参数</span>

<span style="color:#75715e"># 从表达式安装，这个表达式的必须是一个函数，声明为：</span>
<span style="color:#75715e">#   a: derivation { ... }</span>
<span style="color:#75715e"># 假设这个函数名为 f ，调用方式分为如下两种情况：</span>
<span style="color:#75715e">#   1. 不传递 -f 参数或者 -f 参数是一个 channel 的 user-environment 时：f { _combineChannels = [ ]; nixpkgs = import &lt;nixpkgs&gt;;  }</span>
<span style="color:#75715e">#   2. -f  参数传递的是一个包含 default.nix 的目录或压缩包下载链接，或者一个 .nix 源代码文件时：let a = import -f参数值; in f a</span>
nix-env --install --from-expression <span style="color:#e6db74">&#39;a: let pkgs = a.nixpkgs{}; in pkgs.python312&#39;</span>
nix-env --install --from-expression <span style="color:#e6db74">&#39;a: let pkgs = a.nixpkgs{}; in pkgs.python312&#39;</span> -f ~/.nix-defexpr/channels
nix-env --install --from-expression <span style="color:#e6db74">&#39;nixpkgs: let pkgs = nixpkgs{}; in pkgs.python312&#39;</span> -f ~/.nix-defexpr/channels/nixpkgs/

<span style="color:#75715e"># 直接通过 store path 或 store derivation path 安装。</span>
nix-env -i <span style="color:#66d9ef">$(</span>nix-instantiate --expr <span style="color:#e6db74">&#39;let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.python312&#39;</span><span style="color:#66d9ef">)</span></code></pre></div>
<p>默认情况， nix-env 安装的 pname 相同包时，旧的 pname 的包将被删除，并安装这个新的 pname 包，使用 <code>--preserve-installed</code> 参数检测这种情况并直接报错，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.python311
nix-env -iA nixpkgs.python312 --preserve-installed
<span style="color:#75715e"># 报错: error: Unable to build profile. There is a conflict for the following files:</span>
nix-env -iA nixpkgs.python312 
<span style="color:#75715e"># replacing old &#39;python3-3.11.9&#39;</span>
<span style="color:#75715e"># installing &#39;python3-3.12.3&#39;</span>
nix-env --query --installed
<span style="color:#75715e"># 只会输出 python3-3.12.3，不会输出 python3.11</span></code></pre></div>
<p>从其他 profile 中安装，可用于 copy 其他用户的 profile。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA python312 --from-profile /nix/store/xxx-user-environment</code></pre></div>
<p>将包安装到其他的 nix-profile。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p /tmp/myprofiles
nix-env -i -A nixpkgs.python312 nixpkgs.nix --profile /tmp/myprofiles/profile
ls -al /tmp/myprofiles
<span style="color:#75715e"># profile -&gt; profile-1-link</span>
<span style="color:#75715e"># profile-1-link -&gt; /nix/store/3hb6nr0n05a2iwbxm0i50968mw2dd220-user-environment</span></code></pre></div>
<p>重要参数总结，以及其他参数说明如下：</p>

<ul>
<li><code>--prebuilt-only</code> / <code>-b</code> 只从 substitute 中安装与构建的包，永不从源码构建。</li>
<li><code>--remove-all</code> / <code>-r</code> 删除所有其他已安装的包，再执行安装，相当于首先运行 <code>nix-env --uninstall '.*'</code>，只不过一切都发生在单个事务中。</li>
<li><code>--file</code> / <code>-f</code> 从哪里获取 nix 表达式，有两种情况：

<ul>
<li>不填，默认为 nix-channel 维护的 （<a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file#conf-nix-path">Nix 表达式搜索路径</a>） <code>~/.nix-defexpr/channels/</code>，详见后文 nix-channel 。</li>
<li>可以是本地 nix 源代码文件，包含 default.nix 的目录或压缩包下载链接（如 github archive），要求其表达式类型推导最终的类型定义可以是如下六种情况：

<ul>
<li><code>derivation</code></li>
<li><code>[]derivation</code> derivation 列表</li>
<li><code>{}</code> 属性集。</li>
<li><code>a: derivation</code> 函数，其中 a 是属性集。</li>
<li><code>a: derivation[]</code> 函数，其中 a 是属性集。</li>
<li><code>a: {}</code> 函数，其中 a 是属性集。</li>
<li>以上 a 的一般写法为 <code>{ pkgs ? import &lt;nixpkgs&gt; {} }: ...</code> 包含默认值，如需自定义，可以使用 <code>--arg</code> 指定。</li>
</ul></li>
</ul></li>
<li><code>--from-expression</code> / <code>-E</code> 从表达式安装，这个表达式的必须是一个函数，声明为：</li>
<li><code>--attr</code> / <code>-A</code> 可以指定多个，用来指定要安装的包，这里的写法和 <code>--file</code> 参数有关。只有 <code>--file</code> / <code>-f</code> 的最终评估值是一个属性集时，才能使用该参数。</li>
<li><code>--profile</code> 安装到指定的 profile 必须是一个软链的路径。</li>
<li><code>--dry-run</code> 模拟执行。</li>

<li><p><code>-I</code> path，指定包搜索路径，可多次给出，也可以通过 <code>NIX_PATH</code> 环境变量配置，-I 优先级高于环境变量，默认为 <code>~/.nix-defexpr/channels/</code>，主要再如下场景使用：</p>

<ul>
<li>nix 表达式语言的 <code>&lt;nixpkgs&gt;</code> 语法。</li>
<li><code>nix-env --attr nixpkgs.python312</code> 语法。</li>
</ul>

<p>如上吗，底层都是用 <a href="https://nix.dev/manual/nix/2.22/language/builtins#builtins-findFile"><code>builtins.findFile</code></a>，原理是查找对应的目录，且该目录包含 <code>default.nix</code>。</p></li>
</ul>

<h3 id="nix-env-list-generations"><code>nix-env --list-generations</code></h3>

<p>列出当前 profile 的所有版本。</p>

<h3 id="nix-env-query"><code>nix-env --query</code></h3>

<p>查询包（derivation）信息，按 <code>name</code> 排序，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--query | -q} names… [--installed | --available | -a] [{--status | -s}] [{--attr-path | -P}] [--no-name] [{--compare-versions | -c}] [--system] [--drv-path] [--out-path] [--description] [--meta] [--xml] [--json] [{--prebuilt-only | -b}] [{--attr | -A} attribute-path]</pre></div>
<p>有如下两种查询目标选择：</p>

<ul>
<li><code>--installed</code> 查询已安装的包，该选项是默认的。</li>
<li><code>--available</code> 或 <code>-a</code> 查询 channel 中，可用的包的信息。</li>
</ul>

<p>指定查询结构输出格式，默认为文本，可通过 <code>--xml</code>、<code>--json</code> 指定输出为 xml 或 json 格式。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --query --xml
<span style="color:#75715e"># &lt;?xml version=&#39;1.0&#39; encoding=&#39;utf-8&#39;?&gt;</span>
<span style="color:#75715e"># &lt;items&gt;</span>
<span style="color:#75715e">#   &lt;item attrPath=&#34;1&#34; name=&#34;nix-2.24.0pre20240627_b44909ac&#34; outputName=&#34;&#34; pname=&#34;nix&#34; system=&#34;x86_64-linux&#34; version=&#34;2.24&gt;</span>
<span style="color:#75715e">#     &lt;output name=&#34;man&#34; /&gt;</span>
<span style="color:#75715e">#     &lt;output name=&#34;out&#34; /&gt;</span>
<span style="color:#75715e">#   &lt;/item&gt;</span>
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># &lt;/items&gt;</span>
nix-env --query --json
<span style="color:#75715e"># {</span>
<span style="color:#75715e">#   &#34;0&#34;: {</span>
<span style="color:#75715e">#     &#34;name&#34;: &#34;python3-3.12.3&#34;,</span>
<span style="color:#75715e">#     &#34;outputName&#34;: &#34;&#34;,</span>
<span style="color:#75715e">#     &#34;outputs&#34;: {</span>
<span style="color:#75715e">#       &#34;out&#34;: null</span>
<span style="color:#75715e">#     },</span>
<span style="color:#75715e">#     &#34;pname&#34;: &#34;python3&#34;,</span>
<span style="color:#75715e">#     &#34;system&#34;: &#34;x86_64-linux&#34;,</span>
<span style="color:#75715e">#     &#34;version&#34;: &#34;3.12.3&#34;</span>
<span style="color:#75715e">#   },</span>
<span style="color:#75715e">#  ...</span>
<span style="color:#75715e"># }</span></code></pre></div>
<p>其他选项说明如下：</p>

<ul>
<li><code>--prebuilt-only</code> 或 <code>-b</code> 选项，只查询那些能从 substitute 中安装的包，永不从源码构建（主要和 <code>--available</code> 配合使用，主要为了过滤出那些可以快速安装的包）。</li>

<li><p><code>--status</code> 或 <code>-s</code> 选项，查询包的状态，状态包含 3 个字符</p>

<ul>
<li><code>I</code> 已安装到当前 profile。</li>
<li><code>P</code> 已经保存到 store。</li>
<li><code>S</code> 是否在 substitute 中有可用的预购建的产物。</li>
</ul>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --query --status
<span style="color:#75715e">#IPS  nix-2.24.0pre20240627_b44909ac</span>
<span style="color:#75715e">#IPS  python3-3.12.3</span></code></pre></div></li>

<li><p><code>--attr-path</code> 或 <code>-P</code> 选项，（仅能和 <code>--available</code> 一起使用）打印属性路径。</p></li>

<li><p><code>--no-name</code> 选项，不打印 <code>name</code>。</p></li>

<li><p><code>--compare-versions</code> 或 <code>-c</code> 选项，查询已安装的包的版本和 channel 中可用的包的版本的差异，主要用于检测是否有新版本，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --query --compare-versions
<span style="color:#75715e"># nix-2.24.0pre20240627_b44909ac  = 2.24.0pre20240627_b44909ac</span>
<span style="color:#75715e"># python3-3.12.3                  &lt; 3.13.0b3</span></code></pre></div></li>

<li><p><code>--system</code> 选项，打印包的 system 字段。</p></li>

<li><p><code>--drv-path</code> 选项，打印 store derivation 路径。</p></li>

<li><p><code>--out-path</code> 选项，打印 derivation 的输出路径。</p></li>

<li><p><code>--description</code> 选项，打印 derivation 的 <code>meta.description</code> 属性。</p></li>

<li><p><code>meta</code> 选项，打印 derivation 的 <code>meta</code> 属性，该选项只能和 <code>--xml</code> 和 <code>--json</code> 一起使用。</p></li>
</ul>

<h3 id="nix-env-rollback"><code>nix-env --rollback</code></h3>

<p>将当前 profile 回滚到上一版本。</p>

<h3 id="nix-env-set-flag"><code>nix-env --set-flag</code></h3>

<p>修改已安装的包的 meta 下的属性，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --set-flag name value drvnames</code></pre></div>
<p>目前支持三个：</p>

<ul>
<li><p><code>priority</code>，修改已安装包的 <code>meta.priority</code> 字段，影响安装存在冲突的包时的覆盖关系。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.gcc
nix-env -iA nixpkgs.binutils
nix-env --query binutils-wrapper --meta --json | grep priority
<span style="color:#75715e">#       &#34;priority&#34;: 10,</span>

<span style="color:#75715e"># 默认安装将失败</span>
nix-env -iA nixpkgs.gcc
<span style="color:#75715e"># error: Unable to build profile. There is a conflict for the following files:</span>
<span style="color:#75715e"># 报错</span>
<span style="color:#75715e">#          /nix/store/l46fjkzva0bhvy9p2r7p4vi68kr7a1db-binutils-wrapper-2.41/bin/addr2line</span>
<span style="color:#75715e">#         /nix/store/mpm3i0sbqc9svfch6a17179fs64dz2kv-gcc-wrapper-13.3.0/bin/addr2line</span>

<span style="color:#75715e"># 修改优先级后重新安装将成功</span>
nix-env --set-flag priority <span style="color:#ae81ff">1</span> binutils-wrapper
nix-env --query binutils-wrapper --meta --json | grep priority
<span style="color:#75715e">#       &#34;priority&#34;: &#34;1&#34;,</span>
nix-env -iA nixpkgs.gcc
nix-env --query gcc-wrapper-13.3.0 --meta --json | grep priority
<span style="color:#75715e">#      &#34;priority&#34;: 10,</span></code></pre></div></li>

<li><p><code>keep</code>，可以设置为 true 以防止包被升级或替换。如果您想保留旧版本的软件包，这非常有用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --set-flag keep true python3-3.12.3
nix-env --query python3-3.12.3 --meta --json | grep keep</code></pre></div></li>

<li><p><code>active</code>，可以设置为 false 以禁用该包。也就是说，不会生成包文件的符号链接，但它仍然是配置文件的一部分（因此不会被垃圾收集）。可以将其设置回 true 以重新启用该包。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --install -A nixpkgs.python312
which python
<span style="color:#75715e"># /home/rectcircle/.nix-profile/bin/python</span>
nix-env --set-flag active false python3-3.12.3
which python
<span style="color:#75715e"># 找不到</span></code></pre></div></li>
</ul>

<h3 id="nix-env-set"><code>nix-env --set</code></h3>

<p>设置 profile 指向一个特定的 derivation。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env --set drvname</pre></div>
<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --set nix
nix-env --query
<span style="color:#75715e"># 无输出</span>
readlink -f 
<span style="color:#75715e"># /nix/store/9b72q76kfi4v0vm08vrsdllw62wpb1ka-nix-2.24.0pre20240627_b44909ac</span>

nix-env -iA nixpkgs.python312
which nix
<span style="color:#75715e"># 无输出</span></code></pre></div>
<p>可以看出：</p>

<ul>
<li><code>--set</code> 参数实际上可以是 pname 也可以是 name。</li>
<li><code>--set</code> 执行 后直接把 profile 指向了 nix 这个包，并没有生成 user environments。</li>
<li>后续再执行 <code>--install</code>，上面的 <code>--set</code> 的包会丢失。</li>
</ul>

<p>没有想到这个命令的用途，官方的示例是：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --profile /nix/var/nix/profiles/browser --set firefox</code></pre></div>
<p>可能在 NixOS 场景有用吧。</p>

<h3 id="nix-env-switch-generation"><code>nix-env --switch-generation</code></h3>

<p>将当前 profile 切换到指定的版本。语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--switch-generation | -G} generation</pre></div>
<p>本质上是修改 profile 软链指向软链的指向，即 <code>~/.nix-profile</code> 指向的文件 <code>~/.local/state/nix/profiles/profile</code> 指向新的 <code>profile-xxx-link</code>。</p>

<h3 id="nix-env-switch-profile"><code>nix-env --switch-profile</code></h3>

<p>切换用户环境 profile，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--switch-profile | -S} path</pre></div>
<p><code>~/.nix-profile</code> 是整个 nix profile 的入口文件，该命令的职责就是改变这个软链的指向，详见下文。</p>

<h3 id="nix-env-uninstall"><code>nix-env --uninstall</code></h3>

<p>卸载包，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--uninstall | -e} drvnames…</pre></div>
<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env --uninstall gcc-wrapper
<span style="color:#75715e"># uninstalling &#39;gcc-wrapper-13.3.0&#39;</span>
<span style="color:#75715e"># building &#39;/nix/store/xmn76h44mpyh7s50slmfaqj1l89cmznw-user-environment.drv&#39;...</span>
nix-env -iA nixpkgs.gcc
nix-env --uninstall gcc-wrapper-13.3.0
<span style="color:#75715e"># uninstalling &#39;gcc-wrapper-13.3.0&#39;</span>
<span style="color:#75715e"># building &#39;/nix/store/xmn76h44mpyh7s50slmfaqj1l89cmznw-user-environment.drv&#39;...</span></code></pre></div>
<p>可以看出：</p>

<ul>
<li><code>--uninstall</code> 参数实际上可以是 pname 也可以是 name。</li>
<li>卸载操作也会生成一个新的版本，可用以 <code>--rollback</code> 回滚。</li>
</ul>

<h3 id="nix-env-upgrade"><code>nix-env --upgrade</code></h3>

<p>升级一个包，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-env {--upgrade | -u} args [--lt | --leq | --eq | --always] [{--prebuilt-only | -b}] [{--attr | -A}] [--from-expression] [-E] [--from-profile path] [--preserve-installed | -P]</pre></div>
<p>可选择如下 4 种升级策略中一种：</p>

<ul>
<li><code>--lt</code> 选项，默认值。将包升级到 channel 中 pname 相同的更新的版本，如果 channel 降级了，或版本号不变，则啥也不做。</li>
<li><code>--leq</code> 选项，channel 中同名 pname 的版本号大于等于该包，则升级到 channel 中这个版本。</li>
<li><code>--eq</code> 选项，channel 中同名 pname 的版本号等于该包，则升级到 channel 中这个版本，这种情况主要用来处理这个包本身没有变化，但是依赖有变化的场景。</li>
<li><code>--always</code> 选项，不管 channel 中同名 pname 的版本号是否变化，总是重新升级。</li>
</ul>

<p>其他参数：</p>

<ul>
<li><code>--prebuilt-only</code> 或 <code>-b</code> 选项，只升级那些能从 substitute 中安装的包，永不从源码构建。</li>
<li><code>--preserve-installed</code> 或 <code>-P</code> 选项，不清楚这个选项的意义，原文如下：Do not remove derivations with a name matching one of the derivations being installed. Usually, trying to have two versions of the same package installed in the same generation of a profile will lead to an error in building the generation, due to file name clashes between the two versions. However, this is not the case for all packages.</li>
</ul>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 升级 gcc，指定了属性名（推荐，无需计算所有包，速度很快推荐）。</span>
nix-env --upgrade --attr nixpkgs.gcc
<span style="color:#75715e"># 升级 gcc，使用 panme （很慢，要计算 channel 中所有的包的 pname，然后匹配，和下面升级所有包速度一样慢）</span>
nix-env --upgrade gcc-wrapper
<span style="color:#75715e"># 升级所有包（很慢，要计算 channel 中所有的包）。</span>
nix-env --upgrade --always</code></pre></div>
<h2 id="垃圾回收">垃圾回收</h2>

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.22/package-management/garbage-collection">Nix 参考手册 - 6.2 Garbage Collection</a> | <a href="https://nix.dev/manual/nix/2.22/command-ref/nix-collect-garbage">Nix 参考手册 - 8.4.2 nix-collect-garbage</a></p>
</blockquote>

<p>上文的 <code>nix-env --uninstall</code> 仅仅是生成了一个新的 profiles，并没有真正删除存储对象。通过 <code>nix-collect-garbage</code> 命令可以回收那些不可达的存储对象，这个行为被称作垃圾回收。</p>

<p>nix 会遍历如指定路径的 profiles 所有版本直接引用和间接引用（闭包）的存储对象列表，不再该列表的存储对象就是不可达的。</p>

<p>nix 遍历的 profiles 路径如下：</p>

<ul>
<li>在 <a href="https://nix.dev/manual/nix/2.22/command-ref/files/profiles">profile 章节</a> 说明的目录：

<ul>
<li><code>$XDG_STATE_HOME/nix/profiles</code> 普通用户的 profiles，一般情况下为 <code>~/.local/state/nix/profiles</code> （多用户安装模式会遍历所有使用 Nix 的用户路径的该路径）。</li>
<li><code>$NIX_STATE_DIR/profiles/per-user/root</code>，root 用户的 profiles，一般情况下为 <code>/nix/var/nix/profiles/per-user/root</code>。</li>
</ul></li>
<li>兼容历史版本的路径（未来可能有变化）： <code>$NIX_STATE_DIR/profiles</code> 和 <code>$NIX_STATE_DIR/profiles/per-user</code>。</li>
</ul>

<p>在实现上，上述目录都在 <code>/nix/var/nix/gcroots/</code> 目录下存在软链。在执行垃圾回收时，就是根据该目录进行查找的。</p>

<ul>
<li>观察 <code>/nix/var/nix/gcroots/auto</code>，可以看到大量指向 <code>~/.local/state/nix/profiles/profile-xxx-link</code> 的软链。</li>
<li>如果想让某个包，某个 profiles 依赖的包，不被垃圾回收，可以在 <code>/nix/var/nix/gcroots/</code> 目录创建软链。</li>

<li><p><code>nix-store</code>、<code>nix-instantiate</code> 等命令存在一个 <a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/add#opt-add-root"><code>--add-root path</code></a> 选项，可以将某个存储对象添加到 <code>/nix/var/nix/gcroots/auto</code> 目录下 （本质上 <code>nix-env</code> 也是通过这个命令来不被垃圾回收的）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --add-root /home/eelco/bla/result --realise ...
#
$ ls -l /nix/var/nix/gcroots/auto
<span style="color:#75715e"># lrwxrwxrwx    1 ... 2005-03-13 21:10 dn54lcypm8f8... -&gt; /home/eelco/bla/result</span>
$ ls -l /home/eelco/bla/result
<span style="color:#75715e"># lrwxrwxrwx    1 ... 2005-03-13 21:10 /home/eelco/bla/result -&gt; /nix/store/1r11343n6qd4...-f-spot-0.0.10</span></code></pre></div></li>
</ul>

<p><code>nix-collect-garbage</code> 命令，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-collect-garbage [--delete-old] [-d] [--delete-older-than period] [--max-freed bytes] [--dry-run]</pre></div>
<p>选项说明如下：</p>

<ul>
<li>不加任何参数，等价于 <code>nix-store --gc</code>。</li>
<li><code>--delete-old</code> 或 <code>-d</code>，删除所有用户的 profiles 历史，等价于针对每个 profiles 先执行 <code>nix-env --delete-generations old</code>，再执行 <code>nix-store --gc</code>。</li>
<li><code>--delete-older-than period</code>，等价于针对每个 profiles 先执行 <code>nix-env --delete-generations &lt;period&gt;</code>，再执行 <code>nix-store --gc</code>。</li>
</ul>

<h2 id="已知问题">已知问题</h2>

<h3 id="报错-error-this-derivation-has-bad-meta-outputstoinstall">报错 error: this derivation has bad &lsquo;meta.outputsToInstall&rsquo;</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 dig (attribute 是 dig， name 是 bind)，可以安装成功</span>
nix-env -iA nixpkgs.dig
<span style="color:#75715e"># 再安装任意一个包，会报错</span>
nix-env -iA nixpkgs.ruby
<span style="color:#75715e"># error: this derivation has bad &#39;meta.outputsToInstall&#39;</span></code></pre></div>
<p>此时观察 <code>~/.nix-profile/manifest.nix</code> 中元信息内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bind-9.18.28&#34;</span>;  
    meta <span style="color:#f92672">=</span> { 
        mainProgram <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dig&#34;</span>; 
        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bind-9.18.28&#34;</span>; 
        outputsToInstall <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;out&#34;</span> <span style="color:#e6db74">&#34;dnsutils&#34;</span> <span style="color:#e6db74">&#34;host&#34;</span> ]; 
        position <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/7z7lzz187w5in6lplpxrqrzqh215sklb-nixpkgs/nixpkgs/pkgs/servers/dns/bind/default.nix:125&#34;</span>; unfree <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>; 
        <span style="color:#75715e"># ...</span>
    }; 
    outputs <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;dnsutils&#34;</span> ]; 
    dnsutils <span style="color:#f92672">=</span> { outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils&#34;</span>; };
    outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils&#34;</span>; 
    <span style="color:#75715e"># ...</span>
}
<span style="color:#75715e"># ...</span></code></pre></div>
<p>想临时修复这个问题，就是使用包名 <code>nixpkgs.bind</code> 重新安装或者卸载 <code>bind</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 bind (attribute 是 bind， name 是 bind)</span>
nix-env -iA nixpkgs.bind
<span style="color:#75715e"># 或卸载 bind: nix-env --uninstall bind</span>
<span style="color:#75715e"># 后续安装任何包将不会报错</span></code></pre></div>
<p>此时，再观察 <code>~/.nix-profile/manifest.nix</code> 中元信息内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{ 
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bind-9.18.28&#34;</span>; 
    meta <span style="color:#f92672">=</span> { 
        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bind-9.18.28&#34;</span>; 
        outputsToInstall <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;out&#34;</span> <span style="color:#e6db74">&#34;dnsutils&#34;</span> <span style="color:#e6db74">&#34;host&#34;</span> ]; 
        position <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/7z7lzz187w5in6lplpxrqrzqh215sklb-nixpkgs/nixpkgs/pkgs/servers/dns/bind/default.nix:125&#34;</span>; 
        <span style="color:#75715e">## ...</span>
    };
    outputs <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;dnsutils&#34;</span> <span style="color:#e6db74">&#34;host&#34;</span> <span style="color:#e6db74">&#34;out&#34;</span> ]; 
    dnsutils <span style="color:#f92672">=</span> { outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils&#34;</span>; }; 
    host <span style="color:#f92672">=</span> { outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/pwn4dbl6606b3mpcyasks0mlqwjijsyh-bind-9.18.28-host&#34;</span>; }; 
    out <span style="color:#f92672">=</span> { outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/krw6p2q85a4ca6dvpvfvaxgy99r4xjjh-bind-9.18.28&#34;</span>; }; 

    outPath <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/store/krw6p2q85a4ca6dvpvfvaxgy99r4xjjh-bind-9.18.28&#34;</span>; 
    <span style="color:#75715e"># ...</span>
}</code></pre></div>
<p>两者区别在于： 使用 <code>nixpkgs.dig</code> 安装时，meta.outputsToInstall 中有 <code>[ &quot;out&quot; &quot;dnsutils&quot; &quot;host&quot; ]</code>， outputs 中只有 <code>[ &quot;dnsutils&quot; ]</code>，找不到。</p>

<p>在 Nixpkgs 中，源码如下：</p>

<ul>
<li><a href="https://github.com/NixOS/nixpkgs/blob/04e3c70d0e0fce8aa6489d4b3d2e1ed146f3fce4/pkgs/servers/dns/bind/default.nix#L27">bind 包声明</a></li>
<li><a href="https://github.com/NixOS/nixpkgs/blob/04e3c70d0e0fce8aa6489d4b3d2e1ed146f3fce4/pkgs/top-level/all-packages.nix#L24164-L24166">bind、 dnsutils、 dig 列表声明</a></li>
</ul>

<p>相关 Issue 如下：</p>

<ul>
<li><a href="https://github.com/NixOS/nix/issues/9340">&ldquo;bad meta.outputsToInstall&rdquo; error when installing packages #9340</a></li>
</ul>
]]></description></item><item><title>Nix 高级话题之 nix store</title><link>https://www.rectcircle.cn/posts/nix-advanced-store/</link><pubDate>Mon, 17 Jun 2024 01:50:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-advanced-store/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.22.1</p>
</blockquote>

<h2 id="前言">前言</h2>

<p>Nix 存储是 Nix 系统其他能力（如 profile、channel、build 等）的基石。</p>

<p>只有理解了 Nix 存储，才能更好的理解 Nix 的理念，才能更好的理解 Nix 的其他能力，才能了解 Nix 的原理。</p>

<p>因此，本文将从概念、实现以及 nix-store 命令这几个方面介绍 Nix 存储。</p>

<h2 id="概念">概念</h2>

<h3 id="文件系统对象">文件系统对象</h3>

<p>Nix 作为一个编译系统，明确定义了其对文件系统的要求。Nix 使用文件系统的简化模型，该模型由文件系统对象组成。每个文件系统对象都是以下对象之一：</p>

<ul>
<li>文件

<ul>
<li>内容的可能为空的字节序列。</li>
<li>代表是否可执行权限的单个布尔值。</li>
</ul></li>
<li>目录：将名称映射到子文件系统的对象。</li>
<li>符号链接：任意字符串。 Nix 不会为符号链接分配任何语义。</li>
</ul>

<p>文件系统对象及其子对象形成一棵树。文件或符号链接可以是根文件系统对象。</p>

<p>Nix 不编码任何其他文件系统概念，例如硬链接、权限、时间戳或其他元数据。因此， <code>ls -al /nix/store</code> 看到目录项的时间都是 <code>1970-01-01</code>。</p>

<h3 id="存储对象">存储对象</h3>

<p>Nix 存储是存储对象的集合，它们之间具有引用关系。存储对象包括：</p>

<ul>
<li>数据： 文件系统对象。</li>
<li>引用关系（依赖）： 一组存储路径作为对其他存储对象的引用。</li>
</ul>

<p>存储对象是不可变的：一旦创建，它们就不会改变，直到被删除。</p>

<h3 id="存储路径">存储路径</h3>

<p>Nix 将存储对象的引用实现为存储路径。</p>

<p>将存储路径视为不透明的唯一标识符：获取存储路径的唯一方法是添加或构建存储对象。存储路径将始终引用一个存储对象。</p>

<p>存储路径由如下两部分组成：</p>

<ul>
<li>20 字节的 digest 作为 id。</li>
<li>供人阅读的具有含义的名字。</li>
</ul>

<p>例如：</p>

<ul>
<li>Digest: mzg3cka0bbr5jq96ysymwziw74fnk22m</li>
<li>Name: go-1.22.1</li>
</ul>

<p>为了使存储对象可供操作系统进程访问，存储必须通过文件系统暴露存储对象。</p>

<p>存储路径在文件系统路径，由如下部分组成：</p>

<ul>
<li>存储目录 (通常为 <code>/nix/store</code>)</li>
<li>路径分隔符 (<code>/</code>)</li>
<li>使用 base32 编码的 digest</li>
<li>中划线 (<code>-</code>)</li>
<li>名字</li>
</ul>

<p>例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">  /nix/store/mzg3cka0bbr5jq96ysymwziw74fnk22m-go-1.22.1
  |--------| |------------------------------| |-------|
store directory            digest               name</pre></div>
<p>关于 digest 计算的算法，详见： <a href="https://nix.dev/manual/nix/2.22/protocols/store-path">Nix 参考手册 - 10.3 Complete Store Path Calculation</a></p>

<h3 id="存储目录">存储目录</h3>

<p>每个 Nix 存储都有一个存储目录 (通常为 <code>/nix/store</code>)。</p>

<p>并非每个存储都可以通过文件系统访问。但是，如果存储具有文件系统表示形式，则存储目录包含，可以通过存储路径来寻址的存储的文件系统对象。</p>

<p>这意味着，存储路径不仅源自引用的存储对象本身，而且取决于存储对象所在的存储的存储路径 (通常为 <code>/nix/store</code>)。</p>

<p>给定存储对象属于哪个存储非常重要：存储对象中的文件可以包含存储路径，并且进程可以读取这些路径（如动态链接库路径）。 Nix 仅当存储路径不跨越存储边界时才能保证引用完整性。</p>

<p>因此，只有满足以下情况下之一的，才可以将存储对象复制到另一存储：</p>

<ul>
<li>源存储和目标存储的存储目录匹配。</li>
<li>该存储对象没有引用，即不包含存储路径。</li>
</ul>

<p>无法将存储对象复制到具有不同存储目录的存储。相反，它必须连同其所有依赖项一起重建。不使用替换文件内容中的存储目录字符串的原因是：这么做这可能会导致可执行文件的内部偏移量或校验和无效而无法使用。</p>

<h3 id="关系">关系</h3>

<p>在 <a href="/posts/nix-advanced-glossary/">《Nix 高级话题之 概念》</a> 文章中介绍了更多的概念/术语中，很多都与 Nix store 有关。这些术语的关系如下所示：</p>

<p><img src="/image/nix-store-glossary.svg" alt="image" /></p>

<h2 id="存储类型">存储类型</h2>

<p>Nix 提供了多种 Nix 的实现，即多种存储类型，本部分将介绍其中常用的几种。</p>

<h3 id="local-store">Local Store</h3>

<p>该类型是使用单用户模式 <code>--no-daemon</code> 安装 nix 时的默认存储类型（<a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file.html#conf-store">nix.conf 的 store 为 auto</a>）。</p>

<p>该类型的配置 URL 格式为（如下两种是等价的）：</p>

<ul>
<li><code>local?root=/tmp/root</code>。</li>
<li><code>/tmp/root</code> （即一个绝对路径，作为 <code>root</code> 参数）。</li>
</ul>

<p>注意，如上例，如果 <code>root</code> 参数配置为非 <code>/</code> 非空串时：</p>

<ul>
<li>nix 系统会使用 chroot （仅支持启用了 mount namespace 和 user namespace 的 Linux）来构建和运行程序。</li>
<li>物理的存储位置为 <code>/tmp/root/nix/store</code>。</li>
<li>元数据存储为之为 <code>/tmp/root/nix/var/nix/db</code>。</li>
</ul>

<p>也可以通过 <code>store</code> 参数配置逻辑的存储目录为非 <code>/nix/store</code>，但是非常不建议这么做。如果这么做了，因为存储路径不同，将无法使用官方提供的 HTTP Binary Cache Store <code>https://cache.nixos.org/</code>，所有的依赖都要被重新构建。</p>

<p>更多详见： <a href="https://nix.dev/manual/nix/2.22/store/types/local-store">Nix 参考手册 - 4.4.8 Local Store</a>。</p>

<h3 id="local-daemon-store">Local Daemon Store</h3>

<p>该类型是使用多用户模式 <code>--daemon</code> 安装 nix 时的默认存储类型（<a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file.html#conf-store">nix.conf 的 store 为 auto</a>）。</p>

<p>该类型的配置 URL 格式示例如下：</p>

<ul>
<li><code>unix:///nix/var/nix/daemon-socket/socket?root=</code>。</li>
</ul>

<p>更多详见： <a href="https://nix.dev/manual/nix/2.22/store/types/local-daemon-store">Nix 参考手册 - 4.4.7 Local Daemon Store</a>。</p>

<h3 id="dummy-store">Dummy Store</h3>

<p>该类型主要用于调试 nix 表达式。</p>

<p>该类型的配置 URL 格式为： <code>dummy://</code></p>

<p>用法例如： <code>nix eval --store dummy:// --expr '1 + 2'</code>。</p>

<p>更多详见： <a href="https://nix.dev/manual/nix/2.22/store/types/dummy-store">Nix 参考手册 - 4.4.1 Dummy Store</a>。</p>

<h3 id="http-binary-cache-store">HTTP Binary Cache Store</h3>

<p>该类型主要用于作为 <a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file.html#conf-substituters">nix.conf 的 substituter 配置项</a>，用来加速安装。</p>

<p>该类型的配置 URL 格式为： <code>http://...</code> 或 <code>https://...</code></p>

<p>更多详见： <a href="https://nix.dev/manual/nix/2.22/store/types/http-binary-cache-store">Nix 参考手册 - 4.4.5 HTTP Binary Cache Store</a>。</p>

<h3 id="local-overlay-store-实验性">Local Overlay Store （实验性）</h3>

<p>该类型由 <a href="https://replit.com/">replit</a> 贡献，主要用于基于分布式文件系统的 store 作为 overlayfs 的 lower，实现 nix 软件包秒安装。</p>

<p>配置示例如下 <code>~/.config/nix/nix.conf</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">substituters = https://cache.nixos.org/
sandbox = false
store = local-overlay://?lower-store=%2Fnix-lower%3Fread-only%3Dtrue&amp;upper-layer=/nix-store-upper&amp;check-mount=false
extra-experimental-features = nix-command flakes local-overlay-store read-only-local-store
gc-reserved-space = 0</pre></div>
<ul>
<li>store 的 <code>local-overlay://</code> 表示使用 Local Overlay Store，配置参数说明入选。

<ul>
<li>lower-store 作为 overlayfs /nix/store 的 lowerdir 所在的 nix store 的配置字符串，这里配置为 <code>/nix-lower?read-only=true</code>，即 lower-store 为一个 local store，其 root 参数为 <code>/nix-lower</code>，参见上文。</li>
<li>upper-layer 作为 overlayfs /nix/store 的 upperdir 所在的目录，这里配置为 <code>/nix-store-upper</code>。</li>
<li>check-mount 配置为 false，表示不检查 mount 是否成功。</li>
</ul></li>
<li>extra-experimental-features 中必须添加 <code>local-overlay-store</code>，因为该类型为实现性特性。</li>
</ul>

<p>下面是一个 docker 模拟的示例：</p>

<ul>
<li>在宿主机上采用单用户模式安装 nix，并通过 nix-env 安装了 go。</li>
<li>在宿主机构造一个 overlayfs，其 lowerdir 为 <code>/nix/store</code>，upperdir 为宿主机的一个空目录，并经这个 overlayfs mount 到容器的 <code>/nix/store</code>。</li>
<li>将宿主机的 <code>/nix</code> 整个目录挂载到容器的 <code>/nix-lower</code>，配置容器中 nix 存储类型为 Local Overlay Store，将 <code>/nix-lower</code> 配置到 <code>lower-store</code> 参数中。</li>
<li>docker exec 进入容器后，通过 nix-env 安装 go，可实现秒装。</li>
</ul>

<p>相关命令如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在宿主机安装 nix，并 nix-env 安装 go，参考：</span>
bash &lt;<span style="color:#f92672">(</span>curl -L https://nixos.org/nix/install<span style="color:#f92672">)</span> --no-daemon
nix-env -iA nixpkgs.go

<span style="color:#75715e"># 准备要挂载到 /nix/store 的 overlayfs</span>
<span style="color:#75715e"># 使用 root 执行</span>
rm -rf /tmp/nix-local-overlay/nix-store-upper
mkdir -p /tmp/nix-local-overlay/nix-store-upper /tmp/nix-local-overlay/nix-store-work /tmp/nix-local-overlay/nix-store-merged
mount -t overlay overlay -o lowerdir<span style="color:#f92672">=</span>/nix/store,upperdir<span style="color:#f92672">=</span>/tmp/nix-local-overlay/nix-store-upper,workdir<span style="color:#f92672">=</span>/tmp/nix-local-overlay/nix-store-work /tmp/nix-local-overlay/nix-store-merged

<span style="color:#75715e"># 创建容器</span>
docker rm -f nix-local-overlay
<span style="color:#75715e"># 在宿主机安装 nix，并通过 nix-env 安装 go</span>
<span style="color:#75715e"># docker 使用 busybox:latest 镜像，启动 nix-local-overlay 镜像</span>
<span style="color:#75715e"># --security-opt 必须加，否则会报错 error: Operation not permitted</span>
docker run --security-opt seccomp<span style="color:#f92672">=</span>unconfined --user root -d --name nix-local-overlay -v /nix:/nix-lower/nix:ro -v /tmp/nix-local-overlay/nix-store-upper:/nix-store-upper -v /tmp/nix-local-overlay/nix-store-merged:/nix/store busybox:latest tail -f /dev/null

<span style="color:#75715e"># 进入容器验证</span>
docker exec -it nix-local-overlay sh
<span style="color:#75715e"># 添加用户（-u 指定的 uid 必须和宿主机的一致）</span>
adduser -u <span style="color:#ae81ff">1000</span> nix <span style="color:#75715e"># 输入密码</span>
chown nix:nix /nix /nix/store
su nix
cd ~
touch ~/.profile
<span style="color:#75715e"># 安装</span>
wget https://nixos.org/nix/install
sh install --no-daemon
source ~/.profile
which nix
<span style="color:#75715e"># 配置</span>
mkdir -p ~/.config/nix <span style="color:#f92672">&amp;&amp;</span> cat &gt; ~/.config/nix/nix.conf <span style="color:#e6db74">&lt;&lt;EOF
</span><span style="color:#e6db74">substituters = https://cache.nixos.org/
</span><span style="color:#e6db74">sandbox = false
</span><span style="color:#e6db74">store = local-overlay://?lower-store=%2Fnix-lower%3Fread-only%3Dtrue&amp;upper-layer=/nix-store-upper&amp;check-mount=false
</span><span style="color:#e6db74">extra-experimental-features = nix-command flakes local-overlay-store read-only-local-store
</span><span style="color:#e6db74">gc-reserved-space = 0
</span><span style="color:#e6db74">EOF</span>
<span style="color:#75715e"># 验证 nix 相关命令</span>
nix eval  --expr <span style="color:#e6db74">&#39;1 + 2&#39;</span>
nix-channel --update
nix-env -iA nixpkgs.go <span style="color:#75715e"># 秒装</span>
<span style="color:#75715e"># 去宿主机看，upper 层没有 go，直接复用 lower 的</span>
ls -al /tmp/nix-local-overlay/nix-store-upper</code></pre></div>
<h2 id="nix-store-命令"><code>nix-store 命令</code></h2>

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store">Nix 参考手册 - 8.3.3 nix-store</a></p>
</blockquote>

<p>nix-store 命令是 Nix 的主要命令之一，它提供了对 Nix 存储的访问。</p>

<p>该命令是相对底层原始的操作，对于一般用户来说，这个命令一般不需要使用，而是由如 nix-env、nix-channel 等命令间接调用。</p>

<h3 id="nix-store-add-fixed"><code>nix-store --add-fixed</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --add-fixed <span style="color:#f92672">[</span>--recursive<span style="color:#f92672">]</span> algorithm paths…</code></pre></div>
<p><code>--add-fixed</code> 操作将指定路径添加到 Nix 存储。与 <code>--add</code> 不同，路径是使用指定的哈希算法注册的，从而产生与 ixed-output derivation（固定输出派生）相同的输出路径。这可用于无法从公共 URL 获取，或在编写下载表达式后损坏的源。</p>

<ul>
<li><code>algorithm</code> 可选值 &lsquo;md5&rsquo;, &lsquo;sha1&rsquo;, &lsquo;sha256&rsquo;, or &lsquo;sha512&rsquo;。</li>
<li><code>--recursive</code> 使用递归（序列化为 nar 后再序列化）而不是平面（）散列模式，在将目录添加到存储时使用。</li>
</ul>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo abcd &gt; abc.txt
nix-store --add-fixed --recursive sha256 abc.txt
<span style="color:#75715e"># 输出: /nix/store/6g0r26al9a8bg84ny9v7d42xcamyjrhx-abc.txt</span>
<span style="color:#75715e"># 等价于:</span> 
<span style="color:#75715e"># nix-store --add abc.txt</span>
<span style="color:#75715e"># nix --extra-experimental-features nix-command store add abc.txt</span>
<span style="color:#75715e"># nix --extra-experimental-features nix-command store add abc.txt --mode nar --hash-algo sha256</span></code></pre></div>
<h3 id="nix-store-add"><code>nix-store --add</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --add paths…</code></pre></div>
<p>等价于 <code>nix-store --add-fixed --recursive sha256 paths…</code></p>

<h3 id="nix-store-delete"><code>nix-store --delete</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --delete <span style="color:#f92672">[</span>--ignore-liveness<span style="color:#f92672">]</span> paths…</code></pre></div>
<p><code>--delete</code> 操作从 Nix 存储中删除存储路径，但前提是这样做是安全的；也就是说，当从垃圾收集器的根无法到达该路径时。这意味着您只能删除也会被 <code>nix-store --gc</code> 删除的路径。因此，<code>--delete</code> 是 <code>--gc</code> 的更有针对性的版本。</p>

<p>使用选项 <code>--ignore-liveness</code>，会忽略来自根的可达性。但是，如果存储中有其他路径引用该路径（即依赖于它），该路径仍然不会被删除。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --delete /nix/store/6g0r26al9a8bg84ny9v7d42xcamyjrhx-abc.txt</code></pre></div>
<h3 id="nix-store-gc"><code>nix-store --gc</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --gc</code></pre></div>
<p>运行垃圾回收，详见： <a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/gc">官方文档</a>。</p>

<h3 id="nix-store-dump-db"><code>nix-store --dump-db</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --dump-db <span style="color:#f92672">[</span>paths…<span style="color:#f92672">]</span></code></pre></div>
<p>导出指定存储路径的 Nix database 到标准输出。</p>

<ul>
<li>如果 <code>paths...</code> 参数不给出，则备份整个数据库。</li>
<li>如果 <code>paths...</code> 给出，则备份每个 <code>path</code> 的元信息以及直接依赖（注意不是全部依赖，即不是闭包，如果想导出整个闭包的关系，则需要用户把这个闭包的所有 <code>paths...</code> 给出）。</li>
</ul>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 导出 nix 命令所在存储对象的元信息（直接依赖关系）</span>
nix-store --dump-db <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">)</span> &gt; nix-single.reginfo
cat nix-single.reginfo
<span style="color:#75715e"># 导出 nix 命令所在存储对象整个闭包的元信息（直接依赖关系）（nix 安装包里面的 `.reginfo` 文件就是类似的方式导出的）</span>
nix-store --dump-db <span style="color:#66d9ef">$(</span>nix-store --query --requisites <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">))</span> &gt; nix-closure.reginfo
cat nix-closure.reginfo</code></pre></div>
<p>每个 path 的元信包含如下内容：</p>

<ul>
<li>存储路径，如： <code>/nix/store/6sfq258683sg0idsm9c5877pfm3q4y27-nix-2.22.1</code></li>
<li>存储路径对应 nar 文件的 hash 值，如： <code>6cde735c35b3b6a49591f938102bbcf287efd77039191d8b857457e5d8377db0</code></li>
<li>存储路径对应 nar 文件的文件大小，如： <code>19398944</code>。</li>
<li>空行</li>
<li>引用（直接依赖）的数量，如： <code>18</code></li>
<li>所有引用（直接依赖）的存储路径，每个一行，如： <code>/nix/store/1rkhjf55x59w6qm1pbhf80ks2wjpg973-libcpuid-0.6.4</code> 等。</li>
</ul>

<h3 id="nix-store-load-db"><code>nix-store --load-db</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --load-db &lt; nix-closure.reginfo</code></pre></div>
<p><code>--load-db</code> 操作从标准输入读取 <code>--dump-db</code> 创建的 Nix 数据库转储并将其加载到 Nix 数据库中。</p>

<p>nix 安装包中的安装脚本 database 的初始化就是通过该命令实现的。</p>

<h3 id="nix-store-dump"><code>nix-store --dump</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --dump path</code></pre></div>
<p><code>--dump</code> 操作生成一个 NAR (Nix ARchive) 文件，其中包含以 <code>path</code> 参数为根的文件系统树的内容。存档被写入标准输出。</p>

<p>NAR 类似于 tar，但是其是根据上文<a href="#文件系统对象">文件系统对象</a>协议优化过的格式，并且会对目录项进行排序，因此同一个目录 <code>--dump</code> 生成的 NAR 的文件的内容是完全相同，事实上，存储在 Nix 数据库中的存储路径的哈希值（请参阅 <code>nix-store --query --hash</code>）是每个存储路径的 NAR 转储的 SHA-256 哈希值。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --dump <span style="color:#66d9ef">$(</span>readlink <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">))</span>/../.. &gt; nix.nar</code></pre></div>
<p>NAR 文件格式，详见：<a href="https://nix.dev/manual/nix/2.22/protocols/nix-archive">Nix Archive (NAR) format</a>。</p>

<h3 id="nix-store-restore"><code>nix-store --restore</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --restore path</code></pre></div>
<p><code>--restore</code> 操作将 NAR 存档解包到 <code>path</code>，该路径必须不存在。存档是从标准输入读取的。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --restore nix-nar-unpack &lt; nix.nar
tree -L <span style="color:#ae81ff">1</span> nix-nar-unpack <span style="color:#75715e"># 输出如下：</span>
<span style="color:#75715e"># nix-nar-unpack</span>
<span style="color:#75715e"># ├── bin</span>
<span style="color:#75715e"># ├── etc</span>
<span style="color:#75715e"># ├── lib</span>
<span style="color:#75715e"># ├── libexec</span>
<span style="color:#75715e"># └── share</span></code></pre></div>
<h3 id="nix-store-export"><code>nix-store --export</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-store --export paths…</pre></div>
<p><code>--export</code> 操作，将指定存储路径的序列化写入标准输出，其格式可以使用 <code>nix-store --import</code> 导入到另一个 Nix 存储中（包含数据和引用关系）。</p>

<p>这类似于 <code>nix-store --dump</code>，和 <code>nix-store --dump</code> 不同的是：</p>

<ul>
<li><code>nix-store --dump</code> 命令生成的 NAR 存档只数据，不包含允许将其导入另一个 Nix 存储（即路径引用集）所需的元信息（引用关系、nar 的 hash 等）。</li>
<li><code>nix-store --export</code> 命令生成的序列化，包含数据和元信息。</li>
</ul>

<p>也就是说 <code>nix-store --export</code> 是将每个 <code>--dump-db</code> 和 <code>--dump</code> 的职责合二为一了，生成的一种序列化格式。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 导出 nix 命令所在存储对象的数据内容以及元信息</span>
nix-store --export <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">)</span> &gt; nix-single-export.storeobject
<span style="color:#75715e"># 导出 nix 命令所在存储对象的所有闭包存储路径的数据内容以及元信息</span>
nix-store --export <span style="color:#66d9ef">$(</span>nix-store --query --requisites <span style="color:#66d9ef">$(</span>which nix<span style="color:#66d9ef">))</span> &gt; nix-closure-export.storeobject</code></pre></div>
<h3 id="nix-store-import"><code>nix-store --import</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --import</code></pre></div>
<p><code>--import</code> 操作从标准输入读取 <code>nix-store --export</code> 生成的一组存储路径的序列化，并将这些存储路径添加到 Nix 存储中。 Nix 存储中已存在的路径将被忽略。如果一个路径引用 Nix 存储中不存在的另一个路径，则导入失败。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 导入 nix 命令所在存储对象的数据内容以及元信息</span>
nix-store --import &lt; nix-closure-export.storeobject</code></pre></div>
<h3 id="nix-store-generate-binary-cache-key"><code>nix-store --generate-binary-cache-key</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --generate-binary-cache-key key-name secret-key-file public-key-file</code></pre></div>
<p>生成用于二进制缓存的密钥对，详见：</p>

<ul>
<li><a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/generate-binary-cache-key">官方文档</a>。</li>
<li><a href="https://nixos.wiki/wiki/Binary_Cache">Wiki</a>。</li>
</ul>

<h3 id="nix-store-optimise"><code>nix-store --optimise</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --optimise</code></pre></div>
<p><code>--optimise</code> 操作通过在存储中查找相同的文件并将它们彼此硬链接来减少 Nix 存储磁盘空间的使用。它通常会将存储的规模缩小 25-35% 左右。只有常规文件和符号链接才能以这种方式进行硬链接。当文件具有相同的 NAR 存档序列化时，文件被视为相同：也就是说，常规文件必须具有相同的内容和权限（可执行或不可执行），并且符号链接必须具有相同的内容。</p>

<p>完成后或命令中断时，将在标准错误上打印有关所实现节省的报告。</p>

<p>使用 -vv 或 -vvv 获取一些进度指示。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --optimise <span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># 14.74 MiB freed by hard-linking 1622 files</span></code></pre></div>
<h3 id="nix-store-print-env"><code>nix-store --print-env</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --print-env drvpath</code></pre></div>
<p>操作 <code>--print-env</code> 以 shell 可以评估的格式打印出派生的环境。构建器的命令行参数放置在变量 <code>_args</code> 中。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs  <span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># /nix/store/amplsdcav0g3qp9srkkfnqlkr8zc3sn8-hello-2.12.1.drv</span>
nix-store --print-env <span style="color:#66d9ef">$(</span>nix-instantiate -A hello ~/.nix-defexpr/channels/nixpkgs<span style="color:#66d9ef">)</span> <span style="color:#75715e"># 输出类似如下：</span>
<span style="color:#75715e"># export __structuredAttrs; __structuredAttrs=&#39;&#39;</span>

<span style="color:#75715e"># export buildInputs; buildInputs=&#39;&#39;</span>

<span style="color:#75715e"># export builder; builder=&#39;/nix/store/a1s263pmsci9zykm5xcdf7x9rv26w6d5-bash-5.2p26/bin/bash&#39;</span>

<span style="color:#75715e"># export cmakeFlags; cmakeFlags=&#39;&#39;</span>

<span style="color:#75715e"># ...</span>

<span style="color:#75715e"># export system; system=&#39;x86_64-linux&#39;</span>

<span style="color:#75715e"># export version; version=&#39;2.12.1&#39;</span>

<span style="color:#75715e"># export _args; _args=&#39;&#39;-e&#39; &#39;/nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh&#39;&#39;</span></code></pre></div>
<h3 id="nix-store-realise"><code>nix-store --realise</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store <span style="color:#f92672">{</span>--realise | -r<span style="color:#f92672">}</span> paths… <span style="color:#f92672">[</span>--dry-run<span style="color:#f92672">]</span></code></pre></div>
<p>构建或获取存储对象。</p>

<p>每个路径的处理如下：
* 如果路径指向存储派生：
    * 如果无效（不存在），则从 substituter（如二进制缓存） 获取存储派生文件本身。
    * 实现其输出路径：
        * 尝试从 substituter（如二进制缓存） 获取与存储派生闭包中的输出路径关联的存储对象。
            * 使用内容寻址派生（实验性）：通过查询 Nix 数据库中的内容寻址实现条目来确定要实现的输出路径。
        * 对于任何 substituter 中找不到的存储路径，则通过构建生成所需的存储对象：
            * Realise 推导依赖项的所有输出
            * 运行派生的 <code>builder</code> 生成可执行文件
* 否则，如果路径无效：尝试从 substituter 中获取路径闭包中关联的存储对象。</p>

<p>如果没有可用的 substitutes 并且没有给出存储推导，则失败。</p>

<p>生成的路径打印在标准输出上。对于非派生参数，将打印参数本身。</p>

<p>错误码详见：<a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/realise#special-exit-codes-for-build-failure">文档</a>。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs  <span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># /nix/store/amplsdcav0g3qp9srkkfnqlkr8zc3sn8-hello-2.12.1.drv</span>
nix-store --realise <span style="color:#66d9ef">$(</span>nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs<span style="color:#66d9ef">)</span></code></pre></div>
<h3 id="nix-store-read-log"><code>nix-store --read-log</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store <span style="color:#f92672">{</span>--read-log | -l<span style="color:#f92672">}</span> paths…</code></pre></div>
<p><code>--read-log</code> 操作将在标准输出上打印指定存储路径的构建日志。构建日志是派生构建者写入标准输出和标准错误的任何内容。如果存储路径不是派生，则使用存储路径的派生者。</p>

<p>构建日志保存在 /nix/var/log/nix/drvs 中。但是，不能保证构建日志可用于任何特定的存储路径。例如，如果路径是通过替代品作为预构建的二进制文件下载的，则日志不可用。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --read-log <span style="color:#66d9ef">$(</span>nix-store --realise <span style="color:#66d9ef">$(</span>nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs<span style="color:#66d9ef">))</span>
<span style="color:#75715e"># error: build log of derivation &#39;/nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1&#39; is not available</span></code></pre></div>
<h3 id="nix-store-verify-path"><code>nix-store --verify-path</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --verify-path paths…</code></pre></div>
<p><code>--verify-path</code> 操作将给定存储路径的内容与存储在 Nix 数据库中的加密哈希值进行比较。对于每个更改的路径，它都会打印一条警告消息。如果路径没有更改，则退出状态为 0，否则为 1。</p>

<h3 id="nix-store-repair-path"><code>nix-store --repair-path</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --repair-path paths…</code></pre></div>
<p>从 substituter 重新下载路径。</p>

<p>操作 <code>--repair-path</code> 尝试通过使用可用的替代程序重新下载指定的路径来“修复”它们。如果没有可用的替代品，则无法进行修复。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">hellow_out_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>nix-store --realise <span style="color:#66d9ef">$(</span>nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs<span style="color:#66d9ef">))</span>
nix-store --verify-path $hellow_out_path</code></pre></div>
<h3 id="nix-store-verify"><code>nix-store --verify</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --verify <span style="color:#f92672">[</span>--check-contents<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>--repair<span style="color:#f92672">]</span></code></pre></div>
<p><code>--verify</code> 操作验证 Nix 数据库的内部一致性，以及 Nix 数据库和 Nix 存储之间的一致性。遇到的任何不一致都会自动修复。不一致通常是由非 Nix 工具修改 Nix 存储或数据库或 Nix 本身的错误造成的。</p>

<p>该操作有以下选项：
* <code>--check-contents</code> 通过计算内容的 SHA-256 哈希并将其与构建时存储在 Nix 数据库中的哈希进行比较，检查每个有效存储路径的内容是否未被更改。打印出已修改的路径。对于大型存储来说，<code>--check-contents</code> 显然相当慢。
* <code>--repair</code> 如果存储中缺少任何有效路径，或者（如果给出了 <code>--check-contents</code>）有效路径的内容已被修改，则尝试通过重新下载来修复路径。详见上文 <code>nix-store --repair-path</code>。</p>

<h3 id="nix-store-serve"><code>nix-store --serve</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --serve <span style="color:#f92672">[</span>--write<span style="color:#f92672">]</span></code></pre></div>
<p>操作 <code>--serve</code> 通过 stdin 和 stdout 提供对 Nix 存储的访问，旨在用作向受限 ssh 用户提供 Nix 存储访问的一种方法。</p>

<ul>
<li><code>--write</code> 允许连接的客户端请求实现派生。实际上，这可以用来使主机充当远程构建器。</li>
</ul>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/store/types/ssh-store">SSH Store</a></p>

<h3 id="nix-store-query"><code>nix-store --query</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store <span style="color:#f92672">{</span>--query | -q<span style="color:#f92672">}</span> <span style="color:#f92672">{</span>--outputs | --requisites | -R | --references | --referrers | --referrers-closure | --deriver | -d | --valid-derivers | --graph | --tree | --binding name | -b name | --hash | --size | --roots<span style="color:#f92672">}</span> <span style="color:#f92672">[</span>--use-output<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>-u<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>--force-realise<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>-f<span style="color:#f92672">]</span> paths…</code></pre></div>
<p><code>--query</code> 操作显示有关存储路径的各种信息。查询描述如下。最多可以指定一个查询。默认查询是 <code>--outputs</code>。</p>

<p>示例准备工作</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">### 准备</span>
PAGER<span style="color:#f92672">=</span>
hellow_drv_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>nix-instantiate -A hello  ~/.nix-defexpr/channels/nixpkgs<span style="color:#66d9ef">)</span>
echo $hellow_drv_path
<span style="color:#75715e"># /nix/store/amplsdcav0g3qp9srkkfnqlkr8zc3sn8-hello-2.12.1.drv</span>
hellow_out_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>nix-store --realise $hellow_drv_path<span style="color:#66d9ef">)</span>
echo $hellow_out_path
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span></code></pre></div>
<h4 id="查询输出路径">查询输出路径</h4>

<p><code>--outputs</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --outputs $hellow_drv_path
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span>
nix-store --query --outputs $hellow_out_path
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span></code></pre></div>
<h4 id="查询闭包">查询闭包</h4>

<p><code>--requisites</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --requisites $hellow_drv_path
<span style="color:#75715e"># /nix/store/001gp43bjqzx60cg345n2slzg7131za8-nix-nss-open-files.patch</span>
<span style="color:#75715e"># /nix/store/00qr10y7z2fcvrp9b2m46710nkjvj55z-update-autotools-gnu-config-scripts.sh</span>
<span style="color:#75715e"># ...</span>
<span style="color:#75715e"># /nix/store/lbskcsypzh48m1mv2nbz23k0c2s0k451-hello-2.12.1.tar.gz.drv</span>
<span style="color:#75715e"># /nix/store/amplsdcav0g3qp9srkkfnqlkr8zc3sn8-hello-2.12.1.drv</span>
nix-store --query --requisites $hellow_out_path
<span style="color:#75715e"># /nix/store/7n0mbqydcipkpbxm24fab066lxk68aqk-libunistring-1.1</span>
<span style="color:#75715e"># /nix/store/rxganm4ibf31qngal3j3psp20mak37yy-xgcc-13.2.0-libgcc</span>
<span style="color:#75715e"># /nix/store/s32cldbh9pfzd9z82izi12mdlrw0yf8q-libidn2-2.3.7</span>
<span style="color:#75715e"># /nix/store/ddwyrxif62r8n6xclvskjyy6szdhvj60-glibc-2.39-5</span>
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span></code></pre></div>
<h4 id="查询引用">查询引用</h4>

<p><code>--references</code></p>

<p>直接依赖</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --references $hellow_drv_path
<span style="color:#75715e"># /nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh</span>
<span style="color:#75715e"># /nix/store/qkh75fn6sickg70qhdn7j1486ydzfb9i-bash-5.2p26.drv</span>
<span style="color:#75715e"># /nix/store/0c983id41frc440gzr45y3sm5hfpx4lb-stdenv-linux.drv</span>
<span style="color:#75715e"># /nix/store/lbskcsypzh48m1mv2nbz23k0c2s0k451-hello-2.12.1.tar.gz.drv</span>
nix-store --query --references $hellow_out_path
<span style="color:#75715e"># /nix/store/ddwyrxif62r8n6xclvskjyy6szdhvj60-glibc-2.39-5</span>
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span></code></pre></div>
<h4 id="查询被引用者">查询被引用者</h4>

<p><code>--referrers</code></p>

<p>打印存储路径的引用者集，即 Nix 存储中当前存在的引用其中一个路径的存储路径。请注意，与引用相反，引用者集不是恒定的；它可以随着存储路径的添加或删除而改变。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --referrers $hellow_drv_path
<span style="color:#75715e"># 无输出</span>
nix-store --query --referrers $hellow_out_path
<span style="color:#75715e"># 无输出</span></code></pre></div>
<h4 id="查询被引用者闭包">查询被引用者闭包</h4>

<p><code>--referrers-closure</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --referrers-closure $hellow_drv_path
<span style="color:#75715e"># 无输出</span>
nix-store --query --referrers-closure $hellow_out_path
<span style="color:#75715e"># 无输出</span></code></pre></div>
<h4 id="查询派生者">查询派生者</h4>

<p><code>--deriver; -d</code></p>

<p>打印用于构建存储路径的派生程序。如果路径没有派生者（例如，如果它是源文件），或者如果派生者未知（例如，在仅二进制部署的情况下），则打印字符串unknown-deriver。不保证返回的派生程序存在于本地存储中，例如当从二进制缓存替换路径时。使用 <code>--valid-derivers</code> 来仅获取有效路径。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --deriver $hellow_drv_path
<span style="color:#75715e"># unknown-deriver</span>
nix-store --query --deriver $hellow_out_path
<span style="color:#75715e"># /nix/store/4h712p20bpisada9lir5nyd073ybs7nw-hello-2.12.1.drv</span></code></pre></div>
<h4 id="查询验证的派生者">查询验证的派生者</h4>

<p><code>--valid-derivers</code></p>

<p>打印一组派生文件 (.drv)，这些文件在实现时应该生成所述路径。可能不打印任何内容，例如源路径或从二进制缓存替换的路径。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --valid-derivers $hellow_drv_path
<span style="color:#75715e"># 无输出</span>
nix-store --query --valid-derivers $hellow_out_path
<span style="color:#75715e"># /nix/store/amplsdcav0g3qp9srkkfnqlkr8zc3sn8-hello-2.12.1.drv</span></code></pre></div>
<h4 id="查询输出-graphviz-依赖图">查询输出 Graphviz 依赖图</h4>

<p><code>--graph</code></p>

<p>以 <a href="http://www.graphviz.org/">AT&amp;T Graphviz 包</a>的 dot 工具的格式打印存储路径的引用图。这可用于可视化依赖图。要获取构建时依赖图，请将其应用于存储派生。要获取运行时依赖关系图，请将其应用于输出路径。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --graph $hellow_drv_path
<span style="color:#75715e"># dot 绘图文件源码</span>
nix-store --query --graph $hellow_out_path
<span style="color:#75715e"># dot 绘图文件源码，渲染出，如下图所示</span></code></pre></div>
<p><img src="/image/nix-hello-graphviz.svg" alt="image" /></p>

<h4 id="查询打印依赖树">查询打印依赖树</h4>

<p><code>--tree</code></p>

<p>将存储路径的引用图打印为嵌套 ASCII 树。参考文献按闭合尺寸降序排列；这往往会使树变平，使其更具可读性。查询仅在第一次遇到时递归到存储路径；这可以防止图的树表示的放大。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --tree $hellow_drv_path
<span style="color:#75715e"># 略</span>
nix-store --query --tree $hellow_out_path
<span style="color:#75715e"># /nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1</span>
<span style="color:#75715e"># ├───/nix/store/ddwyrxif62r8n6xclvskjyy6szdhvj60-glibc-2.39-5</span>
<span style="color:#75715e"># │   ├───/nix/store/rxganm4ibf31qngal3j3psp20mak37yy-xgcc-13.2.0-libgcc</span>
<span style="color:#75715e"># │   ├───/nix/store/s32cldbh9pfzd9z82izi12mdlrw0yf8q-libidn2-2.3.7</span>
<span style="color:#75715e"># │   │   ├───/nix/store/7n0mbqydcipkpbxm24fab066lxk68aqk-libunistring-1.1</span>
<span style="color:#75715e"># │   │   │   └───/nix/store/7n0mbqydcipkpbxm24fab066lxk68aqk-libunistring-1.1 [...]</span>
<span style="color:#75715e"># │   │   └───/nix/store/s32cldbh9pfzd9z82izi12mdlrw0yf8q-libidn2-2.3.7 [...]</span>
<span style="color:#75715e"># │   └───/nix/store/ddwyrxif62r8n6xclvskjyy6szdhvj60-glibc-2.39-5 [...]</span>
<span style="color:#75715e"># └───/nix/store/dbghhbq1x39yxgkv3vkgfwbxrmw9nfzi-hello-2.12.1 [...]</span></code></pre></div>
<h4 id="查询打印-graphml-依赖图">查询打印 GraphML 依赖图</h4>

<p><code>--graphml</code></p>

<p>以 <a href="http://graphml.graphdrawing.org/">GraphML</a> 文件格式打印存储路径的引用图。这可用于可视化依赖图。要获取构建时依赖图，请将其应用于存储派生。要获取运行时依赖关系图，请将其应用于输出路径。</p>

<h4 id="binding-name"><code>--binding name</code></h4>

<p><code>--binding name; -b name</code></p>

<p>Prints the value of the attribute name (i.e., environment variable) of the store derivations paths. It is an error for a derivation to not have the specified attribute.</p>

<p>（没懂）</p>

<h4 id="查询对应-nar-的-hash">查询对应 nar 的 hash</h4>

<p><code>--hash</code></p>

<p>打印存储路径路径内容的 SHA-256 哈希值（即给定路径上 <code>nix-store --dump</code> 输出的哈希值）。由于哈希值存储在 Nix 数据库中，因此这是一个快速操作。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --hash $hellow_drv_path
<span style="color:#75715e"># sha256:01ddskg72jfsxr3lzmb7d3zv6fn489zp4jvwgar7bn3z5bgznava</span>
nix-store --query --hash $hellow_out_path
<span style="color:#75715e"># sha256:0alzbhjxdcsmr1pk7z0bdh46r2xpq3xs3k9y82bi4bx5pklcvw5x</span></code></pre></div>
<h4 id="查询对应-nar-的大小">查询对应 nar 的大小</h4>

<p><code>--size</code></p>

<p>打印存储路径 paths 内容的大小（以字节为单位）——准确地说，是给定路径上 <code>nix-store --dump</code> 的输出大小。请注意，存储路径所需的实际磁盘空间可能更高，尤其是在具有较大簇大小的文件系统上。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --size $hellow_drv_path
<span style="color:#75715e"># 1584</span>
nix-store --query --size $hellow_out_path
<span style="color:#75715e"># 226560</span></code></pre></div>
<h4 id="查询相关的垃圾回收根">查询相关的垃圾回收根</h4>

<p><code>--roots</code></p>

<p>打印直接或间接指向存储路径的垃圾收集器根。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-store --query --roots $hellow_drv_path
<span style="color:#75715e"># 无输出</span>
nix-store --query --roots $hellow_out_path
<span style="color:#75715e"># 无输出</span></code></pre></div>]]></description></item><item><title>Nix 高级话题之 概念</title><link>https://www.rectcircle.cn/posts/nix-advanced-glossary/</link><pubDate>Sun, 09 Jun 2024 18:48:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-advanced-glossary/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://nix.dev/manual/nix/2.22/glossary.html?highlight=database#glossary">Nix Reference Manual (v2.22) - Glossary</a></p>
</blockquote>

<h2 id="derivation-派生">derivation（派生）</h2>

<p>一个构建任务的描述。派生的结构是一个 store object（存储对象）。派生使用 Nix 表达式的 <code>derivation</code> 函数描述。派生会被转换为低级别的 store derivation（存储派生） （隐式的通过 <code>nix-build</code> 命令或显式的通过 <code>nix-instantiate</code> 命令）。</p>

<h2 id="store-derivation-存储派生">store derivation（存储派生）</h2>

<p>一个派生在 store（存储） 中被表达为 <code>.drv</code> 文件。就像其他存储对象一样，它有一个 store path（存储路径）（例如 <code>/nix/store/g946hcz4c8mdvq2g8vxx42z51qb71rvp-git-2.38.1.drv</code>）。它是 derivation（派生） 的实例化形式。另外，可以通过 <a href="https://nix.dev/manual/nix/2.22/command-ref/new-cli/nix3-derivation-show"><code>nix derivation show</code></a> 命令查看 derivation（派生） 的具体内容（例如： <code>nix derivation show /nix/store/g946hcz4c8mdvq2g8vxx42z51qb71rvp-git-2.38.1.drv --extra-experimental-features nix-command</code>）</p>

<h2 id="instantiate-instantiation-实例化">instantiate,instantiation（实例化）</h2>

<p>将评估好的 derivation（派生） 保存为 Nix store（存储） 中的 store derivation（存储派生），更多参见，<a href="https://nix.dev/manual/nix/2.22/command-ref/nix-instantiate"><code>nix-instantiate</code> 命令</a>。</p>

<h2 id="realise-realisation-实现">realise,realisation （实现）</h2>

<p>获取或构建一个 store object（存储对象），给定一个合法的 nix store path（存储路径），将按照如下方式获取一个 store object（存储对象）：</p>

<ul>
<li>获取从 substituter（替代商） 获取预构建的 store object（存储对象）。</li>
<li>按照对应 derivation（派生）中指定的方式运行可执行的<a href="https://nix.dev/manual/nix/2.22/language/derivations#attr-builder">构建器</a>。</li>
<li>委托给<a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file#conf-builders">远程机器</a>并获取输出。</li>
</ul>

<p>更多算法细节详见： <a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/realise"><code>nix-store --realise</code></a> 的描述。</p>

<h2 id="content-addressed-derivation-内容寻址的派生">content-addressed derivation（内容寻址的派生）</h2>

<p><a href="https://nix.dev/manual/nix/2.22/language/advanced-attributes#adv-attr-__contentAddressed">__contentAddressed</a> 属性设置为 true 的derivation（派生）。</p>

<p>默认情况下，derivation（派生）输出的 Nix store（存储） 的 store path（存储路径）是根据输入属性的哈希值计算的（<code>input-addressed</code>）。</p>

<p>修改 __contentAddressed 属性为 true 后，派生输出的 store path（存储路径） 将根据 derivation（派生） 输出内容的哈希值计算。</p>

<p>该特性是实验性的。</p>

<h2 id="fixed-output-derivation-固定输出的派生">fixed-output derivation（固定输出的派生）</h2>

<p>包含 <a href="https://nix.dev/manual/nix/2.22/language/advanced-attributes#adv-attr-outputHash">outputHash</a> 属性的 derivation（派生）。</p>

<p>在 Nix 实现中，通过 <code>fetchurl</code> 函数生成的 derivation（派生），属于该类型。</p>

<p>这种类型derivation（派生）输出的 Nix store（存储） 的 store path（存储路径）仅根据 <code>name</code> 和 <code>outputHash</code> 属性值计算，以避免更新下载地址后，依赖该 derivation（派生）的所有包都发生重新构建。</p>

<p>如下示例，变更后对应的 store path（存储路径）不会发生变化：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">fetchurl {
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://ftp.gnu.org/pub/gnu/hello/hello-2.1.1.tar.gz&#34;</span>;
    sha256 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465&#34;</span>;
}
<span style="color:#75715e"># 变更为 =&gt;</span>
fetchurl {
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ftp://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz&#34;</span>;
    sha256 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465&#34;</span>;
}</code></pre></div>
<h2 id="store-存储">store（存储）</h2>

<p>store object（存储对象）的集合，以及操作该集合的操作。详见： <a href="https://nix.dev/manual/nix/2.22/store/types/">TODO Nix 高级话题之 Store</a>。</p>

<p>store（存储） 类型有多种，详见： <a href="https://nix.dev/manual/nix/2.22/store/types/">TODO Nix 高级话题之 Store - 存储类型</a>。</p>

<h2 id="binary-cache-二进制缓存">binary cache（二进制缓存）</h2>

<p>binary cache 二进制缓存是一种使用不同格式的 Nix store（存储）：其元数据和签名保存在 <code>.narinfo</code> 文件中，而不是保存在 <code>Nix database</code> 中。这种不同的格式简化了通过网络提供 store object（存储对象）的服务，但无法托管构建。二进制缓存的示例包括 S3 存储桶和 <a href="https://cache.nixos.org/">NixOS 二进制缓存</a>。</p>

<h2 id="store-path-存储路径">store path（存储路径）</h2>

<p>文件系统中 store object（存储对象）的位置，即 Nix store 目录的直接子级。例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/a040m110amc4h71lds2jmr8qrkj2jhxd-git-2.38.1</pre></div>
<h2 id="file-system-object-文件系统对象">file system object（文件系统对象）</h2>

<p>Nix 数据模型，是对常规文件系统数据模型简化的满足 Nix 需求的最小子集。</p>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/store/file-system-object">TODO Nix 高级话题之 Store - 文件系统对象</a>。</p>

<h2 id="store-object-存储对象">store object（存储对象）</h2>

<p>组成 store（存储）集合的元素。</p>

<p>一个 store object（存储对象） 包含 file system object（文件系统对象），指向其他 file system object（文件系统对象）的 references（引用）</p>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/store/file-system-object">TODO Nix 高级话题之 Store - 存储对象</a>。</p>

<h2 id="ifd-import-from-derivation-从派生导入">IFD （Import From Derivation，从派生导入）</h2>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/language/import-from-derivation">Import From Derivation</a>。</p>

<h2 id="input-addressed-store-object-输入寻址存储对象">input-addressed store object（输入寻址存储对象）</h2>

<p>通过构建非 content-addressed derivation（内容寻址派生）、非 fixed-output derivation（固定输出派生）而生成的 store object 存储对象。</p>

<h2 id="content-addressed-store-object-内容寻址存储对象">content-addressed store object（内容寻址存储对象）</h2>

<p>通过构建 content-addressed derivation（内容寻址派生）或  fixed-output derivation（固定输出派生）而生成的 store object 存储对象。</p>

<h2 id="substitute-替代">substitute（替代）</h2>

<p>substitute（替代）是存储在 Nix 数据库中的命令调用，它描述如何绕过正常的构建机制（即 derivation 派生）来构建一个 store object（存储对象）。通常，替代通过从某个服务器下载 store object（存储对象）的预构建版本来构建 store object（存储对象）。</p>

<p>以上来自<a href="https://nix.dev/manual/nix/2.22/glossary.html#gloss-substitute">官方文档</a>，实际上，在 sqlite 表结构中，并没有看到相关字段：<a href="https://github.com/NixOS/nix/blob/2.22.1/src/libstore/unix/schema.sql">https://github.com/NixOS/nix/blob/2.22.1/src/libstore/unix/schema.sql</a> 。</p>

<h2 id="substituter-替代器">substituter（替代器）</h2>

<p>Nix 可以从额外的 store （存储）中获取 store object（存储对象） 而不是构建。通常 substituter （替代器）是二进制缓存，但任何 store（存储） 都充当substituter （替代器）。</p>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/package-management/binary-cache-substituter">Serving a Nix store via HTTP</a>。</p>

<p>注意： 任意一种 store type（存储类型） 的 store （存储），都可以充当 substituter（替代器），常规使用 http cache 只是一种普通的 store type（存储类型），并无特殊性。</p>

<h2 id="purity-纯">purity（纯）</h2>

<p>Nix 总是假设同一个derivation（派生）在运行时总是产生相同的输出。虽然这通常无法得到保证（例如，构建者可以依赖外部输入，例如网络或系统时间），但 Nix 模型假设了这一点。</p>

<h2 id="impure-derivation-不纯派生">impure derivation（不纯派生）</h2>

<p><a href="https://nix.dev/manual/nix/2.22/glossary.html#./contributing/experimental-features.md#xp-feature-impure-derivations">一项实验性功能</a>，允许将 derivation（派生）显式标记为不纯，以便始终重新构建它们，并且它们的输出不会被后续调用重用，而是始终的 realise（实现）他们。</p>

<h2 id="nix-database-nix-数据库">Nix database（Nix 数据库）</h2>

<p>用于跟踪 store object（存储对象）之间的引用的 SQlite 数据库。这是本地存储的实现细节。默认路径位于 /nix/var/nix/db 。</p>

<p>表结构详见： <a href="https://github.com/NixOS/nix/blob/2.22.1/src/libstore/unix/schema.sql">github schema.sql</a> 。</p>

<h2 id="nix-expression-nix-表达式">Nix expression（Nix 表达式）</h2>

<p>1.通常，Nix expression 是软件包及其组合的高级描述。使用 Nix 部署软件需要为您的包编写 Nix 表达式。 Nix 表达式指定 derivation（派生），这些 derivation（派生）作为存储 derivation（派生）实例化到 Nix store（存储）中。然后可以 realised（实现）这些 derivation（派生）以产生输出。
2. Nix 语言在语法上的有效使用的代码片段。例如：<code>.nix</code> 文件的内容形成一个表达式。</p>

<h2 id="reference-引用">reference（引用）</h2>

<p>如果到 P 的 store path（存储路径）出现在 O 的内容中，则称 store object（存储对象） O 具有对存储对象 P 的引用。</p>

<p>store object（存储对象）可以引用其他存储对象及其自身。从存储对象到其自身的引用称为自引用。自引用以外的引用不得形成循环。</p>

<h2 id="reachable-可达的">reachable（可达的）</h2>

<p>如果 Q 位于 P 的引用关系的闭包中，则 store path（存储路径） Q 对另一存储路径 P 是 reachable（可达的）。</p>

<h2 id="closure-闭包">closure（闭包）</h2>

<p>store path （存储路径）的闭包是从该存储路径直接或间接 reachable（可达的）的存储路径的集合；也就是说，它是引用关系下路径的闭合。对于一个包来说，其 derivation（派生）的闭包相当于构建时依赖，而其输出路径的闭包相当于其运行时依赖。为了正确部署，有必要部署整个闭包，否则在运行时文件可能会丢失。命令 nix-store &ndash;query &ndash;requirements 打印出存储路径的闭包。</p>

<p>例如，如果 store path （存储路径） P 包含对存储路径 Q 的引用，则 Q 位于 P 的闭包中。此外，如果 Q 引用 R，则 R 也在 P 的闭包中。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">P ---&gt; Q ---&gt; R</pre></div>
<h2 id="output-输出">output（输出）</h2>

<p>由 derivation（派生）产生的 store object（存储对象）。详见 <a href="https://nix.dev/manual/nix/2.22/language/derivations#attr-outputs">derivation 函数的输出参数</a>。</p>

<h2 id="output-path-输出路径">output path （输出路径）</h2>

<p>derivation（派生） 的 output（输出）的 store path（存储路径）。</p>

<h2 id="output-closure-输出闭包">output closure （输出闭包）</h2>

<p>output path （输出路径）的closure（闭包）。它仅包含输出中那些 reachable（可达的） 的内容。</p>

<h2 id="deriver-派生者">deriver （派生者）</h2>

<p>产生 output path （输出路径）的 store derivation（存储派生）。</p>

<p>可以使用 <a href="https://nix.dev/manual/nix/2.22/command-ref/nix-store/query"><code>nix-store --query</code></a> 的 <code>--deriver</code> 选项来查询output path （输出路径）的 deriver （派生者）。</p>

<h2 id="validity-合法性">validity （合法性）</h2>

<p>A store path is valid if all store objects in its closure can be read from the store.</p>

<p>如果可以从 store（存储）中读取 closure（闭包）中的所有 store objects（存储对象），则 store path（存储路径）是有效的。</p>

<p>对于 <a href="https://nix.dev/manual/nix/2.22/store/types/local-store">local store</a> 来说，这意味着：</p>

<ul>
<li>在该 store （存储）中，store path（存储路径）指向的 store object（存储对象）存在。</li>
<li>store path（存储路径）在 Nix database（Nix 数据库）中列为有效。</li>
<li>store path（存储路径）闭包中的所有存储路径都是有效的。</li>
</ul>

<h2 id="user-environment-用户环境">user environment（用户环境）</h2>

<p>An automatically generated store object that consists of a set of symlinks to “active” applications, i.e., other store paths. These are generated automatically by nix-env. See profiles.</p>

<p>自动生成的 store object （存储对象），由一组指向“活动”应用程序的符号链接（即其他存储路径）组成。这些是由 nix-env 自动生成的。详见 <a href="https://nix.dev/manual/nix/2.22/glossary.html#gloss-profile">TODO Nix 高级话题之 profile</a>。</p>

<h2 id="profile-配置集">profile（配置集）</h2>

<p>指向用户当前 user environment（用户环境）的符号链接，例如 <code>/nix/var/nix/profiles/default</code>。</p>

<h2 id="installable-可安装的">installable (可安装的)</h2>

<p>可以在 Nix store （存储）中 realised （实现）的东西。</p>

<p>详见： <a href="https://nix.dev/manual/nix/2.22/command-ref/new-cli/nix#installables">Installables 文档</a>。</p>

<h2 id="nar-nix-存档">NAR （Nix 存档）</h2>

<p>A Nix ARchive.</p>

<p>这是 Nix 存储中路径的序列化。它可以包含常规文件、目录和符号链接。 NAR 是使用 <code>nix-store --dump</code> 和 <code>nix-store --restore</code> 生成和解压的。</p>

<p>NAR 文件格式，详见：<a href="https://nix.dev/manual/nix/2.22/protocols/nix-archive">Nix Archive (NAR) format</a>。</p>

<h2 id="toc_32">∅</h2>

<p>空集符号。在配置文件历史记录的上下文中，这表示某个包不存在于配置文件的特定版本中。</p>

<h2 id="ε">ε</h2>

<p>epsilon 符号。在包的上下文中，这意味着版本是空的。更准确地说，派生没有版本属性。</p>

<h2 id="package-包">package（包）</h2>

<ul>
<li>一个软件包；文件和其他数据的集合。</li>
<li>一个 package attribute set（包属性集）。</li>
</ul>

<h2 id="package-attribute-set-包属性集">package attribute set（包属性集）</h2>

<p>一个包含 <code>type = &quot;derivation&quot;</code> （<code>derivation</code> 是历史原因）以及其他属性的属性集，如：</p>

<ul>
<li>引用包文件的属性，通常以派生输出的形式，</li>
<li>声明有关如何安装或使用包的属性，</li>
<li>其他元数据或任意属性。</li>
</ul>

<h2 id="string-interpolation-字符串插值">string interpolation（字符串插值）</h2>

<p>详见：<a href="https://nix.dev/manual/nix/2.22/language/string-interpolation">官方文档</a>。</p>

<h2 id="base-directory-基础目录">base directory（基础目录）</h2>

<p>base directory（基础目录）是用于解决相对路径的路径。</p>

<ul>
<li>对于 <code>.nix</code> 文件中的表达式，base directory（基础目录） 是包含该文件的目录。这类似于基本 URL 的目录。</li>
<li>对于使用 <code>--expr</code> 在命令行参数中编写的表达式，base directory（基础目录）是当前工作目录。</li>
</ul>

<h2 id="experimental-feature-实验性特性">experimental feature（实验性特性）</h2>

<p>尚未稳定的功能由命名的实验功能标志保护。这些标志通过配置文件的 <a href="https://nix.dev/manual/nix/2.22/command-ref/conf-file#conf-experimental-features"><code>experimental-features</code></a> 设置项启用或禁用。</p>

<p>更多参见：<a href="https://nix.dev/manual/nix/2.22/contributing/experimental-features">贡献指南 -实验功能的目的和生命周期</a>。</p>
]]></description></item><item><title>Nix 详解（四-2） 实现一个 Nix Mirror 服务的方案</title><link>https://www.rectcircle.cn/posts/nix-4-2-nix-mirror-service/</link><pubDate>Tue, 30 Apr 2024 13:17:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-4-2-nix-mirror-service/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>在第一篇文章，安装部分，在 <code>~/.config/nix/nix.conf</code> 配置的 <code>substituters = https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store https://cache.nixos.org/</code> 就是配置两个二进制缓存地址。</p>

<ul>
<li>第一个 <code>https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store</code> 是清华大学提供的 nixpkgs 二进制缓存的 mirror。</li>
<li>第二个 <code>https://cache.nixos.org/</code> 是 nixpkgs 官方提供的二进制缓存，在中国大陆地区访问缓慢甚至难以访问。</li>
</ul>

<p>以上这些配置，仍然无法保证安装速度原因如下：</p>

<ul>
<li>清华 mirror 同步的并不全且不及时，很多缓存在 <code>https://cache.nixos.org/</code> 存在，但是在清华源并不存在。需要 failback 到官方缓存，速度巨慢。</li>
<li>对于非 nixpkgs 的包的二进制缓存，在大陆地区可能仍然无法访问，每加一个二进制缓存，都需要用户更改 <code>substituters</code> 配置。</li>
<li>有些场景我们需要安装旧版的 nix 包，这个时候我们需要使用 github nixpkgs 项目的 archive 链接指定 channel，github 的访问在国内异常缓慢。</li>
</ul>

<p>为了解决如上问题，本文将设计一个 nix mirror 服务，这个服务可以实现：在 nix 使用的全生命周期的网络请求全覆盖，在无法访问任何 nix 官方的站点的情况下，仍能高速使用 nix。支持如下特性：</p>

<ul>
<li>支持对 nix 安装脚本的 mirror，实现快速 nix 的安装。</li>
<li>支持对 nixpkgs 官方 channel 的 mirror，实现 channel 的快速加载。</li>
<li>支持对 nixpkgs github archive 的 mirror，实现安装旧版 nix 包时，实现 channel 的快速加载。</li>
<li>支持对 nix 二进制缓存的 mirror，实现 nix 二进制缓存的快速访问。</li>
</ul>

<h2 id="官方接口分析">官方接口分析</h2>

<h3 id="安装脚本-nix-install">安装脚本 <code>/nix/install</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://nixos.org/nix/install
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># HTTP/1.1 302
</span><span style="color:#960050;background-color:#1e0010"># Location: https://releases.nixos.org/nix/nix-2.22.0/install</span></code></pre></div>
<p>该接口返回一个 302 重定向，重定向到当前时刻最新稳定版的 release 文件服务的 install 脚本。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://releases.nixos.org/nix/nix-2.22.0/install
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span> <span style="color:#ae81ff">200</span> <span style="color:#a6e22e">OK</span>
Content-Type<span style="color:#f92672">:</span> <span style="color:#ae81ff">text/plain</span>

# #!/bin/sh
# # Use this command-line option to fetch the tarballs using nar-serve or Cachix
# if # ...
# else
#     url=https://releases.nixos.org/nix/nix-2.22.0/nix-2.22.0-$system.tar.xz
# fi</code></pre></div>
<p>这个安装脚本在之前的文章分析过，这里仅需关注 <code>url=https://releases.nixos.org/nix/nix-2.22.0/nix-2.22.0-$system.tar.xz</code> 这一行。这个安装脚本在执行过程中，会从该 URL 中下载 nix 的安装包。因此，在实现 nix mirror 服务时，对这个文件的不能直接原样缓存下来，而是需要把这个 url 替换为当前 mirror 服务的 url。</p>

<p>另外，如果修改了安装脚本，则还需要修改 <code>install.sha256</code> 的值为，这个新脚本的 sha256。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://releases.nixos.org/nix/nix-2.22.0/install.sha256
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># </span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span> <span style="color:#ae81ff">200</span> <span style="color:#a6e22e">OK</span>
<span style="color:#960050;background-color:#1e0010">#</span> <span style="color:#ae81ff">Content-Type: text/plain</span>
<span style="color:#960050;background-color:#1e0010"># </span>
# 4fed7db867186c01ce2a2077da4a6950ed16232efbf78d0cd19700cff80559f9</code></pre></div>
<h3 id="release-文件服务">release 文件服务</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://releases.nixos.org/
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># nix-dev/    # 邮件组
</span><span style="color:#960050;background-color:#1e0010"># nix/        # 各个版本 nix 安装包、安装脚本
</span><span style="color:#960050;background-color:#1e0010">#     # ...
</span><span style="color:#960050;background-color:#1e0010">#     nix-2.22.0/                                # 2.22.0 版本
</span><span style="color:#960050;background-color:#1e0010">#         install                                # 安装脚本
</span><span style="color:#960050;background-color:#1e0010">#         install.sha256                         # 安装脚本的 sha256
</span><span style="color:#960050;background-color:#1e0010">#         nix-2.22.0-${arch}-${os}.tar.xz        # 安装包
</span><span style="color:#960050;background-color:#1e0010">#         nix-2.22.0-${arch}-${os}.tar.xz.sha256 # 安装包的 sha256
</span><span style="color:#960050;background-color:#1e0010"># nixops/     # ??
</span><span style="color:#960050;background-color:#1e0010"># nixos/      # 所有发行过的 nixos 版本的 iso 等文件（和 channel 文件服务的区别是，这里每个小版本都有）。
</span><span style="color:#960050;background-color:#1e0010"># nixpkgs/    # prerelease 的 nixpkgs。
</span><span style="color:#960050;background-color:#1e0010"># patchelf/   # 修改可执行文件的 ld.so 的路径的小工具的。</span></code></pre></div>
<h3 id="channel-文件服务">channel 文件服务</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://channels.nixos.org/
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">nix-latest/
</span><span style="color:#960050;background-color:#1e0010">    install                    # 302 到 最新版本的安装脚本
</span><span style="color:#960050;background-color:#1e0010">nixpkgs-unstable/
</span><span style="color:#960050;background-color:#1e0010">    binary-cache-url            # 二进制缓存站点 url，如： https://cache.nixos.org
</span><span style="color:#960050;background-color:#1e0010">    git-revision                # 当前 channel 的 git commit id
</span><span style="color:#960050;background-color:#1e0010">    nixexprs.tar.xz             # 该 channel nix 表达式代码包
</span><span style="color:#960050;background-color:#1e0010">    packages.json.br            # 该 channel 所有包列表的 json 文件，按 Brotli 格式压缩后的文件
</span><span style="color:#960050;background-color:#1e0010">    src-url                     # 执行编译的源 url，如 https://hydra.nixos.org/eval/1805969
</span><span style="color:#960050;background-color:#1e0010">    store-paths.xz              # 使用 xz 算法压缩的纯文本文件，每行为一个路径，如： /nix/store/9kriq85qac7phcxgpdqbqr25vlr61ifw-go-1.22.2
</span><span style="color:#960050;background-color:#1e0010">nixpkgs-${version}-${os}/
</span><span style="color:#960050;background-color:#1e0010">nixos-unstable/
</span><span style="color:#960050;background-color:#1e0010">nixos-unstable-small/
</span><span style="color:#960050;background-color:#1e0010">nixos-${version}/
</span><span style="color:#960050;background-color:#1e0010">nixos-${version}-small/
</span><span style="color:#960050;background-color:#1e0010">nixos-${version}-aarch64/</span></code></pre></div>
<p>关于 packages.json.br 格式如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#ae81ff">2</span>,
    <span style="color:#f92672">&#34;packages&#34;</span>: {
        <span style="color:#f92672">&#34;AMB-plugins&#34;</span>: {
            <span style="color:#f92672">&#34;meta&#34;</span>: {
                <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;A set of ambisonics ladspa plugins&#34;</span>,
                <span style="color:#f92672">&#34;homepage&#34;</span>: <span style="color:#e6db74">&#34;http://kokkinizita.linuxaudio.org/linuxaudio/ladspa/index.html&#34;</span>,
                <span style="color:#f92672">&#34;license&#34;</span>: {
                    <span style="color:#f92672">&#34;fullName&#34;</span>: <span style="color:#e6db74">&#34;GNU General Public License v2.0 or later&#34;</span>
                },
                <span style="color:#f92672">&#34;longDescription&#34;</span>: <span style="color:#e6db74">&#34;Mono and stereo to B-format panning, horizontal rotator, square, hexagon and cube decoders.\n&#34;</span>,
                <span style="color:#f92672">&#34;maintainers&#34;</span>: [
                    {
                        <span style="color:#f92672">&#34;email&#34;</span>: <span style="color:#e6db74">&#34;bart@magnetophon.nl&#34;</span>,
                        <span style="color:#f92672">&#34;github&#34;</span>: <span style="color:#e6db74">&#34;magnetophon&#34;</span>,
                        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Bart Brouns&#34;</span>
                    }
                ],
                <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;AMB-plugins-0.8.1&#34;</span>,
                <span style="color:#f92672">&#34;platforms&#34;</span>: [
                    <span style="color:#e6db74">&#34;x86_64-linux&#34;</span>
                ],
                <span style="color:#f92672">&#34;position&#34;</span>: <span style="color:#e6db74">&#34;pkgs/applications/audio/AMB-plugins/default.nix:24&#34;</span>,
                <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.8.1&#34;</span>
            },
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;AMB-plugins-0.8.1&#34;</span>,
            <span style="color:#f92672">&#34;pname&#34;</span>: <span style="color:#e6db74">&#34;AMB-plugins&#34;</span>,
            <span style="color:#f92672">&#34;system&#34;</span>: <span style="color:#e6db74">&#34;x86_64-linux&#34;</span>,
            <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.8.1&#34;</span>
        }
    }
}</code></pre></div>
<h3 id="github-archive-接口">github archive 接口</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://github.com/NixOS/nixpkgs/archive/ed4fd067bc0925598221aea1d38887f3d0a26576.tar.gz</span></code></pre></div>
<h3 id="二进制缓存服务">二进制缓存服务</h3>

<h4 id="nix-cache-info"><code>/nix-cache-info</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://cache.nixos.org/nix-cache-info
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># </span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span> <span style="color:#ae81ff">200</span> <span style="color:#a6e22e">OK</span>
<span style="color:#960050;background-color:#1e0010">#</span> <span style="color:#ae81ff">Content-Type: text/x-nix-cache-info</span>
<span style="color:#960050;background-color:#1e0010"># </span>
# StoreDir: /nix/store
# WantMassQuery: 1
# Priority: 40</code></pre></div>
<h4 id="pkg-hash-narinfo"><code>/${pkg_hash}.narinfo</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://cache.nixos.org/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.narinfo
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># </span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span> <span style="color:#ae81ff">200</span> <span style="color:#a6e22e">OK</span>
<span style="color:#960050;background-color:#1e0010">#</span> <span style="color:#ae81ff">Content-Type: text/x-nix-narinfo</span>
<span style="color:#960050;background-color:#1e0010"># </span>
# StorePath: /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
# URL: nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz
# Compression: xz
# FileHash: sha256:0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5
# FileSize: 50160
# NarHash: sha256:1bkbsk4wkk92syg4s7wafy5cxrsprlinax35zgp54y9r0f7a44jz
# NarSize: 226504
# References: 76l4v99sk83ylfwkz8wmwrm4s8h73rhd-glibc-2.35-224 v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
# Deriver: 25i5yk3xxr0g54rab62jfmi2hpmcapiw-hello-2.12.1.drv
# Sig: cache.nixos.org-1:wNCGXAt+CyxXwRFKCama8lAYXI+nz0ON4AWKZ7wCL7ccoJ8UTf1FtQzFi5MXZ7DuebGr90POlbotF7NfcS+iCw==</code></pre></div>
<h4 id="nar-file-hash-nar-xz"><code>/nar/${file_hash}.nar.xz</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET https://cache.nixos.org/nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz</span></code></pre></div>
<h2 id="同步机制">同步机制</h2>

<table>
<thead>
<tr>
<th>类型</th>
<th>同步模式</th>
<th>源</th>
</tr>
</thead>

<tbody>
<tr>
<td>安装脚本</td>
<td>定时</td>
<td><code>nixos.org/nix/install</code></td>
</tr>

<tr>
<td>Release</td>
<td>触发</td>
<td><code>releases.nixos.org</code></td>
</tr>

<tr>
<td>二进制缓存</td>
<td>触发</td>
<td><code>cache.nixos.org</code></td>
</tr>

<tr>
<td>Nixpkgs Channel (具体版本)</td>
<td>触发</td>
<td><code>channels.nixos.org/${nixos-or-nixpkgs}-${version}/**</code></td>
</tr>

<tr>
<td>Nixpkgs Channel (unstable)</td>
<td>定时</td>
<td><code>channels.nixos.org/${nixos-or-nixpkgs}-unstable/**</code></td>
</tr>

<tr>
<td>Nixpkg github commit archive</td>
<td>触发式</td>
<td><code>https://github.com/NixOS/nixpkgs/archive/${commit-id}.tar.gz</code></td>
</tr>
</tbody>
</table>

<p>同步模式说明：
* 触发：采用类似 CDN 回源的模式，当缓存中没有时，直接通过代理返回官方源的内容。然后创建一个后台同步任务，异步的将内容同步到缓存中。优势是及时性更好，只要官方有都能获取到，劣势是如果下载的是小众内容，第一次速度相对较慢（配置了专线，可以保证比直连快）。
* 定时：目前只有 unstable 的 channel 采用该模式，如 <a href="https://channels.nixos.org/nixpkgs/nixpkgs-unstable。同步频率">https://channels.nixos.org/nixpkgs/nixpkgs-unstable。同步频率</a> 5 分钟检测一次。不采用触发模式的原因是，同一个 URL 对应的内容会随着时间变化内容会有所变化。</p>

<h2 id="接口和存储设计">接口和存储设计</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">// release/install-real-path
// release/install-real-path.sync-lock</pre></div>
<table>
<thead>
<tr>
<th>类型</th>
<th>接口</th>
<th>源链接</th>
<th>缓存存储路径</th>
</tr>
</thead>

<tbody>
<tr>
<td>安装脚本</td>
<td><code>/install</code></td>
<td><code>https://nixos.org/nix/install</code></td>
<td><code>/release/install-real-path</code>、<code>/release/install-real-path.sync-lock</code>，详见下方说明 1</td>
</tr>

<tr>
<td>Release</td>
<td><code>/release/**</code></td>
<td><code>https://releases.nixos.org</code></td>
<td><code>/release/**</code></td>
</tr>

<tr>
<td>二进制缓存</td>
<td><code>/cache/**</code></td>
<td><code>https://cache.nixos.org</code></td>
<td><code>/cache/**</code></td>
</tr>

<tr>
<td>Nixpkgs Channel (具体版本)</td>
<td><code>/channel/${nixos-or-nixpkgs}-${version}/**</code></td>
<td><code>https://channels.nixos.org/${nixos-or-nixpkgs}-${version}/**</code></td>
<td><code>/channel/${nixos-or-nixpkgs}-${version}/**</code></td>
</tr>

<tr>
<td>Nixpkgs Channel (unstable)</td>
<td><code>/channel/${nixos-or-nixpkgs}-unstable/**</code></td>
<td><code>https://channels.nixos.org/${nixos-or-nixpkgs}-unstable/**</code></td>
<td><code>channel/${nixos-or-nixpkgs}-unstable/sync-lock</code>、<code>channel/${nixos-or-nixpkgs}-unstable/latest</code>、<code>channel/${nixos-or-nixpkgs}-unstable/commit/${git-revision}/**</code> 详见下方说明 2</td>
</tr>

<tr>
<td>Nixpkg github commit archive</td>
<td><code>/channel/nixpkgs/commit/${commit-id}.tar.gz</code></td>
<td><code>https://github.com/NixOS/nixpkgs/archive/${commit-id}.tar.gz</code></td>
<td><code>/nixpkgs/commit/${commit-id}.tar.gz</code></td>
</tr>
</tbody>
</table>

<ol>
<li>安装脚本存储说明。

<ul>
<li><code>/release/install-real-path</code> 存储内容为 <code>https://nixos.org/nix/install</code> 302 返回的 location。</li>
<li><code>/release/install-real-path.sync-lock</code> 存储内容为 <code>&lt;instance-id&gt; &lt;last-active-time&gt;</code>，其中 <code>instance-id</code> 为当前实例的唯一标识，<code>last-active-time</code> 为上次同步时间。目的是并发控制。</li>
</ul></li>
<li>Nixpkgs Channel (unstable) 存储说明。

<ul>
<li><code>channel/${nixos-or-nixpkgs}-unstable/sync-lock</code> 存储内容为 <code>&lt;instance-id&gt; &lt;last-active-time&gt;</code>，其中 <code>instance-id</code> 为当前实例的唯一标识，<code>last-active-time</code> 为上次同步时间。目的是并发控制。</li>
<li><code>channel/${nixos-or-nixpkgs}-unstable/latest</code> 存储内容为上一次同步的 <code>https://channels.nixos.org/${nixos-or-nixpkgs}-unstable/git-revision</code> 的值。本质上是一个索引，用于定位指向 <code>channel/nixos-unstable/commit/${git-revision}/**</code>。</li>
<li><code>channel/${nixos-or-nixpkgs}-unstable/commit/${git-revision}/**</code> 存储内容为上一次同步的 <code>https://channels.nixos.org/${nixos-or-nixpkgs}-unstable/**</code> 的内容。</li>
</ul></li>
<li>存储可以使用对象存储服务来实现。</li>
</ol>
]]></description></item><item><title>All in one 家庭数据中心</title><link>https://www.rectcircle.cn/posts/all-in-one-home-dc/</link><pubDate>Wed, 14 Feb 2024 17:10:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/all-in-one-home-dc/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>本文将介绍笔者 all in one 家庭数据中心搭建的思路、设计以及落地实施的全过程。</p>

<h2 id="设计">设计</h2>

<h3 id="目标和原则">目标和原则</h3>

<ul>
<li>性价比优先，性能次之。</li>
<li>all in one 虚拟化。</li>
<li>7 * 24 小时不停机（可接受停电不可用）。</li>
<li>搭建家庭 NAS，保证数据安全，数据备份，虚拟机快照。</li>
<li>旁路由透明代理。</li>
<li>局域网支持公网访问。</li>
<li>开源优先，Linux 优先，轻量级优先，免费计划（订阅）优先，一次性费用优先。</li>
<li>影音、办公优先、不考虑游戏。</li>
</ul>

<h3 id="分析">分析</h3>

<p><strong>主机和配件</strong></p>

<p>All in one 且需要长时间运行，需要稳定性和省电，因此建议选择迷你主机。这里提一下选购建议：</p>

<ul>
<li>建议选择 2000 以上的准系统版本，CPU AMD 和 Intel 均可，自行购买内存和硬盘。

<ul>
<li>该价位迷你主机内存，一般有两个插槽，总共最大 64G，单条最大 32G。因为多个虚拟机需同时运行，且内存和 CPU 不一样，属于无法压缩资源，这里建议直接装到最大 64G。</li>
<li>硬盘（系统盘），建议选择 PCIe 3.0 以上， 1T 以上的即可。</li>
</ul></li>
<li>因为要长时间运行，且可能位于卧室，因此低噪声是最为重要，在购买前一定确认噪声情况。</li>
</ul>

<p>组建 NAS 需要将多块硬盘接入主机，这里一个最廉价的（延迟和速度基本够用）方案是：</p>

<ul>
<li>二手机械硬盘（价格 100 元/T）。</li>
<li>亚克力支架（搜索亚克力硬盘架，4 盘位包邮 10 元左右）。</li>
<li>sata 转 usb3 连接线 （搜索硬盘连接线、硬盘易驱线 20元/个）。</li>
<li>12v DC 电源（多个或 1 拆多，20 元/4A）。</li>
<li>USB HUb 1 分 4 （30 元）。</li>
</ul>

<p><strong>系统</strong></p>

<p>根据上述目标，这里选型如下：</p>

<ul>
<li>主机上安装 PVE 虚拟化系统（不选择 EXSI 原因是其是闭源的），其他系统均以虚拟机形式运行在 PVE 中。</li>
<li>旁路由选择 OpenWRT。</li>
<li>桌面系统使用 Windows 11 （核显直通）。</li>
<li>开发系统使用无桌面的 Debian 12， 由 Windows 11 通过 VSCode Remote SSH 连接。</li>
<li>NAS 系统选择 OMV。</li>
<li>Deamon 系统选择 LXDE 桌面 + Debian 12。</li>
</ul>

<p><strong>外部服务</strong></p>

<ul>
<li>使用免费计划的 cloudflare zero trust 实现在公网访问家庭局域网，详见博客： <a href="/posts/cloudflare-free-plan/">Cloudflare 免费计划详解</a>。</li>
<li>使用免费计划的 cloudflare 站点托管和海外 VPS （搜索便宜年付 VPS，约 10~20 美元/年） 以及 OpenWRT 旁路由，即可实现旁路由透明代理。</li>
</ul>

<h3 id="硬件设备">硬件设备</h3>

<p>本文基于的主要硬件设备如下：</p>

<ul>
<li>主机：小米迷你主机准系统版 （十分不建议购买，噪音巨大）， 64G 内存，1 T SSD 硬盘。</li>
<li>外接显卡：用于 AI 炼丹，或双桌面。本方案不考虑网络游戏，因为是虚拟机，会被网络游戏反作弊程序识别，游戏建议另行购买专用游戏主机。

<ul>
<li>雷电显卡扩展坞：<a href="https://item.taobao.com/item.htm?skuId=5278612827653">逍遥君DIY 雷电4雷电3显卡扩展坞 显卡坞Thunderbolt</a> 。</li>
<li>独立显卡：英伟达 RTX 3060 。</li>
<li>台式机电源。</li>
</ul></li>
</ul>

<h3 id="网络模型">网络模型</h3>

<p><img src="/image/all-in-one-home-dc-network.svg" alt="image" /></p>

<h3 id="nas-存储">NAS 存储</h3>

<p><img src="/image/all-in-one-home-dc-nas.svg" alt="image" /></p>

<h2 id="pve-安装和配置">PVE 安装和配置</h2>

<h3 id="设置路由器局域网">设置路由器局域网</h3>

<p>不要使用各大路由器厂商经常用到的网段，如：</p>

<ul>
<li><code>192.168.0.1/24</code></li>
<li><code>192.168.1.1/24</code></li>
<li><code>192.168.31.1/24</code> 小米</li>
</ul>

<p>这里选取的网段为 <code>192.168.29.1/24</code>，打开路由器局域网设置，修改局域网网段，然后其他设备重连即可。</p>

<h3 id="制作安装盘">制作安装盘</h3>

<blockquote>
<p>在其他计算机中进行如下操作。</p>
</blockquote>

<ul>
<li>前往 <a href="https://www.proxmox.com/en/downloads">PVE 下载页</a>，下载最新版 ISO。</li>
<li>安装 <a href="https://etcher.balena.io/#download-etcher">balenaEtcher</a>，下载烧录器。</li>
<li>插入 U 盘，打开 balenaEtcher， 选择下载的 ISO 点击 Flash，等待完成。</li>
</ul>

<h3 id="安装-pve">安装 PVE</h3>

<blockquote>
<p>注意：该操作会清空目标硬盘的所有数据！</p>
</blockquote>

<ul>
<li>主机连接电源、键盘、鼠标、显示器、安装盘、网线。</li>
<li>启动主机，选择图形化安装，并填写相关选项：

<ul>
<li>Target Harddisk: 选择要安装的系统盘</li>
<li>Country: China</li>
<li>Time Zone: Asia/Shanghai</li>
<li>Keyboard Layout: U.S. English</li>
<li>Password: 自己设置即可</li>
<li>Management Interface: 选择有线网卡</li>
<li>Hostname (FQDN): pve.rectcircle.cn （可以用个人博客的域名）</li>
<li>IP Address (CIDR): 192.168.29.2</li>
<li>Gateway: 192.168.29.1</li>
<li>DNS Server: 192.168.29.1</li>
</ul></li>
<li>最后安装重启即可。</li>
</ul>

<h3 id="登录-pve">登录 PVE</h3>

<p>浏览器打开，打开 <a href="https://192.168.29.2:8006/，">https://192.168.29.2:8006/，</a> 高级，继续前往192.168.29.2（不安全）。</p>

<ul>
<li>Language: 选择中文</li>
<li>用户名: root</li>
<li>密码： 上一步设置的密码</li>
</ul>

<p>无有效订阅，点击确认即可，不影响使用。</p>

<h3 id="配置-apt-源">配置 apt 源</h3>

<ul>
<li>注释掉 <code>/etc/apt/sources.list.d/</code> 下所有文件中的所有配置。</li>
<li>参考：<a href="https://mirrors.tuna.tsinghua.edu.cn/help/debian/">清华 Debian 软件源</a>、<a href="https://mirrors.tuna.tsinghua.edu.cn/help/proxmox/">清华 Proxmox 软件仓库</a> 配置 apt。</li>
<li>登录 PVE，选择 pve 节点，点击更新，点击刷新，完成后，点击升级，将 pve 更新到最新版。</li>
</ul>

<h2 id="存储管理">存储管理</h2>

<blockquote>
<p>参考：<a href="https://foxi.buduanwang.vip/virtualization/pve/1434.html/">博客 PVE的local和local-lvm</a>。</p>
</blockquote>

<h3 id="默认配置">默认配置</h3>

<p>pve 将主机的磁盘（以 1 T SSD 为例），使用 GTP 划分为 3 个分区，分别是（<code>fdisk -l</code>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Device           Start        End    Sectors   Size Type
/dev/nvme0n1p1      34       2047       2014  1007K BIOS boot
/dev/nvme0n1p2    2048    2099199    2097152     1G EFI System
/dev/nvme0n1p3 2099200 2000409230 1998310031 952.9G Linux LVM</pre></div>
<p>用户可以使用的就是 Linux LVM，默认情况下，分为如下几部分 （<code>lsblk</code>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">NAME               MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
nvme0n1            259:0    0 953.9G  0 disk 
├─nvme0n1p1        259:1    0  1007K  0 part 
├─nvme0n1p2        259:2    0     1G  0 part /boot/efi
└─nvme0n1p3        259:3    0 952.9G  0 part 
  ├─pve-swap       252:0    0     8G  0 lvm  [SWAP]
  ├─pve-root       252:1    0    96G  0 lvm  /
  ├─pve-data_tmeta 252:2    0   8.3G  0 lvm  
  │ └─pve-data     252:4    0 816.2G  0 lvm  
  └─pve-data_tdata 252:3    0 816.2G  0 lvm  
    └─pve-data     252:4    0 816.2G  0 lvm  </pre></div>
<p><code>lvs</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">  LV   VG  Attr       LSize    Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  data pve twi-a-tz-- &lt;816.21g             0.00   0.23                            
  root pve -wi-ao----   96.00g                                                    
  swap pve -wi-ao----    8.00g    </pre></div>
<p>在 PVE 管理页面，数据中心，存储菜单，默认有如下两个：</p>

<ul>
<li>local 类型为目录存储，路径为 <code>/var/lib/vz</code>，用于存储 ISO 镜像、容器模板、VZDump备份。该目录位于根目录挂载点，对应上面的 pve-root 逻辑卷，只分配了 96G。</li>
<li>local-lvm 类型为块存储，用于存储虚拟机磁盘、容器。对应上面的 pve-data 逻辑卷。</li>
</ul>

<p>这种存储划分，虚拟机磁盘数据直接放到了块设备，具有较高的性能。</p>

<p>但是，这种配置有如下问题：</p>

<ul>
<li>块设备对于用户来说是非透明，看不到磁盘内容。</li>
<li>pve-root 空间太小，需要维护 local 和 local-lvm 两个 lvm 逻辑卷的磁盘大小，这些操作需要了解 lvm，并只能通过命令行来操作，对于小白运维成本高。</li>
</ul>

<h3 id="存储与快照">存储与快照</h3>

<p>虚拟机和裸机相比，最重要的一个优势是，可以低成本的实现快照。在有快照的情况下，可以随意的对操作系统做任何事情，即使玩崩了，也可以将操作系统恢复到快照时候的状态。</p>

<p>快照是依赖底层存储实现的，因此，在 PVE 中，并不是所有的虚拟机的都是支持快照的，是否支持快照取决于创建虚拟机时磁盘配置。</p>

<p>在 PVE 创建虚拟机配置磁盘时，有两个核心选项：存储以及格式。这两个参数决定了当前虚拟机是否支持快照。可以总结为如下规则：</p>

<ul>
<li>存储类别为 <code>lvm-thin</code> 等的支持快照。</li>
<li>其他存储类别的，格式为 <code>qcow2</code> 的支持快照。</li>
<li>其他情况不支持快照。</li>
</ul>

<p>各种情况的枚举参见： <a href="https://foxi.buduanwang.vip/linux/2044.html/">Proxmox VE存储入门</a> 、 <a href="https://foxi.buduanwang.vip/virtualization/pve/1083.html/">PVE虚拟机不能打快照</a> 、 <a href="https://pve.proxmox.com/pve-docs/chapter-pvesm.html">官方文档 Proxmox VE Storage</a>。</p>

<p>特别说明的是，如果想要安装 windows 11，那么则必须要添加一个 TPM 2.0 的磁盘，这个磁盘的格式必须为 <code>row</code>。</p>

<h3 id="推荐配置">推荐配置</h3>

<p>为了解决上述问题，这里推荐的做法：</p>

<ul>
<li>local-lvm (pve-data) 只保留 10 G，用来存放 TPM 之类的格式只能是 row 的磁盘。</li>
<li>其他空间全部分配给 pve-root，能用 qcow2 的磁盘就用，且都放到 local (pve-root) 。</li>
</ul>

<p>操作如下：</p>

<ul>
<li>在 PVE 管理页面，数据中心 -&gt; 存储 -&gt; local -&gt; 内容，所有全都选中，保存。</li>

<li><p>打开 PVE shell，执行如下命令吗，删除 pve-data 并将空间合并到 pve-root：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">lvremove /dev/pve/data <span style="color:#75715e"># 先删除</span>
<span style="color:#75715e"># 参考 https://pve.proxmox.com/wiki/Storage:_LVM_Thin</span>
lvcreate -L 10G -n data pve <span style="color:#75715e"># 重新创建 pv</span>
lvconvert --type thin-pool pve/data <span style="color:#75715e"># 转换为 thin-pool 格式</span>
lvextend -rl +100%FREE /dev/pve/root <span style="color:#75715e"># 将剩余空间全部分配给 pve-root</span>
df -h  <span style="color:#75715e"># 查看</span></code></pre></div></li>
</ul>

<p>其他说明：</p>

<ul>
<li>磁盘文件存储选 local，格式为 qcow2 的将存储在 <code>/var/lib/vz/images/</code> 目录中。</li>
<li>只有 TPM2.0 磁盘存储需要选 local-lvm，格式为 row，将存储在逻辑卷中。</li>
<li>该配置相比默认配置，磁盘性能会存在一定的性能损失。</li>
</ul>

<h3 id="备忘-根目录缩容">备忘：根目录缩容</h3>

<p>某些场景可能需要对根目录进行缩容以腾出空间。此时操作如下：</p>

<ul>
<li>插入 pve 安装盘，在 BIOS 中引导到安装盘。</li>

<li><p>按 <code>CTRL + ALT + F2</code> 进入 tty，执行如下命令。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">e2fsck -f /dev/mapper/pve-root
resize2fs /dev/mapper/pve-root 800G     <span style="color:#75715e"># （仅验证过 ext4）文件系统缩容到 800G</span>
lvreduce -L 800G /dev/mapper/pve-root  <span style="color:#75715e"># 逻辑卷分区缩容到 800G</span></code></pre></div></li>

<li><p>重启即可</p></li>
</ul>

<h2 id="windows11-虚拟机">Windows11 虚拟机</h2>

<h3 id="intel-核显直通准备">Intel 核显直通准备</h3>

<blockquote>
<p>参考： <a href="https://www.bilibili.com/read/cv27608215/">bilibili 文章</a>。</p>
</blockquote>

<ul>
<li>修改 <code>/etc/default/grub</code> 的 <code>GRUB_CMDLINE_LINUX_DEFAULT</code> 行内容为 <code>GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet intel_iommu=on iommu=pt&quot;</code>。</li>

<li><p>修改 <code>/etc/modprobe.d/pve-blacklist.conf</code>，新增如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">blacklist i915
blacklist snd_hda_intel
options vfio_iommu_type1 allow_unsafe_interrupts=1</pre></div></li>

<li><p>应用配置并重启：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">update-grub
update-initramfs -u -k all
reboot</code></pre></div></li>

<li><p><a href="https://github.com/cmd2001/build-edk2-gvtd/releases/tag/v0.1.0">下载 rom</a> 到 <code>/usr/share/kvm/</code> 目录下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd /usr/share/kvm/
wget https://github.com/cmd2001/build-edk2-gvtd/releases/download/v0.1.0/AIO.rom</code></pre></div></li>
</ul>

<h3 id="安装和基础配置">安装和基础配置</h3>

<ul>
<li>前往<a href="https://www.microsoft.com/zh-cn/software-download/windows11/">微软官方下载站点</a>，下载 Win11 ISO，选择如下：

<ul>
<li>Windows 11 (multi-edition ISO)</li>
<li>简体中文</li>
</ul></li>
<li>下载 <a href="https://github.com/virtio-win/kvm-guest-drivers-windows">Windows VirtIO 驱动</a>，点击<a href="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.240-1/">下载地址</a>找到最新版下载。</li>
<li>登录 PVE 管理页面 -&gt; local -&gt; ISO 镜像，将上述 ISO 上传到 local 存储。</li>
<li>创建虚拟机，参数如下：

<ul>
<li>常规

<ul>
<li>名称：win-private 以及 win-company</li>
</ul></li>
<li>操作系统

<ul>
<li>使用CD/DVD光盘镜像文件（ISO）：选择 win11 的 iso</li>
<li>客户机操作系统：windows 11</li>
<li>Add additional drive for VirtIO drivers：添加 virtio-win-0.1.240.iso</li>
</ul></li>
<li>系统

<ul>
<li>显卡：默认 （核显直通，则填写 none）</li>
<li>SCSI 控制器：VirtIO SCSI single</li>
<li>机型：i440fx</li>
<li>Qemu 代理：勾选</li>
<li>BIOS：OVMF （UEFI）</li>
<li>添加TPM：勾选，存储选 local-lvm （注意这个不是 local）</li>
<li>添加EFI：勾选，存储选 local</li>
<li>预注册秘钥：勾选</li>
</ul></li>
<li>磁盘

<ul>
<li>存储：local</li>
<li>磁盘大小（GiB）：128</li>
<li>格式：QEMU映像格式（qcow2）</li>
</ul></li>
<li>CPU

<ul>
<li>类别：x86-64-v2-AES （pve 8 的默认值，可以保证<a href="https://foxi.buduanwang.vip/virtualization/599.html/">可迁移性</a>）</li>
<li>核心：4</li>
</ul></li>
<li>内存（MiB）：16384</li>
<li>网络

<ul>
<li>桥接：vmbr0</li>
<li>模型：VirtIO （半虚拟化）。</li>
</ul></li>
</ul></li>

<li><p>（仅 Intel 核显直通需要的操作）</p>

<ul>
<li><p>修改配置 <code>/etc/pve/qemu-server/xxx.conf</code> 配置文件，添加如下内容。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">args: -set device.hostpci0.addr=02.0 -set device.hostpci0.x-igd-gms=0x2 -set device.hostpci0.x-igd-opregion=on
hostpci0: 0000:00:02.0,legacy-igd=1,romfile=AIO.rom
hostpci1: 0000:00:1f.3</pre></div></li>

<li><p>添加鼠标和键盘设备：打开 PVE 该虚拟机硬件配置菜单，点击添加 USB 设备，将鼠标键盘添加到该虚拟机中。</p></li>

<li><p>将显示器 HDMI 接入主机。</p></li>
</ul></li>

<li><p>启动虚拟机，立即多次按键盘的回车键，即可进入 windows 安装页面，关键点如下：</p>

<ul>
<li>Windows 安装程序：加载驱动程序，选择路径存在 win11 的驱动。</li>
<li>在选国家地区页面，按 shift + f10 执行 <code>oobe\BypassNRO.cmd</code>（可跳过联网和登录微软账号）。</li>
<li>在 “让我们为你连接到网络” 点击我没有 Internet 连接，点击继续执行受限设置。</li>
<li>填写相关信息，创建一个本地账号。</li>
<li>成功进入系统后，打开 VirtIO ISO，双击 virtio-win-guest-tools 安装 QEMU Guest Agent 和 网络驱动。</li>
</ul></li>
</ul>

<h3 id="蓝牙直通">蓝牙直通</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># /etc/modprobe.d/pve-blacklist.conf</span>
<span style="color:#75715e"># blacklist btusb</span>
update-initramfs -u -k all
<span style="color:#75715e"># lsusb</span> </code></pre></div>
<h3 id="nvidia-显卡直通">NVIDIA 显卡直通</h3>

<blockquote>
<p>参考：<a href="https://foxi.buduanwang.vip/virtualization/pve/561.html/">博客</a></p>
</blockquote>

<p>屏蔽显卡驱动</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;blacklist nouveau&#34;</span> &gt;&gt; /etc/modprobe.d/pve-blacklist.conf
echo <span style="color:#e6db74">&#34;blacklist nvidia&#34;</span> &gt;&gt; /etc/modprobe.d/pve-blacklist.conf
<span style="color:#75715e"># echo &#34;blacklist nvidiafb&#34; &gt;&gt; /etc/modprobe.d/pve-blacklist.conf</span>
echo <span style="color:#e6db74">&#34;blacklist snd_hda_intel&#34;</span> &gt;&gt; /etc/modprobe.d/pve-blacklist.conf</code></pre></div>
<p>查看设备 id</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">lspci -nnk
<span style="color:#75715e"># 04:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [GeForce RTX 3060 Lite Hash Rate] [10de:2504] (rev a1)</span>
<span style="color:#75715e">#         Subsystem: NVIDIA Corporation GA106 [GeForce RTX 3060 Lite Hash Rate] [10de:2504]</span>
<span style="color:#75715e">#         Kernel driver in use: nouveau</span>
<span style="color:#75715e">#         Kernel modules: nvidiafb, nouveau</span>
<span style="color:#75715e"># 04:00.1 Audio device [0403]: NVIDIA Corporation GA106 High Definition Audio Controller [10de:228e] (rev a1)</span>
<span style="color:#75715e">#         Subsystem: NVIDIA Corporation GA106 High Definition Audio Controller [10de:2504]</span>
<span style="color:#75715e">#         Kernel modules: snd_hda_intel</span>
lspci -nnk
<span style="color:#75715e"># 04:00.0 0300: 10de:2504 (rev a1)</span>
<span style="color:#75715e"># 04:00.1 0403: 10de:228e (rev a1)</span></code></pre></div>
<p>将设备加入 vfio</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;options vfio-pci ids=10de:2504,10de:228e&#34;</span> &gt;&gt; /etc/modprobe.d/vfio.conf</code></pre></div>
<p>添加内核选项</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;options kvm ignore_msrs=1&#34;</span> &gt; /etc/modprobe.d/kvm.conf</code></pre></div>
<p>更新内核镜像</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">update-initramfs -k all -u </code></pre></div>
<p>打开 pve 管理页面，选择 windows 虚拟机，点击硬件，点击添加，选择添加 pcie 设备，选择上面的设备号，并勾选所有功能、PCI-Express，取消勾选主 GPU。</p>

<p>保持显示为默认，打开虚拟机，等待 windows 自动安装显卡驱动。</p>

<p>确认安装完成后，停止虚拟机，并将显示设置为 none。</p>

<h3 id="vm-停止后归还设备">vm 停止后归还设备</h3>

<p>略，参见：</p>

<ul>
<li><a href="https://github.com/HelloZhing/pvevm-hooks">github</a>。</li>
<li><a href="https://foxi.buduanwang.vip/virtualization/pve/1590.html/">博客</a>。</li>
</ul>

<h3 id="设置主机名">设置主机名</h3>

<p>打开设置，系统，系统信息，重新命名这台电脑。</p>

<h3 id="安装配置-barrier">安装配置 barrier</h3>

<blockquote>
<p>详见： <a href="https://github.com/debauchee/barrier">github</a> 。</p>
</blockquote>

<p>barrier 是一个开源的鼠标键盘多设备流转的软件。当安装多个桌面系统时，可以使用单套键盘鼠标操作多个系统。</p>

<ul>
<li>打开 github release 页，下载 exe ，并安装。</li>
<li>打开，引导页，语言选择中文，设置为客户端。</li>
<li>配置服务端 IP，填写 Deamon 虚拟机的 IP。</li>
<li>点击菜单栏，Barrier，更改设置：勾掉 开启SSL，勾选最小化启动，勾选最小化到系统托盘（这里的自动启动实测不生效）。</li>
<li>设置 barrier 在登录账号前启动。

<ul>
<li>打开任务计划程序。</li>
<li>点击创建任务

<ul>
<li>常规

<ul>
<li>名称：barrier</li>
<li>选择：不管用户是否登录都要运行</li>
</ul></li>
<li>触发器，新建，选择启动时</li>
<li>操作：新建，选择启动程序

<ul>
<li>配置程序或脚本为 <code>C:\Program Files\Barrier\barrier</code></li>
</ul></li>
<li>条件：勾掉只有在计算机使用交流电源时才启动此任务</li>
<li>设置：勾掉如果任务运行超过一下时间，停止任务</li>
</ul></li>
</ul></li>
</ul>

<h2 id="dev-虚拟机">Dev 虚拟机</h2>

<ul>
<li>前往 <a href="https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd/">Debian DVD ISO 下载页</a>，下载无需网络依赖的 ISO （大小约 3.7 G）</li>
<li>上传到 PVE local 存储。</li>
<li>创建虚拟机，要点选项如下（其他选项默认）：

<ul>
<li>客户机操作系统：Linux 6.x - 2.6 Kernel</li>
<li>Qemu代理：勾选</li>
<li>CPU：2 核</li>
<li>内存：4G</li>
</ul></li>
<li>操作系统安装：

<ul>
<li>软件选择：仅保留标准系统工具和 SSH Server</li>
<li>安装 GRUB 启动引导器，选择识别到的硬盘</li>
</ul></li>
<li>操作系统配置，<code>su root</code> 执行：

<ul>
<li>参考<a href="https://mirrors.tuna.tsinghua.edu.cn/help/debian/">清华镜像源</a>，配置 apt。</li>
<li>安装常用的软件： <code>apt install vim curl git sudo</code>。</li>
<li>配置免密 sudo：<code>echo 'rectcircle ALL=(ALL:ALL)  NOPASSWD:ALL' &gt; /etc/sudoers.d/rectcircle</code>。</li>
</ul></li>
</ul>

<h2 id="deamon-虚拟机">Deamon 虚拟机</h2>

<h3 id="安装虚拟机">安装虚拟机</h3>

<p>步骤和 <a href="#dev-虚拟机">Dev 虚拟机</a> 基本一致，差别在于：</p>

<ul>
<li>创建虚拟机

<ul>
<li>CPU： 1 核</li>
<li>内存：2G</li>
</ul></li>
<li>操作系统安装：

<ul>
<li>软件选择：Debian 桌面环境、LXDE、标准系统工具、 SSH Server</li>
</ul></li>
<li>打开虚拟机配置选项，开启开机自启动</li>
</ul>

<h3 id="配置开机自动进入桌面">配置开机自动进入桌面</h3>

<p>修改 /etc/lightdm/lightdm.conf 文件，在文件中找到 <code>#autologin-user=</code> （位于 <code>[Seat:*]</code> 段）。</p>

<h3 id="安装配置-barrier-1">安装配置 barrier</h3>

<blockquote>
<p><a href="https://github.com/debauchee/barrier">github</a></p>
</blockquote>

<p>安装</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">apt update
apt install barrier</code></pre></div>
<p>配置 barrier</p>

<ul>
<li>打开终端，输入 barrier 回车启动。</li>
<li>选择中文，作为服务端。</li>
<li>点击设置服务端。</li>
<li>添加一个屏幕，配置屏幕名为对应设备的主机名。</li>
<li>点击菜单栏，Barrier，更改设置：勾掉 开启SSL，勾选自动启动。</li>
</ul>

<p>配置自动启动</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p ~/.config/autostart
ln -s /usr/share/applications/barrier.desktop ~/.config/autostart/barrier.desktop</code></pre></div>
<h2 id="omv-虚拟机">OMV 虚拟机</h2>

<h3 id="安装虚拟机-1">安装虚拟机</h3>

<p>步骤和 <a href="#dev-虚拟机">Dev 虚拟机</a> 基本一致，差别在于：</p>

<ul>
<li>前往 <a href="https://www.openmediavault.org/download.html">OMV 下载页</a>，下载 ISO。</li>
<li>上传到 PVE local 存储。</li>
<li>创建虚拟机

<ul>
<li>CPU： 1 核</li>
<li>内存：2G</li>
</ul></li>
<li>打开虚拟机硬件添加硬盘所在的 USB 设备。</li>
<li>打开虚拟机选项，开启开机自启动。</li>
</ul>

<h3 id="基础配置">基础配置</h3>

<ul>
<li>ssh 登录 root：

<ul>
<li>参考<a href="https://mirrors.tuna.tsinghua.edu.cn/help/openmediavault/">清华镜像源 omv 配置说明</a>，配置 apt。</li>
<li><code>apt update &amp;&amp; apt upgrade -y</code></li>
</ul></li>
<li>浏览器输入 ip 地址，输入默认用户名 (admin)、密码 (openmediavault)登录。</li>
<li>修改登录密码：点击右上角用户图标 -&gt; 更改密码，修改密码。</li>
<li>配置仪表盘：点击右上角用户图标 -&gt; 仪表盘，启用所有。</li>
<li>系统 -&gt; 工作台：

<ul>
<li>自动登出：禁用。</li>
</ul></li>
<li>使用备份用硬盘，创建【备份文件系统】。

<ul>
<li>存储器 -&gt; 文件系统，点击新建，选 ext4，选择备份用的硬盘创建。</li>
</ul></li>
<li>其他硬盘，由 LVM 管理，并创建【主文件系统】（lvm 参见：<a href="/posts/linux-lvm/">文章</a>）。

<ul>
<li>系统 -&gt; 插件，搜索 lvm，安装。</li>
<li>存储器 -&gt; LVM

<ul>
<li>多个物理卷，将物理硬盘添加进去。</li>
<li>多个卷组，新建一个卷组。</li>
<li>逻辑卷，创建一个逻辑卷。</li>
</ul></li>
<li>存储器 -&gt; 文件系统，点击新建，选 ext4，选择逻辑卷。</li>
</ul></li>
<li>将【主文件系统】通过 SMB/NFS 导出。

<ul>
<li>用户 -&gt; 用户，新建，该操作会新建一个 id 为 1000 用户组为 100 (users) 的 Linux 用户。</li>
<li>存储器 -&gt; 共享文件系统，新建：

<ul>
<li>名称：main</li>
<li>文件系统：【主文件系统】</li>
<li>相对路径：<code>/</code></li>
<li>权限：按需选项</li>
</ul></li>
<li>服务 -&gt; SMB/CIFS

<ul>
<li>设置：勾选已启用。</li>
<li>共享，新建：

<ul>
<li>选择 main 共享文件系统。</li>
<li>勾掉隐藏点文件。</li>
</ul></li>
</ul></li>
<li>服务 -&gt; NFS

<ul>
<li>设置：勾选已启动，保存并应用。</li>
<li>共享：

<ul>
<li>Shared folder： 选择 main</li>
<li>客户端： 填写 <code>192.168.29.0/24</code>。</li>
<li>权限： 设为读写。</li>
<li>扩展选项填写： <code>subtree_check,insecure,no_root_squash</code>。</li>
</ul></li>
</ul></li>
</ul></li>
<li>配置备份任务。

<ul>
<li>创建相关共享文件夹，存储器-&gt; 共享文件夹：

<ul>
<li>创建：important-source，选择【主文件系统】，相对路径填写 <code>00-Important/</code></li>
<li>创建：important-backup，选择【备份文件系统】，相对路径填写 <code>/</code></li>
</ul></li>
<li>创建 Rsync 任务，服务 -&gt; Rsync -&gt; 任务，新建：

<ul>
<li>类型：本地。</li>
<li>源共享文件夹：important-source。</li>
<li>目标共享文件夹：important-backup。</li>
<li>执行时间：凌晨 5 点。</li>
<li>试运行：勾选（先测试一次）。</li>
</ul></li>
<li>调试，服务 -&gt; Rsync -&gt; 任务，选择上一步创建的，点击运行。</li>
<li>勾掉试运行，保存。</li>
</ul></li>
</ul>

<h3 id="其他系统使用">其他系统使用</h3>

<ul>
<li>安卓手机：ES 文件浏览器 -&gt; 网络 -&gt; 局域网 -&gt; 扫描。</li>
<li>Mac：访达 -&gt; 前往 -&gt; 连接服务器。</li>

<li><p>Windows （SMB 协议）:</p>

<ul>
<li>打开资源管理器，网络</li>
<li>双击 OMV，输入上一小节步骤新建的用户和密码连接。</li>
<li>右击 main 目录，映射网络驱动器。</li>
<li>勾选使用其他凭据链接。</li>
<li>点击完成，输入上一小节步骤新建的用户和密码连接。</li>
</ul></li>

<li><p>Linux: <a href="https://www.expoli.tech/articles/2022/12/23/use-systemd-mount-any-device">使用 systemd 挂载</a>。</p>

<ul>
<li><p>创建挂载点和配置文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p /home/rectcircle/omv
sudo touch /etc/systemd/system/<span style="color:#66d9ef">$(</span>systemd-escape -p --suffix<span style="color:#f92672">=</span>mount <span style="color:#e6db74">&#34;/home/rectcircle/omv&#34;</span><span style="color:#66d9ef">)</span>
sudo touch /etc/systemd/system/<span style="color:#66d9ef">$(</span>systemd-escape -p --suffix<span style="color:#f92672">=</span>automount <span style="color:#e6db74">&#34;/home/rectcircle/omv&#34;</span><span style="color:#66d9ef">)</span></code></pre></div></li>

<li><p><code>/etc/systemd/system/home-rectcircle-omv.mount</code> 内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[Unit]
Description=OVM NFS mount

[Mount]
What=192.168.29.7:/export/main
Where=/home/rectcircle/omv
Type=nfs


[Install]
WantedBy=multi-user.target</pre></div></li>

<li><p><code>/etc/systemd/system/home-rectcircle-omv.automount</code> 内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[Unit]
Description=OVM NFS automount

[Automount]
Where=/home/rectcircle/omv
TimeoutIdleSec=10

[Install]
WantedBy=multi-user.target</pre></div></li>

<li><p>启用配置：<code>sudo systemctl enable home-rectcircle-omv.automount --now</code></p></li>
</ul></li>
</ul>

<h3 id="安装-omv-extras">安装 omv-extras</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 方式1: 自动安装 (能访问外网场景推荐，推荐配置完成 OpenWRT 再执行)</span>
wget -O - https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/install | bash
<span style="color:#75715e"># https://mirrors.tuna.tsinghua.edu.cn/OpenMediaVault/openmediavault-plugin-developers/pool/main/o/openmediavault-omvextrasorg/</span>

<span style="color:#75715e"># 方式2: 大陆地区</span>
<span style="color:#75715e"># 如下针对：omv 6</span>
apt --yes --no-install-recommends install dirmngr gnupg
wget https://mirrors.tuna.tsinghua.edu.cn/OpenMediaVault/openmediavault-plugin-developers/pool/main/o/openmediavault-omvextrasorg/openmediavault-omvextrasorg_6.3.6_all.deb
dpkg -i openmediavault-omvextrasorg_6.3.6_all.deb
apt-get update
rm -rf openmediavault-omvextrasorg_6.3.6_all.deb
<span style="color:#75715e"># 设置清华源</span>
omv-env set OMV_APT_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/OpenMediaVault/public&#34;</span>
omv-env set OMV_APT_ALT_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/OpenMediaVault/packages&#34;</span>
omv-env set OMV_APT_KERNEL_BACKPORTS_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/debian&#34;</span>
omv-env set OMV_APT_SECURITY_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/debian-security&#34;</span>
omv-env set OMV_EXTRAS_APT_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/OpenMediaVault/openmediavault-plugin-developers&#34;</span>
omv-env set OMV_DOCKER_APT_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/debian&#34;</span>
omv-env set OMV_PROXMOX_APT_REPOSITORY_URL <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian&#34;</span>
<span style="color:#75715e"># 使得环境变量更改生效</span>
omv-salt stage run all</code></pre></div>
<ul>
<li>浏览器 omv 管理页面强制刷新 <code>ctrl + shift + r</code>。</li>
<li>注意 ：omv-extra 6.3 已经将 docker, portainer 和 yacht 移除了，只能使用 openmediavault-compose，参见：<a href="https://forum.openmediavault.org/index.php?thread/47983-omv-extras-6-3-openmediavault-compose-6-7/">官方论坛</a>。</li>
</ul>

<h3 id="安装其他常用插件">安装其他常用插件</h3>

<ul>
<li><del><code>openmediavault-compose</code> 管理 docker。安装完成后，可在：服务 -&gt; Compose 菜单中使用。</del> （不建议安装，可能会造成网络问题）</li>
<li><code>openmediavault-downloader</code> 下载器。安装完成后，可在：服务 -&gt; Downloader 菜单中使用。</li>
<li><code>openmediavault-ftp</code> ftp 服务。</li>
</ul>

<h2 id="openwrt-虚拟机">OpenWRT 虚拟机</h2>

<h3 id="安装虚拟机-2">安装虚拟机</h3>

<blockquote>
<p><a href="https://openwrt.org/docs/guide-user/installation/openwrt_x86">官方文档</a></p>
</blockquote>

<ul>
<li>从 <a href="https://mirrors.tuna.tsinghua.edu.cn/openwrt/releases/23.05.2/targets/x86/64/">清华源</a> 下载最新版镜像  <a href="https://mirrors.tuna.tsinghua.edu.cn/openwrt/releases/23.05.2/targets/x86/64/openwrt-23.05.2-x86-64-generic-ext4-combined-efi.img.gz"><code>openwrt-23.05.2-x86-64-generic-ext4-combined-efi.img.gz</code></a>。</li>
<li>解压为 img 文件。</li>
<li>打开 pve local 存储，上传 img 文件。</li>

<li><p>后续步骤和 <a href="#dev-虚拟机">Dev 虚拟机</a> 基本一致，差别在于：</p>

<ul>
<li>创建虚拟机

<ul>
<li>名称：openwrt</li>
<li>CPU： 1 核</li>
<li>操作系统：不适用任何介质</li>
<li>内存：2G</li>
<li>删除默认磁盘</li>
</ul></li>

<li><p>打开 pve shell，执行如下命令，导入磁盘</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">qm disk import <span style="color:#ae81ff">105</span> /var/lib/vz/template/iso/openwrt-23.05.2-x86-64-generic-ext4-combined-efi.img local --format qcow2</code></pre></div></li>

<li><p>打开虚拟机配置：</p>

<ul>
<li>硬件，双击选中未使用的磁盘 0，点击添加。</li>
<li>选项，引导顺序，将硬盘添加到第一个。</li>
</ul></li>

<li><p>点击启动，即可进入系统。</p></li>
</ul></li>
</ul>

<h3 id="基础配置-1">基础配置</h3>

<ul>
<li>打开控制台，配置 lan 口：需修改 <code>/etc/config/network</code> 的 lan 口的 ipaddr 为分配的地址，如 <code>192.168.29.254</code>，执行 <code>service network restart</code> 重启网络。</li>
<li>打开 openwrt 控制台 <a href="http://192.168.29.254">http://192.168.29.254</a> ，进行如下基础配置：

<ul>
<li>通过引导页，配置 root 密码。</li>
<li>System -&gt; Administration -&gt; SSH Access 开启 ssh 登录。</li>
</ul></li>
<li>ssh 登录 openwrt，进行如下基础配置：

<ul>
<li>参考 <a href="https://mirrors.tuna.tsinghua.edu.cn/help/openwrt/">清华源</a>，配置。</li>
<li>配置 hostname，<code>vim /etc/config/system</code>，hostname 为 <code>openwrt</code>。</li>
</ul></li>
<li>打开 openwrt 控制台 <a href="http://192.168.29.254">http://192.168.29.254</a> 。

<ul>
<li>配置 lan 口：Network -&gt; Interfaces，选择 br-lan，点击 Edit，编辑：常规设置 -&gt; 网关、高级设置 -&gt; DNS 为硬路由的 192.168.29.1 。</li>
<li>System -&gt; Sofeware，点击 Update list，

<ul>
<li>搜索 <code>luci-i18n-base-zh-cn</code>，安装中文包。</li>
<li>搜索 <code>qemu-ga</code>，安装 qemu-agent。</li>
</ul></li>
</ul></li>
<li>配置旁路由（详见：<a href="https://easonyang.com/posts/transparent-proxy-in-router-gateway/">博客</a>）。

<ul>
<li>打开 openwrt 控制台 <a href="http://192.168.29.254">http://192.168.29.254</a> 。

<ul>
<li>Network -&gt; Interfaces，选择 br-lan，点击 Edit。

<ul>
<li>DHCP -&gt; 高级设置 -&gt; DHCP 选项，添加：

<ul>
<li>网关 <code>3,192.168.29.254</code>。</li>
<li>DNS <code>6,192.168.29.254</code>。</li>
</ul></li>
<li>DHCP -&gt; 高级设置，勾选强制。</li>
<li>DHCP -&gt; IPv6 设置， RA 服务、 DHCPv6 服务、NDP 服务均关闭。</li>
</ul></li>
<li>网络 -&gt; 防火墙，关闭 SYN-flood 防御，区域 lan =&gt; wan 勾选 IP 动态伪装（实测不勾选，网络不稳定）。</li>
</ul></li>
<li>打开硬路由控制台 <a href="http://192.168.29.1">http://192.168.29.1</a> ，关闭 DHCP 服务。</li>
<li>将主机和虚拟机设置为 HDCP 静态地址。</li>
</ul></li>

<li><p>磁盘扩容</p>

<ul>
<li>打开 pve 控制台，openwrt 虚拟机硬件，选择硬盘，磁盘操作，调整磁盘大小，增量大小，调整到 4 G。</li>

<li><p>ssh 连接到 openwrt，执行如下命令（参考：<a href="https://openwrt.org/docs/guide-user/advanced/expand_root">官方文档</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">opkg update
opkg install parted losetup resize2fs
wget -U <span style="color:#e6db74">&#34;&#34;</span> -O expand-root.sh <span style="color:#e6db74">&#34;https://openwrt.org/_export/code/docs/guide-user/advanced/expand_root?codeblock=0&#34;</span>
. ./expand-root.sh
sh /etc/uci-defaults/70-rootpt-resize</code></pre></div></li>
</ul></li>
</ul>

<h3 id="旁路由透明代理">旁路由透明代理</h3>

<p>受限于中国大陆法律法规，不做介绍，如有需要自行搜索。</p>

<h3 id="异地组网">异地组网</h3>

<ul>
<li>（推荐） 使用 cloudflare zero trust，详见博客： <a href="/posts/cloudflare-free-plan/">Cloudflare 免费计划详解</a>。</li>
<li>手动配置 Wireguard：

<ul>
<li>安装 <code>luci-proto-wireguard</code> 软件包，重启虚拟机。</li>
<li>更多参见：<a href="/posts/linux-net-virual-06-wireguard/">博客</a>。</li>
</ul></li>

<li><p>使用 <a href="https://my.zerotier.com/">zerotier</a>，注意：大陆地区速度极慢基本不可用。参考：</p>

<ul>
<li><a href="https://kevron2u.com/set-up-a-zerotier-network-on-openwrt/">博客</a>。</li>

<li><p><a href="https://openwrt.org/docs/guide-user/services/vpn/zerotier">官方 wiki</a>，注意该文命令，有错误，正确写法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cat /etc/config/zerotier
<span style="color:#75715e"># config zerotier sample_config</span>
<span style="color:#75715e">#         option enabled 0</span>

<span style="color:#75715e">#         # persistent configuration folder (for ZT controller mode)</span>
<span style="color:#75715e">#         #option config_path &#39;/etc/zerotier&#39;</span>
<span style="color:#75715e">#         # copy &lt;config_path&gt; to RAM to prevent writing to flash (for ZT controller mode)</span>
<span style="color:#75715e">#         #option copy_config_path &#39;1&#39;</span>

<span style="color:#75715e">#         #option port &#39;9993&#39;</span>

<span style="color:#75715e">#         # path to the local.conf</span>
<span style="color:#75715e">#         #option local_conf &#39;/etc/zerotier.conf&#39;</span>

<span style="color:#75715e">#         # Generate secret on first start</span>
<span style="color:#75715e">#         option secret &#39;&#39;</span>

<span style="color:#75715e">#         # Join a public network called Earth</span>
<span style="color:#75715e">#         list join &#39;8056c2e21c000001&#39;</span>
<span style="color:#75715e">#         #list join &#39;&lt;other_network&gt;&#39;</span>

uci delete zerotier.sample_config
uci set zerotier.rectcircle<span style="color:#f92672">=</span>zerotier
uci add_list zerotier.rectcircle.join<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;xxx&#39;</span>
uci set zerotier.rectcircle.enabled<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;1&#39;</span>
uci commit zerotier
service zerotier restart</code></pre></div></li>
</ul></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://foxi.buduanwang.vip/category/virtualization/pve/">佛西博客</a></li>
</ul>

<!-- 

核心绑定 `lscpu -e`。

* 当然建议大家使用virtio-scsi-single的磁盘控制器，以获得最佳性能。 https://foxi.buduanwang.vip/virtualization/pve/1226.html/ https://foxi.buduanwang.vip/virtualization/pve/1214.html/
* ID https://foxi.buduanwang.vip/virtualization/pve/bestpractice/1643.html/
* 存储 https://foxi.buduanwang.vip/linux/2044.html/ https://pve.proxmox.com/pve-docs/chapter-pvesm.html
* 镜像 https://foxi.buduanwang.vip/virtualization/pve/1574.html/
* 系统监控 https://foxi.buduanwang.vip/virtualization/pve/615.html/ -->
]]></description></item><item><title>Cloudflare 免费计划详解</title><link>https://www.rectcircle.cn/posts/cloudflare-free-plan/</link><pubDate>Tue, 13 Feb 2024 16:06:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/cloudflare-free-plan/</guid><description type="html"><![CDATA[

<h2 id="介绍">介绍</h2>

<p>Cloudflare (<a href="https://www.cloudflare.com">https://www.cloudflare.com</a>) 是全球最大 CDN 服务提供商。是当今世界上，最重要的互联网基础设施之一。</p>

<p>虽然 Cloudflare 是一家美国公司，但是在中国大陆也可以使用。</p>

<p>Cloudflare 很多产品都提供免费计划，免费计划对于学生、开发者、中小团队来说是足够的。</p>

<p>本文将主要介绍：</p>

<ul>
<li>包含在 Cloudflare 免费计划中的产品以及可用的功能。</li>
<li>学生、开发者、中小团队如何利用这些产品特性节约成本、提升效率、解决实际问题。</li>
</ul>

<h2 id="注册">注册</h2>

<p>打开 <a href="https://dash.cloudflare.com/sign-up">cloudflare 注册页</a>，输入一个电子邮件地址即可完成注册。</p>

<h2 id="域名托管">域名托管</h2>

<h3 id="添加站点">添加站点</h3>

<ul>
<li>进入 <a href="https://dash.cloudflare.com">cloudflare 控制台</a>。</li>
<li>点击右上角，将页面语言切换到简体中文。</li>
<li>点击 添加站点 按钮，输入你已经购买的或者正要购买的域名。</li>
<li>选择最下方的 Free 免费计划，点击继续。</li>
</ul>

<p>至此，在 cloudflare 上的操作已经完成，剩下的步骤，需要去你的域名购买平台，修改 DNS 服务器为 cloudflare 控制台 -&gt; 刚加入的站点 -&gt; 概述页面，更新名称服务器：您的已分配的 Cloudflare 名称服务器下面的两个域名，示例下：</p>

<ul>
<li><code>jo.ns.cloudflare.com</code></li>
<li><code>kirk.ns.cloudflare.com</code></li>
</ul>

<p>更新完成后，等待 10 分钟左右，然后前往 cloudflare 控制台 -&gt; 刚加入的站点 -&gt; 概述页面，点击立即检查名称服务器，通过后，概述页面将会展示一些统计指标。</p>

<p>如果还未购买域名：</p>

<ul>
<li>可以前往任意国内的公有云平台购买一个域名即可（在阿里云，可以购买一个 68 元 10 年的域名，支持的域名为主域为 6~8 位数字的后缀为 xyz 的域名，即 <code>\d{6,8}.xyz</code>）。</li>
<li>如果，担心隐私问题，可以前往海外域名注册商购买。</li>
</ul>

<h3 id="dns-配置">DNS 配置</h3>

<p>前往 cloudflare 控制台 -&gt; 刚加入的站点 -&gt; DNS -&gt; 记录，点击添加记录，就可以添加一条 DNS 记录。DNS 记录有多种类型，定义该子域名对应内容的类型，常用的有：</p>

<ul>
<li>A 该子域名对应的是一个 IPv4 地址。</li>
<li>AAAA 该子域名对应的是一个 IPv6 地址。</li>
<li>CNAME 该子域名对应的内容委托给另一个子域名。</li>
<li>TXT 该域名对应一串纯文本，可以用来做一些配置。</li>
</ul>

<p>和其他云厂商 DNS 解析配置相比， A、AAAA、CNAME 记录类型，Cloudflare 有一个代理的选项（橙色云朵）：</p>

<ul>
<li>如果不打开，则 cloudflare 就是常规的 DNS 解析服务提供商，此时客户端执行 DNS 查询时，获取到的就是配置的 IP 地址。</li>

<li><p>如果打开，cloudflare 就会变成一个 7 层（HTTP/HTTPS） 反向代理器（可以理解为一个 Nginx），此时客户端执行 DNS 查询时，获取到的将是 cloudflare 的边缘节点，此时流量路径如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">User ---&gt; cloudflare global network ---&gt; 源站 （DNS 记录指向地址/Tunnel）</pre></div>
<p>这里特别说明：</p>

<ul>
<li>源站必须是一个 HTTP/HTTPS 服务器，否则无法进行代理。</li>
<li>源站如果是 HTTP 协议，则必须监听在 80 端口，如果是 HTTPS，则必须监听在 443 端口。</li>
<li>对源站的配置参见下文： <a href="#ssltls-配置">SSL/TLS 配置</a>。</li>
<li>源站除了可以是一个具体的 IP 地址外，还可以是一个 Zero Trust 的 Tunnel，参见下文： <a href="#tunnel">Tunnel</a>。</li>
<li>支持 websocket，可在：控制台 -&gt; 刚加入的站点 -&gt; 网络，进行配置。</li>
<li>cloudflare 自动会为域名颁发，受信任的 CA 证书，参见： 控制台 -&gt; 刚加入的站点 -&gt; SSL/TLS -&gt; 边缘证书。</li>
</ul></li>
</ul>

<h3 id="ssl-tls-配置">SSL/TLS 配置</h3>

<blockquote>
<p><a href="https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/">官方文档</a></p>
</blockquote>

<p>在 DNS 配置中，开启了代理后。</p>

<p>需要在  控制台 -&gt; 刚加入的站点 -&gt; SSL/TLS -&gt; 概述， 配置用户到 cloudflare，cloudflare 到源站的流量类型：</p>

<ul>
<li>关闭 (Off (no encryption))：全链路使用 HTTP（不建议）。</li>
<li>灵活 (Flexible)：用户到 cloudflare 可以采用 HTTPS，cloudflare 到源站采用 HTTP。</li>
<li>完全 (Full)：用户到 cloudflare 可以采用 HTTPS，cloudflare 到源站采用 HTTPS，但 cloudflare <strong>不校验</strong> 源站 SSL/TLS 证书是否合法。</li>
<li>完全（严格） (Full (strict))：用户到 cloudflare 可以采用 HTTPS，cloudflare 到源站采用 HTTPS，cloudflare 会校验 SSL/TLS 证书的合法性。这要求，源站的 SSL/TLS 证书必须是，受信任的 CA 证书或 Cloudflare Origin CA 证书。</li>
</ul>

<p>需要注意的时，如上配置的主要是 cloudflare 到源站的流量情况，而用户到 cloudflare 是否强制使用 https，需要到：控制台 -&gt; 刚加入的站点 -&gt; SSL/TLS -&gt; 边缘证书， 始终使用 HTTPS 进行配置（推荐打开）。</p>

<p>为了方便，可以直接使用灵活 (Flexible) 模式。</p>

<p>如果为了安全，建议使用：完全（严格） (Full (strict)) 模式，使用该模式，需要给服务器配置证书，有几种方式：</p>

<ul>
<li><p>（推荐） 签发一个 Cloudflare Origin CA 证书（优势是有效期很长）。前往： 控制台 -&gt; 刚加入的站点 -&gt; SSL/TLS -&gt; 源服务器，创建一个证书，然后将该证书，下载、上传并配置到源站服务器上，以 Nginx 为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">server</span> {
    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">xxx.example.com</span>;
    <span style="color:#f92672">ssl_protocols</span> <span style="color:#e6db74">TLSv1</span> <span style="color:#e6db74">TLSv1.1</span> <span style="color:#e6db74">TLSv1.2</span> <span style="color:#e6db74">TLSv1.3</span>;
    <span style="color:#f92672">ssl_ciphers</span> <span style="color:#e6db74">ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE</span>;
    <span style="color:#f92672">ssl_prefer_server_ciphers</span> <span style="color:#66d9ef">on</span>;
    <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/nginx/certificate/example.com.pem</span>;
    <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/nginx/certificate/example.com.key</span>;
    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
        <span style="color:#75715e"># ...
</span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
    }
}</code></pre></div></li>

<li><p>使用 acme.sh 签发受信任的 CA 证书。</p></li>
</ul>

<h2 id="页面托管-faas-ai-和-baas">页面托管、FaaS、AI 和 BaaS</h2>

<p>略，和 <a href="https://www.netlify.com/pricing">netlify</a> 类似。重点关注其免费计划的限制：</p>

<ul>
<li><a href="https://developers.cloudflare.com/pages/platform/limits">pages limits</a>。</li>
<li><a href="https://developers.cloudflare.com/workers/platform/limits">worker limits</a></li>
</ul>

<h2 id="零信任网络-zero-trust">零信任网络 （Zero Trust）</h2>

<h3 id="启用">启用</h3>

<p>Zero Trust 需要单独开启，开启步骤如下：</p>

<ul>
<li>控制台 -&gt; Zero Trust。</li>
<li>输入 组织名，点击 Next。</li>
<li>选择 Free 计划，进入付款页面，选择 paypal（没有的可以注册一个）。</li>
<li>再输入一次组织名，点击 Finish setup 即可。</li>
</ul>

<p>后续，只需前往：控制台 -&gt; <a href="https://one.dash.cloudflare.com/">Zero Trust</a> 即可进入 Zero Trust 后台。</p>

<h3 id="概念和配置">概念和配置</h3>

<p>Cloudflare Zero Trust 本质提供的是面向组织的内网搭建服务，要想理解 Cloudflare Zero Trust 能做的事情，需要了解如下概念：</p>

<ul>
<li>组织 (organization) /团队 (team)： 一个管理概念，对应一个虚拟的网络，一个组织包含多个用户，每个用户可以拥有多个设备，Zero Trust 售卖的服务和该概念绑定，免费计划最多允许注册 50 个用户。</li>

<li><p>用户 (User)： 可以连入组织网络的人，用户的设备可通过 <code>WARP</code> 客户端连入网络，在管理后台的 Settings -&gt; WARP Client -&gt; Device enrollment -&gt; Device enrollment permissions -&gt; Manage -&gt; Add a rule 可以配置那些用户可以登录。</p>

<p>配置某个用户可以在 WARP 客户端通过邮箱（验证码）登录 Zero Trust：</p>

<ul>
<li>Rule name： 填写 my。</li>
<li>Rule action： 选择 Allow。</li>
<li>Include：

<ul>
<li>Selector： Emails。</li>
<li>Value： 填写邮箱地址。</li>
</ul></li>
</ul>

<p>用户在 WARP 客户端登录过后，可以在 My Team -&gt; Users 管理用户。</p></li>

<li><p>Device：某个用户通过 WARP 客户端登录后将会存在一个设备，在 My Team -&gt; Devices 可以查看和管理。用户设备自身是无法再 WARP 客户端配置的，而是由组织管理员在管理后台的 Settings -&gt; WARP Client -&gt; Device settings 中进行配置，系统预置了一个名为 Default 的默认配置。这里点击 Default 右侧三个点菜单，点击 Duplicate 创建一个新的配置，并进行编辑：</p>

<ul>
<li>Name： 配置名，填写 my。</li>
<li>Build an expression：设置该配置关联的用户，和创建用户的规则类似。

<ul>
<li>Selector： 填写 User email</li>
<li>Operator： 填写 is</li>
<li>Value：填写邮箱地址。</li>
</ul></li>
<li>Split Tunnels： 这个配置是非常重要的，定义的是，设备的流量的路由规则：

<ul>
<li>Include IPs and domains： <strong>在</strong>该名单里的的 IP 和域名会通过 Cloudflare Zero Trust 中转，其他流量直连。</li>
<li>Exclude IPs and domains： <strong>不在</strong>该名单里的的 IP 和域名会通过 Cloudflare Zero Trust 中转，其他流量直连。选择这个，并修改规则：

<ul>
<li>删除 <code>100.64.0.0/10</code>。这个网段是 WARP 的分配的虚拟 IP。删除后，各个设备之间就可以通过该虚拟 IP 通讯了（还有一个配置参见下文 WARP to WARP）。</li>
<li><del>添加各个常用的家用路由器默认网段：如 <code>192.168.0.0/24</code>，<code>192.168.1.0/24</code>，<code>192.168.31.0/24</code>。</del> （这一点应该不需要）</li>
</ul></li>
</ul></li>
</ul></li>

<li><p>Network： 通过管理后台的 Settings -&gt; Network 可以配置组织的网络情况，这里修改 Firewall 如下：</p>

<ul>
<li>Proxy 开启并勾选 TCP、UDP、ICMP 全部。</li>
<li>开启 WARP to WARP。</li>
</ul></li>

<li><p>Tunnel： 通过一个可以连接公网的设备和 Cloudflare Zero Trust 建立一个安全 Tunnel （隧道）。</p>

<p>可以实现：</p>

<ul>
<li>public hostname： 将某个端口分配一个上文<a href="#dns-配置">域名托管 DNS 子域名</a>，实现端口暴露。</li>
<li>private network： 将整个网络加入到 Cloudflare Zero Trust 网络中，可以实现虚拟局域网（异地组网），通过 WARP 加入 Cloudflare Zero Trust 网络中的设备都可以直接通过内网 IP 访问这个局域网的任意 IP。</li>
</ul>

<p>更多参见下文： <a href="#tunnel">Tunnel</a>。</p></li>

<li><p>接入</p>

<ul>
<li>WARP： 用户接入 Cloudflare Zero Trust 网络的客户端，基本使用，参见下文： WARP 章节。</li>
<li>cloudflared： 用于建立 Tunnel 的客户端，更多参见下文： <a href="#tunnel">Tunnel</a>。</li>
</ul></li>
</ul>

<h3 id="warp">WARP</h3>

<p>以安卓端为例，Google 搜索 <code>WARP apk 下载</code> 进行下载安装，安装完成后，打开 <code>1.1.1.1</code> App，登录方式如下：</p>

<ul>
<li>点击右上角菜单 -&gt; 账户 -&gt; 登录到 Cloudflare Zero Trust -&gt; 下一步 -&gt; 接受。</li>
<li>输入组织名，调转到浏览器。</li>
<li>输入《概念和配置》小节配置的邮箱，点击发送验证码。</li>
<li>登录邮箱，获取验证码，并回到手机填入验证码，点击 Sign in。</li>
</ul>

<p>WARP 客户端，下载地址 <a href="https://1.1.1.1/">https://1.1.1.1/</a></p>

<p>登录完成后，进入 WARP 客户端后点击连接即可加入 Cloudflare Zero Trust 网络。</p>

<p>WARP 本质上就是一个 VPN 客户端。</p>

<h3 id="tunnel">Tunnel</h3>

<p>Tunnel 是在一台可连接公网的设备和 Cloudflare Zero Trust 网络之间建立的一条虚拟网络链路。通过这条隧道可以实现虚拟组网和端口暴露。</p>

<p>假设想将家庭局域网暴露加入 Cloudflare Zero Trust 网络，并实现远程访问，则需要在管理后台的 Networks -&gt; Tunnels：</p>

<ul>
<li>点击 <code>Create a tunnel</code>。</li>
<li>选择 Cloudflared，填写名字。</li>
<li>根据操作系统和架构，执行相关命令安装 Cloudflared 并创建一个 Tunnel，openwrt 详见 <a href="https://openwrt.org/docs/guide-user/services/vpn/cloudfare_tunnel">openwrt wiki</a>：

<ul>
<li>修改 <code>/etc/config/cloudflared</code>：

<ul>
<li><code>enabled</code> 设为 true。</li>
<li><code>token</code> 从页面复制填入。</li>
</ul></li>
<li>执行：

<ul>
<li><code>/etc/init.d/cloudflared enable</code>。</li>
<li><code>/etc/init.d/cloudflared start</code>。</li>
</ul></li>
</ul></li>
</ul>

<p>回到管理后台的 Networks -&gt; Tunnels： 等待 Tunnel 的 Status 变为 HEALTHY。</p>

<ul>
<li>点击菜单（右侧三个点） 的 Configure。</li>
<li>点击 Private Network 私有网络，Add a provider network。

<ul>
<li>CIDR： 私有网段。</li>
<li>Description： my。</li>
</ul></li>
</ul>

<p>配置完成后，回到任意通过 WARP 连接到 Cloudflare Zero Trust 网络的设备上即可通过如上私有网络通讯。</p>

<h3 id="其他说明">其他说明</h3>

<ul>
<li>WARP 在中国大陆地区没有节点，所有节点均在海外，这带来了一些好处和坏处：

<ul>
<li>好处就像大家常用的，所有流量都经过一个海外 VPS 节点一样，在此就不多言了。</li>
<li>坏处是：

<ul>
<li>延迟高，访问大陆站点慢，不稳定。</li>
<li>中国移动宽带/数据在晚高峰时期， WARP 拥堵严重（20240212）。</li>
</ul></li>
</ul></li>
<li>通过 IP 查询， 开启 WARP 后，显示的地理位置仍在中国大陆，这让一些基于 IP 地理位置的应用可以正常工作。（实际上出口 IP Geo 是 Cloudflare 伪造的）。</li>

<li><p>网络拓扑如下图所示（来源：<a href="https://developers.cloudflare.com/reference-architecture/architectures/sase/#tunnels-to-self-hosted-applications">官方文档</a>）：</p>

<p><img src="/image/cf-zero-trust-arch.svg" alt="image" /></p>

<ul>
<li>DeviceAgent 即 WARP 。</li>
<li>上版部分：指用户（通过手机）通过公网，经过 cloudflared 创建的 Tunnel 暴露到公网的 Public Hostname，最终访问部署在 IaaS 的服务。</li>
<li>下半部分：指员工（通过电脑的 DeviceAgent 即 WARP），通过公网，经过由 cloudflared 创建的 Tunnel 注册的内网，访问的位于组织数据中心的内网。</li>
</ul></li>
</ul>

<h2 id="应用场景">应用场景</h2>

<h3 id="一键开启-https">一键开启 HTTPS</h3>

<p>在有一台 VPS 的情况下，如果想为该服务器开启 HTTPS， 传统方法是：</p>

<ul>
<li>购买一个 SSL 证书或使用 acme.sh 申请证书。</li>
<li>将证书配置到 VPS Nginx 中。</li>
<li>配置 DNS 解析。</li>
<li>要记得证书是否过期，过期后还需重新申请。</li>
</ul>

<p>上述操作很麻烦。如果使用 Cloudflare 免费计划，只需将配置 DNS 解析这一步即可。而证书过期续期全部由 Cloudflare 自动处理，十分方便。</p>

<h3 id="零成本建站">零成本建站</h3>

<p>如果想建立一个网站，传统的做法需要购买一个 VPS 自己搭建或者购买云厂商的相关服务，如果是大陆地区还需要备案等，非常麻烦。</p>

<p>很多时候，对于个人站点或流量不大的非商业性站点，上述流程成本偏高，很没必要。</p>

<p>现在，只需将服务部署到位于家庭宽带的个人设备（废旧手机/电脑）上，只需购买一个域名，通过 Zero Trust 的 cloudflared 建立一个 Tunnel，并在 Zero Trust 管理后台的 Networks -&gt; Tunnels 页面的创建的 Tunnel 配置的 Public Hostname  Tab 页上，配置暴露内网中的服务即可。</p>

<p>（一个限制是：目前不支持 UDP）</p>

<h3 id="站点加速">站点加速</h3>

<p>由于 Cloudflare 是全球最大 CDN 服务提供商，因此接入 Cloudflare 的站点，在全球都有很好的访问速度。</p>

<p>因此通过 Cloudflare 的免费计划可以为站点加速，举个例子，如： <a href="/posts/blog-migration/#2024-02-10-使用-cloudflare-加速">利用 Cloudflare 给托管到 netlify 的个人博客加速</a>。</p>

<h3 id="加速海外服务器访问">加速海外服务器访问</h3>

<p>在中国大陆，访问自己的 VPS IP 会被众所周知的原因阻断。此时可以利用 Cloudflare 来给这个 VPS 重获新生，在大陆地区正常使用。</p>

<p>由于法律原因，具体示例在此就不多赘述了。</p>

<h3 id="虚拟局域网-异地组网">虚拟局域网（异地组网）</h3>

<p>通过 Cloudflare Zero Trust 的 WARP 和 Tunnel 即可实现虚拟局域网（异地组网）。</p>

<p>和其他免费方案（如 zerotier）相比，体验最好，基础使用（SSH、VSCode Remote SSH 等）基本够用。</p>

<p>实现方式参见上文： <a href="#tunnel">Tunnel</a>。</p>

<h3 id="暴露-nat-后服务的端口">暴露 NAT 后服务的端口</h3>

<p>和上文<a href="#零成本建站">零成本建站</a>原理类似，这里想表达的是可以利用一些免费的可以访问互联网的计算资源，在上面搭建一些服务，并暴露到互联网上自用或小范围使用。</p>

<p>这里有个利用 replit 资源的例子，脚本参见：</p>

<ul>
<li>打开 <a href="https://replit.com/@rectcircle">https://replit.com/@rectcircle</a> 。</li>
<li>点击 <code>v****-and-cloudflared</code>。</li>
<li>打开 main.sh 查看原理，或 fork 执行（需配置 <code>cloudflared_tunnel_secret</code> 和 <code>v****_client_id</code> 这两个 Secret）。</li>
</ul>

<p>（由于法律原因， <code>****</code> 自行参照步骤查看）</p>

<p>注意：</p>

<ul>
<li>replit 的<a href="https://blog.replit.com/announcing-outbound-data-transfer-limits">出流量每月只有 10 G</a>，非视频自用基本足够。</li>
<li>运行时不能关闭 replit 页面，因为页面关闭后 几秒钟后，资源会立即被 replit 回收，服务进程也会被 kill。</li>
</ul>

<h3 id="保护网站浏览记录隐私">保护网站浏览记录隐私</h3>

<p>HTTPS 虽然能保护浏览的内容不被中间人劫持，但是由于：</p>

<ul>
<li>DNS 是明文的，中间人可以获取到某出口 IP 的域名解析记录。</li>
<li>HTTPS SNI 是明文的，中间人仍可以获取到某出口 IP 访问 HTTPS 站点的域名。</li>
</ul>

<p>而使用 WARP 后，配置的流量都会通过加密的 wireguard 协议通过 Zero Trust 网络的出口 IP 出去。这样，除了 Cloudflare 外，将没人知道用户的浏览记录。</p>
]]></description></item><item><title>Linux LVM 逻辑卷管理</title><link>https://www.rectcircle.cn/posts/linux-lvm/</link><pubDate>Mon, 01 Jan 2024 00:49:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-lvm/</guid><description type="html"><![CDATA[

<h2 id="背景知识">背景知识</h2>

<p>在文章 <a href="/posts/fuse/">《用户态文件系统 fuse》</a> 中，介绍了 Linux 对文件系统的抽象，fuse 文件系统的底层数据存储是可以任意定制的，并不一定会和块设备打交道。</p>

<p>但是，Linux 常用的本地文件系统 （如 ext4） 数据存储都是基于块设备实现的。</p>

<p>Linux 作为冯诺依曼架构计算机的操作系统，必然实现对输入输出（IO）硬件的支持。</p>

<p>但是，IO 硬件千奇百怪，且会不断迭代发展。因此， Linux 根据和 IO 硬件数据读写方式的不同，对 IO 类硬件的抽象，称之为设备 (Device)，主要包含如下两类：</p>

<ul>
<li>字符设备（杂项设备），数据是流式的读写，如：鼠标、键盘、终端。</li>
<li>块设备，数据支持随机读写，如：硬盘。</li>
</ul>

<p>如上这些设备在内核启动过程中，由这些设备驱动（据说 Linux 内核中，驱动代码占比一半），将这些设备加载到内核中。通过 <code>/dev</code> 文件系统（udev）暴露给应用程序使用。以硬盘为例，将存在类似于 <code>/dev/sda</code> 的设备文件。</p>

<p>Linux 还支持将同一块硬盘划分为多个独立相互不影响的区域，即对硬盘进行分区。以实现在不同的区域，格式化为不同的文件系统。</p>

<p>在 Linux 中，可以使用 <a href="https://man7.org/linux/man-pages/man8/parted.8.html">parted</a> 命令进行分区（可以用 <code>parted -l</code> 查看本机磁盘分区情况）。完成分区后，在 <code>/dev</code> 目录中，除了会看到硬盘对应的设备文件外，每个分区也会对应一个块设备文件（如 <code>/dev/sda1</code>）。通过  <a href="https://man7.org/linux/man-pages/man8/lsblk.8.html">lsblk</a> （list block devices） 命令可以看到所有块设备以及分区关系，示例如下（debian 12 默认 + 挂载两块 1G 的空硬盘）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   32G  0 disk 
├─sda1   8:1    0   31G  0 part /
├─sda2   8:2    0    1K  0 part 
└─sda5   8:5    0  975M  0 part [SWAP]
sdb      8:16   0    1G  0 disk 
sdc      8:32   0    1G  0 disk </pre></div>
<p>有了块设备后，即可使用 <code>mkfs.xxx</code> 命令，对硬盘或分区进行格式化。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkfs.ext4 /dev/sdb</code></pre></div>
<p>之后就可以使用 mount 命令将这个磁盘 mount 到文件系统中了。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p /mnt/test-disk1
mount -t ext4 /dev/sdb /mnt/test-disk1
<span style="color:#75715e"># umount /mnt/test-disk1  # 恢复现场</span></code></pre></div>
<p>此时这个文件系统就可以用了（注意， mount 命令只对本次启动有效，重启后将会失效）。</p>

<p>Linux 根目录的挂载和上述类似。即：在启动过程中，通过读取 <code>/etc/fstab</code> 配置文件（该文件的 UUID 可通过 <code>blkid</code> 命令查看），然后用 mount 系统调用挂载。</p>

<h2 id="概述">概述</h2>

<p>上述介绍的分区，是将一个硬盘划分多个相互独立的区域，本质上是在当前硬盘中记录了每个区域的起始结束偏移量，难以实现如下场景：</p>

<ul>
<li>分区空间调整，对一个分区的调整受限于前后分区的情况，基本难以扩缩容。</li>
<li>分区是针对单块硬盘的，分区的最大大小受限于磁盘的大小。</li>
</ul>

<p>LVM (Logical Volume Manager) 逻辑卷管理（lvm2, kernel&gt;=2.6），就来解决单 Linux 主机，多磁盘管理的技术，具有如下能力：</p>

<ul>
<li>管理多块硬盘</li>
<li>自由扩缩容</li>
<li>软 RAID</li>
<li>快照</li>
</ul>

<p>简单来说， LVM 就是在多个块设备（PV）之上，虚拟出多个块设备（LV）。</p>

<h2 id="核心概念">核心概念</h2>

<ul>
<li>VG (Volume Group) 卷组，可以理解为存储池。</li>
<li>PV (Physical Volume) 物理卷，对应一个块设备可以是整块磁盘或某个物理分区。一个 PV 如果要被使用，必须加入一个 VG。</li>
<li>LV (Logical Volume) 逻辑卷，LVM 生成的虚拟块设备。从 VG 中创建，必然属于某个 VG。</li>
</ul>

<h2 id="安装">安装</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">apt install lvm2</code></pre></div>
<p>lvm 分为两个部分： 内核和命令行工具。debian 12 内核相关功能已存在，如上命令会安装 lvm2 <a href="https://packages.debian.org/bookworm/amd64/lvm2/filelist">命令行工具</a>。</p>

<h2 id="场景">场景</h2>

<blockquote>
<p>参考: <a href="https://linux.die.net/man/8/lvm">lvm(8) - Linux man page</a></p>
</blockquote>

<h3 id="实验环境">实验环境</h3>

<p>debian 12 附加两块 1 G 的硬盘，所有命令以 root 用户执行。</p>

<h3 id="创建存储池-pv-和-vg">创建存储池（PV 和 VG）</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 查看块设备</span>
lsblk
<span style="color:#75715e"># 创建 pv</span>
pvcreate /dev/sdb  <span style="color:#75715e"># 将块设备格式化为 pv，该盘所有数据将丢失！</span>
pvcreate /dev/sdc  <span style="color:#75715e"># 将块设备格式化为 pv，该盘所有数据将丢失！</span>
pvs                <span style="color:#75715e"># 打印所有 pv</span>
<span style="color:#75715e"># 创建 vg</span>
vgcreate VG_TEST /dev/sdb /dev/sdc
vgdisplay VG_TEST  <span style="color:#75715e"># 打印某 vg 属性</span></code></pre></div>
<h3 id="创建文件系统-lv">创建文件系统（LV）</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建 lv</span>
lvcreate -L 500M -n lv_test1 VG_TEST
lvs  <span style="color:#75715e"># 查看所有 lv</span>
<span style="color:#75715e"># 查看对应的块设备（位于 /dev/mapper/ 目录下以及 /dev/&lt;VG_NAME&gt;/&lt;LV_NAME&gt;）</span>
ls -al /dev/mapper/VG_TEST-lv_test1
mkfs.ext4 /dev/mapper/VG_TEST-lv_test1  <span style="color:#75715e"># 格式化文件系统</span>
mkdir -p /mnt/test-lv1
mount -t ext4 /dev/mapper/VG_TEST-lv_test1 /mnt/test-lv1  <span style="color:#75715e"># 挂载</span>
echo abc &gt; /mnt/test-lv1/abc
cat /mnt/test-lv1/abc
df -h /mnt/test-lv1
<span style="color:#75715e"># umount /mnt/test-lv1  # 恢复现场</span></code></pre></div>
<h3 id="文件系统在线扩容-lv">文件系统在线扩容 (LV)</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一步: 扩容 LV</span>
lvresize -L +700M /dev/mapper/VG_TEST-lv_test1
lvs
<span style="color:#75715e"># 第二步: 文件系统扩容 (ext4 文件系统支持，其他文件系统可能不支持)</span>
resize2fs /dev/mapper/VG_TEST-lv_test1
df -h /mnt/test-lv1</code></pre></div>
<p>注意：</p>

<ul>
<li>不要求 umount</li>
<li>执行顺序不能颠倒</li>
</ul>

<h3 id="文件系统离线缩容-lv">文件系统离线缩容 (LV)</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一步： umount</span>
umount /mnt/test-lv1
<span style="color:#75715e"># 第二步： 检查文件系统 (ext4 文件系统支持，其他文件系统可能不支持)</span>
e2fsck -f /dev/mapper/VG_TEST-lv_test1
<span style="color:#75715e"># 第三步： 文件系统缩容 (ext4 文件系统支持，其他文件系统可能不支持)</span>
resize2fs /dev/mapper/VG_TEST-lv_test1 500M
<span style="color:#75715e"># 第四步： 缩容 LV</span>
lvreduce -L 500M /dev/mapper/VG_TEST-lv_test1
<span style="color:#75715e"># 验证</span>
mount -t ext4 /dev/mapper/VG_TEST-lv_test1 /mnt/test-lv1  <span style="color:#75715e"># 挂载</span>
cat /mnt/test-lv1/abc
df -h /mnt/test-lv1
<span style="color:#75715e"># umount /mnt/test-lv1  # 恢复现场</span></code></pre></div>
<p>注意：</p>

<ul>
<li>必须要 umount</li>
<li>执行顺序不能颠倒</li>
</ul>

<h3 id="硬盘迁移到新设备">硬盘迁移到新设备</h3>

<blockquote>
<p>注意：如下是不停机迁移，如果旧设备可以关机，则直接关机将硬盘插入新设备即可，无需做任何操作。</p>
</blockquote>

<p>如下命令在旧设备中执行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 0. 卸载 lv 所有挂载点</span>
<span style="color:#75715e"># umount /mnt/test-lv1</span>
<span style="color:#75715e"># 1. 停用所有 lv</span>
lvchange -an /dev/mapper/VG_TEST-lv_test1
<span style="color:#75715e"># 2. 停用卷组</span>
vgchange -an VG_TEST
<span style="color:#75715e"># 3. 导出卷组</span>
vgexport VG_TEST</code></pre></div>
<p>拔下所有硬盘，插入新设备。</p>

<p>如下命令在新设备中执行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 1. 扫描 pv</span>
pvscan
vgs
<span style="color:#75715e"># 2. 导入 vg</span>
vgimport VG_TEST
<span style="color:#75715e"># 3. 激活 vg</span>
vgchange -ay VG_TEST
<span style="color:#75715e"># 4. 挂载到挂载点</span>
mkdir -p /mnt/test-lv1
mount -t ext4 /dev/mapper/VG_TEST-lv_test1 /mnt/test-lv1  <span style="color:#75715e"># 挂载</span>
cat /mnt/test-lv1/abc</code></pre></div>
<h3 id="扩充存储池-vg">扩充存储池（VG）</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">pvcreate /dev/sdd
vgextend VG_TEST /dev/sdd</code></pre></div>
<h3 id="软-raid">软 RAID</h3>

<p>略，参见：</p>

<ul>
<li><a href="https://program-think.medium.com/%E6%89%AB%E7%9B%B2-linux-%E9%80%BB%E8%BE%91%E5%8D%B7%E7%AE%A1%E7%90%86-lvm-%E5%85%BC%E8%B0%88-raid-%E4%BB%A5%E5%8F%8A-%E7%A3%81%E7%9B%98%E5%8A%A0%E5%AF%86%E5%B7%A5%E5%85%B7%E7%9A%84%E6%95%B4%E5%90%88-170e975320a7">扫盲 Linux 逻辑卷管理（LVM） — — 兼谈 RAID 以及“磁盘加密工具的整合”</a></li>
<li><a href="https://linux.die.net/man/8/lvcreate">lvcreate(8) - Linux man page</a></li>
</ul>
]]></description></item><item><title>用户态文件系统 fuse</title><link>https://www.rectcircle.cn/posts/fuse/</link><pubDate>Fri, 29 Dec 2023 18:10:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/fuse/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>Linux 应用程序对文件系统操作的函数，是有一套标准的，这个标准就是 POSIX。</p>

<p>POSIX 文件系统接口本质上是一套 C 语言函数声明，如： <code>open</code>、<code>write</code>、<code>read</code>、<code>mkdir</code>、<code>unlink</code> 等函数。</p>

<p>也就是说 Linux 的所有文件系统都需要满足 POSIX 文件系统接口。</p>

<p>POSIX 作为面向使用者的接口，其设计重点考虑的是通用性和易用性，不会重点关注是否易于实现。站在 Linux 系统开发者的角度，如果不做任何抽象，就需要所有的文件系统的开发都需要实现这套 POSIX 标准，存在大量重复代码，处理各种细节，开发成本会非常高。</p>

<p>因此 Linux 系统提供了一套面相文件系统开发者的抽象： <a href="https://www.kernel.org/doc/html/next/filesystems/vfs.html">VFS</a>，VFS 主要定义的就是就是围绕如下几个结构体的操作：</p>

<ul>
<li><code>file_system_type</code> 文件系统类型，该结构体定义文件系统名，flag 参数，mount 参数，mount 系统调用回调函数等。</li>
<li><a href="https://litux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec5.html"><code>superblock</code></a> 记录某个 mount 了的文件系统的元信息，如顶级目录的 inode，挂载点，关联的设备，空间大小等。</li>
<li><a href="https://litux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec6.html"><code>inode</code></a> 文件系统中的每一个文件（包括目录）在读写时，都会对应一个内存中的 inode 结构体（硬链接会多对一），主要记录这个文件的元信息，如：inode 编号 (<code>i_ino</code>)，文件类型，文件大小，相关时间，文件权限和类型（<code>i_mode</code>）、所有者、所属组。</li>
<li><a href="https://litux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html"><code>dentry</code></a> 目录项，POSIX 文件系统本质上是一个由目录组成树状结构，通过路径定位。<code>dentry</code> 代表着目录树节点，基于此还实现了 inode 缓存以及文件路径到 inode 的快速定位。<code>dentry</code> 主要包含了 inode 指针，文件的 basename，父 <code>dentry</code> 指针、子 <code>dentry</code> 列表。此外，还存在一个用于缓存 dentry 的 hash 表（hash key 由文件 basename 和父 <code>dentry</code> 的指针组成），在读取一个路径时，会按照目录结构依次从 hash 表中查找 <code>dentry</code>，如果找不到才会从文件系统中重新读取（参见：<a href="https://zhuanlan.zhihu.com/p/261669249">知乎文章头图</a>，<a href="https://bean-li.github.io/vfs-inode-dentry/">博客</a>）。</li>
</ul>

<p>因此，要实现一个文件系统，需要编写一个内核模块，实现 Linux 定义的一系列对上述结构体的函数接口。</p>

<p>编写内核模块的成本和难度是比较高的。而 fuse 提供了一种，在用户态的普通应用程序，即可实现一个自定义文件系统的框架。</p>

<h2 id="架构">架构</h2>

<p>fuse 框架主要包含如下几个部分：</p>

<ul>
<li>位于内核的 fuse 内核模块，主流的 Linux 发行版（如 debian）均有启用 （<code>/lib/modules/*/kernel/fs/fuse/</code>）。</li>
<li>用于内核态和用户态通讯的设备文件 <code>/dev/fuse</code>，以及用于用户态和内核态通讯的一套通讯协议。</li>
<li>fuse 命令行工具集 （以 debian 为例： <a href="https://packages.debian.org/buster/amd64/fuse3/filelist">fuse3/filelist</a>）

<ul>
<li><code>/bin/fusermount</code> (<code>/bin/fusermount3</code>)</li>
<li><code>/sbin/mount.fuse</code> (<code>/sbin/mount.fuse3</code>)</li>
</ul></li>
<li>用户程序库：

<ul>
<li>官方提供的 C 语言的动态链接库 （<a href="https://github.com/libfuse/libfuse">github</a> 、<a href="https://packages.debian.org/buster/libfuse3-3">debian 包</a>）。</li>
<li>其他编程语言三方库，如 Go 语言的 <a href="https://github.com/hanwen/go-fuse">hanwen/go-fuse</a>。</li>
</ul></li>
</ul>

<p>通讯架构如下：</p>

<p><img src="/image/FUSE_structure.png" alt="image" /></p>

<p>（图片来源： <a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">wikipedia</a>）</p>

<p>更多参见：</p>

<ul>
<li><a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">Filesystem in Userspace - wikipedia</a></li>
<li><a href="https://www.kernel.org/doc/html/next/filesystems/fuse.html">FUSE — The Linux Kernel documentation</a></li>
</ul>

<h2 id="库">库</h2>

<h3 id="libfuse">libfuse</h3>

<p>fuse 官方提供的编程框架就是 C 语言的动态链接库 <a href="https://github.com/libfuse/libfuse">libfuse</a>。</p>

<p>作为 gopher，本部分不多介绍。</p>

<h3 id="hanwen-go-fuse">hanwen/go-fuse</h3>

<p>目前主流的 go native 实现的 fuse 库有两个： <a href="https://github.com/hanwen/go-fuse">hanwen/go-fuse</a> 和 <a href="https://github.com/bazil/fuse">bazil/fuse</a>。2023 年 12 月这个时点看来，<a href="https://github.com/hanwen/go-fuse">hanwen/go-fuse</a> 这个库各方面综合表现更好一些。</p>

<p>该库官方 go docs 说明和示例相当丰富，参见：<a href="https://pkg.go.dev/github.com/hanwen/go-fuse/v2@v2.4.2">go docs</a>。</p>

<h3 id="核心包">核心包</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;github.com/hanwen/go-fuse/v2/fuse&#34;</span> <span style="color:#75715e">// fuse 协议相关实现，启动 fuse deamon 的相关参数。
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;github.com/hanwen/go-fuse/v2/fs&#34;</span>   <span style="color:#75715e">// 面向 fuse 文件系统开发者的编程接口。
</span><span style="color:#75715e"></span>)</code></pre></div>
<h3 id="主流程">主流程</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// fuse 文件系统的 rootInode 的实现，推荐的写法是内嵌一个 fs.Inode。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">HelloRoot</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">Inode</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 创建文件系统的 rootInode 该实现必须 fs.InodeEmbedder。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">rootInode</span> <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">InodeEmbedder</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">HelloRoot</span>{}
	<span style="color:#75715e">// 和 /dev/fuse 通讯。并挂载文件系统。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 默认会使用 /bin/fusermount 来进行挂载，官方文档说会更新 /etc/mtab，如果这个可执行文件不存在则报错。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 可通过 fs.Options.MountOptions.DirectMount 直接使用 syscall.Mount 挂载而不是 /bin/fusermount。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">server</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;/mount/to/target/dir&#34;</span>,  <span style="color:#a6e22e">rootInode</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">Options</span>{})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;Mount fail: %v\n&#34;</span>, <span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// 等待退出信号
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>)
	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Interrupt</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTERM</span>) <span style="color:#75715e">// nolint
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#75715e">// 接受到信号后，取消挂载，server.Wait 会返回
</span><span style="color:#75715e"></span>		<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">c</span>
		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">Unmount</span>()
	}()
	<span style="color:#75715e">// 等待 server 退出
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">Wait</span>()
}</code></pre></div>
<h3 id="示例实现">示例实现</h3>

<ul>
<li><a href="https://pkg.go.dev/github.com/hanwen/go-fuse/v2@v2.4.2/fs#example-package">内存文件系统</a>： 只包含一个文件的文件系统（根目录是只读的，文件是可写的），具体内存文件的读写实现参见： <code>fs.MemRegularFile</code> 。</li>
<li><a href="https://pkg.go.dev/github.com/hanwen/go-fuse/v2@v2.4.2/fs#example-package-Mount">loopback 到另一目录的文件系统</a>：具体实现参见 <code>fs.NewLoopbackRoot</code>。</li>
</ul>

<h2 id="实现">实现</h2>

<blockquote>
<p>参考: <a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace#Remote/distributed_file_system_clients">wiki</a></p>
</blockquote>

<ul>
<li><a href="https://github.com/s3fs-fuse/s3fs-fuse">s3fs</a></li>
<li><a href="https://github.com/juicedata/juicefs">JuiceFS</a></li>
<li><a href="https://github.com/libfuse/sshfs">sshfs</a></li>
<li><a href="https://github.com/containers/fuse-overlayfs">fuse-overlayfs</a></li>
<li><a href="https://github.com/tuxera/ntfs-3g">ntfs-3g</a></li>
<li>&hellip;</li>
</ul>
]]></description></item><item><title>Linux 软件构建一次到处运行</title><link>https://www.rectcircle.cn/posts/linux-build-once-run-anywhere/</link><pubDate>Sun, 17 Dec 2023 23:47:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-build-once-run-anywhere/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>在 Linux 平台，使用包管理工具（如 apt、yum）来安装软件时，我们想要的软件包的版本在包管理工具里面找不到的问题。</p>

<p>比如：在遗留的 Debian8 操作系统中，只能安装 python3.4；node18 只能安装在 Debian10 及以上版本的操作系统上。</p>

<p>出现该情况最主要的原因是， Linux 平台的软件更新的版本，对动态链接库（特别是 glibc）的最低版本要求不断提高。而超过了维护周期的 Linux 发行版（如 Debian8），其动态链接库将得不到升级，从而导致较新版本的 Linux 软件无法再遗留的 Linux 发行版中运行（当然内核版本是另一个重要原因，本文重点关注 glibc 问题，关于内核版本不多讨论）。</p>

<p>为了解决这个问题，业界有一些解决方案，如：</p>

<ul>
<li>在编程语言层面，如：在 Java 提供了一次编译到处运行；Golang 编译器原生支持交叉和静态编译。</li>
<li>容器技术，将依赖和应用组织成镜像，天然就解决了这个问题。</li>
</ul>

<p>但是，设想如下场景：</p>

<p>一个 C/C++ 编写的项目，依赖 glibc 等动态链接库，我们希望这个项目的编译出来的产物，可以在任意的 Linux 发行版上运行。</p>

<p>要实现上述目标，可能得解决方案如下：</p>

<ol>
<li>如果该项目仅依赖 libc 而非 glibc，那可以搜索该项目是否可以使用支持静态编译的 libc 库，如 musl。</li>
<li>如果找不到上面的方案，或者并不是官方支持的。还有一种通用方案就是，将所有动态链接库，包括 glibc 都打包到软件里面。该方案是 NixOS 的最重要的技术基石之一。</li>
</ol>

<h2 id="纯静态编译">纯静态编译</h2>

<p>以 dropbear ssh 为例，参见博客： <a href="/posts/lightweight-ssh-dropbear/#编译安装">轻量级 SSH 开源项目 Dropbear - 编译安装</a></p>

<h2 id="打包所有-so-依赖">打包所有 so 依赖</h2>

<p>本小结以在 Debian8 中，运行 node18 为例，介绍如何将 glibc 等动态链接库打包到软件包里面，来实现将一个只支持较新版本 glibc 的可执行文件，运行在任意 Linux 发行版中。</p>

<h3 id="示例-在任意-linux-运行-node18">示例: 在任意 Linux 运行 node18</h3>

<p>通过从 <a href="https://github.com/nodejs/node/blob/v18.19.0/BUILDING.md">nodejs/node BUILDING.md</a> 可以看到，node18 依赖的 glibc 版本 &gt;= 2.28。</p>

<p>也就是说直接从 nodejs 官方下载下来的 nodejs 是无法再旧版的 glibc 的 Linux 发行版中运行的，如 debian8/glibc2.19，会报错，验证如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian8 中执行</span>
wget https://nodejs.org/dist/v18.12.0/node-v18.12.0-linux-x64.tar.xz
tar -xvJf node-v18.12.0-linux-x64.tar.xz
cd node-v18.12.0-linux-x64
./node-v18.12.0-linux-x64/bin/node
<span style="color:#75715e"># 报错如下:</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.27&#39; not found (required by ./node-v18.12.0-linux-x64/bin/node)</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25&#39; not found (required by ./node-v18.12.0-linux-x64/bin/node)</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28&#39; not found (required by ./node-v18.12.0-linux-x64/bin/node)</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.9&#39; not found (required by ./node-v18.12.0-linux-x64/bin/node)</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.21&#39; not found (required by ./node-v18.12.0-linux-x64/bin/node)</span></code></pre></div>
<p>虽然 nodejs 通过非官方构建提供了基于 glibc2.17 的版本，但是这个版本是实验性支持。因此，最好的做法是：将 glibc 的 so 打包到 nodejs 中。</p>

<h4 id="编译-glibc-以及其他-so">编译 glibc 以及其他 so</h4>

<p>使用如下命令，编译一个 glibc 2.31，这些命令需要再较新版本的 Linux 中执行（如 debian 11）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian 11 中执行</span>
wget http://ftp.gnu.org/gnu/glibc/glibc-2.31.tar.gz
tar -zxvf glibc-2.31.tar.gz
glibc_prefix<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/glibc-2.31-target
cd glibc-2.31/
rm -rf build <span style="color:#f92672">&amp;&amp;</span> mkdir -p build <span style="color:#f92672">&amp;&amp;</span> cd build
sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt install -y gcc make gdb texinfo gawk bison sed python3-dev python3-pip
../configure --prefix<span style="color:#f92672">=</span>$glibc_prefix
make -j4
make install
cd ../../
<span style="color:#75715e"># nodejs 依赖 libstdc++.6 和 libgcc_s.so.1</span>
<span style="color:#75715e"># (最好重新编译，这里简单复制一下，要求该操作系统的 glibc 版本就是 2.31，debian11 满足需求)</span>
cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28 ./glibc-2.31-target/lib
ln -s libstdc++.so.6.0.28 ./glibc-2.31-target/lib/libstdc++.so.6
cp /lib/x86_64-linux-gnu/libgcc_s.so.1 ./glibc-2.31-target/lib

<span style="color:#75715e"># 压缩</span>
tar -czvf glibc-2.31-target.tar.gz glibc-2.31-target/</code></pre></div>
<p>传输 glibc-2.31-target.tar.gz 到 debian 8 版本中验证。</p>

<p>如下命令在 debian 8 中执行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian 8 中执行</span>
tar -zxvf glibc-2.31-target.tar.gz</code></pre></div>
<h4 id="修改-elf-动态链接库加载器">修改 ELF 动态链接库加载器</h4>

<p>NixOS 提供了一个实用工具 <a href="https://github.com/NixOS/patchelf">patchelf</a>，该工具可以修改可执行文件的动态链接库加载器路径（参考： <a href="https://stackoverflow.com/questions/847179/multiple-glibc-libraries-on-a-single-host">stackoverflow</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian 8 中执行</span>
sudo apt install -y patchelf
patchelf --set-interpreter <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/glibc-2.31-target/lib/ld-linux-x86-64.so.2 ./node-v18.12.0-linux-x64/bin/node
<span style="color:#75715e"># 也可以安装 github 上的静态编译版本</span>
<span style="color:#75715e"># wget https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-x86_64.tar.gz</span>
<span style="color:#75715e"># tar -zxvf patchelf-0.18.0-x86_64.tar.gz -C patchelf/</span></code></pre></div>
<h4 id="验证">验证</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian 8 中执行</span>
./node-v18.12.0-linux-x64/bin/node
<span style="color:#75715e"># 可以正确进入解释器。</span>
<span style="color:#75715e"># Welcome to Node.js v18.12.0.</span>
<span style="color:#75715e"># Type &#34;.help&#34; for more information.</span>
<span style="color:#75715e"># &gt;</span> </code></pre></div>
<h4 id="其他说明">其他说明</h4>

<ul>
<li><p>可执行文件在运行时， glibc 库所在的目录必须是 glibc 编译设置的 <code>--prefix</code> 目录一样，不能是其他路径，通过如下命令可以验证：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在 debian 8 中执行</span>
mv glibc-2.31-target glibc-2.31-target-2
patchelf --set-interpreter <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/glibc-2.31-target-2/lib/ld-linux-x86-64.so.2 ./node-v18.12.0-linux-x64/bin/node
./node-v18.12.0-linux-x64/bin/node
<span style="color:#75715e"># 报错</span>
<span style="color:#75715e"># ./node-v18.12.0-linux-x64/bin/node: error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory</span>

<span style="color:#75715e"># 恢复现场</span>
mv glibc-2.31-target-2  glibc-2.31-target
patchelf --set-interpreter <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/glibc-2.31-target/lib/ld-linux-x86-64.so.2 ./node-v18.12.0-linux-x64/bin/node</code></pre></div></li>

<li><p>实际上可执行文件依赖的是部分 <code>lib/*.so*</code>，可以按需裁剪。</p></li>
</ul>
]]></description></item><item><title>分布式存储之对象存储 (MinIO)</title><link>https://www.rectcircle.cn/posts/distributed-storage-object-storage-minio/</link><pubDate>Sat, 16 Dec 2023 18:39:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/distributed-storage-object-storage-minio/</guid><description type="html"><![CDATA[

<blockquote>
<p>版本： <a href="https://github.com/minio/minio/releases/tag/RELEASE.2023-12-09T18-17-51Z">RELEASE.2023-12-09T18-17-51Z</a></p>
</blockquote>

<h2 id="简介">简介</h2>

<p>目前广泛使用对象存储的形态最早确定于 AWS S3：</p>

<ul>
<li>用于存储非结构化的数据（称为对象），如照片、音频、视频、二进制制品等（类似于文件系统的文件）。</li>
<li>对象通过唯一标识符访问（类似于文件系统的路径，但对象存储并不提供层次化的目录树的概念，是扁平的）。</li>
<li>对象在创建时除了指定唯一标识符外，还可以设置一些元数据（键值对格式）。</li>
<li>对象是不可变的，也就是说，一个对象一旦创建其元数据和内容不可改变，如需修改则只能覆盖。</li>
<li>对象标准的读写方式是一套标准的 HTTP API 协议进行，而 AWS S3 作为现代对象存储的缔造者，AWS S3 的 API 规范就是事实上的对象存储 API 标准。</li>
<li>对象存储由于业务简单；在存储容量，可以无限平行扩展，支持 EB 级甚至更高容量；采用分布式架构具有高可靠和高可用性。</li>
<li>所有主流的云厂商均提供对象存储服务，除了云厂商外，还有很多开源的项目可以自建对象存储服务如 MinIO。</li>
</ul>

<p>本文，将重点介绍：</p>

<ul>
<li>在单台 Linux 上部署一个单节点的 MinIO 。</li>
<li>MinIO 的管理能力。</li>
<li>AWS S3 核心 API。</li>
<li>MinIO 架构简述。</li>
</ul>

<h2 id="minio-单节点安装运行">MinIO 单节点安装运行</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装</span>
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/

<span style="color:#75715e"># 运行</span>
mkdir ~/minio
minio server ~/minio --console-address :9090</code></pre></div>
<p>上述命令， minio 会暴露两个端口，分别是 9000 和 9090。其中 9000 是兼容 S3 的 API 接口，9090 是 minio 内置的管理控制台 UI。</p>

<p>minio 还提供了一个命令行工具 <code>mc</code>，可以通过如下命令安装配置。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/mc

mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
mc admin info local</code></pre></div>
<h2 id="minio-管理">MinIO 管理</h2>

<p>打开 <a href="http://127.0.0.1:9090">http://127.0.0.1:9090</a> ，输入 root 用户命和密码 (均为 <code>minioadmin</code>) 可以看到 MinIO Console 菜单向分为了三类：</p>

<ul>
<li>User: 对象浏览、Access Token。</li>
<li>Administrator: bucket 管理、用户和权限管理等。</li>
<li>Subscription: 付费订阅的企业级能力，本文不做介绍。</li>
</ul>

<h3 id="bucket-管理">bucket 管理</h3>

<p>对象存储系统使用 bucket 来组织对象，bucket 类似于一个文件系统， 上传的对象必然归属某个 bucket。</p>

<p>在创建 bucket 的时候，可以配置 bucket 的一些特性配置，这些配置会对该 bucket 下的所有对象生效，如版本化（同一个对象保存多版本），对象锁（避免被删除），Quota（数据量限制），保留规则（版本化的旧版本自动删除）。这部分内容本文不做介绍，有兴趣详见：<a href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management.html">官方文档</a>。</p>

<p>打开 <a href="http://127.0.0.1:9090/buckets/add-bucket">Bucket 创建页面</a>， 创建一个名为 <code>bucket-test</code> 所有特性开关保持默认全部关闭的 bucket。</p>

<p>这里特别说明的是，可以通过 <a href="http://127.0.0.1:9090/buckets/bucket-test/admin/prefix">Anonymous 页面</a>，配置该 bucket 的那些对象的可以被匿名访问，本例中，开启匿名访问，Prefix 为 <code>/</code>，Access 为 <code>readonly</code>。至此，可以通过 <code>&lt;minioServer&gt;/&lt;bucketName&gt;/&lt;objectName&gt;</code> 方式访问，如： <code>http://127.0.0.1:9000/bucket-test/dir1/dir2/file3</code>。</p>

<h3 id="用户和权限管理">用户和权限管理</h3>

<p>MinIO 采用 PBAC （Policy-Based Access Control , 基于策略的访问控制），有如下概念：</p>

<ul>
<li>策略，定义对那些资源拥有哪些行为。</li>
<li>用户，鉴权主体，默认使用用户名密码认证，可以关联多个策略。</li>
<li>组，一组策略的集合，用户可以选择加入多个组，组下的用户继承该组关联所有策略。</li>
<li>服务账号 (Service Accounts, Access Keys)，用户可以创建多服务账号，服务账号默认会继承该用户的所有策略（也可以配置用户策略的一个子集），这个服务账号包含 Access Key 和 Secret Key，服务账号用于给开发者编写的程序与对象存储系统进行认证鉴权的方式。</li>
</ul>

<p>上一步我们创建一个名为 <code>bucket-test</code> 的 bucket。这里基于如上机制，创建一个用户和服务账号，且约束其只能操作 <code>bucket-test</code>这个 bucket，步骤如下：</p>

<ol>
<li><p>打开 <a href="http://127.0.0.1:9090/policies">Policies 页面</a>，点击新建，Policy Name 填写 <code>bucket-test-rw</code>，Write Policy 内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
    <span style="color:#f92672">&#34;Statement&#34;</span>: [
        {
            <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
            <span style="color:#f92672">&#34;Action&#34;</span>: [
                <span style="color:#e6db74">&#34;s3:*&#34;</span>
            ],
            <span style="color:#f92672">&#34;Resource&#34;</span>: [
                <span style="color:#e6db74">&#34;arn:aws:s3:::bucket-test/*&#34;</span>
            ]
        }
    ]
}</code></pre></div>
<ul>
<li>Version 为版本号。</li>
<li>Statement 为策略表达式数组，其中值包含一个对象。

<ul>
<li><code>&quot;Effect&quot;: &quot;Allow&quot;</code> 表示允许对 <code>Resource</code> 做 <code>Action</code>。</li>
<li><code>Action</code> 表示允许执行的动作，<code>&quot;s3:*&quot;</code> 表示所有 AWS S3 API 可以执行所有操作都允许执行。</li>
<li><code>Resource</code> 表示允许操作的资源， <code>&quot;arn:aws:s3:::bucket-test/*&quot;</code> 表示只允许操作 bucket 名为 <code>bucket-test</code> 的 bucket</li>
</ul></li>
<li>更多关于策略 JSON 的编写，参见：<a href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure">官方文档</a>。</li>
</ul></li>

<li><p>打开 <a href="http://127.0.0.1:9090/identity/users">Identity - Users 页面</a>，点击创建用户，填写用户名 <code>bucket-test-user</code> 密码 <code>12345678</code> （仅测试）， Assign Policies 选择 <code>bucket-test-rw</code>，点击保存。</p></li>

<li><p>退出登录 root 用户，使用上述创建账号登录，这个用户能看到 Administrator 菜单项只有 Buckets，只能管理 <code>bucket-test</code> 这个 bucket 了。</p></li>

<li><p>打开 <a href="http://127.0.0.1:9090/access-keys">Access Keys</a>，点击 Create Access Key 即可创建一个服务账号，可以获取到 Access Key 和 Secret Key （如  Access Key: <code>1qJ4sGlF6HzTWIHsakYK</code>，Secret Key: <code>UwkpCLEMX2ODx5Cg9FfsxGGokIWXRofFwO8Chiq0</code>）。需要注意的是，创建服务的 Secret Key 只有首次创建的时候才能获取，后续将服务从后台拿到，需谨慎保管。</p></li>
</ol>

<p>至此，就可以通过上面创建的服务账号通过 AWS S3 API 操作这个 <code>bucket-test</code> 这个 bucket 了。</p>

<h2 id="aws-s3-api">AWS S3 API</h2>

<p>MinIO 实现 <a href="https://docs.aws.amazon.com/pdfs/AmazonS3/latest/API/s3-api.pdf#Type_API_Reference">AWS S3 API</a> 规范的最大子集 （未实现部分参见：<a href="https://min.io/docs/minio/linux/operations/concepts/thresholds.html#s3-api-limits">Minio 官方文档</a>）。</p>

<p>本节，将通过如下示例了解 AWS S3 API：</p>

<ul>
<li>使用 Go SDK 将构造一个目录存储到上传到上述的测试 Bucket，并查看元信息，下载内容。</li>
<li>这里我们使用 <code>github.com/minio/minio-go/v7</code> 来连接 Minio Server。</li>
</ul>

<p>示例代码库： <a href="https://github.com/rectcircle/learn-aws-s3-api">rectcircle/learn-aws-s3-api</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;bytes&#34;</span>
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io&#34;</span>

	<span style="color:#e6db74">&#34;github.com/minio/minio-go/v7&#34;</span>
	<span style="color:#e6db74">&#34;github.com/minio/minio-go/v7/pkg/credentials&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 配置参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>()
	<span style="color:#a6e22e">endpoint</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;127.0.0.1:9000&#34;</span>
	<span style="color:#a6e22e">accessKeyID</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;1qJ4sGlF6HzTWIHsakYK&#34;</span>
	<span style="color:#a6e22e">secretAccessKey</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;UwkpCLEMX2ODx5Cg9FfsxGGokIWXRofFwO8Chiq0&#34;</span>
	<span style="color:#a6e22e">useSSL</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>

	<span style="color:#75715e">// 初始化 minio 客户端
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">minioClient</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">endpoint</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">Options</span>{
		<span style="color:#a6e22e">Creds</span>:  <span style="color:#a6e22e">credentials</span>.<span style="color:#a6e22e">NewStaticV4</span>(<span style="color:#a6e22e">accessKeyID</span>, <span style="color:#a6e22e">secretAccessKey</span>, <span style="color:#e6db74">&#34;&#34;</span>),
		<span style="color:#a6e22e">Secure</span>: <span style="color:#a6e22e">useSSL</span>,
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// bucket 配置
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">bucketName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;bucket-test&#34;</span>
	<span style="color:#75715e">// location := &#34;us-east-1&#34;  // minio 部署的时候可以配置，默认测试的服务的默认值是 &#34;us-east-1&#34;
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 测试数据
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// &#34;dir1/file1&#34; 为 object name，格式一般写法为类似于文件系统的路径，
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              以 / 分割，但最前面不需要有 /，即使有也会被忽略 TrimPrefix 掉。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">testData</span> <span style="color:#f92672">:=</span> [][<span style="color:#ae81ff">2</span>]<span style="color:#66d9ef">string</span>{
		{<span style="color:#e6db74">&#34;dir1/file1&#34;</span>, <span style="color:#e6db74">&#34;abcdef&#34;</span>},
		{<span style="color:#e6db74">&#34;dir1/file2&#34;</span>, <span style="color:#e6db74">&#34;abcdef&#34;</span>},
		{<span style="color:#e6db74">&#34;dir1/dir2/file3&#34;</span>, <span style="color:#e6db74">&#34;abcdef&#34;</span>},
		{<span style="color:#e6db74">&#34;file2&#34;</span>, <span style="color:#e6db74">&#34;abcdef&#34;</span>},
	}

	<span style="color:#75715e">// 上传文件
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">item</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">testData</span> {
		<span style="color:#a6e22e">filePath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">item</span>[<span style="color:#ae81ff">0</span>]
		<span style="color:#a6e22e">fileContent</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">item</span>[<span style="color:#ae81ff">1</span>]

		<span style="color:#a6e22e">info</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">minioClient</span>.<span style="color:#a6e22e">PutObject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">bucketName</span>, <span style="color:#a6e22e">filePath</span>, <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">NewBufferString</span>(<span style="color:#a6e22e">fileContent</span>), int64(len(<span style="color:#a6e22e">fileContent</span>)),
			<span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">PutObjectOptions</span>{
				<span style="color:#a6e22e">ContentType</span>: <span style="color:#e6db74">&#34;text/plain&#34;</span>, <span style="color:#75715e">// 给对象设置 MIME 媒体类型，会影响匿名开发下载链接返回的媒体类型。
</span><span style="color:#75715e"></span>			})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;upload %s success\n&#34;</span>, <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">Key</span>)
	}

	<span style="color:#75715e">// 读取某个文件
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">obj</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">minioClient</span>.<span style="color:#a6e22e">GetObject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">bucketName</span>, <span style="color:#e6db74">&#34;file2&#34;</span>, <span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">GetObjectOptions</span>{})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">content</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">obj</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;GetObject file2, content is %s\n&#34;</span>, string(<span style="color:#a6e22e">content</span>))

	<span style="color:#75715e">// 遍历读取文件
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 特别说明的是，object name 仅仅是个字符串，没有目录的那种层级关系。因此：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1. 要想类似于文件系统目录方式检索对象，其实现是基于字符串前缀的方式。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2. 没有空目录的概念，如果想实现需要通过将目录元信息编码为一个对象。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">dir1RecursiveItemsChan</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">minioClient</span>.<span style="color:#a6e22e">ListObjects</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">bucketName</span>, <span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">ListObjectsOptions</span>{
		<span style="color:#a6e22e">Prefix</span>:    <span style="color:#e6db74">&#34;dir1&#34;</span>,
		<span style="color:#a6e22e">Recursive</span>: <span style="color:#66d9ef">true</span>,
	})
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">item</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">dir1RecursiveItemsChan</span> {
		<span style="color:#a6e22e">obj</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">minioClient</span>.<span style="color:#a6e22e">GetObject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">bucketName</span>, <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">Key</span>, <span style="color:#a6e22e">minio</span>.<span style="color:#a6e22e">GetObjectOptions</span>{})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">content</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">obj</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ListObjects dir1, objectName is %s, content is %s\n&#34;</span>, <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">Key</span>, string(<span style="color:#a6e22e">content</span>))
	}

	<span style="color:#75715e">// 由于此 bucket 这设置了匿名访问，所以可以通过如下链接直接下载内容
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// curl http://127.0.0.1:9000/bucket-test/dir1/dir2/file3
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">upload dir1/file1 success
upload dir1/file2 success
upload dir1/dir2/file3 success
upload file2 success
GetObject file2, content is abcdef
ListObjects dir1, objectName is dir1/dir2/file3, content is abcdef
ListObjects dir1, objectName is dir1/file1, content is abcdef
ListObjects dir1, objectName is dir1/file2, content is abcdef</pre></div>
<h2 id="minio-架构简述">MinIO 架构简述</h2>

<p>上面我们部署的是一个单节点的 MinIO，这种部署方式只能用作学习和测试使用，不能在生产场景使用。本小结将简要介绍的 MinIO 在生产场景的架构特点。</p>

<p>和常规的分布式存储相比， MinIO 是去中心化的，也就是说:</p>

<ul>
<li>MinIO 没有 Master 节点。</li>
<li>所有 MinIO 节点都是对等的。</li>
<li>所有 MinIO 节点配置都相同。</li>
<li>所有 MinIO 节点都有集群的完整全貌。</li>
<li>任意一个 MinIO 节点都可以对外提供 HTTP 服务。</li>
</ul>

<p>因此：</p>

<ul>
<li>在部署之前需要规划好每个节点的磁盘数和配置，一旦确定后期将无法在线更改。</li>
<li>MinIO 集群需要外部负载均衡器（如 Nginx）将流量均衡的打到 MinIO 节点。</li>
</ul>

<p>值得特别说明的是：由于 MinIO server 是 Go 编写的，因此安装配置 MinIO 非常容易，只需要下载一个二进制文件，通过启动参数或环境变量给出整个集群的全貌配置以及磁盘配置即可。如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">minio server https://minio<span style="color:#f92672">{</span><span style="color:#ae81ff">1</span>...4<span style="color:#f92672">}</span>.example.net/mnt/disk<span style="color:#f92672">{</span><span style="color:#ae81ff">1</span>...4<span style="color:#f92672">}</span> https://minio<span style="color:#f92672">{</span><span style="color:#ae81ff">5</span>...8<span style="color:#f92672">}</span>.example.net/mnt/disk<span style="color:#f92672">{</span><span style="color:#ae81ff">1</span>...4<span style="color:#f92672">}</span></code></pre></div>
<p>关于扩容：</p>

<p>在过去 MinIO 提供了一种联邦集群的模式来进行扩容，但是这种需要引入一个中心化的存储，和 MinIO 的极简架构设计违背。因此，现在 MinIO 提供一种基于 Server Pool 的机制来给集群扩容。需要特别注意的是，目前的这种扩容会造成秒级的服务不可用（添加新的 Server Pool 后需要更改旧的节点的配置，将新的 Server Pool 加到配置里面，并在重启或启动所有节点，<a href="https://min.io/docs/minio/linux/operations/install-deploy-manage/expand-minio-deployment.html#expansion-is-non-disruptive">官方文档</a>特别强调，不能滚动重启）。</p>

<p>关于 MinIO 架构，更多参见官方文档：</p>

<ul>
<li><a href="https://min.io/docs/minio/linux/operations/concepts/architecture.html">部署架构</a></li>
<li><a href="https://min.io/docs/minio/linux/operations/concepts.html">核心运维概念</a></li>
<li><a href="https://min.io/docs/minio/linux/operations/install-deploy-manage/expand-minio-deployment.html">对已存在集群进行扩容</a></li>
</ul>
]]></description></item><item><title>容器核心技术（十） OverlayFS</title><link>https://www.rectcircle.cn/posts/container-core-tech-10-overlayfs/</link><pubDate>Sun, 19 Nov 2023 22:48:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-10-overlayfs/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://docs.kernel.org/filesystems/overlayfs.html">Linux kernel 文档: Overlay Filesystem</a></p>
</blockquote>

<h2 id="简介">简介</h2>

<p>OverlayFS 是一种写时复制的文件系统，是容器的默认文件系统。OverlayFS 只需指定 n 个 lowerdir 和 1 个 upperdir。在云原生领域容器引擎：会将包含多个层的镜像会解压到目录中，作为 OverlayFS 的 lowerdir，创建一个空目录作为 upperdir。当容器的进行修改镜像里的文件时会将文件复制到 upperdir 中然后进行修改。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          rootfs (overlayfs)                 image

              +-----------+
mount point   |  merged   |
              +-----------+
                    ^
               work | mount -t overlay overlay -olowerdir=lowern:lower...:lower1,upperdir=upper,workdir=work merged
                    |
              +-----------+
upperdir      |   upper   |
              +-----------+

              +-----------+              +------------------+
              |  lowern   |              | layer n tar.gz   |
              +-----------+    unpack    +------------------+
lowerdir      | lower...  |   &lt;-------   | layer ... tar.gz |
              +-----------+              +------------------+
              |  lower1   |              | layer 1 tar.gz   |
              +-----------+              +------------------+</pre></div>
<ul>
<li>关于 oci image： <a href="/posts/oci-image-spec/">OCI 镜像格式规范</a></li>
<li>从镜像到 rootfs 生成过程： <a href="/posts/containerd-4-overlayfs-snapshotter/">Containerd 详解（四） OverlayFS snapshotter</a></li>
</ul>

<p>下文将从几方面探索 overlayfs 的特性：</p>

<ul>
<li>观察多个 lowers 生成的 merged 目录。</li>
<li>操作 merged 目录，观察 upper 目录的变化。</li>
<li>操作 upper 或 lower 目录，观察 merged 目录的变化。</li>
</ul>

<h2 id="实验代码库">实验代码库</h2>

<p>本系列实验代码库位于：<a href="https://github.com/rectcircle/container-core-tech-experiment">rectcircle/container-core-tech-experiment</a> 的 <code>src/shell/03-overlayfs</code>目录</p>

<h2 id="观察-merged-目录">观察 merged 目录</h2>

<p>overlayfs 可以通过 mount 命令 (系统调用) 创建。其 lowerdir 选项允许多个，且是必须提供；而 upperdir 和 workdir 是可选的；如果 upperdir 和 workdir 两个选项不提供，则生成的 merged 目录则是只读的。</p>

<p>下面实现代码将两个 lowerdir 通过 overlayfs 生成到 merged 目录中。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span><span style="color:#75715e"># sudo ./src/shell/03-overlayfs/00-lowers-to-merged.sh</span>

<span style="color:#75715e"># 创建并进入测试目录</span>
exp_base_dir<span style="color:#f92672">=</span>/tmp/overlayfs-exp/00-lowers-to-merged
umount $exp_base_dir/merged &gt;/dev/null <span style="color:#ae81ff">2</span>&gt;&amp;<span style="color:#ae81ff">1</span>
rm -rf $exp_base_dir <span style="color:#f92672">&amp;&amp;</span> mkdir -p $exp_base_dir
cd $exp_base_dir

<span style="color:#75715e"># 准备 lower、merged 目录</span>
mkdir -p lower1 lower2 merged
<span style="color:#75715e"># lower1/</span>
mkdir -p lower1/from-lower1-dir lower1/from-lower1-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-lower2-file
mkdir -p lower1/from-lower1-lower2-dir lower1/from-lower1-lower2-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-lower2-dir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-lower2-dir/from-lower1-file
<span style="color:#75715e"># lower2/</span>
mkdir -p lower2/from-lower2-dir lower2/from-lower2-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower2-dir/file
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower2-file
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lower2-file
mkdir -p lower2/from-lower1-lower2-dir lower2/from-lower1-lower2-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lower2-dir/file
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lower2-dir/from-lower2-file
touch -t <span style="color:#ae81ff">197001010000</span> lower2/from-lower1-lower2-dir

<span style="color:#75715e"># 生成 merged</span>
mount -t overlay overlay -olowerdir<span style="color:#f92672">=</span>lower2:lower1 merged

<span style="color:#75715e"># 观察情况</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree lower1/&#39;</span>
tree lower1
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree lower2/&#39;</span>
tree lower2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged/&#39;</span>
tree merged/
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower1-lower2-file&#39;</span>
cat merged/from-lower1-lower2-file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower1-lower2-dir/file&#39;</span>
cat merged/from-lower1-lower2-dir/file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; stat merged/from-lower1-lower2-dir&#39;</span>
stat merged/from-lower1-lower2-dir
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&gt;&gt;&gt; tree lower1/
lower1
├── from-lower1-dir
│   ├── file
│   └── subdir
├── from-lower1-file
├── from-lower1-lower2-dir
│   ├── file
│   ├── from-lower1-file
│   └── subdir
└── from-lower1-lower2-file

4 directories, 5 files

&gt;&gt;&gt; tree lower2/
lower2
├── from-lower1-lower2-dir
│   ├── file
│   ├── from-lower2-file
│   └── subdir
├── from-lower1-lower2-file
├── from-lower2-dir
│   ├── file
│   └── subdir
└── from-lower2-file

4 directories, 5 files

&gt;&gt;&gt; tree merged/
merged/
├── from-lower1-dir
│   ├── file
│   └── subdir
├── from-lower1-file
├── from-lower1-lower2-dir
│   ├── file
│   ├── from-lower1-file
│   ├── from-lower2-file
│   └── subdir
├── from-lower1-lower2-file
├── from-lower2-dir
│   ├── file
│   └── subdir
└── from-lower2-file

6 directories, 8 files

&gt;&gt;&gt; cat merged/from-lower1-lower2-file
from-lower2

&gt;&gt;&gt; cat merged/from-lower1-lower2-dir/file
from-lower2

&gt;&gt;&gt; stat merged/from-lower1-lower2-dir
  文件：merged/from-lower1-lower2-dir
  大小：4096            块：8          IO 块：4096   目录
设备：36h/54d   Inode：1625488     硬链接：1
权限：(0755/drwxr-xr-x)  Uid：(    0/    root)   Gid：(    0/    root)
最近访问：2023-11-18 21:52:10.813161498 +0800
最近更改：1970-01-01 00:00:00.000000000 +0800
最近改动：2023-11-18 21:52:10.809161535 +0800
创建时间：2023-11-18 21:52:10.809161535 +0800</pre></div>
<p>可以得出如下结论：</p>

<ul>
<li><code>lowerdir</code> 支持多个目录。这多个目录以 <code>:</code> 分割。</li>
<li><code>lowerdir</code> 的多个目录，左侧的位于上层。这些 lowerdir 如果存在同路径的文件，则在上层（左侧）目录中的文件将覆盖右侧的文件。

<ul>
<li>读取 <code>merged/from-lower1-lower2-file</code> 相当于读取 <code>lower2/from-lower1-lower2-file</code>。</li>
</ul></li>
<li><code>lowerdir</code> 的多个目录中，如果存在同名的目录，在 merged 目录看来，会包含这些 lowerdir 中的所有文件，而目录的元信息则取最左侧的目录的元信息。

<ul>
<li>读取 <code>merged/from-lower1-lower2-dir</code> 目录项目，包含 <code>lower2/from-lower1-lower2-file</code> 和 <code>lower1/from-lower1-lower2-file</code> 目录的全部内容。</li>
<li>读取 <code>merged/from-lower1-lower2-dir</code> 目录元信息，内容为 <code>lower2/from-lower1-lower2-file</code> 的元信息。</li>
</ul></li>
<li>性能（无 cache 情况）：可以看出 overlayfs 和常规文件系统相比，其列出目录项的性能和 lowerdir 的数目有关，如果层数为 n，则 overlayfs 列出目录项的时间复杂度为 <code>O(n)</code>。</li>
</ul>

<h2 id="操作-merged-目录">操作 merged 目录</h2>

<p>下面实现代码将一个 lowerdir 通过 overlayfs 生成到 merged 目录中，并在 merged 目录中执行：新建、修改、删除、移动操作。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span><span style="color:#75715e"># sudo apt install attr</span>
<span style="color:#75715e"># sudo ./src/shell/03-overlayfs/01-operate-merged.sh</span>

<span style="color:#75715e"># 创建并进入测试目录</span>
exp_base_dir<span style="color:#f92672">=</span>/tmp/overlayfs-exp/01-operate-merged
umount $exp_base_dir/merged &gt;/dev/null <span style="color:#ae81ff">2</span>&gt;&amp;<span style="color:#ae81ff">1</span>
rm -rf $exp_base_dir <span style="color:#f92672">&amp;&amp;</span> mkdir -p $exp_base_dir
cd $exp_base_dir

<span style="color:#75715e"># 准备 lower、merged、upper、work 目录</span>
mkdir -p lower1 merged upper work
mkdir -p lower1/from-lower1-dir lower1/from-lower1-dir2 lower1/from-lower1-dir3
mkdir -p lower1/from-lower1-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir/subdir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir2/file1
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir2/file2
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-file2
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir3/file1
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir3/file2
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-file3

<span style="color:#75715e"># 生成 merged</span>
mount -t overlay overlay -olowerdir<span style="color:#f92672">=</span>lower1,upperdir<span style="color:#f92672">=</span>upper,workdir<span style="color:#f92672">=</span>work merged

<span style="color:#75715e"># 在 merged 新建</span>
echo <span style="color:#e6db74">&#39;from-merged&#39;</span> &gt; merged/from-merged-file
echo <span style="color:#e6db74">&#39;from-merged&#39;</span> &gt; merged/from-lower1-dir/from-merged-file
mkdir -p merged/from-merged-dir/subdir
mkdir -p merged/from-lower1-dir/from-merged-dir/subdir

<span style="color:#75715e"># 在 merged 修改</span>
echo <span style="color:#e6db74">&#39;from-merged&#39;</span> &gt;&gt; merged/from-lower1-file
echo <span style="color:#e6db74">&#39;from-merged&#39;</span> &gt;&gt; merged/from-lower1-dir/file
touch -t <span style="color:#ae81ff">197001010000</span> merged/from-lower1-dir/subdir

<span style="color:#75715e"># 在 merged 删除</span>
rm -rf merged/from-lower1-dir2
rm -rf merged/from-lower1-file2

<span style="color:#75715e"># 在 merged 移动</span>
mv merged/from-lower1-file3 merged/from-lower1-file3-moved
mv merged/from-lower1-dir3 merged/from-lower1-dir3-moved

<span style="color:#75715e"># 观察情况</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged/&#39;</span>
tree merged/
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree upper/&#39;</span>
tree upper/
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat upper/from-lower1-file&#39;</span>
cat upper/from-lower1-file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat upper/from-lower1-dir/file&#39;</span>
cat upper/from-lower1-dir/file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; stat upper/from-lower1-dir/subdir&#39;</span>
stat upper/from-lower1-dir/subdir
echo


echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; stat upper/from-lower1-dir2&#39;</span>
stat upper/from-lower1-dir2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; attr -l upper/from-lower1-dir2&#39;</span>
attr -l upper/from-lower1-dir2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; stat upper/from-lower1-file2&#39;</span>
stat upper/from-lower1-file2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; attr -l upper/from-lower1-file2&#39;</span>
attr -l upper/from-lower1-file2
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&gt;&gt;&gt; tree merged/
merged/
├── from-lower1-dir
│   ├── file
│   ├── from-merged-dir
│   │   └── subdir
│   ├── from-merged-file
│   └── subdir
│       └── file
├── from-lower1-dir3-moved
│   ├── file1
│   └── file2
├── from-lower1-file
├── from-lower1-file3-moved
├── from-merged-dir
│   └── subdir
└── from-merged-file

7 directories, 8 files

&gt;&gt;&gt; tree upper/
upper/
├── from-lower1-dir
│   ├── file
│   ├── from-merged-dir
│   │   └── subdir
│   ├── from-merged-file
│   └── subdir
├── from-lower1-dir2
├── from-lower1-dir3
├── from-lower1-dir3-moved
│   ├── file1
│   └── file2
├── from-lower1-file
├── from-lower1-file2
├── from-lower1-file3
├── from-lower1-file3-moved
├── from-merged-dir
│   └── subdir
└── from-merged-file

7 directories, 11 files

&gt;&gt;&gt; stat upper/from-lower1-dir2
  文件：upper/from-lower1-dir2
  大小：0               块：0          IO 块：4096   字符特殊文件
设备：fe01h/65025d      Inode：1625522     硬链接：4     设备类型：0,0
权限：(0000/c---------)  Uid：(    0/    root)   Gid：(    0/    root)
最近访问：2023-11-18 22:31:33.590898113 +0800
最近更改：2023-11-18 22:31:33.590898113 +0800
最近改动：2023-11-18 22:31:33.758896324 +0800
创建时间：2023-11-18 22:31:33.590898113 +0800

&gt;&gt;&gt; attr -l upper/from-lower1-dir2

&gt;&gt;&gt; stat upper/from-lower1-file2
  文件：upper/from-lower1-file2
  大小：0               块：0          IO 块：4096   字符特殊文件
设备：fe01h/65025d      Inode：1625522     硬链接：4     设备类型：0,0
权限：(0000/c---------)  Uid：(    0/    root)   Gid：(    0/    root)
最近访问：2023-11-18 22:31:33.590898113 +0800
最近更改：2023-11-18 22:31:33.590898113 +0800
最近改动：2023-11-18 22:31:33.758896324 +0800
创建时间：2023-11-18 22:31:33.590898113 +0800

&gt;&gt;&gt; attr -l upper/from-lower1-file2</pre></div>
<p>可以得出如下结论：</p>

<ul>
<li>在 merged 中新增文件或目录，会在 upper 中创建并存储。</li>
<li>在 merged 中修改文件或目录。

<ul>
<li>如果修改的是文件，则会将文件从 lower 中复制到 upper 中，并修改。</li>
<li>如果修改的是目录，则会将在 upper 中创建同路径的目录，其他属性和 lower 最左侧的相同，时间属性不同。</li>
</ul></li>
<li>在 merged 中删除文件或目录，会在 upper 中同级路径，创建一个设别号为 0/0 的字符设备。</li>
<li>在 merged 中移动文件或目录，会在 upper 中将原路径创建一个设别号为 0/0 的字符设备，在新路径中创建一个新的文件或目录。</li>
<li>性能（无 cache 情况）：

<ul>
<li>新增，和常规文件系统相比类似。</li>
<li>修改，和常规文件系统相比，会多出一个 copy 的耗时，对于大文件来说劣化严重。</li>
<li>删除，和常规文件系统相比，行为不同，但是性能类似。</li>
<li>移动，和常规文件系统相比，行为不同，但是性能类似。</li>
</ul></li>
</ul>

<h2 id="操作-lower-目录">操作 lower 目录</h2>

<p>下面实现代码将一个 lower1 lower2 通过 overlayfs 生成到 merged 目录后，再在 lower2 目录中执行：新建、覆盖、隐藏操作。</p>

<p>注意，该操作（在线修改 overlayfs 的底层文件系统），在<a href="https://docs.kernel.org/filesystems/overlayfs.html#changes-to-underlying-filesystems">内核文档</a>中，是未定义的，但文档也明确说明了该行为不会导致 crash 或死锁。</p>

<p>本部分就是探索在 Linux 的实现中，操作 lower 目录的行为到底是什么样的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span><span style="color:#75715e"># sudo apt install attr</span>
<span style="color:#75715e"># sudo ./src/shell/03-overlayfs/02-operate-lower.sh</span>

<span style="color:#75715e"># 创建并进入测试目录</span>
exp_base_dir<span style="color:#f92672">=</span>/tmp/overlayfs-exp/02-operate-lower
umount $exp_base_dir/merged &gt;/dev/null <span style="color:#ae81ff">2</span>&gt;&amp;<span style="color:#ae81ff">1</span>
rm -rf $exp_base_dir <span style="color:#f92672">&amp;&amp;</span> mkdir -p $exp_base_dir
cd $exp_base_dir

<span style="color:#75715e"># 准备 lower、merged、upper、work 目录</span>
mkdir -p lower1 lower2 merged upper work
mkdir -p lower1/from-lower1-dir lower1/from-lower1-dir2 lower1/from-lower1-lowner2-dir lower1/from-lower2-opaquedir
mkdir -p lower1/from-lower1-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-dir2/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower1-lowner2-dir/file
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/file1
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/file2
echo <span style="color:#e6db74">&#39;from-lower1&#39;</span> &gt; lower1/from-lower2-opaquedir/file1

<span style="color:#75715e"># 生成 merged</span>
mount -t overlay overlay -olowerdir<span style="color:#f92672">=</span>lower2:lower1,upperdir<span style="color:#f92672">=</span>upper,workdir<span style="color:#f92672">=</span>work merged
mkdir -p merged/from-merged-dir
echo <span style="color:#e6db74">&#39;from-merged&#39;</span> &gt; merged/from-merged-dir/file1

<span style="color:#75715e"># 操作之前</span>
echo <span style="color:#e6db74">&#39;=== before ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/file1&#39;</span>
cat merged/file1
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; ls -al merged/from-lower1-lowner2-dir&#39;</span>
ls -al merged/from-lower1-lowner2-dir
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; ls -al merged/from-lower2-opaquedir&#39;</span>
ls -al merged/from-lower2-opaquedir
echo

<span style="color:#75715e"># lower2 新增文件</span>
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower2-file
mkdir -p lower2/from-lower2-dir
mkdir -p lower2/from-lower1-lowner2-dir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lowner2-dir/from-lower2-file
mkdir -p lower2/from-lower1-lowner2-dir/subdir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lowner2-dir/subdir/from-lower2-file
mkdir -p lower2/from-merged-dir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-merged-dir/file2
touch -t <span style="color:#ae81ff">197001010000</span> lower2/from-merged-dir

<span style="color:#75715e"># lower2 覆盖文件</span>
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/file1
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/file2
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower1-lowner2-dir/file

<span style="color:#75715e"># lower2 隐藏文件目录</span>
mknod lower2/from-lower1-dir c <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>
mkdir -p lower2/from-lower1-dir2
mknod lower2/from-lower1-dir2/file c <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>

<span style="color:#75715e"># lower2 opaque 目录</span>
mkdir -p lower2/from-lower2-opaquedir
echo <span style="color:#e6db74">&#39;from-lower2&#39;</span> &gt; lower2/from-lower2-opaquedir/file2
setfattr -n <span style="color:#e6db74">&#39;trusted.overlay.opaque&#39;</span> -v <span style="color:#e6db74">&#39;y&#39;</span> lower2/from-lower2-opaquedir  <span style="color:#75715e"># 不能用 attr 命令，因为 attr 会自动添加 user. 前缀</span>

<span style="color:#75715e"># 操作之后</span>
echo <span style="color:#e6db74">&#39;=== after ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged&#39;</span>
tree merged
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/file1&#39;</span>
cat merged/file1
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/file2&#39;</span>
cat merged/file2
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower1-lowner2-dir/file&#39;</span>
cat merged/from-lower1-lowner2-dir/file
echo


<span style="color:#75715e"># 清理缓存后</span>
echo <span style="color:#ae81ff">2</span> &gt; /proc/sys/vm/drop_caches
echo <span style="color:#e6db74">&#39;=== after clear cache ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged&#39;</span>
tree merged
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/file1&#39;</span>
cat merged/file1
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/file2&#39;</span>
cat merged/file2
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower1-lowner2-dir/file&#39;</span>
cat merged/from-lower1-lowner2-dir/file
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== before ===
&gt;&gt;&gt; cat merged/file1
from-lower1

&gt;&gt;&gt; ls -al merged/from-lower1-lowner2-dir
总用量 12
drwxr-xr-x 2 root root 4096 11月 19 02:57 .
drwxr-xr-x 1 root root 4096 11月 19 02:57 ..
-rw-r--r-- 1 root root   12 11月 19 02:57 file

&gt;&gt;&gt; ls -al merged/from-lower2-opaquedir
总用量 12
drwxr-xr-x 2 root root 4096 11月 19 02:57 .
drwxr-xr-x 1 root root 4096 11月 19 02:57 ..
-rw-r--r-- 1 root root   12 11月 19 02:57 file1

=== after ===
&gt;&gt;&gt; tree merged
merged
├── file1
├── file2
├── from-lower1-dir2
├── from-lower1-lowner2-dir
│   └── file
├── from-lower2-dir
├── from-lower2-file
├── from-lower2-opaquedir
│   └── file1
└── from-merged-dir
    └── file1

5 directories, 6 files

&gt;&gt;&gt; cat merged/file1
from-lower1

&gt;&gt;&gt; cat merged/file2
from-lower2

&gt;&gt;&gt; cat merged/from-lower1-lowner2-dir/file
from-lower1

=== after clear cache ===
&gt;&gt;&gt; tree merged
merged
├── file1
├── file2
├── from-lower1-dir2
├── from-lower1-lowner2-dir
│   ├── file
│   ├── from-lower2-file
│   └── subdir
│       └── from-lower2-file
├── from-lower2-dir
├── from-lower2-file
├── from-lower2-opaquedir
│   └── file2
└── from-merged-dir
    └── file1

6 directories, 8 files

&gt;&gt;&gt; cat merged/file1
from-lower2

&gt;&gt;&gt; cat merged/file2
from-lower2

&gt;&gt;&gt; cat merged/from-lower1-lowner2-dir/file
from-lower2</pre></div>
<p>在 lower2 上的操作，可以得出如下结论（内核版本： 5.10.0-20-amd64）：</p>

<ul>
<li>总体上， overlayfs 在设计上存在一个假设，即 lowerdir 是不可变的。因此，overlay 会对 lowerdir 的 inode 进行 cache。</li>
<li>新增：当新增的文件或目录的所在目录，没被 cache 过（目录没被读取过），则在该目录下新建的文件在 merged 可见，否则不可见。</li>
<li>覆盖：当要覆盖的文件的所在目录，没被 cache 过（目录没被读取过），则在覆盖不生效，否则生效。</li>
<li>删除：创建设备号为 0,0 的字符设备可以实现，当要删除的文件所在目录，没被 cache 过（目录没被读取过），则在删除不生效，否则生效。</li>
<li>让下层变透明：通过设置 lower2 目录 <code>from-lower2-opaquedir</code> 的 <a href="https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories"><code>trusted.overlay.opaque</code> 属性为 <code>y</code></a> 可以隐藏掉 lower1 目录 <code>from-lower2-opaquedir</code> 中的所有内容（让下层变透明，即表示查找当前层就停止，不要再向下遍历），如果该目录没被 cache 过（目录没被读取过），则在设置不生效，否则生效。</li>
<li>可以通过 <code>echo 2 &gt; /proc/sys/vm/drop_caches</code> 清理目录项和 inode， lower2 的变更立即生效（就像重新 mount 一样）。</li>
<li>在 merged 新建一个在所有 lower 中都没有的目录后，再在 lower2 中创建这个目录，则 lower2 中目录的这个目录将被完全的遮蔽，即使清理缓存，重新挂载都不能被 merged 中看到。原因是： overlayfs 会尽量让 inode 和底层文件系统保持一致，而在本例中 lower2 和 upper 在同一个底层文件系统，overlayfs 在遍历过程中，会遍历并取 inode 最小的那个，并停止遍历，因此 <code>lower2/from-merged-dir/file2</code> 永远看不到。如果想让 file2 被看到，解决办法是：

<ul>
<li>让 lower2 目录和 upper 目录处于不同的文件系统中，如 lower2 在 NFS，upper 在本地磁盘，这样 merged 的 inode 将不会复用底层文件系统的，而是从 0 开始生成。</li>
<li>让 upper 目录的 inode 比 lower 目录的大（在 upper 目录，先 cp 备份，再 rm，mv 回来）。</li>
</ul></li>
</ul>

<h2 id="操作-upper-目录">操作 upper 目录</h2>

<p>下面实现代码将一个 lower 通过 overlayfs 生成到 merged 目录后，再在 upper 目录中执行：新建、覆盖、删除、透明操作。</p>

<p>注意，该操作（在线修改 overlayfs 的底层文件系统），在<a href="https://docs.kernel.org/filesystems/overlayfs.html#changes-to-underlying-filesystems">内核文档</a>中，是未定义的，但文档也明确说明了该行为不会导致 crash 或死锁。</p>

<p>本部分就是探索在 Linux 的实现中，操作 upper 目录的行为到底是什么样的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span><span style="color:#75715e"># sudo apt install attr</span>
<span style="color:#75715e"># sudo ./src/shell/03-overlayfs/03-operate-upper.sh</span>

<span style="color:#75715e"># 创建并进入测试目录</span>
exp_base_dir<span style="color:#f92672">=</span>/tmp/overlayfs-exp/03-operate-upper
umount $exp_base_dir/merged &gt;/dev/null <span style="color:#ae81ff">2</span>&gt;&amp;<span style="color:#ae81ff">1</span>
rm -rf $exp_base_dir <span style="color:#f92672">&amp;&amp;</span> mkdir -p $exp_base_dir
cd $exp_base_dir

<span style="color:#75715e"># 准备 lower、merged、upper、work 目录</span>
mkdir -p lower merged upper work
mkdir -p lower/from-lower-dir1
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-dir1/from-lower-file
mkdir -p lower/from-lower-dir2
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-dir2/from-lower-file
mkdir -p lower/from-lower-dir3
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-dir3/from-lower-file
mkdir -p lower/from-lower-dir4
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-dir4/from-lower-file
mkdir -p lower/from-lower-dir5
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-dir5/from-lower-file
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-file1
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-file2
echo <span style="color:#e6db74">&#39;from-lower&#39;</span> &gt; lower/from-lower-file3

<span style="color:#75715e"># 生成 merged</span>
mount -t overlay overlay -olowerdir<span style="color:#f92672">=</span>lower,upperdir<span style="color:#f92672">=</span>upper,workdir<span style="color:#f92672">=</span>work merged

<span style="color:#75715e"># 操作之前</span>
echo <span style="color:#e6db74">&#39;=== before ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-file1&#39;</span>
cat merged/from-lower-file1
echo
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; ls merged/from-lower-dir1&#39;</span>
cat merged/from-lower-dir1
echo

<span style="color:#75715e"># 新增</span>
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-upper-file
mkdir -p upper/from-lower-dir1
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-dir1/from-upper-file
mkdir -p upper/from-lower-dir2
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-dir2/from-upper-file
mkdir -p upper/from-upper-dir

<span style="color:#75715e"># 覆盖</span>
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-file1
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-file2
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-dir1/from-lower-file
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-dir2/from-lower-file

<span style="color:#75715e"># 删除</span>
mknod upper/from-lower-file3 c <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>
mknod upper/from-lower-dir3 c <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>
mkdir upper/from-lower-dir4
mknod upper/from-lower-dir4/from-lower-file c <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>

<span style="color:#75715e"># 透明</span>
mkdir upper/from-lower-dir5
setfattr -n <span style="color:#e6db74">&#39;trusted.overlay.opaque&#39;</span> -v <span style="color:#e6db74">&#39;y&#39;</span> upper/from-lower-dir5  <span style="color:#75715e"># 不能用 attr 命令，因为 attr 会自动添加 user. 前缀</span>
echo <span style="color:#e6db74">&#39;from-upper&#39;</span> &gt; upper/from-lower-dir5/from-upper-file


<span style="color:#75715e"># 观察</span>
<span style="color:#75715e"># 操作后</span>
echo <span style="color:#e6db74">&#39;=== after ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged/&#39;</span>
tree merged/
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-file1&#39;</span>
cat merged/from-lower-file1
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-file2&#39;</span>
cat merged/from-lower-file2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-dir1/from-lower-file&#39;</span>
cat merged/from-lower-dir1/from-lower-file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-dir2/from-lower-file&#39;</span>
cat merged/from-lower-dir2/from-lower-file
echo


<span style="color:#75715e"># 清理缓存后</span>
echo <span style="color:#ae81ff">2</span> &gt; /proc/sys/vm/drop_caches
echo <span style="color:#e6db74">&#39;=== after clear cache ===&#39;</span>
echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; tree merged/&#39;</span>
tree merged/
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-file1&#39;</span>
cat merged/from-lower-file1
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-file2&#39;</span>
cat merged/from-lower-file2
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-dir1/from-lower-file&#39;</span>
cat merged/from-lower-dir1/from-lower-file
echo

echo <span style="color:#e6db74">&#39;&gt;&gt;&gt; cat merged/from-lower-dir2/from-lower-file&#39;</span>
cat merged/from-lower-dir2/from-lower-file
echo</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== before ===
&gt;&gt;&gt; cat merged/from-lower-file1
from-lower

&gt;&gt;&gt; ls merged/from-lower-dir1
cat: merged/from-lower-dir1: 是一个目录

=== after ===
&gt;&gt;&gt; tree merged/
merged/
├── from-lower-dir1
│   └── from-lower-file
├── from-lower-dir2
│   ├── from-lower-file
│   └── from-upper-file
├── from-lower-dir4
├── from-lower-dir5
│   └── from-upper-file
├── from-lower-file1
├── from-lower-file2
├── from-upper-dir
└── from-upper-file

5 directories, 7 files

&gt;&gt;&gt; cat merged/from-lower-file1
from-lower

&gt;&gt;&gt; cat merged/from-lower-file2
from-upper

&gt;&gt;&gt; cat merged/from-lower-dir1/from-lower-file
from-lower

&gt;&gt;&gt; cat merged/from-lower-dir2/from-lower-file
from-upper

=== after clear cache ===
&gt;&gt;&gt; tree merged/
merged/
├── from-lower-dir1
│   ├── from-lower-file
│   └── from-upper-file
├── from-lower-dir2
│   ├── from-lower-file
│   └── from-upper-file
├── from-lower-dir4
├── from-lower-dir5
│   └── from-upper-file
├── from-lower-file1
├── from-lower-file2
├── from-upper-dir
└── from-upper-file

5 directories, 8 files

&gt;&gt;&gt; cat merged/from-lower-file1
from-upper

&gt;&gt;&gt; cat merged/from-lower-file2
from-upper

&gt;&gt;&gt; cat merged/from-lower-dir1/from-lower-file
from-upper

&gt;&gt;&gt; cat merged/from-lower-dir2/from-lower-file
from-upper</pre></div>
<p>在 upper 上的操作，可以得出如下结论（内核版本： 5.10.0-20-amd64）：</p>

<ul>
<li>总体上， overlayfs 假设 upper 目录只有 overlayfs 内核能力操作 。因此，overlay 会对 upper 的 inode 进行 cache。</li>
<li>如果在 merged 目录，对应的 upper 目录或文件被读取过，则对这些在 upper 目录中的文件或目录进行手动修改，在 merged 目录中是不可见的。</li>
<li>当 cache 失效或者手动执行 <code>echo 2 &gt; /proc/sys/vm/drop_caches</code> 清理缓存，对 upper 目录的操作将会在 merged 目录中可见。</li>
<li>对 upper 目录的 新建、覆盖、删除、透明操作的行为，和对 lower 目录的操作一致，在此不多赘述。</li>
</ul>
]]></description></item><item><title>Containerd 详解（五） 自定义 snapshotter</title><link>https://www.rectcircle.cn/posts/containerd-5-custom-snapshotter/</link><pubDate>Thu, 05 Oct 2023 00:36:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/containerd-5-custom-snapshotter/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://github.com/containerd/containerd/tree/v1.7.0">v1.7.0</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>上文已经详细了解了 overlay snapshotter 的源码。本文将以如下场景为例，实现一个自定义的 snapshotter。</p>

<p>一般情况下，如果想给容器添加一些额外的文件，一般都是通过挂载宿主目录的的方式来实现。</p>

<p>但是有些场景（如：编译场景的依赖库缓存加速），需要基于以这些文件为基础进行修改，又要求这些更改，不影响其他容器时，宿主机挂载就无法满足需求。</p>

<p>这种场景，可以利用 overlayfs 的特性，在镜像的 lower 层之上，再添加一个 lower 层目录来实现。</p>

<h2 id="设计">设计</h2>

<p>基于 containerd 内置的 overlay snapshotter 实现一个自定义 snapshotter 插件： 这个插件会通过 snapshotter labels 指定附加的宿主机目录，添加到 mount option 的 lower 中。</p>

<h2 id="实现">实现</h2>

<blockquote>
<p>源码：<a href="https://github.com/rectcircle/overlay-custom-add-lower-snapshotter">rectcircle/overlay-custom-add-lower-snapshotter</a></p>
</blockquote>

<p><code>snapshotter/constants.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">snapshotter</span>

<span style="color:#66d9ef">const</span> (
	<span style="color:#75715e">// 改插件默认的存储路径
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">DefaultRootDir</span> = <span style="color:#e6db74">&#34;/var/lib/containerd/cn.rectcircle.containerd.overlay-custom-add-lower-snapshotter&#34;</span>
	<span style="color:#75715e">// 该插件提供 grpc 服务的 socks 文件名，路径为 paths.Join(rootDir, SocksFileName)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 默认为 /var/lib/containerd/cn.rectcircle.containerd.overlay-custom-add-lower-snapshotter/grpc.socks
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">SocksFileName</span> = <span style="color:#e6db74">&#34;grpc.socks&#34;</span>
	<span style="color:#75715e">// 实现添加自定义 lower 路径的 label key，支持多个路径，以分号 : 分隔。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// label 必须以 containerd.io/snapshot/ 开头，参见，containerd 源码：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   `snapshots/snapshotter.go@FilterInheritedLabels`
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   `metadata/snapshot.go@createSnapshot`
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">LabelCustomAddLowerPaths</span> = <span style="color:#e6db74">&#34;containerd.io/snapshot/overlay-custom-add-lower.paths&#34;</span>
)</code></pre></div>
<p><code>cmd/overlay-custom-add-lower-snapshotter/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;path&#34;</span>

	<span style="color:#e6db74">&#34;github.com/urfave/cli/v2&#34;</span>

	<span style="color:#a6e22e">snapshotsapi</span> <span style="color:#e6db74">&#34;github.com/containerd/containerd/api/services/snapshots/v1&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/contrib/snapshotservice&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/snapshots/overlay&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/overlay-custom-add-lower-snapshotter/snapshotter&#34;</span>
	<span style="color:#e6db74">&#34;google.golang.org/grpc&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {

	<span style="color:#a6e22e">app</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">App</span>{
		<span style="color:#a6e22e">Name</span>:  <span style="color:#e6db74">&#34;overlay-custom-add-lower-snapshotter&#34;</span>,
		<span style="color:#a6e22e">Usage</span>: <span style="color:#e6db74">&#34;Run a custom-add-lower overlay containerd snapshotter&#34;</span>,
		<span style="color:#a6e22e">Flags</span>: []<span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">Flag</span>{
			<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">StringFlag</span>{
				<span style="color:#a6e22e">Name</span>:  <span style="color:#e6db74">&#34;root-dir&#34;</span>,
				<span style="color:#a6e22e">Value</span>: <span style="color:#a6e22e">snapshotter</span>.<span style="color:#a6e22e">DefaultRootDir</span>,
				<span style="color:#a6e22e">Usage</span>: <span style="color:#e6db74">&#34;Adds as an optional label \&#34;containerd.io/snapshot/overlay.upperdir\&#34;&#34;</span>,
			},
			<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">BoolFlag</span>{
				<span style="color:#a6e22e">Name</span>:  <span style="color:#e6db74">&#34;async-remove&#34;</span>,
				<span style="color:#a6e22e">Value</span>: <span style="color:#66d9ef">true</span>,
				<span style="color:#a6e22e">Usage</span>: <span style="color:#e6db74">&#34;Defers removal of filesystem content until the Cleanup method is called&#34;</span>,
			},
			<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">BoolFlag</span>{
				<span style="color:#a6e22e">Name</span>:  <span style="color:#e6db74">&#34;upperdir-label&#34;</span>,
				<span style="color:#a6e22e">Value</span>: <span style="color:#66d9ef">false</span>,
				<span style="color:#a6e22e">Usage</span>: <span style="color:#e6db74">&#34;AsynchronousRemove defers removal of filesystem content until the Cleanup method is called&#34;</span>,
			},
		},
		<span style="color:#a6e22e">Action</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">cli</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
			<span style="color:#75715e">// 创建 snapshotter
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">root</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;root-dir&#34;</span>)
			<span style="color:#a6e22e">sOpts</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">Opt</span>{}
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Bool</span>(<span style="color:#e6db74">&#34;async-remove&#34;</span>) {
				<span style="color:#a6e22e">sOpts</span> = append(<span style="color:#a6e22e">sOpts</span>, <span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">AsynchronousRemove</span>)
			}
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Bool</span>(<span style="color:#e6db74">&#34;upperdir-label&#34;</span>) {
				<span style="color:#a6e22e">sOpts</span> = append(<span style="color:#a6e22e">sOpts</span>, <span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">WithUpperdirLabel</span>)
			}
			<span style="color:#a6e22e">sn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">snapshotter</span>.<span style="color:#a6e22e">NewSnapshotter</span>(<span style="color:#a6e22e">root</span>, <span style="color:#a6e22e">sOpts</span><span style="color:#f92672">...</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}
			<span style="color:#75715e">// 封装成 grpc service
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">service</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">snapshotservice</span>.<span style="color:#a6e22e">FromSnapshotter</span>(<span style="color:#a6e22e">sn</span>)
			<span style="color:#75715e">// 创建一个 rpc server
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">rpc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">NewServer</span>()
			<span style="color:#75715e">// 将 grpc service 注册到 grpc server
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">snapshotsapi</span>.<span style="color:#a6e22e">RegisterSnapshotsServer</span>(<span style="color:#a6e22e">rpc</span>, <span style="color:#a6e22e">service</span>)
			<span style="color:#75715e">// Listen and serve
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">socksPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">root</span>, <span style="color:#a6e22e">snapshotter</span>.<span style="color:#a6e22e">SocksFileName</span>)
			<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">RemoveAll</span>(<span style="color:#a6e22e">socksPath</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}
			<span style="color:#a6e22e">l</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Listen</span>(<span style="color:#e6db74">&#34;unix&#34;</span>, <span style="color:#a6e22e">socksPath</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
			}
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">rpc</span>.<span style="color:#a6e22e">Serve</span>(<span style="color:#a6e22e">l</span>)
		},
	}

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
}</code></pre></div>
<p><code>snapshotter/snapshotter.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">snapshotter</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>

	<span style="color:#e6db74">&#34;github.com/containerd/containerd/mount&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/snapshots&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/snapshots/overlay&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewSnapshotter</span>(<span style="color:#a6e22e">root</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">Opt</span>) (<span style="color:#a6e22e">snapshots</span>.<span style="color:#a6e22e">Snapshotter</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">sn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">NewSnapshotter</span>(<span style="color:#a6e22e">root</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span>{<span style="color:#a6e22e">sn</span>}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// overlayCustomAddLowerSnapshotter 继承 overlay Snapshotter，在返回 mounts 的地方进行改造
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">snapshots</span>.<span style="color:#a6e22e">Snapshotter</span>
}

<span style="color:#75715e">// Mounts implements snapshots.Snapshotter.
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span>) <span style="color:#a6e22e">Mounts</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) ([]<span style="color:#a6e22e">mount</span>.<span style="color:#a6e22e">Mount</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">mounts</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Snapshotter</span>.<span style="color:#a6e22e">Mounts</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">tryAddLowers</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">mounts</span>)
}

<span style="color:#75715e">// Prepare implements snapshots.Snapshotter.
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span>) <span style="color:#a6e22e">Prepare</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">parent</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">snapshots</span>.<span style="color:#a6e22e">Opt</span>) ([]<span style="color:#a6e22e">mount</span>.<span style="color:#a6e22e">Mount</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">mounts</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Snapshotter</span>.<span style="color:#a6e22e">Prepare</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">parent</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">tryAddLowers</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">mounts</span>)
}

<span style="color:#75715e">// View implements snapshots.Snapshotter.
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span>) <span style="color:#a6e22e">View</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">parent</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">snapshots</span>.<span style="color:#a6e22e">Opt</span>) ([]<span style="color:#a6e22e">mount</span>.<span style="color:#a6e22e">Mount</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">mounts</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Snapshotter</span>.<span style="color:#a6e22e">View</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">parent</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">tryAddLowers</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">mounts</span>)
}

<span style="color:#75715e">// tryAddLowers 所有返回 mounts 的地方，都需要调用该函数，根据 label ，给 lower 选项添加自定义的 lower 路径。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">overlayCustomAddLowerSnapshotter</span>) <span style="color:#a6e22e">tryAddLowers</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">mounts</span> []<span style="color:#a6e22e">mount</span>.<span style="color:#a6e22e">Mount</span>) ([]<span style="color:#a6e22e">mount</span>.<span style="color:#a6e22e">Mount</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">mounts</span>) <span style="color:#f92672">!=</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">mounts</span>[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">Type</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;overlay&#34;</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mounts</span>, <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#a6e22e">info</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Snapshotter</span>.<span style="color:#a6e22e">Stat</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">lowerPathString</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">Labels</span>[<span style="color:#a6e22e">LabelCustomAddLowerPaths</span>]
	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">lowerPathString</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mounts</span>, <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#a6e22e">lowerPaths</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">lowerPathString</span>, <span style="color:#e6db74">&#34;:&#34;</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">p</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">lowerPaths</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">p</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
			<span style="color:#66d9ef">continue</span>
		}
		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">MkdirAll</span>(<span style="color:#a6e22e">p</span>, <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o755</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;mkdir lower path %s error: %s&#34;</span>, <span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">o</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">mounts</span>[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">Options</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">HasPrefix</span>(<span style="color:#a6e22e">o</span>, <span style="color:#e6db74">&#34;lowerdir=&#34;</span>) {
			<span style="color:#a6e22e">mounts</span>[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">Options</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#34;lowerdir=&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">lowerPathString</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;:&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimPrefix</span>(<span style="color:#a6e22e">o</span>, <span style="color:#e6db74">&#34;lowerdir=&#34;</span>)
			<span style="color:#66d9ef">break</span>
		}
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mounts</span>, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<h2 id="编译">编译</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go build ./cmd/overlay-custom-add-lower-snapshotter</code></pre></div>
<h2 id="containerd-使用">containerd 使用</h2>

<h3 id="启动">启动</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ./overlay-custom-add-lower-snapshotter</code></pre></div>
<h3 id="配置">配置</h3>

<p><code>/etc/containerd/config.toml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">version</span> = <span style="color:#ae81ff">2</span>

[<span style="color:#a6e22e">proxy_plugins</span>]
  [<span style="color:#a6e22e">proxy_plugins</span>.<span style="color:#a6e22e">overlay</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#a6e22e">custom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#a6e22e">add</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#a6e22e">lower</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#a6e22e">snapshotter</span>]
    <span style="color:#a6e22e">type</span> = <span style="color:#e6db74">&#34;snapshot&#34;</span>
    <span style="color:#a6e22e">address</span> = <span style="color:#e6db74">&#34;/var/lib/containerd/cn.rectcircle.containerd.overlay-custom-add-lower-snapshotter/grpc.socks&#34;</span></code></pre></div>
<p>配置 containerd namespace 的默认 snapshotter。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ctr namespace label default containerd.io/defaults/snapshotter<span style="color:#f92672">=</span>overlay-custom-add-lower-snapshotter
<span style="color:#75715e"># 验证完恢复现场如下：</span>
<span style="color:#75715e"># sudo ctr namespace label default containerd.io/defaults/snapshotter=</span></code></pre></div>
<h3 id="验证">验证</h3>

<p><strong>拉取一个新的镜像</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ctr images pull docker.io/library/nginx:1.25</code></pre></div>
<p>观察路径， <code>sudo ls -al /var/lib/containerd/cn.rectcircle.containerd.overlay-custom-add-lower-snapshotter/snapshots</code> 有输出子目录。</p>

<p><strong>使用上述 snapshotter label 启动容器</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ctr run --snapshotter-label containerd.io/snapshot/overlay-custom-add-lower.paths<span style="color:#f92672">=</span>/tmp/overlayfs-custom-lower --rm -t docker.io/library/nginx:1.25 nginx-with-custom-lower bash</code></pre></div>
<p>观察：</p>

<ul>
<li>在容器外执行 <code>sudo ctr snapshot --snapshotter overlay-custom-add-lower-snapshotter info nginx-with-custom-lower</code> 可以观察到 label <code>containerd.io/snapshot/overlay-custom-add-lower.paths</code> 存在。</li>
<li>在容器内执行 <code>ls -al /</code> 是个标准的 linux 目录。</li>
<li>在容器外执行 <code>sudo mkdir /tmp/overlayfs-custom-lower/test_dir</code>。</li>
<li>在容器内执行 <code>ls -al /</code> 发现，多了一个 /test_dir 目录。</li>
<li>在容器内执行 <code>touch /test_dir/incontainer</code>。</li>
<li>在容器外执行 <code>sudo ls -al /tmp/overlayfs-custom-lower/test_dir</code> 仍然是空目录。</li>
<li>在容器外执行 <code>sudo touch /tmp/overlayfs-custom-lower/test_dir/after-outcontainer</code></li>
<li>在容器内执行 <code>ls -al /test_dir</code> 发现 <code>after-outcontainer</code> 和 <code>incontainer</code> 均存在。</li>
</ul>

<h2 id="kubernetes-适配">kubernetes 适配</h2>

<p>一般自定义 snapshotter 都是要在 kubernetes 中使用的，因此需要配置 cri 的 snapshotter 为自定义 snapshotter，值为 <code>proxy_plugins.xxx</code> 的 <code>xxx</code>。</p>

<p><code>/etc/containerd/config.toml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a6e22e">version</span> = <span style="color:#ae81ff">2</span>
[<span style="color:#a6e22e">plugins</span>.<span style="color:#e6db74">&#34;io.containerd.grpc.v1.cri&#34;</span>.<span style="color:#a6e22e">containerd</span>]
  <span style="color:#a6e22e">snapshotter</span> = <span style="color:#e6db74">&#34;overlay-custom-add-lower-snapshotter&#34;</span></code></pre></div>
<p>此外，默认情况下，cri 默认不会传递任何 snapshot labels 到 pod 业务容器的 snapshot 中，而如上改动是依赖特殊 label 传递配置的。而在 kubernetes 特殊定制的特性一般通过 pod 的 annotation 或 label 来传递。</p>

<p>因此，为了将该特性透传到在 kubernetes 中，需要：</p>

<ul>
<li>定义该特性的 pod annotation 或 label 的 key 以及语义。</li>
<li>修改 cri 的 CreateContainer （<code>pkg/cri/server/container_create.go</code>）逻辑，在 <code>sOpts, err := snapshotterOpts(c.config.ContainerdConfig.Snapshotter, config)</code> 源码附近，根据需求将 kubernetes pod annotation 或 label 转化为 snapshot label。</li>
</ul>

<p>具体实现本文不再赘述。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://github.com/containerd/containerd/blob/main/docs/PLUGINS.md">containerd docs: plugin</a></li>
<li><a href="https://github.com/containerd/containerd/blob/main/docs/cri/config.md#snapshotter">containerd docs: CRI config - snapshotter</a></li>
</ul>
]]></description></item><item><title>Containerd 详解（四） overlayfs snapshotter</title><link>https://www.rectcircle.cn/posts/containerd-4-overlayfs-snapshotter/</link><pubDate>Tue, 03 Oct 2023 22:53:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/containerd-4-overlayfs-snapshotter/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://github.com/containerd/containerd/tree/v1.7.0">v1.7.0</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>前面几篇也有提到了 snapshotter 的一些内容，但并未深入细节。</p>

<p>本文将介绍 overlayfs snapshotter 的注册、执行流程以及 Snapshotter 接口的设计考量。为下一篇介绍如何实现一个自定义的 Snapshotter、或魔改 Snapshotter 提供准备。</p>

<h2 id="概念">概念</h2>

<p><strong>联合文件系统</strong></p>

<p>即 Union File System，是一种类型文件系统的统称，该类文件系统的特征是：将多个目录按照一定规则联合挂载到目标目录形成一个全新的文件系统，更多参见：<a href="https://en.wikipedia.org/wiki/UnionFS">Wiki</a>。</p>

<p>最早的联合系统是 <a href="https://unionfs.filesystems.org/">UnionFS</a>（因此此类文件系统都被叫了这个名字）。</p>

<p>Docker 作为 OCI 标准的参考实现，最早的使用的联合文件系统是 <a href="https://en.wikipedia.org/wiki/Aufs">AUFS</a>，值得一提的是，对该文件系统的使用，在 OCI Image Spec 中可以看到相关遗产（如： <a href="/posts/oci-image-spec/#whiteout">OCI Image Spec without 文件规范</a>）</p>

<p>随着 <a href="https://en.wikipedia.org/wiki/OverlayFS">OverlayFS</a>，进入 Linux 内核，Docker 和衍生自 Docker 的 containerd 使用了该文件系统作为其快照系统的默认实现，因此本文选择该实现来做介绍。</p>

<p><strong>镜像</strong></p>

<p>俗称 Docker 镜像，本质上是对某个联合文件系统的打包，目的是实现可复现，易可分发。因此业界指定了 OCI Image Spec。该规范的本质实际上是对 AUFS 的文件系统底层目录的压缩打包和组织。更多参见：<a href="/posts/oci-image-spec/">OCI Image Spec</a>。</p>

<p><strong>快照</strong></p>

<p>快照是本文的核心概念，在 containerd 中：</p>

<ul>
<li>镜像中的每一层、每个容器都会对应一个快照。</li>
<li>快照可以存在一个 parent 指针，指向另一个快照。</li>
<li>镜像的层对应的快照的 ID 是根据镜像层链表生成的，即 <a href="/posts/oci-image-spec/#layer-chainid">ChainID</a>。</li>
<li>每个快照必须能返回一个最终调用 mount 系统调用的参数集，这个参数集，主要在如下场景使用：

<ul>
<li>拉取镜像时，从底到上的一层一层的的解压。</li>
<li>运行容器时，构造容器的 rootfs。</li>
</ul></li>
</ul>

<h2 id="snapshotter-接口">Snapshotter 接口</h2>

<p>Snapshotter (<code>snapshots/snapshotter.go</code>) 接口本质上就是对快照管理的抽象，该接口在语义上，有两个方面的要求：</p>

<ul>
<li>元数据 <code>snapshots.Info</code> 的管理。在内置的 snapshotter 实现中，一般使用 <code>storage.MetaStore</code> 来进行元数据管理（底层是 <code>&quot;go.etcd.io/bbolt&quot;</code> 一个简单的 kv 数据库）。</li>
<li>根据要实现的文件系统类型，对快照的文件目录进行管理。</li>
</ul>

<h2 id="环境准备">环境准备</h2>

<p>参见：<a href="/posts/containerd-3-containerd-source-framework/#环境准备">Containerd 详解（三） containerd 源码框架 - 环境准备</a></p>

<p>要点如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 清理环境 (Linux 测试机)</span>
sudo systemctl stop containerd
sudo rm -rf /var/lib/containerd
<span style="color:#75715e"># dlv 启动 (Linux 测试机)</span>
sudo ~/go/bin/dlv exec ./bin/containerd --headless --listen <span style="color:#ae81ff">0</span>.0.0.0:2345 --api-version <span style="color:#ae81ff">2</span>
<span style="color:#75715e"># 打开 VSCode F5 启动 (本地设备)</span></code></pre></div>
<h2 id="源码结构">源码结构</h2>

<p>overlayfs snapshotter 是 containerd 的内建插件，也是默认的 snapshotter。其实现位于 containerd 项目的 <code>snapshots/overlay</code> 目录，仅有 3 个代码文件（不包含测试）。</p>

<ul>
<li><code>snapshots/overlay/plugin/plugin.go</code> 插件注册：containerd 在启动时会调用 import 该包，执行里面的 init 函数，注册插件。</li>
<li><code>snapshots/overlay/overlay.go</code> 插件实现：实现了 <code>snapshots/snapshotter.go@Snapshotter</code> 接口。</li>
<li><code>snapshots/overlay/overlayutils/check.go</code> 插件实现依赖的工具函数。</li>
</ul>

<h2 id="插件注册">插件注册</h2>

<p>containerd 的插件注册是基于 go 的 init 函数机制实现的，调用链路为：<code>cmd/containerd/main.go</code> -&gt; <code>cmd/containerd/builtins/builtins_linux.go</code> -&gt; <code>snapshots/overlay/plugin/plugin.go</code>。</p>

<p><code>snapshots/overlay/plugin/plugin.go</code> 中，注册了一个 ID 为 <code>&quot;overlayfs&quot;</code> Type 为 <code>&quot;&quot;io.containerd.snapshotter.v1&quot;&quot;</code> 的插件。在 <code>InitFn</code> 函数，最终调用 <code>overlay.NewSnapshotter</code>，参数值：</p>

<ul>
<li><code>root</code>: <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs&quot;</code></li>
<li><code>opts</code>: <code>overlay.AsynchronousRemove</code>，即异步删除。</li>
</ul>

<p>按照如上参数， <code>overlay.NewSnapshotter</code> 返回：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">&amp;</span><span style="color:#a6e22e">overlay</span>.<span style="color:#a6e22e">snapshotter</span>{
	<span style="color:#a6e22e">root</span>:          <span style="color:#e6db74">&#34;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs&#34;</span>,
	<span style="color:#a6e22e">ms</span>:            <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">storage</span>.<span style="color:#a6e22e">MetaStore</span>{ 
		<span style="color:#a6e22e">dbfile</span>: <span style="color:#e6db74">&#34;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/metadata.db&#34;</span>,
	},
	<span style="color:#a6e22e">asyncRemove</span>:   <span style="color:#66d9ef">true</span>,
	<span style="color:#a6e22e">upperdirLabel</span>: <span style="color:#66d9ef">false</span>,
	<span style="color:#a6e22e">indexOff</span>:      <span style="color:#66d9ef">true</span>, <span style="color:#75715e">// /sys/module/overlay/parameters/index
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">userxattr</span>:     <span style="color:#66d9ef">false</span>,
}</code></pre></div>
<h2 id="流程分析">流程分析</h2>

<p>该部分将介绍 containerd 各种操作对 overlayfs snapshotter 函数调用，以及内部细节，这些函数，除非特别说明均在 <code>snapshots/overlay/overlay.go</code> 文件中。</p>

<p>为了方便追踪，打开 <code>snapshots/overlay/overlay.go</code>，在将所有导出的函数添加断点。</p>

<h3 id="拉新的镜像">拉新的镜像</h3>

<p>删除 <code>/var/lib/containerd</code> 并重启启动调试，执行 <code>sudo ctr images pull docker.io/library/nginx:1.25</code>，流程如下：</p>

<ul>
<li>处理镜像的第 1 层

<ul>
<li><code>Prepare</code> 函数

<ul>
<li>参数为：

<ul>
<li><code>key</code>: <code>&quot;default/1/extract-675469558-0Gcu sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e&quot;</code></li>
<li><code>parent</code>: <code>&quot;&quot;</code></li>
<li><code>opts</code>: <code>{snapshots.WithLabels({...})}</code></li>
</ul></li>
<li>逻辑如下：

<ul>
<li>调用 <code>createSnapshot</code> 函数

<ul>
<li>创建 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/new-xxx/{fs,work}&quot;</code> 临时目录。</li>
<li>调用 <code>storage.CreateSnapshot</code> 创建一个元数据 <code>snapshots.Info</code>，并返回 <code>s := storage.Snapshot{ID: &quot;1&quot;, Kind: KindActive(2), ParentIDs: nil}</code>。</li>
<li>构造快照目录路径 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1&quot;</code> （这里的 1 为 <code>s.ID</code>）。</li>
<li>将 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/new-xxx&quot;</code> 重命名为 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1&quot;</code>。</li>
</ul></li>
<li>调用 <code>mounts</code> 函数，参数为 <code>storage.Snapshot{ID: &quot;1&quot;, Kind: KindActive(2), ParentIDs: nil}</code>，因为 ParentIDs 为 nil，所以返回： <code>[]mount.Mount{ { Source: &quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs&quot;, Type: &quot;bind&quot;, Options: []string{&quot;rw&quot;, &quot;rbind&quot;} } }</code></li>
</ul></li>
</ul></li>
<li>调用 diff api，解压并处理该层，观察 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs</code> 目录，里面已经存在镜像第一层内容。</li>
<li><code>Commit</code> 函数

<ul>
<li>参数为：

<ul>
<li><code>name</code>: <code>&quot;default/2/sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e&quot;</code>
*<code>key</code>: <code>&quot;default/1/extract-347006525-FddQ sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e&quot;</code></li>
<li><code>opts</code>: <code>{snapshots.WithLabels({...})}</code></li>
</ul></li>
<li>逻辑如下：

<ul>
<li>获取 <code>storage.Snapshot</code> 的 ID，并构造 upper 目录： <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs</code></li>
<li>统计上述目录使用 inode 和 磁盘空间。</li>
<li>调用 <code>storage.CommitActive(..., key, name, ...)</code> 标记该快照已提交 (<code>Kind: snapshots.KindCommitted(3)</code>)，实现为：根据 <code>key</code> 查询到 <code>storage.Snapshot</code>，填充相关参数，并保存到 id 为 <code>name</code> 的 bucket 中。</li>
</ul></li>
</ul></li>
</ul></li>
<li>处理镜像的第 2~n 层 （以第 2 层为例）

<ul>
<li><code>Prepare</code> 函数

<ul>
<li>参数为：

<ul>
<li><code>key</code>: <code>&quot;default/3/extract-975267327-qRqB sha256:7e87866b23143eb30086086a669b2e902368a5836446a885b2411d3feef18bef&quot;</code></li>
<li><code>parent</code>: <code>&quot;default/2/sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e&quot;</code></li>
<li><code>opts</code>: <code>{snapshots.WithLabels({...})}</code></li>
</ul></li>
<li>逻辑如下：

<ul>
<li>调用 <code>createSnapshot</code> 函数

<ul>
<li>创建 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/new-xxx/{fs,work}&quot;</code> 临时目录。</li>
<li>调用 <code>storage.CreateSnapshot</code> 创建一个元数据 <code>snapshots.Info</code>，并返回 <code>s := storage.Snapshot{ID: &quot;2&quot;, Kind: KindActive(2), ParentIDs: []string{&quot;1&quot;}}</code>，这里的 ParentIDs 如果有多个，是从顶层到底层排序的。</li>
<li>修改 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/new-xxx/fs&quot;</code> 的权限和 <code>s.ParentIDs[0]</code> 一致（即 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs</code>）。</li>
<li>构造快照目录路径 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2&quot;</code> （这里的 2 为 <code>s.ID</code>）。</li>
<li>将 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/new-xxx&quot;</code> 重命名为 <code>&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2&quot;</code>。</li>
</ul></li>
<li>调用 <code>mounts</code> 函数，参数为 <code>storage.Snapshot{ID: &quot;1&quot;, Kind: KindActive(2), ParentIDs: nil}</code>，因为 ParentIDs 不为 nil，所以返回： <code>[]mount.Mount{ { Source: &quot;overlay&quot;, Type: &quot;overlay&quot;, Options: []string{...} } }</code>，options 如下：

<ul>
<li><code>&quot;index=off&quot;</code></li>
<li><code>&quot;workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2/work&quot;</code></li>
<li><code>&quot;upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2/fs&quot;</code></li>
<li><code>&quot;lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs&quot;</code> 多个以 <code>:</code> 分隔。</li>
</ul></li>
</ul></li>
</ul></li>
<li>调用 diff api，解压并处理该层，观察 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2/fs</code> 目录，里面已经存在镜像第 2 层内容。</li>
<li><code>Commit</code> 函数，和第 1 层处理逻辑一致。</li>
</ul></li>
</ul>

<h3 id="diff-api">diff api</h3>

<p>上文和前篇文档都介绍到了，针对镜像的每一层，mount 完成后，都会调用 diff api 将镜像内容解压到指定路径上，这里来介绍下具体过程。</p>

<p>为了方便追踪，打开 <code>diff/apply/apply.go</code>，给 <code>Apply</code> 函数添加断点。并删除 <code>/var/lib/containerd</code> 并重启启动调试，执行 <code>sudo ctr images pull docker.io/library/nginx:1.25</code>。</p>

<p>观察到，流程如下：</p>

<ul>
<li>构建一个面向 <code>&quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;</code> 格式的解压处理器 <code>processor</code> 和镜像层文件流构造一个新的流 <code>io.Reader</code> 要求这个流的格式为 tar。</li>
<li>调用 <code>apply</code>。三个参数分别是：<code>ctx</code>、<code>snapshotter.Prepare</code> 返回的 <code>mounts</code>、上一步获取到的 <code>io.Reader</code>。该步骤在不同的操作系统平台的处理逻辑是不同的，这里仅介绍 Linux 平台的逻辑。在 Linux 平台中，针对不同的挂载模式也是不同的逻辑。以 overlay snapshotter 为例，第 1 层 mounts 是 bind、第 2~n 层是 overlay。

<ul>
<li>针对 bind 类型 mount，调用 <code>mount.WithTempMount</code> 函数：创建一个临时目录 （如 <code>&quot;/var/lib/containerd/tmpmounts/containerd-mount1544590108&quot;</code>），执行 mount 命令，构造一个文件系统，然后，调用 <code>archive.Apply</code> 函数，参数为：

<ul>
<li><code>root</code> 为临时的 mount 目录。</li>
<li><code>r</code> 为第一步构建的流。</li>
<li><code>opts</code> 为 nil。</li>
</ul></li>
<li>针对 overlay 类型 mount，实际上并不会执行该 mount，而是解析可写层 (upper) 路径和 lowers 路径，调用 <code>archive.Apply</code>。

<ul>
<li><code>root</code>  upper 路径。</li>
<li><code>r</code> 为第一步构建的流。</li>
<li><code>opts</code> 有两个，分别为：

<ul>
<li><code>archive.WithConvertWhiteout(archive.OverlayConvertWhiteout)</code></li>
<li><code>archive.WithParents(parents)</code></li>
</ul></li>
</ul></li>
</ul></li>
</ul>

<p>最终，如上两种情况，都会调用 <code>applyNaive</code> （<code>archive/tar.go</code>）：</p>

<ul>
<li>使用上面创建的流构建一个 go 标准库的 <code>tar.Reader</code> 对象。</li>
<li>遍历 tar 的每个目录和文件，将文件解压到指定目录。这里需要特别强调的是对 without 文件的处理： containerd 使用的 OCI 镜像标准是面相联合文件系统的（如 Overlayfs），因此镜像是分层的。该类文件系统规范都需要支持上层对下层文件的删除。此时，多数都是通过特殊的标记文件实现。OCI 镜像也是如此，OCI 镜像标准使用 <code>.wh.</code> 前缀（本质是 aufs 的规范）来标记（更多参见：<a href="/posts/oci-image-spec/#whiteout">OCI 镜像格式规范</a>）。而 containerd 要支持多种文件系统，containerd 定义了一个函数 <code>type archive.ConvertWhiteout func(header *tar.Header, path string) (writeFile bool, false error)</code> 需要将 OCI 镜像的 <code>.wh.</code> 格式转换为对该文件系统的操作。

<ul>
<li>该函数的语义是：

<ul>
<li>参数：

<ul>
<li>header 该 tar item 的 header</li>
<li>path 该目标目录根目录 join 上 tar item 的 name</li>
</ul></li>
<li>行为

<ul>
<li>path 如果是一个删除标记，根据当期文件系统情况转换为该文件系统能识别的行为。并返回 <code>false, nil</code>。参见下文。</li>
<li>path 如果不是一个删除标记，返回 <code>true, nil</code>。</li>
</ul></li>
<li>返回

<ul>
<li>writeFile tar item 是否需要写入。</li>
</ul></li>
</ul></li>
<li>几种实现为：

<ul>
<li>默认实现 （位于 <code>archive/tar.go@applyNaive</code> 内）：如果是删除标记，这直接调用系统调用（如 <code>os.RemoveAll</code>）将对应位置的文件删除。</li>
<li><code>OverlayConvertWhiteout</code> （位于 <code>archive/tar_opts_linux.go</code>），如果是删除目录，则给目录添加一个属性 <code>trusted.overlay.opaque:y</code>，如果是删除文件，则创建一个字符设备。</li>
</ul></li>
</ul></li>
</ul>

<p>总结：</p>

<ul>
<li>简单而言，diff api 实际上就是将 OCI 标准镜像层写入 snapshotter <code>Prepare</code> 函数返回的 <code>mounts</code> 构造的文件系统中。这里的写入并不是简单的解压到目录，而是需要处理 OCI 标准镜像的 without 规范，对标记删除的目录、文件进行删除。</li>
<li>针对联合文件系统（Overlayfs、aufs 等），做了特殊优化：不真正 mount，而是直接写入对应的 upper 层路径，并对将OCI 标准镜像的 without 规范，转化为对应文件系统的规范。</li>
</ul>

<h3 id="拉已存在的镜像">拉已存在的镜像</h3>

<p>在上文拉新的镜像完成后，再次执行 <code>sudo ctr images pull docker.io/library/nginx:1.25</code>，流程如下为：针对每一层，调用 <code>Stat</code> 函数</p>

<ul>
<li>参数 <code>key</code> 为 <code>&quot;default/2/sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e&quot;</code>。逻辑如下：</li>
<li>逻辑为 调用 <code>storage.GetInfo</code> 获取到 snapshots.Info 并返回。</li>
</ul>

<h3 id="运行容器">运行容器</h3>

<p>在上述执行完成后，执行下述命令（对应源码为 <code>cmd/ctr/commands/run/run.go</code>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ctr run --mount <span style="color:#e6db74">&#39;type=bind,src=https://www.rectcircle.cn/tmp,dst=/host-tmp,options=rbind:ro&#39;</span> docker.io/library/nginx:1.25 nginx</code></pre></div>
<ul>
<li><code>cmd/ctr/commands/run/run_unix.go@NewContainer</code> 阶段

<ul>
<li>查询镜像状态（<code>image.go@IsUnpacked</code>）：调用 <code>Stat</code> 函数，参数 <code>key</code> 为 <code>&quot;default/14/sha256:1c69f36a0d9b59b762eaba410fa9fd01b85140670a8d49199a7b37702cc956c0&quot;</code>，查询该镜像最上层的状态。</li>
<li>构建容器的 Rootfs (<code>container_opts.go@WithNewSnapshot</code>)：调用 <code>Prepare</code> 函数，参数 <code>key</code> 为 <code>&quot;default/15/nginx&quot;</code>、参数 <code>parent</code> 为 <code>&quot;default/14/sha256:1c69f36a0d9b59b762eaba410fa9fd01b85140670a8d49199a7b37702cc956c0&quot;</code>。和上述拉新镜像的过程一样返回一个 type 为 overlay 的 mount。</li>
</ul></li>
<li><code>container.go@NewTask</code>

<ul>
<li>调用 <code>Mounts</code>，参数 <code>key</code> 为 <code>&quot;default/15/nginx&quot;</code>，返回值和上述 <code>Prepare</code> 返回值一致，即 mounts。</li>
</ul></li>
</ul>

<h3 id="其他行为">其他行为</h3>

<ul>
<li><code>Update</code> 更新快照，只支持更新标签。如： <code>sudo ctr snapshot label labelkey=labelvalue nginx</code>。</li>
<li><code>Usage</code> 查询当前层，资源使用情况（只返回一层），即磁盘使用量和 inode。如果当前快照类型为 <code>Active</code> 将递归统计（如：<code>sudo ctr snapshot usage nginx</code>）；如果当前快照是 <code>Committed</code> 将从元数据存储中返回（如 <code>sudo ctr snapshot usage sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e</code>）。</li>
<li><code>View</code> 从一个类型为 <code>committed</code> 的 Key 创建一个新的快照，这个快照，且这个快照是只读的，如： <code>sudo ctr snapshot view nginx-ro sha256:1c69f36a0d9b59b762eaba410fa9fd01b85140670a8d49199a7b37702cc956c0</code></li>
<li><code>Remove</code> 删除某个快照元数据，删除的这个快照必须是最顶层，不能被其他快照的 Parent 直接或间接引用。在 snapshotter 这个层级，不感知该快照是否正在被使用（即某个快照正在被其他容器使用，也可以被删掉），不删除磁盘占用，磁盘空间需调用 <code>Cleanup</code> 清理，如：<code>sudo ctr snapshot rm nginx-ro</code>。</li>
<li><code>Walk</code> 遍历遍历所有的快照元信息，如 <code>sudo ctr snapshot ls</code>。</li>
<li><code>Cleanup</code> 清理未被使用的磁盘空间。</li>
</ul>

<h3 id="labels-特别说明">labels 特别说明</h3>

<p>在 containerd 中， snapshotter 的 lables 默认是存储在 containerd 自己 metadate 中，不会透传到 snapshotter 中。而 <code>containerd.io/snapshot/</code> 开头的 labels 是个例外。</p>

<p>因此，在自定义 snapshotter 使用 labels 作为额外参数传递时，需要以 <code>containerd.io/snapshot/</code> 开头。</p>

<p>源码参见：</p>

<ul>
<li><code>snapshots/snapshotter.go@FilterInheritedLabels</code></li>
<li><code>metadata/snapshot.go@createSnapshot</code></li>
</ul>
]]></description></item><item><title>容器核心技术（九） cgroup</title><link>https://www.rectcircle.cn/posts/container-core-tech-9-cgroup/</link><pubDate>Wed, 27 Sep 2023 00:17:37 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-9-cgroup/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>cgroup 即 Control groups (控制组)，是 Linux 内核提供的对进程资源的使用进行限制和监控的机制。</p>

<p>Linux 内核通过文件系统（VFS）提供 cgroup 的 API 。也就是说，用户可以通过文件系统 API 来控制进程的 cgroup 情况。</p>

<p>下面介绍 cgroup 的一些概念：</p>

<ul>
<li>cgroup (控制组)：控制一组进程的 1 种或者多种 Linux 资源的概念。</li>
<li>hierarchy (层级)：cgroup 作为节点构成一颗树。 hierarchy 通过 mount 系统调用来创建，每个 hierarchy 会绑定一种或多种 subsystem。</li>

<li><p>subsystem (子系统, resource controllers, 资源控制器)： Linux 内核支持控制的资源类型，有如下类型：</p>

<ul>
<li>cpu (v1 since 2.6.24, v2 since 4.15) 限制 cpu 使用（在 v2 中，还包含 cpuacct）。</li>
<li>cpuacct (v1 since 2.6.24) cpu 使用情况统计。</li>
<li>cpuset (v1 since 2.6.24, v2 since 5.0) 限制使用那些 cpu 核心。</li>
<li>memory (v1 since 2.6.25, v2 since 4.5) 限制和统计内存使用。</li>
<li>devices (v1 since 2.6.26) 控制设备使用。</li>
<li>freezer (v1 since 2.6.28, v2 since 5.0) suspend 和 restore (resume) 该进程。</li>
<li>net_cls (v1 since 2.6.29) 给进程网络包打标。</li>
<li>blkio (v1 since 2.6.33), io (v2 since 4.5) 控制块设备的 io 。</li>
<li>perf_event (v1 since 2.6.39, v2 since 4.11) 统计 cgroup 组粒度的 perf 事件。</li>
<li>net_prio (v1 since 3.3) 配置网络接口的优先级。</li>
<li>hugetlb (v1 since 3.5, v2 since 5.6) 限制对大页表的使用。</li>
<li>pids (v1 since 4.3, v2 since 4.5) 限制允许创建的进程数量。</li>
<li>rdma (v1 since 4.11, v2 since 4.11) 限制 RDMA (远程直接内存访问)。</li>
</ul></li>
</ul>

<p>目前有 v1 和 v2 两个版本，API 层面 v1 和 v2 相互不兼容。另外 v2 并不能完全覆盖 v1 的能力，因此，在较新的发行版中，可以同时使用 v1 和 v2 两个版本的 cgroup API，对于 v2 不支持的部分可以继续使用 v1 的 API。</p>

<h2 id="cgroup-v1">cgroup v1</h2>

<blockquote>
<p>参考: <a href="https://man7.org/linux/man-pages/man7/cgroups.7.html#CGROUPS_VERSION_1">cgroups(7) — Linux manual page#CGROUPS VERSION 1</a></p>
</blockquote>

<h3 id="配置-debian-11-使用-v1">配置 debian 11 使用 v1</h3>

<blockquote>
<p>参考：<a href="https://www.vvave.net/archives/introduction-to-linux-kernel-control-groups-v2.html">在新 Linux 发行版上切换 cgroups 版本</a></p>
</blockquote>

<p>Debian 11 已经默认启用了 cgroup v2。可以通过如下方式切换到 cgroup v1：</p>

<p>编辑 <code>/etc/default/grub</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># GRUB_CMDLINE_LINUX_DEFAULT=&#34;quiet&#34;
GRUB_CMDLINE_LINUX_DEFAULT=&#34;quiet systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false&#34;</pre></div>
<p>执行生成 grub 配置。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo grub-mkconfig -o /boot/grub/grub.cfg</code></pre></div>
<p>重启系统。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo reboot</code></pre></div>
<h3 id="观察-debian-的-cgroup">观察 debian 的 cgroup</h3>

<p>在 Linux 中，目前主流使用进程管理器(1 号进程)，是 systemd。 systemd 在系统初始化阶段会自动的创建相关 cgroup。通过 <code>mount | grep cgroup</code> 可以看到：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,size=4096k,nr_inodes=1024,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)</pre></div>
<p>上面的每一行，都是一个 hierarchy，以 <code>cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)</code> 为例。。</p>

<ul>
<li>hierarchy 与 <code>cpu</code> 和 <code>cpuacct</code> 子系统关联。也就是说：systemd 在引导阶段，通过类似于命令 <code>mount -t cgroup -o cpu,cpuacct none /sys/fs/cgroup/cpu,cpuacct</code> 的系统调用创建了该 hierarchy。</li>

<li><p>hierarchy 的根 cgroup 为 <code>/sys/fs/cgroup/cpu,cpuacct</code>。<code>ls -al /sys/fs/cgroup/cpu,cpuacct</code> 可以看到该根 cgroup 有如下文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">-rw-r--r--  1 root root   0 11月 28 20:54 cgroup.clone_children
-rw-r--r--  1 root root   0 11月 28 20:46 cgroup.procs
-r--r--r--  1 root root   0 11月 28 20:54 cgroup.sane_behavior
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.stat
-rw-r--r--  1 root root   0 11月 28 20:54 cpuacct.usage
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_all
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_percpu
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_percpu_sys
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_percpu_user
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_sys
-r--r--r--  1 root root   0 11月 28 20:54 cpuacct.usage_user
-rw-r--r--  1 root root   0 11月 28 20:54 cpu.cfs_period_us
-rw-r--r--  1 root root   0 11月 28 20:54 cpu.cfs_quota_us
-rw-r--r--  1 root root   0 11月 28 20:54 cpu.shares
-r--r--r--  1 root root   0 11月 28 20:54 cpu.stat
-rw-r--r--  1 root root   0 11月 28 20:54 notify_on_release
-rw-r--r--  1 root root   0 11月 28 20:54 release_agent
-rw-r--r--  1 root root   0 11月 28 20:54 tasks</pre></div></li>

<li><p>通过对这些文件的读写，可以设置和获取该 cgroup 资源限制和使用情况，比如通过 <code>cat /sys/fs/cgroup/cpu,cpuacct/cgroup.procs</code> 可以获取关联到该 cgroup 的进程有哪些。</p></li>

<li><p>通过在 <code>/sys/fs/cgroup/cpu,cpuacct/</code> 目录创建文件，如 <code>mkdir test</code>，可以创建一个子 cgroup，此时 <code>ls -al test</code>，将看到和 <code>ls -al /sys/fs/cgroup/cpu,cpuacct</code> 类似的内容。</p></li>

<li><p>通过将 pid 写入 <code>cgroup.procs</code> 的文件，如 <code>sudo sh -c &quot;echo $$ &gt; test/cgroup.procs&quot;</code>， 可以将当前 shell 加入指定的 cgroup，注意加入该 cgroup 后创建的进程将自动和该 cgroup 关联。</p></li>

<li><p>如果 <code>/test</code> cgroup 没有关联的进程（将当前 shell 移出 <code>sudo sh -c &quot;echo $$ &gt; cgroup.procs&quot;</code>，否则报错：设备或资源忙），则可以通过 <code>sudo rmdir test</code> 命令删除掉该 cgroup（注意 <code>rm</code> 命令不行）。</p></li>
</ul>

<p>通过 <code>cat /proc/self/cgroup</code> 可以看到当前 <code>cat /proc/$$/cgroup</code> 可以看出，当前系统的进程，都自动的关联到了 systemd 在 <code>/sys/fs/cgroup/</code> 目录下创建的 cgroup 中了。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">12:rdma:/
11:devices:/user.slice
10:perf_event:/
9:hugetlb:/
8:memory:/user.slice/user-1000.slice/session-3.scope
7:cpuset:/
6:pids:/user.slice/user-1000.slice/session-3.scope
5:net_cls,net_prio:/
4:cpu,cpuacct:/
3:blkio:/
2:freezer:/
1:name=systemd:/user.slice/user-1000.slice/session-3.scope
0::/user.slice/user-1000.slice/session-3.scope</pre></div>
<h3 id="cgroup-v1-文件系统">cgroup v1 文件系统</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/sys/fs/cgroup/
    cpu,cpuacct/           -+                         -+
        cgroup.procs        +--&gt; &lt;cgroup&gt;              |
        ...                -+                          |
        docker/            -+                          |
            cgroup.procs    +--&gt; &lt;cgroup&gt;              +----&gt; &lt;hierarchy&gt; (cpu,cpuacct)
            ...            -+                          |
            container1/    -+--&gt; &lt;cgroup&gt;              |
            container2/    -+--&gt; &lt;cgroup&gt;              |
            container3/    -+--&gt; &lt;cgroup&gt;             -+
    memory/                -+--&gt; &lt;cgroup&gt;             -+----&gt; &lt;hierarchy&gt; (memory)
    ...</pre></div>
<ul>
<li>所有的 hierarchy 都位于 <code>/sys/fs/cgroup</code> 目录下，该目录是一个 tmpfs，由 <code>systemd</code> 通过类似 <code>mount -t tmpfs -o size=4096K tmpfs /sys/fs/cgroup</code> 命令的系统调用创建。</li>
<li>每个 hierarchy 由 <code>systemd</code> 通过类似于 <code>mount -t cgroup -o cpu,cpuacct none /sys/fs/cgroup/cpu,cpuacct</code> 类似的系统调用创建。</li>
<li>cgroup 在文件系统重表现为 hierarchy 目录下的任意一级目录，如 <code>&lt;hierarchy&gt;/</code>、<code>&lt;hierarchy&gt;/A</code> <code>&lt;hierarchy&gt;/A/B</code> 就是三个 cgroup。</li>
<li>每个 cgroup 包含如下信息：

<ul>
<li>该 cgroup 关联的 subsystem 为调用 <code>mount -t cgroup</code> 时 -o 指定的些（通过 mount 可以看到）。</li>
<li>该 cgroup 目录包含如下两类：

<ul>
<li>文件：用于设置或者查看该 cgroup 的资源限制配置、占用情况、关联的进程。</li>
<li>目录：该 cgroup 的子 cgroup。</li>
</ul></li>
</ul></li>
<li>通过将 PID 写入 <code>&lt;cgroup&gt;/cgroup.procs</code>，可以将一个进程移动到指定的 cgroup 中。</li>
<li>假设 cgroup A 对应的路径为 <code>&lt;hierarchy&gt;/A</code>，B 对应的路径为 <code>&lt;hierarchy&gt;/A/B</code>，则可以说 A 是 B 的父 cgroup。此时，B 对资源的限制默认继承 A 的配置，且 B 不能超过 A 的配置上限。</li>
<li>如果一个进程已经位于某个子系统为 <code>cpu,cpuacct</code> 的 hierarchy 中，那么该进程就不能加入其他只有 <code>cpu</code> 的 hierarchy。因为如果允许这种情况存在，内核就无法确定该进程的 CPU 该以哪个为准。</li>
</ul>

<h2 id="常用的-cgroup-子系统">常用的 cgroup 子系统</h2>

<p>本部分，仅介绍 cgroup v1 的内容。</p>

<h3 id="cpu-cpuacct-和-cpuset">cpu、cpuacct 和 cpuset</h3>

<h4 id="cpu-描述">cpu 描述</h4>

<p>自 Linux 2.6 起，Linux 的 CPU 调度器使用的是 CFS (完全公平调度器)。 cgroup 的 cpu 子系统中，通过 <a href="https://docs.kernel.org/translations/zh_CN/scheduler/sched-bwc.html">CFS 调度器带宽控制</a> 特性，以实现控制进程的 CPU 使用率的目的（只对 <code>non-RT</code> 非实时策略生效）。相关参数（文件）如下：</p>

<ul>
<li><code>cpu.cfs_quota_us</code> 一个周期内最大运行时间，默认为 -1（即不限制）。需要注意的是，如果是多核情况，该值的取值范围为 <code>(0, 核数*cpu.cfs_period_us]</code>。</li>
<li><code>cpu.cfs_period_us</code> 单个 CPU 一个周期的长度，默认为 100000 us （即 100 ms），不能超过 1 s。</li>
<li><code>cpu.cfs_burst_us</code> 略，参见：<a href="https://docs.kernel.org/translations/zh_CN/scheduler/sched-bwc.html">CFS 带宽控制</a>。</li>
</ul>

<p>除了如上和 CFS 相关的参数，还有如下参数（文件）可以配置：</p>

<ul>
<li><code>cpu.shares</code> 在同一层级下。组内 CPU 的权重。需要注意的是，当 CPU 空闲时，所有组内的 CPU 都是可以占有全部的 CPU 的，当 CPU 繁忙时，各个组的 CPU 时间片的分配则按照这个参数按比例分配，数值越高分配的 CPU 越多。默认为 1024。</li>
</ul>

<p>说明：除了 cgroup 外，还有一种方式可以 CPU affinity 的方式将进程绑定到某些核心下来控制 CPU 的使用。可以通过 <a href="https://man7.org/linux/man-pages/man1/taskset.1.html"><code>taskset</code></a> 命令或 <a href="https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html"><code>sched_setaffinity(2) 系统调用</code></a>设置，也可以通过 <a href="https://docs.kernel.org/admin-guide/cgroup-v1/cpusets.html?highlight=cpuset">cpuset cgroup 子系统</a>进行配置（<code>/sys/fs/cgroup/cpuset</code>），本文不多赘述。</p>

<p>总结：</p>

<ul>
<li>配置 CPU 繁忙时调度的优先级，可以通过 <code>cpu.shares</code> 进行配置，该设置不影响系统空闲时最大 CPU 使用量。</li>
<li>要限制最大 CPU 使用量，可以使用 <code>cpu.cfs_quota_us</code> 进行配置。即，最大使用核心数等于 <code>cpu.cfs_quota_us/cpu.cfs_period_us</code>。一般情况下 <code>cpu.cfs_period_us</code> 保持默认，只需要配置 <code>cpu.cfs_quota_us</code> 就可以了。Kubernetes 中的 resources.limit.cpu 也是通过这种方式实现的。</li>
</ul>

<p>参考：</p>

<ul>
<li><a href="https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt">glommer/memcg cpu_stat/Documentation/cgroups/cpu.txt</a></li>
<li><a href="https://docs.kernel.org/scheduler/sched-bwc.html">CFS Bandwidth Control</a> | <a href="https://docs.kernel.org/translations/zh_CN/scheduler/sched-bwc.html">CFS 带宽控制</a></li>
<li><a href="https://blog.csdn.net/liukuan73/article/details/53358423">Cgroup的CPU资源隔离介绍&amp;docker cpu限制</a></li>
</ul>

<h4 id="cpuacct-描述">cpuacct 描述</h4>

<p>cpuacct 子系统用于统计该 cgroup 下的进程的 CPU 使用情况。在一般的 Linux 系统中和 cpu 子系统位于同一个层次目录下即： <code>/sys/fs/cgroup/cpu,cpuacct</code>。常用文件如下：</p>

<ul>
<li><code>cpuacct.usage</code> 系统运行到现在，当前 cgroup 下的进程所使用的 cpu 时间，单位为纳秒。</li>
<li><code>cpuacct.usage_all</code> 系统运行到现在，当前 cgroup 下的进程在每个核心下，用户态和内核态，分别使用的 cpu 时间，单位为纳秒。</li>
<li><code>cpuacct.usage_percpu</code> 系统运行到现在，当前 cgroup 下的进程在每个核心下，分别使用的 cpu 时间，单位为纳秒。</li>
</ul>

<p>通过这些文件可以计算出某个 cgroup 下 CPU 的其他指标，简单的算法为：</p>

<ul>
<li>获取当前时刻 <code>cpuacct</code> 相关文件内容 （记为 before）。</li>
<li>sleep 1 秒后，再次获取 <code>cpuacct</code> 相关文件内容（记为 after）。</li>
<li><code>after - before</code>，即可获取 1 秒内 CPU 时间消耗情况。</li>
</ul>

<p>最后，一些常用的指标如下：</p>

<ul>
<li>cgroup cpu 核心使用数： <code>(after{cpuacct.usage} - before{cpuacct.usage}) / 1000000000</code>。取值范围和 cgroup 分配的核心数有关，如：

<ul>
<li><code>cpu.cfs_period_us</code> 为 100000。</li>
<li><code>cpu.cfs_quota_us</code> 为 400000。</li>
<li>则上述公式取值范围为 <code>[0, 4]</code></li>
<li>假设某一秒内其值为 <code>2.1</code> 则表示，使用了 2.1 个 CPU 核，即 <code>210%</code> 个 CPU。</li>
</ul></li>
<li>cgroup cpu 总体使用率：<code>(after{cpuacct.usage} - before{cpuacct.usage}) / 1000000000 / (cpu.cfs_period_us / cpu.cfs_quota_us)</code>。取值范围为 <code>[0, 1]</code>，即分配给该 cgroup 的全部的 cpu 资源的使用率。</li>
</ul>

<p>这些指标在容器资源监控场景非常有用，通过 kubernetes 中，内置到 Kubelet 的 <a href="https://github.com/google/cadvisor"><code>cAdvisor</code></a> 对容器的 CPU 的监控的原理和上述类似，关于 kubernetes 的 metrics 相关，参见：<a href="https://kubernetes.io/zh-cn/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/">官方文档</a>。</p>

<h4 id="cpuset-描述">cpuset 描述</h4>

<p>cpu 子系统控制的是进程在 cpu 时间片上的分配，无法控制 cpu 被调度到哪个核心。而 cpuset 子系统控制的就是：进程能被调度到那些 cpu。一般情况下，该子系统位于位于 <code>/sys/fs/cgroup/cpuset</code> hierarchy 目录。</p>

<p>常用文件如下：</p>

<ul>
<li><code>cpuset.cpus</code> 当前 cgroup 下的进程可以使用的 cpu 核心的范围，例如 <code>0-5</code>。</li>
</ul>

<p>kubernetes 的 CPU 策略管理中，如果 kubelet 开启了 static 策略，那么，QoS 为 Guaranteed 的 Pod， 则会利用到了该特性来分配独占 CPU，参见： <a href="https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/cpu-management-policies/#static-policy">官方文档</a>。</p>

<h4 id="实验">实验</h4>

<blockquote>
<p>参考：<a href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/8/html/managing_monitoring_and_updating_the_kernel/setting-cpu-limits-to-applications-using-cgroups-v1_setting-limits-for-applications">使用 cgroups-v1 为应用程序设置 CPU 限制</a></p>
</blockquote>

<p>实验规划如下：</p>

<ul>
<li>创建一个协程，改协程实现一个死循环，用来占满一个 CPU。</li>
<li>打印当前进程 CPU 占用率，此时应该是 100% 左右。</li>
<li>创建一个 demo cpu cgroup，将 CPU 上线设置为 20%。并将当前进程加入到该 cgroup 中。</li>
<li>打印当前进程 CPU 占用率，应该应该是 20% 左右。</li>
</ul>

<p>实验代码 <code>src/go/01-namespace/07-cgroup/v1/01-cpu/main.go</code>，如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;path&#34;</span>
	<span style="color:#e6db74">&#34;strconv&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/shirou/gopsutil/v3/process&#34;</span>
	<span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">usage100PercentCPU</span>() {
	<span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
	<span style="color:#66d9ef">for</span> {
		<span style="color:#a6e22e">i</span> = <span style="color:#a6e22e">i</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">printSelfCPUPercent</span>(<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">Process</span>) {
	<span style="color:#a6e22e">cpuPercent</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Percent</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;%.2f%s\n&#34;</span>, <span style="color:#a6e22e">cpuPercent</span>, <span style="color:#e6db74">&#34;%&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">attachSelfToCgroup</span>(<span style="color:#a6e22e">cgroupPath</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">cgroupPath</span>, <span style="color:#e6db74">&#34;cgroup.procs&#34;</span>), []byte(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())), <span style="color:#ae81ff">0644</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">printSelfCgroup</span>(<span style="color:#a6e22e">subsys</span> <span style="color:#66d9ef">string</span>) {
	<span style="color:#a6e22e">selfCgroupBytes</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#e6db74">&#34;/proc/self/cgroup&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">line</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(string(<span style="color:#a6e22e">selfCgroupBytes</span>), <span style="color:#e6db74">&#34;\n&#34;</span>) {
		<span style="color:#a6e22e">subSystems</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">line</span>, <span style="color:#e6db74">&#34;:&#34;</span>)[<span style="color:#ae81ff">1</span>], <span style="color:#e6db74">&#34;,&#34;</span>)
		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">now</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">subSystems</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">now</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">subsys</span> {
				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">line</span>)
				<span style="color:#66d9ef">return</span>
			}
		}
	}
	panic(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;subsys not found: %s&#34;</span>, <span style="color:#a6e22e">subsys</span>))
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {

	<span style="color:#75715e">// 启动 CPU 负载
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">usage100PercentCPU</span>()

	<span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">NewProcess</span>(int32(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>()))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// 打印默认的情况
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;当前进程的 cpu cgroup 为: &#34;</span>)
	<span style="color:#a6e22e">printSelfCgroup</span>(<span style="color:#e6db74">&#34;cpu&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;当前进程的 cpu 使用率为: &#34;</span>)
	<span style="color:#a6e22e">printSelfCPUPercent</span>(<span style="color:#a6e22e">p</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#75715e">// 在默认 cpu 层级，根 cgroup 创建一个名为 demo 的 cgroup。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cpuDemoCgroupDir</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;/sys/fs/cgroup/cpu/demo&#34;</span>
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Mkdir</span>(<span style="color:#a6e22e">cpuDemoCgroupDir</span>, <span style="color:#ae81ff">777</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#75715e">// 使用 unix 的 rmdir 系统调用。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 参考 https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L231
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">Rmdir</span>(<span style="color:#a6e22e">cpuDemoCgroupDir</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">ENOENT</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
	}()
	<span style="color:#75715e">// 配置 CPU quota 为 0.2 核，0.2 * 100000 = 20000
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cpuCFSQuotaPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">cpuDemoCgroupDir</span>, <span style="color:#e6db74">&#34;cpu.cfs_quota_us&#34;</span>)
	<span style="color:#a6e22e">cpuCFSPeriodPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">cpuDemoCgroupDir</span>, <span style="color:#e6db74">&#34;cpu.cfs_period_us&#34;</span>)
	<span style="color:#a6e22e">cpuCFSPeriod</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cpuCFSPeriodPath</span>) <span style="color:#75715e">// 100000
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">cpuCFSPeriodInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">ParseInt</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">cpuCFSPeriod</span>)), <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">32</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">cpuCore</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0.2</span>
	<span style="color:#a6e22e">cpuCFSQuotaInt</span> <span style="color:#f92672">:=</span> int(<span style="color:#a6e22e">cpuCore</span> <span style="color:#f92672">*</span> float64(<span style="color:#a6e22e">cpuCFSPeriodInt</span>))
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">cpuCFSQuotaPath</span>, []byte(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">cpuCFSQuotaInt</span>)), <span style="color:#ae81ff">0644</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">cpuCFSQuota</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cpuCFSQuotaPath</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;创建 demo cpu cgroup, 配置 core: %f, 即\n  cpu.cfs_quota_us: %s\n  cpu.cfs_quota_us: %s\n&#34;</span>,
		<span style="color:#a6e22e">cpuCore</span>,
		<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">cpuCFSQuota</span>)),
		<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">cpuCFSPeriod</span>)),
	)
	<span style="color:#75715e">// 将当前进程加入到当前 cgroup
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">attachSelfToCgroup</span>(<span style="color:#a6e22e">cpuDemoCgroupDir</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#75715e">// 进程退出前，将当前进程脱离 demo cgroup，如果不处理，删除 cgroup 将报错 device or resource busy
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">attachSelfToCgroup</span>(<span style="color:#e6db74">&#34;/sys/fs/cgroup/cpu&#34;</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
	}()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;当前进程已加入 demo cpu cgroup&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;当前进程的 cpu cgroup 为: &#34;</span>)
	<span style="color:#a6e22e">printSelfCgroup</span>(<span style="color:#e6db74">&#34;cpu&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;当前进程的 cpu 使用率为: &#34;</span>)
	<span style="color:#a6e22e">printSelfCPUPercent</span>(<span style="color:#a6e22e">p</span>)
}</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">当前进程的 cpu cgroup 为: 4:cpu,cpuacct:/
当前进程的 cpu 使用率为: 93.24%

创建 demo cpu cgroup, 配置 core: 0.200000, 即
  cpu.cfs_quota_us: 20000
  cpu.cfs_quota_us: 100000
当前进程已加入 demo cpu cgroup

当前进程的 cpu cgroup 为: 4:cpu,cpuacct:/demo
当前进程的 cpu 使用率为: 21.37%</pre></div>
<h3 id="memory">memory</h3>

<h4 id="背景知识">背景知识</h4>

<p>首先需要了解，在 Linux 中，一个进程关于内存占用的一些指标概念：</p>

<ul>
<li>RSS (Resident Set Size)，常驻内存大小。进程在物理内存中实际保存的总内存（包含共享库占用的共享内存总数，不包含已 swap 到磁盘中的内存），可以按照进程粒度进程统计。</li>
<li>Page Cache (Buffer/Cache)，主要是 IO （文件系统）的缓存（读写文件），该部分内存一般不会回收，会根据配置达到一定水平线后进行回收，该部分内存是多个进程共享呢，由内核统一管理，不会和进程关联，在使用 cgroup 时，可以按照 cgroup 粒度进行统计。</li>
</ul>

<h4 id="描述">描述</h4>

<p>cgroup 对内存的控制的相关主要参数（文件）如下所示（只介绍 <a href="https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/fs/memory.go#L20">runc</a> 使用的那些，位于 <code>/sys/fs/cgroup/memory</code> 层级目录）：</p>

<ul>
<li><code>memory.limit_in_bytes</code> rw，默认值 9223372036854771712（0x7FFFFFFFFFFFF000 基本上等于无限制），内存使用（硬）限制，对应的指标为 RSS + Page Cache（不包含 <code>swap</code>），写入 -1 表示无限制。需要注意的是：

<ul>
<li>cgroup 对应指标超过该值时，内核行为由 <code>memory.oom_control</code> 参数决定：

<ul>
<li><code>memory.oom_control.oom_kill_disable = 0</code> 内核将 kill -9 该 cgroup 中 <code>/proc/&lt;pid&gt;/oom_score + /proc/&lt;pid&gt;/oom_score_adj</code> 数值最高的进程（内存占用最高的）（<code>oom_adj</code> 是旧的 api，应该使用 <code>oom_score_adj</code>）。

<ul>
<li><code>oom_score</code> 是由内核计算出来的，消耗内存越多分越高，存活时间越长分越低，取值范围为。</li>
<li><code>oom_score_adj</code> 可以由用户配置，取值范围为 <code>['-1000', '1000']</code>（<code>-1000</code> 永远不会被 kill，<code>1000</code> 首先被 kill），如果想修改 <code>oom_score_adj</code> 则需要 <code>CAP_SYS_RESOURCE</code> 特权（参见：<a href="https://man7.org/linux/man-pages/man5/proc.5.html">proc(5) 手册</a>）。</li>
<li>更多关于 oom killer 参见：<a href="https://lwn.net/Articles/317814/">Taming the OOM killer</a>。</li>
</ul></li>
<li><code>memory.oom_control.oom_kill_disable = 1</code> 该 cgroup 中的进程，调用 <a href="https://man7.org/linux/man-pages/man2/brk.2.html">brk(2) 系统调用</a> （即 malloc 等）分配内存时，会进入不可中断休眠，表现是进程卡住。在这种场景，可以通过 <code>cgroup.event_control</code> 文件来监听到 oom 事件，并交由用户进程进行更精细的处理，而不是简单的 kill，更多参见下文 <code>cgroup.event_control</code>。</li>
</ul></li>
<li>该值会影响 Swap 分区的使用，假设该参数设置为 <code>100MB</code>，但是 <code>swap</code> 还剩余 <code>100MB</code>。则在该 cgroup 的进程看来，只要申请的内存总量 <code>&gt;= 200MB</code> 才会被 Kill。因此，在测试该参数时，使用 <code>sudo swapoff -a</code> 先关闭 Swap 以排除 Swap 的干扰。</li>
</ul></li>
<li><code>memory.soft_limit_in_bytes</code> rw，默认值和 <code>memory.limit_in_bytes</code> 一致，内存使用软限制，在 <code>CONFIG_PREEMPT_RT</code> 系统中不可用，对应的指标为 RSS + Page Cache。cgroup 对应指标超过该值，将触发内核，回收超过限额的进程占用的内存（猜测是回收 Page Cache），使之尽量和该值靠拢。</li>
<li><code>memory.memsw.limit_in_bytes</code> rw，默认值和 <code>memory.limit_in_bytes</code> 一致，对应的指标为 RSS + Page Cache + Swap，行为和 <code>memory.limit_in_bytes</code> 一致。</li>
<li><code>memory.usage_in_bytes</code> r，该 cgroup 中使用的内存总数，对应的指标为 RSS + Page Cache。</li>
<li><code>memory.max_usage_in_bytes</code> r，该 cgroup 使用的中内存+swap总数，对应的指标为 RSS + Page Cache + Swap。</li>
<li><code>memory.swappiness</code> rw，默认值 60，配置内存交换的发生时机，越小交换的次数越少，参见：<a href="https://linuxhint.com/understanding_vm_swappiness/">Understanding vm.swappiness</a>。</li>
<li><code>memory.oom_control</code> rw，

<ul>
<li>写入 <code>1</code>，可以配置禁用内核 oom killer（默认启用 oom killer）。</li>
<li>读取可以获取到如下数据：

<ul>
<li>oom_kill_disable 默认为 0，是否禁用内核 oom killer。参见 <code>memory.limit_in_bytes</code> 描述。</li>
<li>under_oom bool 值类型，表示当前 cgroup 是否处于缺少内存的状态。如果该 cgroup 缺少内存，则会暂停它里面的进程。under_oom 条目报告值为 1，否则为 0。</li>
<li>oom_kill ??</li>
</ul></li>
</ul></li>
<li><code>cgroup.event_control</code> w，事件通知虚拟文件，某进程可以创建一个 eventfd ，并将将该 eventfd 的文件描述符写入 <code>cgroup.event_control</code>，然后内核就会将 oom 事件写入该 eventfd 文件描述符，这个进程通过 epoll 获取到该事件。有两种常见用途：

<ul>
<li><code>memory.oom_control.oom_kill_disable = 0</code> ，监听 oom 事件，进行监控报警，辅助调度。</li>
<li>配置 <code>memory.oom_control.oom_kill_disable = 1</code> ，在用户态实现 oom-killer，而不是使用内核的 oom-killer。</li>
</ul></li>
<li><code>memory.stat</code> 获取各种 cgroup 粒度的内存相关统计数据，更多参见：<a href="https://docs.kernel.org/admin-guide/cgroup-v1/memory.html#stat-file">Memory Resource Controller - 5.2 stat file</a>。</li>
<li><code>memory.use_hierarchy</code> rw，默认值 1，已弃用，是否将子 cgroup 的内存使用情况统计到当前cgroup里面。</li>
<li><code>memory.failcnt</code> rw，查看示当前 cgroup 命中限制的次数，可以通过写入 0 重置该计数器。</li>
<li><code>memory.numa_stat</code> r，类似于 <code>memory.stat</code> 用于查看 numa 架构相关内存状态。</li>
</ul>

<p>参考：</p>

<ul>
<li><a href="https://docs.kernel.org/admin-guide/cgroup-v1/memory.html">Linux Kernal Docs - Memory Resource Controller</a></li>
<li><a href="https://zorrozou.github.io/docs/books/cgroup_linux_memory_control_group.html">Cgroup - Linux内存资源管理</a></li>
<li><a href="https://www.jianshu.com/p/f2403e33c766">使用event_control监听memory cgroup的oom事件</a></li>
<li><a href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/7/html/resource_management_guide/sec-memory">Redhat 资源管理指南 - A.7. memory</a></li>
<li><a href="https://blog.csdn.net/u010278923/article/details/105688107">linux内核的oom score是咋算出来的</a></li>
<li><a href="https://lwn.net/Articles/317814/">Taming the OOM killer</a></li>
<li><a href="https://man7.org/linux/man-pages/man5/proc.5.html">proc(5) 手册</a></li>
<li><a href="https://cloud.tencent.com/developer/article/1503835">Linux swappiness参数设置与内存交换</a></li>
<li><a href="https://linuxhint.com/understanding_vm_swappiness/">Understanding vm.swappiness</a></li>
<li><a href="https://segmentfault.com/a/1190000008125359">Linux Cgroup系列（04）：限制cgroup的内存使用（subsystem之memory</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/524431768">Cgroup 内存使用的监测手段</a></li>
<li><a href="https://www.cnblogs.com/charlieroro/p/10180827.html">docker cgroup 技术之memory（首篇）</a></li>
</ul>

<h4 id="实验-1">实验</h4>

<p>实验规划如下：</p>

<ol>
<li>启动一个监控进程，创建一个 demo memory cgroup，将 <code>memory.limit_in_bytes</code> 设置为 100 MB，其他保持默认。</li>
<li>监控进程创建两个子进程，加入上面创建的 demo memory cgroup，并申请 70 MB 内存，观察是否有一个被 kill，另外一个仍然存活。</li>
<li>监控进程再创建一个子进程，加入上面创建的 demo memory cgroup，并设置该进程的 oom_score_adj 为 1000，并申请 50 MB 内存，观察是否有该进程被优先 kill。</li>
<li>监控进程写入 1 到 <code>memory.oom_control</code> 禁用内核 oom killer，并通过 <code>cgroup.event_control</code> 文件配置当前进程接受 OOM 事件。</li>
<li>监控再创建一个子进程，加入上面创建的 demo memory cgroup，并申请 50 MB 内存，观察是否有进程被 kill，观察两个进程的进程状态，观察是否 OOM 事件通知，观察 <code>memory.usage_in_bytes</code>、<code>memory.oom_control</code> 文件内容。</li>
</ol>

<p>实验代码 <code>src/go/01-namespace/07-cgroup/v1/02-memory/main.go</code>，如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;path&#34;</span>
	<span style="color:#e6db74">&#34;path/filepath&#34;</span>
	<span style="color:#e6db74">&#34;strconv&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/shirou/gopsutil/v3/process&#34;</span>
	<span style="color:#e6db74">&#34;golang.org/x/sys/unix&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">MB</span>                  = <span style="color:#ae81ff">1024</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">1024</span>
	<span style="color:#a6e22e">memoryDemoCgroupDir</span> = <span style="color:#e6db74">&#34;/sys/fs/cgroup/memory/demo&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">SubCommandMonitor</span> = <span style="color:#e6db74">&#34;monitor&#34;</span>
	<span style="color:#a6e22e">SubCommandAlloc</span>   = <span style="color:#e6db74">&#34;alloc&#34;</span>
)

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">PinnedAllocMemory</span> [][]<span style="color:#66d9ef">byte</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">allocMemory</span>(<span style="color:#a6e22e">size</span> <span style="color:#66d9ef">int64</span>) {
	<span style="color:#a6e22e">b</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">byte</span>, <span style="color:#a6e22e">size</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> int64(<span style="color:#ae81ff">0</span>); <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">size</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
		<span style="color:#a6e22e">b</span>[<span style="color:#a6e22e">i</span>] = byte(<span style="color:#a6e22e">i</span>)
	}
	<span style="color:#a6e22e">PinnedAllocMemory</span> = append(<span style="color:#a6e22e">PinnedAllocMemory</span>, <span style="color:#a6e22e">b</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">printRSSMemory</span>(<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">Process</span>) {
	<span style="color:#a6e22e">m</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">MemoryInfo</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">PinnedAllocMemory</span>[<span style="color:#ae81ff">0</span>]); <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
		<span style="color:#a6e22e">s</span> <span style="color:#f92672">+=</span> int(<span style="color:#a6e22e">PinnedAllocMemory</span>[<span style="color:#ae81ff">0</span>][<span style="color:#a6e22e">i</span>])
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;  pid %d: %s\n&#34;</span>, <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">String</span>())
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleOOMEvent</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;=== handle oom event ...&#34;</span>)
	<span style="color:#a6e22e">usage_in_bytes</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">`memory.usage_in_bytes`</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;memory.usage_in_bytes: \n%s\n&#34;</span>, string(<span style="color:#a6e22e">usage_in_bytes</span>))
	<span style="color:#a6e22e">ommControlBytes</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">`memory.oom_control`</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;memory.oom_control: \n%s\n&#34;</span>, string(<span style="color:#a6e22e">ommControlBytes</span>))
	<span style="color:#a6e22e">procsBytes</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;cgroup.procs&#34;</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">procs</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(string(<span style="color:#a6e22e">procsBytes</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">pidStr</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">procs</span> {
		<span style="color:#a6e22e">pidStr</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">pidStr</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">pidStr</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
			<span style="color:#66d9ef">break</span>
		}
		<span style="color:#a6e22e">pid64</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">ParseInt</span>(<span style="color:#a6e22e">pidStr</span>, <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">32</span>)
		<span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">NewProcess</span>(int32(<span style="color:#a6e22e">pid64</span>))
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">status</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Status</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">oom_score</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;/proc/%d/oom_score&#34;</span>, <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Pid</span>))
		<span style="color:#a6e22e">oom_adj</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;/proc/%d/oom_adj &#34;</span>, <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Pid</span>))
		<span style="color:#a6e22e">oom_score_adj</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;/proc/%d/oom_score_adj&#34;</span>, <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Pid</span>))
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;pid %d state is: %v (oom_adj=%s, oom_score=%s, oom_score_adj=%s)\n&#34;</span>, <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">status</span>, <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">oom_adj</span>)), <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">oom_score</span>)), <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(string(<span style="color:#a6e22e">oom_score_adj</span>)))
		<span style="color:#75715e">// 如果要模拟内核的 oom-killer 的逻辑： oom_score + oom_adj + oom_score_adj 排序，并给最大的那个发送 SIGKILL(9) 信号。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 但是 oom-killer 作为 Linux 内存分配的最后的保证，非常不建议禁用 oom-killer。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 因此推荐如下：
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//   1. 通过调整 `oom_score_adj` 来调整进程的内存优先级，以自定义 oom 时 killer 的顺序。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//   2. 可以通过监听 oom killer 事件，统计 oom 发生的频率，以辅助调度。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//   2. 如需收集记录进程被 kill 的日志，只能通过 `dmesg` 或 /var/log/syslog 内核日志获取信息 （killed process）。
</span><span style="color:#75715e"></span>	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">createOOMEventHandler</span>() {
	<span style="color:#75715e">// https://www.jianshu.com/p/f2403e33c766
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/7/html/resource_management_guide/sec-memory
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">events</span> [<span style="color:#ae81ff">128</span>]<span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EpollEvent</span>
	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">buf</span> [<span style="color:#ae81ff">8</span>]<span style="color:#66d9ef">byte</span>
	<span style="color:#75715e">// 创建epoll实例
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">epollFd</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EpollCreate1</span>(<span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EPOLL_CLOEXEC</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 创建eventfd实例
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">efd</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">Eventfd</span>(<span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EFD_CLOEXEC</span>)

	<span style="color:#a6e22e">event</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EpollEvent</span>{
		<span style="color:#a6e22e">Fd</span>:     int32(<span style="color:#a6e22e">efd</span>),
		<span style="color:#a6e22e">Events</span>: <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EPOLLHUP</span> | <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EPOLLIN</span> | <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EPOLLERR</span>,
	}
	<span style="color:#75715e">// 将eventfd添加到epoll中进行监听
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EpollCtl</span>(<span style="color:#a6e22e">epollFd</span>, <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EPOLL_CTL_ADD</span>, int(<span style="color:#a6e22e">efd</span>), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">event</span>)

	<span style="color:#75715e">// 打开oom_control文件
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">evtFile</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Open</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;memory.oom_control&#34;</span>))

	<span style="color:#75715e">// 注册oom事件，当有oom事件时，eventfd将会有数据可读
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">data</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%d %d&#34;</span>, <span style="color:#a6e22e">efd</span>, <span style="color:#a6e22e">evtFile</span>.<span style="color:#a6e22e">Fd</span>())
	<span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;cgroup.event_control&#34;</span>), []byte(<span style="color:#a6e22e">data</span>), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o700</span>)

	<span style="color:#66d9ef">for</span> {
		<span style="color:#75715e">// 开始监听oom事件
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">n</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">EpollWait</span>(<span style="color:#a6e22e">epollFd</span>, <span style="color:#a6e22e">events</span>[:], <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">n</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
				<span style="color:#75715e">// 消费掉eventfd的数据
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">Read</span>(int(<span style="color:#a6e22e">events</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">Fd</span>), <span style="color:#a6e22e">buf</span>[:])
				<span style="color:#75715e">// 处理 oom envent
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">handleOOMEvent</span>()
			}
		}
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">monitor</span>() {
	<span style="color:#75715e">// 1.1 在默认 memory 层级，根 cgroup 创建一个名为 demo 的 cgroup。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Mkdir</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o755</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#75715e">// 使用 unix 的 rmdir 系统调用。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 参考 https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L231
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">Rmdir</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">ENOENT</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
	}()
	<span style="color:#75715e">// 1.2 配置该 cgroup 的 memory.limit_in_bytes 为 100 MB
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;memory.limit_in_bytes&#34;</span>), []byte(<span style="color:#e6db74">&#34;100M&#34;</span>), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#75715e">// 2.1 创建两个进程，先加入 cgroup，再分配 70MB 左右内存（注意：实测先分配内存再加入不会被 cgroup 感知？）。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">proc1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/proc/self/exe&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#ae81ff">70</span><span style="color:#f92672">*</span><span style="color:#a6e22e">MB</span>))
	<span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Start</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">proc1Done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc1Done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Wait</span>(); close(<span style="color:#a6e22e">proc1Done</span>) }()
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Kill</span>(); <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Wait</span>() }()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc1 %d start ...\n&#34;</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)

	<span style="color:#a6e22e">proc2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/proc/self/exe&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#ae81ff">70</span><span style="color:#f92672">*</span><span style="color:#a6e22e">MB</span>))
	<span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Start</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">proc2Done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc2Done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Wait</span>(); close(<span style="color:#a6e22e">proc2Done</span>) }()
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Kill</span>(); <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Wait</span>() }()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc2 %d start ...\n&#34;</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)

	<span style="color:#75715e">// 2.3 观察两个进程的存活状态
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">proc1Done</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc1 %d has exited: %s\n&#34;</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">ProcessState</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#66d9ef">default</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc1 %d running\n&#34;</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	}
	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">proc2Done</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc2 %d has exited: %s\n&#34;</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">ProcessState</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#66d9ef">default</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc2 %d is running\n&#34;</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#75715e">// 3.1 再创建一个子进程，加入上面创建的 demo memory cgroup，并设置该进程的 oom_score_adj 为 1000，并申请 50 MB 内存
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">proc3</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/proc/self/exe&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#ae81ff">50</span><span style="color:#f92672">*</span><span style="color:#a6e22e">MB</span>), <span style="color:#e6db74">&#34;1000&#34;</span>)
	<span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Start</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">proc3Done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc3Done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Wait</span>(); close(<span style="color:#a6e22e">proc3Done</span>) }()
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Kill</span>(); <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Wait</span>() }()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc3 %d start ...\n&#34;</span>, <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)

	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">proc1Done</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc1 %d has exited: %s\n&#34;</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">ProcessState</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#66d9ef">default</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc1 %d running\n&#34;</span>, <span style="color:#a6e22e">proc1</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	}
	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">proc2Done</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc2 %d has exited: %s\n&#34;</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">ProcessState</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#66d9ef">default</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc2 %d is running\n&#34;</span>, <span style="color:#a6e22e">proc2</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	}
	<span style="color:#66d9ef">select</span> {
	<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">proc3Done</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc3 %d has exited: %s\n&#34;</span>, <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>, <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">ProcessState</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#66d9ef">default</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc3 %d is running\n&#34;</span>, <span style="color:#a6e22e">proc3</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#75715e">// ! 注意：这里只是一个演示，在生产环境不建议禁用 oom-killer
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 4.1 监控进程写入 1 到 `memory.oom_control` 禁用内核 oom killer，并通过 `cgroup.event_control` 文件配置当前进程接受 OOM 事件。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;memory.oom_control&#34;</span>), []byte(<span style="color:#e6db74">&#34;1&#34;</span>), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">createOOMEventHandler</span>()
	<span style="color:#75715e">// 5.1 监控再创建一个子进程，加入上面创建的 demo memory cgroup，并申请 50 MB 内存
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">proc4</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/proc/self/exe&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#ae81ff">50</span><span style="color:#f92672">*</span><span style="color:#a6e22e">MB</span>))
	<span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Start</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">proc4Done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc4Done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Wait</span>(); close(<span style="color:#a6e22e">proc4Done</span>) }()
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Kill</span>(); <span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Wait</span>() }()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;proc4 %d start ...\n&#34;</span>, <span style="color:#a6e22e">proc4</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// time.Sleep(60 * time.Second)
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">// sudo swapoff -a
</span><span style="color:#75715e">// go build ./src/go/01-namespace/07-cgroup/v1/02-memory &amp;&amp; sudo ./02-memory; rm -rf ./02-memory
</span><span style="color:#75715e">// sudo dmesg -T | egrep -i &#39;killed process&#39;
</span><span style="color:#75715e">// sudo swapon -a
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span> {
		<span style="color:#a6e22e">monitor</span>()
	} <span style="color:#66d9ef">else</span> {
		<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">ParseInt</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>], <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">64</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#75715e">// 2.1.a 加入 Cgroup。
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">memoryDemoCgroupDir</span>, <span style="color:#e6db74">&#34;cgroup.procs&#34;</span>), []byte(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#75715e">// 3.1.a 配置进程的 `oom_score_adj`
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">3</span> {
			<span style="color:#a6e22e">adj</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">ParseInt</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">2</span>], <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">64</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#e6db74">&#34;/proc/self/oom_score_adj&#34;</span>, []byte(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">adj</span>)), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
		}
		<span style="color:#75715e">// 2.2.b 申请内存
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">allocMemory</span>(<span style="color:#a6e22e">s</span>)
		<span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">NewProcess</span>(int32(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>()))
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">printRSSMemory</span>(<span style="color:#a6e22e">p</span>)
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	}
}</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">proc1 380232 start ...
  pid 380232: {&#34;rss&#34;:80896000,&#34;vms&#34;:1247854592,&#34;hwm&#34;:0,&#34;data&#34;:0,&#34;stack&#34;:0,&#34;locked&#34;:0,&#34;swap&#34;:0}
proc2 380256 start ...
  pid 380256: {&#34;rss&#34;:82972672,&#34;vms&#34;:1247789056,&#34;hwm&#34;:0,&#34;data&#34;:0,&#34;stack&#34;:0,&#34;locked&#34;:0,&#34;swap&#34;:0}
proc1 380232 has exited: signal: killed
proc2 380256 is running

proc3 380270 start ...
proc1 380232 has exited: signal: killed
proc2 380256 is running
proc3 380270 has exited: signal: killed

proc4 380295 start ...
=== handle oom event ...
memory.usage_in_bytes: 
104857600

memory.oom_control: 
oom_kill_disable 1
under_oom 1
oom_kill 2

pid 380256 state is: [sleep] (oom_adj=, oom_score=669, oom_score_adj=0)
pid 380295 state is: [sleep] (oom_adj=, oom_score=668, oom_score_adj=0)</pre></div>
<h4 id="最佳实现">最佳实现</h4>

<ul>
<li>oom-killer 作为 Linux 内存资源的硬限制，不建议通过 <code>memory.oom_control</code> 禁用。</li>
<li>如有对进行内存优先级的自定义需求，可以通过 <code>/proc/&lt;pid&gt;/oom_score_adj</code> 来调整。</li>
<li><code>cgroup.event_control</code> 仅建议用来，监听 oom killer 事件，统计 oom 发生的频率，以辅助调度。</li>
<li>如需收集记录进程被 kill 的日志，只能通过 <code>dmesg</code> 或 <code>/var/log/syslog</code> 内核日志获取信息 （killed process） 来获取。</li>
</ul>

<h4 id="kubernetes-驱逐">kubernetes 驱逐</h4>

<ul>
<li>kubernetes 对内存管理的最小粒度是 Pod。</li>
<li>kubernetes 将 Pod <code>resources.limits.memory</code> 设置到 cgroup 的 <code>memory.limit_in_bytes</code>。</li>
<li>kubernetes 会按照 <a href="https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/quality-service-pod/">QoS</a> 配置 Pod 中进程的 <code>/proc/&lt;pid&gt;/oom_score_adj</code>。</li>
<li>kubernetes 的后台进程 (kubelet)，会在后台监控宿主机（node）的资源水位，在触发 cgroup 的 oom-killer 之前，主动的杀死 Pod （压力驱逐，该机制仅针对类似内存之类的不可压缩资源），更多参见：<a href="https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/node-pressure-eviction/">节点压力驱逐</a>。</li>
<li>总结一下，站在 Pod 角度。可以分两种情况，来讨论 kubernetes Pod 因内存问题而出现异常：

<ul>
<li>Pod 进程实际使用的内存总和的内存超过了 <code>resources.limits.memory</code> 的限制。此时 cgroup 的 <code>memory.limit_in_bytes</code> 发生作用，会 kill 掉这个内存使用超限的进程。在这种情况下，操作的粒度是进程，因此 Pod 并不一定会被会退出，而是某个（些）进程退出。</li>
<li>Node 压力过大，Pod 申请的内存没有达到限制。此时 kubernetes 的节点压力驱逐机制生效，会按照策略驱逐某个（些）Pod，并重新调度到其他的 Node 中重新创建。在这种情况下，操作的粒度是 Pod，因此整个 Pod 都会退出。</li>
</ul></li>
</ul>

<h3 id="其他">其他</h3>

<p>在 kubernetes 中暂未使用，本文不做说明，如需了解，参见如下链接：</p>

<ul>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt">blkio</a> 限制进程的磁盘IO带宽和IO操作。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt">devices</a> 允许或禁止进程访问指定的设备。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt">freezer</a> 暂停或恢复进程执行。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/hugetlb.txt">hugetlb</a> 限制进程对大页内存的使用。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/net_cls.txt">net_cls</a> 通过标记网络数据包来分类控制网络流量。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/net_prio.txt">net_prio</a> 设置进程生成的网络流量的优先级。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt">pids</a> 限制进程可 fork的进程数。</li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v1/rdma.txt">rdma</a> 限制和隔离进程对 RDMA/IB （Remote Direct Memory Access 即远程直接内存访问） 设备的访问。</li>
</ul>

<h2 id="cgroup-v2">cgroup v2</h2>

<blockquote>
<p><a href="https://www.kernel.org/doc/Documentation/cgroup-v2.txt">Linxu 内核文档</a></p>
</blockquote>

<p><a href="https://kubernetes.io/zh-cn/docs/concepts/architecture/cgroups/#cgroup-v2">kubernetes</a></p>

<h3 id="和-v1-对比">和 v1 对比</h3>

<blockquote>
<p><a href="https://man7.org/linux/man-pages/man7/cgroups.7.html#CGROUPS_VERSION_2">cgroups(7) — Linux manual page - CGROUPS VERSION 2</a></p>
</blockquote>

<ul>
<li>cgroups v2 使用一个统一的层次结构，且所有控制器都自动安装到这个层次结构（根）。也就是说，v2 只有一颗 cgroup 树。以 <code>memory.limit_in_bytes</code> 为例，其根 cgroup 的路径在 v1 和 v2 分别为：

<ul>
<li><code>/sys/fs/cgroup/memory/memory.stat</code></li>
<li><code>/sys/fs/cgroup/memory.stat</code></li>
</ul></li>
<li>除了根 cgroup 外，进程只能添加到叶子节点的 cgroup 中 （这让资源管理更加好理解，实现也更加容易）。</li>
<li>cgroup v2 节点要挂载那些控制器需要通过 <code>cgroup.controllers</code> 和 <code>cgroup.subtree_control</code> 配置，而不是 v1 之前的自动继承层次绑定的控制器（更加灵活了）。</li>
<li>cgroup v2 删除了 <code>tasks</code>（该文件在 v1 中用来配置线程的），通过 <code>cgroup.type</code> 来决定管理的粒度。</li>
<li>cgroup v2 删除了 <code>cpuset</code> 使用的 <code>cgroup.clone_children</code> 文件。</li>
<li>新增 cgroup.events 文件提供统一的通知机制。</li>
<li>v2 实现了较为安全的委派机制，参见下结。</li>
</ul>

<h3 id="委托机制-delegation">委托机制 (delegation)</h3>

<p>委派指的是，允许非 root 用户创建自己的子 cgroup，并在这个子 cgroup 中作资源管理。几个关键点:</p>

<ul>
<li>支持用户命名空间 (user namespace)</li>
<li>设置 cgroup 所有者</li>
<li>控制文件访问权限</li>
<li>不强制要求 root 权限</li>
</ul>

<p>委托方式有两种：</p>

<ul>
<li>委托给用户：修改即可对应的目录文件的权限（如 chown 命令修改）。</li>
<li>重新 mount，如： <code>mount -t cgroup2 -o remount,nsdelegate none /sys/fs/cgroup/unified</code>。</li>
</ul>

<p>更多参见： <a href="https://www.kernel.org/doc/Documentation/cgroup-v2.txt">cgroup-v2</a>。</p>

<p>cgroup v2 的委托，主要在 rootless 容器场景有用：</p>

<ul>
<li><a href="https://docs.docker.com/engine/security/rootless/#limiting-resources">docker rootless</a></li>
<li><a href="https://rootlesscontaine.rs/getting-started/common/cgroup2/">rootless cgroup v2</a></li>
<li><a href="https://systemd.io/CGROUP_DELEGATION/">systemd cgroup delegation</a></li>
</ul>

<h2 id="cgroup-namespace">cgroup namespace</h2>

<blockquote>
<p><a href="https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html">cgroup_namespaces(7) — Linux manual page</a></p>
</blockquote>

<p>通过 <code>/proc/pid/cgroup</code> 可以看到当前进程所属 cgroup 的层次结构（格式为 <code>hierarchy_id:controller_list:cgroup_path</code>）。</p>

<p>默认情况下，在 <code>cgroup_path</code> 部分，可以看到从根 cgroup 到当前 cgroup 的真个路径，这可能泄漏一些信息。</p>

<p>如果创建进程时，新建一个 cgroup namespace（clone 系统调用带有 <code>CLONE_NEWCGROUP</code> 选项），则该进程当前进程的 cgroup 根将变为这个 cgroup。</p>

<p>简单而言，cgroup namespace 就是重设 cgroup 根，让让进程看不到祖先 cgroup。</p>

<h2 id="其他-1">其他</h2>

<h3 id="管理命令和-api">管理命令和 API</h3>

<ul>
<li>直接通过常规的文件系统工具操作 cgroup 文件系统。</li>
<li><a href="https://packages.debian.org/bookworm/cgroup-tools">cgroup-tools 命名行工具</a>。</li>
<li><a href="https://github.com/libcgroup/libcgroup"><code>libcgroup</code> C 库</a></li>
<li><a href="https://github.com/containerd/cgroups"><code>containerd/cgroups</code> Golang 库</a>。</li>
</ul>

<h3 id="docker-kubernetes-使用-cgroup">docker、 kubernetes 使用 cgroup</h3>

<blockquote>
<p>以 cgroup v1 为例</p>
</blockquote>

<p>两者在底层都使用了 runc，具体 cgroup 路径有所不同。</p>

<ul>
<li>docker 创建 <code>/sys/fs/cgroup/$hierarchy/docker/容器ID</code> 并 mount binding 到容器 rootfs 的 <code>/sys/fs/cgroup/$hierarchy</code>。</li>
<li>kubernetes 创建 <code>/sys/fs/cgroup/memory/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2</code> 并 mount binding 到容器 rootfs 的 <code>/sys/fs/cgroup/$hierarchy</code>。 （手动观察命令参见：<a href="https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/pod-overhead/#%E9%AA%8C%E8%AF%81-pod-cgroup-%E9%99%90%E5%88%B6">文档</a>）</li>
</ul>

<h3 id="非容器场景使用-cgroup-管理进程">非容器场景使用 cgroup 管理进程</h3>

<ul>
<li>虽然 Linux 没有限制创建自己的 cgroup hierarchy。但是，一般情况下，没有必要重新创建自己的 cgroup hierarchy。因为在多数情况下，我们对每种系统资源的管控通过一棵树就可以实现。因此，直接在 <code>/sys/fs/cgroup/&lt;hierarchy&gt;/</code> (v2 为 <code>/sys/fs/cgroup</code>)  目录下建立自己应用的 cgroup (目录) 即可。</li>
<li>在 cgrouo v2 中，可以通过 systemd cgroup 委托机制来管理 cgroup，非使用 root 权限来管理。</li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://tech.meituan.com/2015/03/31/cgroups.html">美团技术团队 - Linux资源管理之cgroups简介</a></li>
<li><a href="https://www.jianshu.com/p/7c18075aa735">RunC 源码通读指南之 Cgroup</a></li>
<li><a href="https://www.jianshu.com/p/fdfeabcb08b4">docker cgroup 配置</a></li>
<li><a href="https://github.com/opencontainers/runc/blob/96a61d3bf0dcc26343bfafe5112934d73d280dd3/libcontainer/rootfs_linux.go#L255">runc mount v1 cgroup 路径</a></li>
</ul>
]]></description></item><item><title>Go 兼容性（二） 向前兼容（GOTOOLCHAIN 工具链管理）</title><link>https://www.rectcircle.cn/posts/go-compatibility-2-forward/</link><pubDate>Sun, 17 Sep 2023 20:08:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-compatibility-2-forward/</guid><description type="html"><![CDATA[

<blockquote>
<p>Go 1.21.0 | <a href="https://github.com/rectcircle/go-compatibility-example">示例代码</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Golang 作为一门现代编程语言，Golang 工具链自身的版本管理一直是缺失的。以至于，有一些第三方 Go 工具链版本管理工具，弥补这一空白，如 <a href="https://github.com/moovweb/gvm">gvm</a>。但是 gvm 只是一种外部的传统的 Go 工具链版本管理工具，并没有很好的和 Go 项目进行集成。</p>

<p>在 Go 1.21，Go 终于引入了，工具链版本管理机制。利用该机制和 Go Module、Go Workspace 等深度集成，可以有效的避免之前没有定义的向前兼容的问题。</p>

<h2 id="向前兼容">向前兼容</h2>

<blockquote>
<p>本文中的编译器和工具链均代指 Go 工具链，不关心两者细微差异。</p>
</blockquote>

<p>Golang 的向前兼容（Forward Compatibility）指的是，用旧版的 Go 编译器编译，面向新版本的 Go 代码，是否仍能保证代码编译通过，且运行无误。如：即使几年前发布的 Go 1.18 编译器，编译今天面向 Go 1.21 的的项目。</p>

<p>显然，任意一门编程语言都不可能满足这个需求，因为编程语言新版本的特性必然是旧版本的超集。除非这个编程语言不发展了，才有这种可能性，Go 自身显然是做不到的。</p>

<p>显然，Go 不支持向前兼容。但在 Go 1.21 之前，Go 并没有禁止使用旧版编译器编译新版项目。也就是说，某些在 <code>go.mod</code> 中 <code>go line</code> 中声明的版本比 go 编译器的版本新，go 编译器不会拒绝编译，仍然会尝试编译，直到遇到当前旧编译器不认识的语法。举个例子，一个 go 1.18 的项目，使用 go 1.17 的编译器编译，会出现如下解决：</p>

<ul>
<li>如果项目中没有使用泛型等 go 1.18 新特性，则编译通过。</li>
<li>如果项目使用了泛型等 go 1.18 新特性，则编译不通过，报错是语法错误。</li>
</ul>

<p>这样的行为很不一致，很让人困惑。另外出现了问题，用户还需要手动安装新版的 Go 编译器。</p>

<p>在 Go 1.21 及其之后， go 编译器通过如下手段解决了这个问题：</p>

<ul>
<li>go 编译器会 go.mod 或 go.work 的 <code>go line</code> 或 <code>toolchain line</code> 和 <code>GOTOOLCHAIN</code> 环境变量的配置，确定最合适 go 编译器版本。</li>
<li>如果找不到合适的版本则直接报错。否则使用上面指定的 go 编译器版本执行命令。</li>
</ul>

<p>通过如上机制，Go 编译器解决了上述的问题。</p>

<p>此外，这个能力相当于在 Go 工具链自身实现了一个 Go 工具链版本管理的能力，但开发者又无需感知工具链版本管理的复杂性，这非常符合 Go 极简的哲学。</p>

<p>上述过程细节参见下文： <a href="#go-工具链">Go 工具链</a>。</p>

<h2 id="环境准备">环境准备</h2>

<blockquote>
<p>上一篇执行过，可以忽略。</p>
</blockquote>

<p>目前 Golang 已发布到 1.21.1。但为了方便后续验证，在 Linux amd64 操作系统， 安装 <code>1.21.0</code> 版本。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo mkdir -p /opt/go
sudo tar -xzvf go1.21.0.linux-amd64.tar.gz -C /opt/go
sudo mv /opt/go/go /opt/go/go1.21.0
echo <span style="color:#e6db74">&#39;export GOROOT=/opt/go/go1.21.0&#39;</span> &gt;&gt; ~/.bashrc
echo <span style="color:#e6db74">&#39;export PATH=/opt/go/go1.21.0/bin:$PATH&#39;</span> &gt;&gt; ~/.bashrc
rm -rf go1.21.0.linux-amd64.tar.gz
go env -w GOPROXY<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;https://goproxy.cn|direct&#39;</span></code></pre></div>
<h2 id="go-工具链">Go 工具链</h2>

<h3 id="go-line">go line</h3>

<p>在 Go 1.21 及其之后，如果项目的 go module 的 <code>go line</code> 版本小于其直接或间接依赖的 module 的最大值，进行编译会报错，举例如下：</p>

<ul>
<li>项目 <code>go line</code> 版本为 <code>go 1.21.0</code> （示例代码 <code>./02-gotoolchain/02-goline-old-require-new</code>）</li>
<li>项目依赖的 module 的 <code>go line</code> 版本为 <code>go 1.21.1</code> （示例代码 <code>./02-gotoolchain/01-goline-new</code>）</li>
</ul>

<p>此时，分两种情况讨论：</p>

<ul>
<li>Go 编译器版本为 <code>go1.21.0</code> 时，执行编译（<code>GOTOOLCHAIN=go1.21.0 go run ./</code>）将报错 <code>go: module ../01-goline-new requires go &gt;= 1.21.1 (running go 1.21.0)</code>。</li>
<li>Go 编译器版本为 <code>go1.21.1</code> 时，执行编译（<code>GOTOOLCHAIN=go1.21.1 go run ./</code>）将报错 <code>go: updates to go.mod needed; to update it: go mod tidy</code>。

<ul>
<li>执行 <code>go mod tidy</code> 后， <code>go line</code> 将变更为 <code>go 1.21.1</code>。</li>
</ul></li>
</ul>

<p>总结：在 Go 1.21 及其之后，编译器保证，项目的 <code>go line</code> 版本必须大于等于依赖的  <code>go line</code> 版本（<code>go.work</code> 和 <code>go.mod</code> 均需满足）。</p>

<h3 id="toolchain-line">toolchain line</h3>

<p><code>go.mod</code> 或 <code>go.work</code> 中新增了 <code>toolchain &lt;tname&gt;</code> 语法，下文称该语法为 <code>toolchain line</code>，可以用来配置工具链版本。</p>

<p>如果项目只使用 <code>go line</code>，那么只要 go 编译器的版本要大于等于 <code>go line</code> 就会进行编译。</p>

<p>如果项目同时配置了 <code>go line</code> 和 <code>toolchain line</code>，则编译器版本需要同时大于等于 <code>go line</code> 和 <code>toolchain line</code> 才能进行编译。</p>

<p>通过 <code>go get go@1.22.1</code> 或 <code>go get toolchain@1.24rc1</code> 可以配置 <code>go.mod</code> 或 <code>go.work</code> 中的 <code>go line</code> 或 <code>toolchain line</code> 。</p>

<p>此外，<code>go work use</code> 会检查 <code>go.work</code> 中的 <code>go line</code> 或 <code>toolchain line</code> 和 mod 中的版本。</p>

<h3 id="版本">版本</h3>

<p>引入工具链版本管理后，Go 有了两个版本名：<code>go line</code> 版本，<code>toolchain line</code> go 工具链版本。这两个版本名的规则如下：</p>

<ul>
<li><code>go line</code> 版本，格式为 <code>1.N.P</code> 或 <code>1.N</code>。</li>
<li>go 工具链版本，格式为 <code>go1.N.P-suffix</code> （或 <code>go1.N-suffix</code> go1.21 之前）。</li>
</ul>

<p>为了兼容，这里有一些小细节是：</p>

<ul>
<li>在 go1.21 之前，go 工具链版本和 <code>go line</code> 版本可以一一对应。</li>
<li>在 go1.21 及其之后，<code>go line</code> 版本仍然支持 <code>1.N</code>，而 go 工具链版本不再支持 <code>go1.N-suffix</code> 格式。</li>
</ul>

<p>这连个版本的比较规则如下：</p>

<ul>
<li>go 工具链版本去除前导的 <code>go</code> 和后缀 <code>-suffix</code>，得到 <code>V2</code>； 和 <code>go line</code> 版本 <code>V1</code> 进行比较。</li>
<li>在 Go 1.21 之前，排序规则为： <code>1.20rc1 &lt; 1.20rc2 &lt; 1.20rc3 &lt; 1.20 &lt; 1.20.1</code></li>
<li>在 Go 1.21 及其之后，排序规则为： <code>1.21 &lt; 1.21rc1 &lt; 1.21rc2 &lt; 1.21.0 &lt; 1.21.1 &lt; 1.21.2</code></li>
</ul>

<p>也就是说：</p>

<ul>
<li>在 1.21 之前，<code>go line</code> 声明为 <code>1.N</code> 则只能使用稳定版进行编译。</li>
<li>在 Go 1.21 及其之后，<code>go line</code> 声明为 <code>1.N</code> 可以使用 rc 版进行编译。</li>
</ul>

<h3 id="gotoolchain">GOTOOLCHAIN</h3>

<blockquote>
<p>假设项目的 <code>go line</code> 版本为 <code>v_go_line</code>，<code>toolchain line</code> 的版本为 <code>v_toolchain_line</code>，当前执行的 go 命令版本为 <code>v_local</code>。</p>
</blockquote>

<p>GOTOOLCHAIN 是 go 1.21 新增的一个环境变量，可以通过 <code>export</code>、<code>go env -w</code> 方式配置。</p>

<p>该参数控制了 Go 工具链的选择，该变量值的格式有如下五种情况：</p>

<ul>
<li><code>local+auto</code> (<code>auto</code>) 默认值：选取的版本为 <code>v=max(v_local, v_go_line, v_toolchain_line)</code>。

<ul>
<li>如果 <code>v==v_local</code>，则直接使用当前的 go 命令执行，否则进行下一步。</li>
<li><code>PATH</code> 中查找对应的可执行文件，如： 当 <code>v=go1.21.3</code> 时，查找的文件名为 <code>go1.21.3</code>。找到后则执行，否则进行下一步。</li>
<li>前往 GOPROXY 下载对应版本工具链并安装，然后执行该版本。</li>
</ul></li>
<li><code>local+path</code> (<code>path</code>)：选取的版本为 <code>v=max(v_local, v_go_line, v_toolchain_line)</code>。

<ul>
<li>如果 <code>v==v_local</code>，则直接使用当前的 go 命令执行，否则进行下一步。</li>
<li><code>PATH</code> 中查找对应的可执行文件，如： 当 <code>v=go1.21.3</code> 时，查找的文件名为 <code>go1.21.3</code>。找到后则执行，否则进行下一步。</li>
<li>直接报错，如 <code>go: cannot find &quot;go1.21.4&quot; in PATH</code>。</li>
</ul></li>
<li><code>&lt;name&gt;</code>：忽略 <code>v_go_line</code> 和 <code>v_toolchain_line</code>，选取的版本为 <code>v=&lt;name&gt;</code>。

<ul>
<li>如果 <code>v==v_local</code>，则直接使用当前的 go 命令执行，否则进行下一步。</li>
<li><code>PATH</code> 中查找对应的可执行文件，如： 当 <code>v=go1.21.3</code> 时，查找的文件名为 <code>go1.21.3</code>。找到后则执行，否则进行下一步。</li>
<li>前往 GOPROXY 下载对应版本工具链并安装，然后执行该版本。</li>
</ul></li>
<li><code>&lt;name&gt;+auto</code>：选取的版本为 <code>v=max(&lt;name&gt;, v_go_line, v_toolchain_line)</code>。

<ul>
<li>如果 <code>v==v_local</code>，则直接使用当前的 go 命令执行，否则进行下一步。</li>
<li><code>PATH</code> 中查找对应的可执行文件，如： 当 <code>v=go1.21.3</code> 时，查找的文件名为 <code>go1.21.3</code>。找到后则执行，否则进行下一步。</li>
<li>前往 GOPROXY 下载对应版本工具链并安装，然后执行该版本。</li>
</ul></li>
<li><code>&lt;name&gt;+path</code>：选取的版本为 <code>v=max(&lt;name&gt;, v_go_line, v_toolchain_line)</code>。

<ul>
<li>如果 <code>v==v_local</code>，则直接使用当前的 go 命令执行，否则进行下一步。</li>
<li><code>PATH</code> 中查找对应的可执行文件，如： 当 <code>v=go1.21.3</code> 时，查找的文件名为 <code>go1.21.3</code>。找到后则执行，否则进行下一步。</li>
<li>直接报错，如 <code>go: cannot find &quot;go1.21.4&quot; in PATH</code>。</li>
</ul></li>
</ul>

<h2 id="ide-集成">IDE 集成</h2>

<p>VSCode golang (gopls) 已可以正确识别 <code>go line</code> 和 <code>toolchain line</code> 配置的工具链。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://golang.org/doc/toolchain">Go 官方文档：toolchain</a></li>
</ul>
]]></description></item><item><title>Go 兼容性（一） 向后兼容（GODEBUG）</title><link>https://www.rectcircle.cn/posts/go-compatibility-1-backward/</link><pubDate>Sun, 17 Sep 2023 17:24:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-compatibility-1-backward/</guid><description type="html"><![CDATA[

<blockquote>
<p>Go 1.21.0 | <a href="https://github.com/rectcircle/go-compatibility-example">示例代码</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Golang 作为一门现代编程语言，和上世纪的编程语言相比，一大优势就是提供了强有力的兼容性保证。</p>

<p>本文将重点介绍 Golang 1.21 带来新的兼容性保证和相关切实的机制，通过这些机制，可以切实提升 Go 开发者的 “升级率”：</p>

<ul>
<li>消除了开发者对现存项目升级新版 Go 版本（编译器和语法）的顾虑，让开发者可以低成本的享受 Golang 的新特性，而不用担心升级带来的不兼容风险。</li>
<li>十分有利于 Go 的发展，避免出现类似于 Java TLS 都出 Java 17，大家还在使用 Java 8 的尴尬场面。</li>
</ul>

<p>有了这些兼容性的机制，Go 1.21 的重要性不亚于 Golang 1.11 Go Module 带来依赖管理，具有里程碑意义。</p>

<p>本文将详细介绍这些机制，希望给 Go 开发者建立一个认知：现在可以将保持在 Go 1.16、Go 1.18 等旧版本的项目，升级 Go 1.21；并在之后 Go 版本发布后，始终让自己的 Go 项目可以保持跟随。</p>

<h2 id="向后兼容">向后兼容</h2>

<p>Golang 的向后兼容（Backward Compatibility）指的是，用新版的 Go 编译器编译，历史上的 Go 代码，仍能保证代码编译通过，且运行无误。如：即使 10 年前编写的 Go 代码，使用当前最新的 go1.21 编译器进行编译运行，也不会出现任何问题。</p>

<p>在 Golang 1.21 发布之前，Golang 官方通过 <a href="https://go.dev/doc/go1compat">Go 1 and the Future of Go Programs</a> 规范定义了 Go 1 的兼容性。但是这篇规范有两个显著的问题：</p>

<ul>
<li>暗示未来 Go 2 的到来，Go 1 的代码可能无法再 Go 2 中编译，就像 Python2 和 Python3 那样。这是令人难以接受。</li>
<li>某些特性的更新，不违反该规范 Go 1 兼容性承诺，但是仍然可能让旧的代码在新的 Go 编译器中编译后运行的行为会失败或不一致。</li>
</ul>

<p>随着 Golang 1.21 的发布，Golang 官方通过 <a href="https://go.dev/blog/compat">Backward Compatibility, Go 1.21, and Go 2</a> 博客，明确了上述两个问题：</p>

<ul>
<li>承诺，上述意义的 Go 2 永远不会到来，即永远不会破坏向后兼容。实际上，在 Go 官方看来，自 2017 年来，Go 已经逐步到走向了 Go 2，也就是说现在的 Go 已经是 Go 2 了。</li>
<li>分析并归纳了那些 <strong>不违反 Go 1 兼容性承诺但是仍然后可能破坏兼容性的变更</strong> （输入更改、输出更改、协议变更），并通过规范 <code>GODEBUG</code> 的使用，来解决这种问题，参见下文：<a href="#godebug">GODEBUG</a>。</li>
</ul>

<h2 id="环境准备">环境准备</h2>

<p>目前 Golang 已发布到 1.21.1。但为了方便后续验证，在 Linux amd64 操作系统， 安装 <code>1.21.0</code> 版本。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo mkdir -p /opt/go
sudo tar -xzvf go1.21.0.linux-amd64.tar.gz -C /opt/go
sudo mv /opt/go/go /opt/go/go1.21.0
echo <span style="color:#e6db74">&#39;export GOROOT=/opt/go/go1.21.0&#39;</span> &gt;&gt; ~/.bashrc
echo <span style="color:#e6db74">&#39;export PATH=/opt/go/go1.21.0/bin:$PATH&#39;</span> &gt;&gt; ~/.bashrc
rm -rf go1.21.0.linux-amd64.tar.gz
go env -w GOPROXY<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;https://goproxy.cn|direct&#39;</span></code></pre></div>
<h2 id="godebug">GODEBUG</h2>

<blockquote>
<p>示例代码： <a href="https://github.com/rectcircle/go-compatibility-example">rectcircle/go-compatibility-example</a></p>
</blockquote>

<p>自 Go 1.21 开始，go 编译器开始识别一个 GODEBUG 环境变量。当新的 Go 版本引入了一些 <strong>不违反 Go 1 兼容性承诺但是仍然后可能破坏兼容性的变更</strong> 时，会增加一个开关，通过这个 GODEBUG 来控制 Go 编译器的行为。</p>

<p>GODEBUG 是一个键值对列表，示例格式为： <code>GODEBUG=http2client=0,http2server=0</code>。</p>

<p>更重要的是， GODEBUG 的默认值是根据 <code>go.mod</code> 中的 <code>go line</code> 来自动生成的。基于这可以实现：升级到新版 Go 编译器后，只要开发者不明确修改 <code>go line</code>，那么在新的编译器编译产物的行为和之前一致（一些有明确废弃计划的开关除外，如 <code>x509sha1</code> 开关将于 go1.22 版本移除）。</p>

<p>GODEBUG 的默认只可以通过 <code>go list -f '{{.DefaultGODEBUG}}' my/main/package</code> 观察。</p>

<p>下面有个示例</p>

<p>在 go1.21，引入了一个 <strong>不违反 Go 1 兼容性承诺但是仍然后可能破坏兼容性的变更</strong>，即 <code>panic(nil)</code> 的行为：</p>

<ul>
<li>在 go1.21 及其之后 <code>panic(nil)</code>，<code>recover</code> 将返回 <a href="https://tip.golang.org/pkg/runtime/#PanicNilError"><code>*runtime.PanicNilError</code></a>。</li>
<li>在 go1.21 之前 <code>panic(nil)</code>，<code>recover</code> 将返回 nil。</li>
</ul>

<p>编写与一个简单的 go 程序如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;runtime&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> recover(); <span style="color:#a6e22e">r</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;recover: &#34;</span>, <span style="color:#a6e22e">r</span>)
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;recover is nil&#34;</span>)
		}
	}()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">Version</span>())
	panic(<span style="color:#66d9ef">nil</span>)
}</code></pre></div>
<p>在不同 go module 中， go.mod 的 <code>go line</code> 不同的，使用 <code>go1.21.0</code> 编译器，执行 <code>go run ./</code> 和 <code>go list -f '{{.DefaultGODEBUG}}' ./</code> 执行结果如下：</p>

<ul>
<li><p>go.mod 声明为 <code>go 1.20</code>:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go run ./
<span style="color:#75715e"># go1.21.0</span>
<span style="color:#75715e"># recover is nil</span>
go list -f <span style="color:#e6db74">&#39;{{.DefaultGODEBUG}}&#39;</span> ./
<span style="color:#75715e"># panicnil=1</span></code></pre></div>
<p>说明：只要 go.mod 声明了 <code>go 1.20</code> 即使使用 go 1.21.0 的编译器，编译器也会自动的配置合理的 GODEBUG，编译器让程序的行为和 go1.20 一样，保证了兼容性。</p></li>

<li><p>go.mod 声明为 <code>go 1.20.0</code>:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go run ./
<span style="color:#75715e"># go1.21.0</span>
<span style="color:#75715e"># recover:  panic called with nil argument</span>
go list -f <span style="color:#e6db74">&#39;{{.DefaultGODEBUG}}&#39;</span> ./
#</code></pre></div>
<p>说明：声明切换到 <code>go 1.21.0</code> 后，新的行为被应用了。</p></li>

<li><p>go.mod 声明为 <code>go 1.20.1</code>:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go run ./
<span style="color:#75715e"># go1.21.1</span>
<span style="color:#75715e"># recover:  panic called with nil argument</span>
go list -f <span style="color:#e6db74">&#39;{{.DefaultGODEBUG}}&#39;</span> ./
#</code></pre></div>
<p>可以观察到 go 的编译器版本都变了，行为和 <code>go 1.21.0</code> 一致。属于 GOTOOLCHAIN 能力，具体参见下一篇文章。</p></li>
</ul>

<h2 id="最佳实践">最佳实践</h2>

<h3 id="永远使用最新版-go-编译器">永远使用最新版 Go 编译器</h3>

<p>从上文来看，升级 Go 编译器后，只要 <code>go line</code> 不变，Go 编译器通过 <code>GODEBUG</code> 机制，可以保证程序的行为和旧版编译器一致。</p>

<p>而新的 Go 编译器一般会带来性能和安全性的提升。因此，如果信任 Go 官方的话，可以无需任何额外成本的升级 Go 编译器的版本。</p>

<h3 id="升级-go-line-的标准工作流">升级 <code>go line</code> 的标准工作流</h3>

<p>升级了 Go 的编译器，但是此时声明的 <code>go line</code> 仍然是旧版，此时并不是一个好的状态。因此，最好的做法是，升级 <code>go line</code> 到最新版本。</p>

<p>和升级 Go 编译器不同，升级 <code>go line</code> 的版本需要一些额外的成本，即需要评估项目代码是否依赖 GODEBUG 中声明的不兼容的变更，操作路径如下：</p>

<ul>
<li>完成上述的 Go 编译器升级。</li>
<li>在需要升级的 go module 中，执行 <code>go list -f '{{.DefaultGODEBUG}}' ./</code> 获取可能不兼容的变更的列表。</li>
<li>根据 <a href="https://go.dev/doc/godebug#history">GODEBUG History</a> 文档中的说明，对照项目代码，评估是否有影响，如果有影响，进行代码修改。</li>
<li>最后执行 <code>go get go@latest</code> 升级 <code>go line</code>。</li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://go.dev/doc/go1compat">Go 官方兼容性规范：Go 1 和 Go 程序的未来</a></li>
<li><a href="https://go.dev/blog/compat">Go 官方博客：向后兼容性、Go 1.21 和 Go 2</a></li>
<li><a href="https://go.dev/doc/godebug">Go 官方文档：Go、向后兼容性和 GODEBUG</a></li>
<li><a href="https://go.dev/dl/">Go 官方发行列表</a></li>
</ul>
]]></description></item><item><title>Kubernetes 工作进程高负载退出场景</title><link>https://www.rectcircle.cn/posts/kubernetes-work-process-high-load-exit/</link><pubDate>Sun, 06 Aug 2023 23:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/kubernetes-work-process-high-load-exit/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>在 Kubernetes 中，高负载可以分为高 CPU 占用和高内存占用，而 CPU 属于可压缩资源，因此 CPU 占用高并不会影响工作进程的稳定性，而内存属于不可压缩资源，高内存占用可能导致工作进程重启的问题。因此本文不讨论高 CPU 占用的问题，只讨论内存占用高的问题。</p>

<h2 id="实验">实验</h2>

<blockquote>
<p>测试代码库： <a href="https://github.com/rectcircle/kubernetes-work-proc-high-load-exp">rectcircle/kubernetes-work-proc-high-load-exp</a>。</p>
</blockquote>

<h3 id="准备环境">准备环境</h3>

<p>使用 k3s 搭建一个单节点的测试 Kubernetes。</p>

<p>参见：<a href="/posts/extend-kubernetes-01-k3s-testing-env/">扩展 Kubernetes （一） k3s 测试环境搭建</a>。</p>

<h3 id="测试程序">测试程序</h3>

<p>该程序接收 2 个命令行参数，第一个为申请占用的内存大小 （单位 MB），第二个为申请前睡眠的描述。程序启动后，先睡眠，然后开始申请并使用内存，然后进入永久睡眠。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">const</span> size_t MB <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">1024</span>;

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[]) {
    <span style="color:#66d9ef">if</span> (argc <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">||</span> argc <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">3</span>) {
        printf(<span style="color:#e6db74">&#34;usage: %s &lt;alloc memory size mb&gt; &lt;delay seconds, default 10&gt;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, argv[<span style="color:#ae81ff">0</span>]);
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>;
    }
    size_t allocSize <span style="color:#f92672">=</span> atoi(argv[<span style="color:#ae81ff">1</span>]) <span style="color:#f92672">*</span> MB;
    <span style="color:#66d9ef">if</span> (allocSize <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
        printf(<span style="color:#e6db74">&#34;error: alloc memory size mb must greate than 0</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>;
    }
    <span style="color:#66d9ef">int</span> sleepSec <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">if</span> (argc <span style="color:#f92672">==</span> <span style="color:#ae81ff">3</span>) {
        sleepSec <span style="color:#f92672">=</span> atoi(argv[<span style="color:#ae81ff">2</span>]);
    }
    <span style="color:#66d9ef">if</span> (sleepSec <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
        sleepSec <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>;
    }
    <span style="color:#75715e">// 睡眠再申请内存
</span><span style="color:#75715e"></span>    sleep(sleepSec);
    <span style="color:#75715e">// 申请内存 （VSZ）
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>m <span style="color:#f92672">=</span> (<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>)malloc(allocSize <span style="color:#f92672">*</span> <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">char</span>));
    <span style="color:#66d9ef">if</span> (m <span style="color:#f92672">==</span> NULL) {
        printf(<span style="color:#e6db74">&#34;error: alloc %dMB size memory failed</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, allocSize <span style="color:#f92672">/</span> MB);
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>;
    }
    <span style="color:#75715e">// 使用内存 （RSS）
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">for</span> (size_t i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> allocSize; i<span style="color:#f92672">++</span>) {
        m[i] <span style="color:#f92672">=</span> i <span style="color:#f92672">%</span> <span style="color:#ae81ff">256</span>;
    }
    <span style="color:#66d9ef">while</span>(<span style="color:#ae81ff">1</span>) {
        sleep(<span style="color:#ae81ff">1</span>);
    }
}</code></pre></div>
<p>静态编译安装到 PATH。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gcc -static mem-alloc.c -o mem-alloc
sudo cp mem-alloc /usr/local/bin</code></pre></div>
<p>验证。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">./mem-alloc <span style="color:#ae81ff">100</span> <span style="color:#ae81ff">10</span> <span style="color:#75715e"># shell 1</span>
<span style="color:#75715e"># 等待 10 秒执行</span>
ps aux | grep mem-alloc | grep -v grep <span style="color:#75715e"># shell 2</span>
<span style="color:#75715e"># 输出第 6 列为内存占用，单位为  KB</span>
<span style="color:#75715e"># rectcir+  430460  0.6  0.6 103428 103108 pts/1   S+   23:06   0:00 ./mem-alloc 100</span></code></pre></div>
<h3 id="场景1-node-空闲但-c1-内存超过-limit">场景1：node 空闲但 c1 内存超过 limit</h3>

<h4 id="总体设计">总体设计</h4>

<p>本文将构造如下 pod：</p>

<ul>
<li>包含两个 container：c1 和 c2。</li>
<li>c1 是主容器，包含2个进程 p1 和 p2，p1 为 1 号进程。该容器的内存资源规格为：

<ul>
<li>request: 1g</li>
<li>limit: 2g</li>
</ul></li>
<li>c2 是 sidecar 容器，包含 1 个进程 p3。

<ul>
<li>request: 0.25g</li>
<li>limit: 0.5g</li>
</ul></li>
</ul>

<h4 id="非-1-号进程内存直接超过-limit">非 1 号进程内存直接超过 limit</h4>

<p><code>case1/pod1.yaml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v1
kind: Pod
metadata:
  name: case1<span style="color:#ae81ff">-1</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 2500 1 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">100</span> <span style="color:#ae81ff">5</span>
    resources:
      requests:
        memory: 1G
      limits:
        memory: 2G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc
  - name: p2
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
      - /bin/sh
    args: 
      - -c
      - sleep infinity
    resources:
      requests:
        memory: <span style="color:#ae81ff">0.</span>25G
      limits:
        memory: <span style="color:#ae81ff">0.</span>5G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc</code></pre></div>
<ul>
<li>挂载 <code>mem-alloc</code> 进程模拟内存占用。</li>
<li>在 c1 容器中，先启动一个子进程申请 2500 MB，再让 1 号进程申请 100 MB。</li>
<li>观察 pod、容器、进程情况</li>
</ul>

<p>创建 pod</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl apply -f case1/pod1.yaml</code></pre></div>
<p>观察现象</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 等待 10 秒钟</span>
sudo kubectl get pod case1-1
<span style="color:#75715e"># 输出如下，说明在 Kubernetes 看来，pod 和 容器启动正常</span>
<span style="color:#75715e"># case1-1   2/2     Running   0          85s</span>
sudo kubectl exec -it case1-1 -- top
<span style="color:#75715e"># 输出如下，7 号进程已经被僵尸状态（STAT 为 Z），已经被 Kill，1 号进程正常</span>
<span style="color:#75715e"># Mem: 3956532K used, 12436616K free, 2252K shrd, 198904K buff, 1877444K cached</span>
<span style="color:#75715e"># CPU:  2.5% usr  0.0% sys  0.0% nic 97.5% idle  0.0% io  0.0% irq  0.0% sirq</span>
<span style="color:#75715e"># Load average: 0.23 0.26 0.18 2/439 20</span>
<span style="color:#75715e">#   PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</span>
<span style="color:#75715e">#     1     0 root     S     101m  0.6   3  0.0 mem-alloc 100 5</span>
<span style="color:#75715e">#    14     0 root     R     4404  0.0   3  0.0 top</span>
<span style="color:#75715e">#     7     1 root     Z        0  0.0   2  0.0 [mem-alloc]</span></code></pre></div>
<p>清理现场</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl delete pod case1-1</code></pre></div>
<h4 id="1-号进程内存直接超过-limit">1 号进程内存直接超过 limit</h4>

<p><code>case1/pod2.yaml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v1
kind: Pod
metadata:
  name: case1<span style="color:#ae81ff">-2</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 100 1 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">2500</span> <span style="color:#ae81ff">10</span>
    resources:
      requests:
        memory: 1G
      limits:
        memory: 2G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc
  - name: p2
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
      - /bin/sh
    args: 
      - -c
      - sleep infinity
    resources:
      requests:
        memory: <span style="color:#ae81ff">0.</span>25G
      limits:
        memory: <span style="color:#ae81ff">0.</span>5G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc</code></pre></div>
<ul>
<li>挂载 <code>mem-alloc</code> 进程模拟内存占用。</li>
<li>在 c1 容器中，先启动一个子进程申请 100 MB，再让 1 号进程申请 2500 MB。</li>
<li>观察 pod、容器、进程情况</li>
</ul>

<p>创建 pod</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl apply -f case1/pod2.yaml</code></pre></div>
<p>观察现象</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 多次执行如下命令可以得到如下几种输出</span>
sudo kubectl get pod case1-2
<span style="color:#75715e"># 1. 说明在未申请内存时，状态正常</span>
<span style="color:#75715e"># NAME      READY   STATUS    RESTARTS   AGE</span>
<span style="color:#75715e"># case1-2   2/2     Running   0          5s</span>
<span style="color:#75715e"># 2. 说明 p1 1 号进程被 oomkill，Kubernetes 观测到了，Kubernetes 进行了重启。</span>
<span style="color:#75715e"># NAME      READY   STATUS      RESTARTS      AGE</span>
<span style="color:#75715e"># case1-2   1/2     OOMKilled   1 (21s ago)   35s</span>
<span style="color:#75715e"># 3. 说明 Kubernetes 对容器 p1 进行了多次 4 重启。</span>
<span style="color:#75715e"># NAME      READY   STATUS             RESTARTS      AGE</span>
<span style="color:#75715e"># case1-2   1/2     CrashLoopBackOff   4 (72s ago)   2m42s</span>

sudo kubectl describe pod case1-2
<span style="color:#75715e"># 输出部分内容如下，说明 p1 经过了多次重启，p2 一直正常，没有任何问题，不会收到影响。</span>
<span style="color:#75715e"># Containers:</span>
<span style="color:#75715e">#   p1:</span>
<span style="color:#75715e">#     State:          Waiting</span>
<span style="color:#75715e">#       Reason:       CrashLoopBackOff</span>
<span style="color:#75715e">#     Last State:     Terminated</span>
<span style="color:#75715e">#       Reason:       OOMKilled</span>
<span style="color:#75715e">#       Exit Code:    137</span>
<span style="color:#75715e">#       Started:      Sun, 06 Aug 2023 19:53:50 +0800</span>
<span style="color:#75715e">#       Finished:     Sun, 06 Aug 2023 19:54:02 +0800</span>
<span style="color:#75715e">#     Ready:          False</span>
<span style="color:#75715e">#     Restart Count:  4</span>
<span style="color:#75715e">#   p2:</span>
<span style="color:#75715e">#     State:          Running</span>
<span style="color:#75715e">#       Started:      Sun, 06 Aug 2023 19:51:30 +0800</span>
<span style="color:#75715e">#     Ready:          True</span>
<span style="color:#75715e">#     Restart Count:  0</span>
<span style="color:#75715e"># Conditions:</span>
<span style="color:#75715e">#   Type              Status</span>
<span style="color:#75715e">#   Initialized       True</span> 
<span style="color:#75715e">#   Ready             False</span> 
<span style="color:#75715e">#   ContainersReady   False</span> 
<span style="color:#75715e">#   PodScheduled      True</span> 
<span style="color:#75715e"># Events:</span>
<span style="color:#75715e">#   Type     Reason     Age                   From               Message</span>
<span style="color:#75715e">#   ----     ------     ----                  ----               -------</span>
<span style="color:#75715e">#   Normal   Scheduled  3m56s                 default-scheduler  Successfully assigned default/case1-2 to pve-vm-dev</span>
<span style="color:#75715e">#   Normal   Pulled     3m53s                 kubelet            Container image &#34;busybox:1.36&#34; already present on machine</span>
<span style="color:#75715e">#   Normal   Created    3m52s                 kubelet            Created container p2</span>
<span style="color:#75715e">#   Normal   Started    3m52s                 kubelet            Started container p2</span>
<span style="color:#75715e">#   Warning  BackOff    103s (x7 over 3m27s)  kubelet            Back-off restarting failed container p1 in pod case1-2_default(3d63226e-9063-4442-a3b8-effc6e1017ba)</span>
<span style="color:#75715e">#   Normal   Pulled     92s (x5 over 3m55s)   kubelet            Container image &#34;busybox:1.36&#34; already present on machine</span>
<span style="color:#75715e">#   Normal   Created    92s (x5 over 3m53s)   kubelet            Created container p1</span>
<span style="color:#75715e">#   Normal   Started    92s (x5 over 3m53s)   kubelet            Started container p1</span></code></pre></div>
<p>清理现场</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl delete pod case1-2</code></pre></div>
<h4 id="容器总内存超过-limit-且-非-1-号进程内存占用更多">容器总内存超过 limit 且 非 1 号进程内存占用更多</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># 将 p1 的 args 改为</span>
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 2000 1 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">500</span> <span style="color:#ae81ff">5</span>
<span style="color:#75715e"># 或</span>
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 2000 5 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">500</span> <span style="color:#ae81ff">1</span></code></pre></div>
<p>现象都和 <a href="#非-1-号进程内存直接超过-limit">非 1 号进程内存直接超过 limit</a> 一致。</p>

<h4 id="容器总内存超过-limit-且-1-号进程内存占用更多">容器总内存超过 limit 且 1 号进程内存占用更多</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># 将 p1 的 args 改为</span>
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 500 1 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">2000</span> <span style="color:#ae81ff">5</span>
<span style="color:#75715e"># 或</span>
    - <span style="color:#e6db74">|
</span><span style="color:#e6db74">      mem-alloc 500 5 &amp;</span>
      exec mem-alloc <span style="color:#ae81ff">2000</span> <span style="color:#ae81ff">1</span></code></pre></div>
<p>现象都和 <a href="#1-号进程内存直接超过-limit">1 号进程内存直接超过 limit</a> 一致。</p>

<h3 id="场景2-node-负载高发生驱逐">场景2：node 负载高发生驱逐</h3>

<h4 id="配置-k3s-kubelet-的-eviction-hard">配置 k3s kubelet 的 eviction-hard</h4>

<p>参考 <a href="https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/reserve-compute-resources/">为系统守护进程预留计算资源</a>、 <a href="https://docs.k3s.io/zh/installation/configuration#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">k3s - 配置 - 安装配置选项 - 配置文件</a>、<a href="https://docs.k3s.io/zh/cli/agent#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%87%E5%BF%97">k3s - CLI 工具 - agent - 自定义标志</a>，配置 k3s kubelet 的 eviction-hard。</p>

<p>假设当前设备总内存为 16G，配置当前节点 Pod 最大可用 4G。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo sh -c <span style="color:#e6db74">&#39;echo &#34;kubelet-arg: \&#34;--eviction-hard=memory.available&lt;12Gi\&#34;&#34; &gt; /etc/rancher/k3s/config.yaml&#39;</span>
sudo systemctl restart k3s
sudo kubectl get node
sudo kubectl describe node pve-vm-dev
<span style="color:#75715e"># 部分输出如下，可以看出可用内存不到 4G</span>
<span style="color:#75715e"># Allocatable:</span>
<span style="color:#75715e">#   cpu:                4</span>
<span style="color:#75715e">#   ephemeral-storage:  31861548Ki</span>
<span style="color:#75715e">#   hugepages-1Gi:      0</span>
<span style="color:#75715e">#   hugepages-2Mi:      0</span>
<span style="color:#75715e">#   memory:             3810236Ki</span>
<span style="color:#75715e">#   pods:               110</span></code></pre></div>
<h4 id="多个-pod-未超过-limit-但实际内存超过节点-available">多个 Pod 未超过 limit 但实际内存超过节点 available</h4>

<ul>
<li>创建 4 个 Pod。</li>
<li>内存实际占用分别为 512MB、768MB、1024MB、2048MB。</li>
<li>4 个 pod 的 request 和 limit 为 128MB、3GB。</li>
</ul>

<p><code>case2/pod.yaml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v1
kind: Pod
metadata:
  name: case2<span style="color:#ae81ff">-1</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - exec mem-alloc <span style="color:#ae81ff">512</span> <span style="color:#ae81ff">1</span>
    resources:
      requests:
        memory: 128M
      limits:
        memory: 3G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc
---
apiVersion: v1
kind: Pod
metadata:
  name: case2<span style="color:#ae81ff">-2</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - exec mem-alloc <span style="color:#ae81ff">768</span> <span style="color:#ae81ff">3</span>
    resources:
      requests:
        memory: 128M
      limits:
        memory: 3G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc
---
apiVersion: v1
kind: Pod
metadata:
  name: case2<span style="color:#ae81ff">-3</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - exec mem-alloc <span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">6</span>
    resources:
      requests:
        memory: 128M
      limits:
        memory: 3G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc
---
apiVersion: v1
kind: Pod
metadata:
  name: case2<span style="color:#ae81ff">-4</span>
spec:
  volumes:
  - name: mem-alloc
    hostPath:
      path: /usr/local/bin/mem-alloc
  containers:
  - name: p1
    image: busybox:<span style="color:#ae81ff">1.36</span>
    command:
    - /bin/sh
    args: 
    - -c
    - exec mem-alloc <span style="color:#ae81ff">2048</span> <span style="color:#ae81ff">10</span>
    resources:
      requests:
        memory: 128M
      limits:
        memory: 3G
    volumeMounts:
    - mountPath: /bin/mem-alloc
      name: mem-alloc</code></pre></div>
<p>创建这些 Pod</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl apply -f case2/pod.yaml</code></pre></div>
<p>观察 pod 情况</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 等待一分钟后执行</span>
sudo kubectl get pod
<span style="color:#75715e"># 输出如下，发现有两个状态为 Error (ContainerStatusUnknown) 停止了。</span>
<span style="color:#75715e"># NAME      READY   STATUS    RESTARTS   AGE</span>
<span style="color:#75715e"># case2-1   1/1     Running   0          2m59s</span>
<span style="color:#75715e"># case2-3   1/1     Running   0          2m59s</span>
<span style="color:#75715e"># case2-2   0/1     Error     0          2m59s</span>
<span style="color:#75715e"># case2-4   0/1     Error     0          2m59s</span>

<span style="color:#75715e"># 观察其中一个 Error 的 pod</span>
sudo kubectl describe pod case2-2
<span style="color:#75715e"># 部分输出如下，可以观察到触发了节点低内存，该 Pod 被驱逐。</span>
<span style="color:#75715e"># Status:           Failed</span>
<span style="color:#75715e"># Reason:           Evicted</span>
<span style="color:#75715e"># Message:          The node was low on resource: memory. Threshold quantity: 12Gi, available: 12536420Ki. Container p1 was using 788244Ki, request is 128M, has larger consumption of memory.</span> 
<span style="color:#75715e"># Conditions:</span>
<span style="color:#75715e">#   Type               Status</span>
<span style="color:#75715e">#   DisruptionTarget   True</span> 
<span style="color:#75715e">#   Initialized        True</span> 
<span style="color:#75715e">#   Ready              False</span> 
<span style="color:#75715e">#   ContainersReady    False</span> 
<span style="color:#75715e">#   PodScheduled       True</span> 
<span style="color:#75715e"># Events:</span>
<span style="color:#75715e">#   Type     Reason               Age    From               Message</span>
<span style="color:#75715e">#   ----     ------               ----   ----               -------</span>
<span style="color:#75715e">#   Normal   Scheduled            5m32s  default-scheduler  Successfully assigned default/case2-2 to pve-vm-dev</span>
<span style="color:#75715e">#   Normal   Pulled               5m29s  kubelet            Container image &#34;busybox:1.36&#34; already present on machine</span>
<span style="color:#75715e">#   Normal   Created              5m27s  kubelet            Created container p1</span>
<span style="color:#75715e">#   Normal   Started              5m27s  kubelet            Started container p1</span>
<span style="color:#75715e">#   Warning  Evicted              5m16s  kubelet            The node was low on resource: memory. Threshold quantity: 12Gi, available: 12536420Ki. Container p1 was using 788244Ki, request is 128M, has larger consumption of memory.</span>
<span style="color:#75715e">#   Normal   Killing              5m16s  kubelet            Stopping container p1</span>
<span style="color:#75715e">#   Warning  ExceededGracePeriod  5m6s   kubelet            Container runtime did not kill the pod within specified grace period.</span>

<span style="color:#75715e"># 释放资源观察是否能恢复</span>
sudo kubectl delete pod case2-1 case2-3 case2-4
<span style="color:#75715e"># 观察 Pod 无法恢复</span>
sudo kubectl get pod
<span style="color:#75715e"># NAME      READY   STATUS  RESTARTS   AGE</span>
<span style="color:#75715e"># case2-2   0/1     Error   1          5m49s</span></code></pre></div>
<p>清理现场</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo kubectl delete pod case2-1 case2-2 case2-3 case2-4</code></pre></div>
<h2 id="结论">结论</h2>

<ul>
<li>如果 Pod 中所有进程实际使用内存总和大于 limit 的限制。

<ul>
<li>Linux 内核的 OOMKiller 特性会挑选一个进程 kill 掉。OOMKiller 挑选 Kill 进程的原则是：优先 Kill 内存占用高的、启动时间晚的。（TODO，更多参见：<a href="https://github.com/rectcircle/rectcircle-blog/blob/master/content/posts/container-core-tech-9-cgroup.md">容器核心技术（九） CGroup</a>）</li>
<li>如果 Kill 的是容器中 PID 非 1 的进程，则 Kubernetes 没有感知，什么事都不做。针对这种场景，需要在业务层自己恢复或者通过容器的探针机制检查，并重启容器。</li>
<li>如果 Kill 的是容器中 PID 为 1 的进程，则当容器直接退出，Kubernetes 将容器状态设置为 OOMKilled 或 CrashLoopBackOff，并重启该容器，Pod 中其他运行中的容器不受影响。</li>
</ul></li>
<li>如果一个节点的每一个 Pod 中所有进程实际使用内存总和均未超过 limit 的限制，但是当前节点所有 Pod 使用的内存总和超过了节点的 <code>Allocatable.memory</code> （<code>kubectl describe node xxx</code>）。

<ul>
<li>将触发 Kubernetes 的节点压力驱逐逻辑，这种情况节点上的 Kubelet 会选择选择一些 Pod 将这些 Pod 的资源回收掉，这些 Pod 的状态将变为 Error / ContainerStatusUnknown，后续即使当节点空闲内存足够了，也不会恢复。</li>
<li>如果被驱逐的 Pod 是由 Deployment 管理的，则会删除掉该 Pod，并选择另一个节点重新启动该 Pod。</li>
</ul></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://izsk.me/2023/02/09/Kubernetes-Out-Of-Memory-1/">Kubernetes学习(kubernetes中的OOM-killer和应用程序运行时含义)</a></li>
</ul>
]]></description></item><item><title>Containerd 详解（三） containerd 源码框架</title><link>https://www.rectcircle.cn/posts/containerd-3-containerd-source-framework/</link><pubDate>Sun, 28 May 2023 21:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/containerd-3-containerd-source-framework/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://github.com/containerd/containerd/tree/v1.7.0">v1.7.0</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Containerd <a href="https://github.com/containerd/containerd">项目源码</a> 根目录有几十个目录和文件，想要了解该项目的源码，需要先了解该项目的整体框架。</p>

<p>从前文可知， containerd 的安装实际上是在操作系统中启动了一个 deamon 进程，该进程的可执行文件为 <code>bin/containerd</code>，对应的 main 函数为 <a href="https://github.com/containerd/containerd/blob/v1.7.0/cmd/containerd/main.go"><code>cmd/containerd/main.go</code></a>。</p>

<p>本文将从 containerd deamon 的视角，从 <code>cmd/containerd/main.go</code> 源码文件入手，探索该项目源码的框架结构。</p>

<h2 id="环境准备">环境准备</h2>

<p>为了更好的跟踪流程，本文通过 VSCode Golang 扩展 + dlv 提供的可视化 debug 能力，观测整体流程。特别说明的时，下文：</p>

<ul>
<li>编译安装、dlv 启动：在 Linux 测试机执行</li>
<li>vscode attach：在本地设备执行</li>
</ul>

<h3 id="编译安装">编译安装</h3>

<blockquote>
<p><a href="https://github.com/containerd/containerd/blob/v1.7.0/BUILDING.md">BUILDING.md</a></p>
</blockquote>

<ul>
<li>Linux amd64 系统环境</li>

<li><p>Go v1.20+</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 1. clone 代码并检出指定版本</span>
git clone https://github.com/containerd/containerd.git
cd containerd
git checkout v1.7.0
<span style="color:#75715e"># 2. 安装 protobuild 等命令 (实测不需要)</span>
<span style="color:#75715e"># script/setup/install-dev-tools</span>
<span style="color:#75715e"># 安装 runc cni 等 （第一篇已经安装了可以忽略）</span>
<span style="color:#75715e"># make install-deps</span>
<span style="color:#75715e"># 3. 带调试符号的构建</span>
make GODEBUG<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span></code></pre></div></li>
</ul>

<h3 id="dlv-启动">dlv 启动</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一篇如果已经启动了 containerd 需停止，并清空目录</span>
sudo systemctl stop containerd
sudo rm -rf /var/lib/containerd
<span style="color:#75715e"># 安装 dlv 调试器</span>
go install github.com/go-delve/delve/cmd/dlv@latest
<span style="color:#75715e"># 使用 dlv 启动</span>
sudo ~/go/bin/dlv exec ./bin/containerd --headless --listen <span style="color:#ae81ff">0</span>.0.0.0:2345 --api-version <span style="color:#ae81ff">2</span></code></pre></div>
<h3 id="vscode-attach">vscode attach</h3>

<blockquote>
<p>在本地设备执行</p>
</blockquote>

<p>clone 代码，并用 vscode 打开</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/containerd/containerd.git
cd containerd
git checkout v1.7.0
code ./</code></pre></div>
<p>配置调试器</p>

<p><code>.vscode/launch.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
    <span style="color:#f92672">&#34;configurations&#34;</span>: [
        {
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Connect to server&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
            <span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;attach&#34;</span>,
            <span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;remote&#34;</span>,
            <span style="color:#f92672">&#34;remotePath&#34;</span>: <span style="color:#e6db74">&#34;/home/rectcircle/test/containerd&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端</span> <span style="color:#960050;background-color:#1e0010">Linux</span> <span style="color:#960050;background-color:#1e0010">编译路径</span>
            <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">2345</span>,
            <span style="color:#f92672">&#34;dlvLoadConfig&#34;</span>: {
                <span style="color:#f92672">&#34;followPointers&#34;</span>: <span style="color:#66d9ef">true</span>,
                <span style="color:#f92672">&#34;maxVariableRecurse&#34;</span>: <span style="color:#ae81ff">1</span>,
                <span style="color:#f92672">&#34;maxStringLen&#34;</span>: <span style="color:#ae81ff">2048</span>,
                <span style="color:#f92672">&#34;maxArrayValues&#34;</span>: <span style="color:#ae81ff">64</span>,
                <span style="color:#f92672">&#34;maxStructFields&#34;</span>: <span style="color:#ae81ff">-1</span>
            },
            <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;192.168.31.7&#34;</span>, <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">远端</span> <span style="color:#960050;background-color:#1e0010">Linux</span> <span style="color:#960050;background-color:#1e0010">IP</span>
        }
    ]
}</code></pre></div>
<p>按 F5 连接</p>

<h2 id="启动流程">启动流程</h2>

<p><code>cmd/containerd/main.go</code> 流程如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">cmd/containerd/main.go                     # 程序入口文件
  +-&gt; cmd/containerd/builtins/             # 内建插件注册 (插件，参见下文)
  +-&gt; init()                               # 初始化随机数种子，设置 SHA256 hasher
  +-&gt; main()                               # main 函数
        +-&gt; cmd/containerd/command/main.go   # 构建 App
              +-&gt; metrics/                   # 初始化 metrics
              +-&gt; init()                     # 初始化日志和版本打印函数
              +-&gt; App()                      # 初始化 app 对象，配置全局命令行参数，子命令
        +-&gt; app.Run()                      # 启动 app
              +-&gt; cmd/containerd/command/main.go@App().Action  # 调用该命令的 Action 函数，参见下文</pre></div>
<p><code>cmd/containerd/command/main.go@App().Action</code> 流程如下（忽略 metrics server、ttrpc 相关外围逻辑）：</p>

<ul>
<li>通过命令行参数 <code>--config</code> 获取配置文件参数路径（默认为 <code>/etc/containerd/config.toml</code>），并解析配置文件，并进行参数校验。</li>
<li>调用 <code>services/server/server.go@CreateTopLevelDirectories</code> 创建 containerd 数据和状态存储目录，默认参见 <code>defaults/defaults_unix.go</code>。</li>
<li>注册信号处理函数，以实现优雅退出。</li>
<li>创建临时挂载目录 <code>/var/lib/containerd/tmpmounts</code>，并清空当前目录下的所有挂载点。</li>
<li>创建并初始化一个 Server， <code>services/server/server.go@New</code>。

<ul>
<li>如果配置了的话，注册 <code>snapshot</code> 及 <code>content</code> 类型的插件。 （插件，参见下文）</li>
<li>如果配置了的话，注册二进制流处理程序（主要用来处理镜像内容流，一般不需要配置）。</li>
<li>创建 GRPC 的配置 options，并创建 grpcServer。</li>
<li>获取所有注册的插件，执行插件初始化，并获取到插件的 service instance。</li>
<li>针对每个插件的 service instance，调用将 grpcServer 作为参数传递给 Register 函数，将服务注册到 grpcServer 中。</li>
<li>返回 <code>services/server/server.go@Server</code> 结构体。</li>
</ul></li>
<li>在协程中调用 grpcServer.Serve 函数 （<code>services/server/server.go@ServeGRPC</code>），启动 grpc 服务。</li>
<li>等待信号处理函数关闭 done channel。</li>
</ul>

<h2 id="插件体系">插件体系</h2>

<p>上文介绍了 containerd 的启动流程，其核心逻辑是初始化并启动一个 grpc 服务。而 containerd 的业务逻辑是通过 containerd 定义的一套插件体系实现的初始化和依赖注入的。</p>

<ul>
<li>一个插件实现完成后，通过  <code>plugin.Register</code> （源码位于：<code>plugin/plugin.go@Register</code>）函数，传递 <code>plugin.Registration</code> 结构体指针 （源码位于：<code>plugin/plugin.go@Registration</code>）参数，来进行注册，该函数一般在 init 函数中调用。</li>
<li>注册的插件在 <code>services/server/server.go@New</code> 进行初始化、其中 GPRC 类型的插件注册到 grpcServer 中。</li>
<li>在 containerd 中，插件按照业务域进行划分，其的能力一般通过 pb （gprc）进行声明（源码位于：<code>api/services</code> 目录）。</li>
</ul>

<p>下面以 <code>containers</code> 为例，其会注册两种类型的插件 <code>plugin.ServicePlugin</code> 和 <code>plugin.GRPCPlugin</code> （源码位于 <code>services/containers</code> 目录）：</p>

<ul>
<li>gprc 声明位于 <code>api/services/containers/v1/containers.proto</code>。</li>
<li>service 实现位于 <code>services/containers/local.go</code>。</li>
<li><code>plugin.ServicePlugin</code> 插件注册位于 <code>services/containers/local.go@init</code></li>
<li><code>plugin.GRPCPlugin</code> 插件注册位于 <code>services/containers/service.go@init</code>。</li>
</ul>

<p><code>plugin.Registration</code> 结构体字段说明如下：</p>

<ul>
<li><code>Type</code> 插件类型，如 <code>&quot;io.containerd.service.v1&quot;</code>、<code>&quot;io.containerd.grpc.v1&quot;</code>、<code>&quot;io.containerd.snapshotter.v1&quot;</code> （源码位于：<code>plugin/plugin.go#L52</code>）。</li>
<li><code>ID</code> 插件 ID，如 <code>containers-service</code>。</li>
<li><code>Config</code> 插件的默认配置，在初始化时，会反序列化配置文件的 <code>[plugins]</code> 段对应的配置，填充到该结构体中。</li>
<li><code>Requires</code> 该插件依赖插件类型。</li>
<li><code>InitFn</code> 初始化函数，传递 <code>InitContext</code> 参数（源码位于：<code>plugin/context.go</code>），返回一个插件实例对象，类型为 <code>interface{}</code>。</li>
<li><code>Disable</code> 是否禁用插件。</li>
</ul>

<p>了解了这些信息后，再看初始化流程中关于插件的流程：</p>

<ul>
<li><code>cmd/containerd/main.go</code> import 段中的 <code>_ &quot;github.com/containerd/containerd/cmd/containerd/builtins&quot;</code> 调用，实际上调用的一系列 init 函数，进行插件注册。</li>
<li>在 <code>services/server/server.go@New</code> 中：

<ul>
<li><code>plugins, err := LoadPlugins(ctx, config)</code>

<ul>
<li>注册 <code>&quot;io.containerd.content.v1&quot;</code> 插件。</li>
<li>注册配置文件中的配置的 ProxyPlugin <code>&quot;io.containerd.content.v1&quot;</code>、<code>&quot;io.containerd.snapshotter.v1&quot;</code>。</li>
<li>调用 <code>plugin.Graph</code>，对插件按照 <code>Requires</code> 声明的依赖关系进行排序，并返回 <code>[]*plugin.Registration</code>。</li>
</ul></li>
<li><code>for _, p := range plugins {</code> 遍历插件列表，针对每个插件，这里的 p 是 <code>*plugin.Registration</code> 类型：

<ul>
<li>构造 <code>plugin.InitContext</code>。</li>
<li>反序列化 <code>/etc/containerd/config.toml</code> 配置文件中 <code>[plugins]</code>，填充 <code>plugin.Registration</code> 的 Config 字段。</li>
<li><code>result := p.Init(initContext)</code>，该函数会调用 <code>p.InitFn</code>，构造并返回 <code>*plugin.Plugin</code>， <code>result.instance</code> 字段为 <code>p.InitFn</code> 的返回值。</li>
<li>如果插件类型是 grpc 类型，那么 <code>result.instance</code> 一定实现了 <code>grpcService</code> 接口，则将 <code>result.instance</code> 添加到 <code>grpcServices</code> 数组中。</li>
</ul></li>
<li><code>for _, service := range grpcServices {</code> 调用 <code>service.Register(grpcServer)</code> 注册 grpc 服务。</li>
</ul></li>
</ul>

<h2 id="snapshot-和-proxyplugin">snapshot 和 ProxyPlugin</h2>

<p>上文提到了如果配置文件配置了 <code>&quot;io.containerd.content.v1&quot;</code>、<code>&quot;io.containerd.snapshotter.v1&quot;</code> 类型的 ProxyPlugin。初始化流程会初始化这些插件。</p>

<p>ProxyPlugin 是 Containerd 提供的一种扩展机制，可以实现在不修改 containerd 源码的情况下定制 snapshot 的实现，cotainerd 和这些 ProxyPlugin 的通讯方式为 gprc 调用。文档具体参见：<a href="https://github.com/containerd/containerd/tree/v1.7.0/docs/snapshotters">docs</a>。</p>

<p>这里介绍一下，在源码角度 snapshot 相关 api 的调用过程。</p>

<ul>
<li>containerd client

<ul>
<li>调用 <code>GetLabel</code> 获取当前 namespace 下 <code>&quot;containerd.io/defaults/snapshotter&quot;</code> 标签的值，该值为具体要使用的 snapshot 的插件的 ID，如果不存在则返回默认实现 <code>overlayfs</code>。</li>
<li>调用使用 <code>snapshots/proxy/proxy.go@NewSnapshotter</code> 获取到一个 <code>snapshots.Snapshotter</code> 客户端。</li>
<li>调用 <code>snapshots.Snapshotter</code> 相关 函数，如 <code>Stat</code>，会调用 containerd 的 <code>api/services/snapshots/v1/snapshots.proto</code>，将第一步获取到 snapshotter 值填充到 req 的 <code>Snapshotter</code> 字段。</li>
</ul></li>
<li>containerd deamon

<ul>
<li>流量会先到达 grpc 插件，这些插件的依赖关系是：

<ul>
<li>注册于 <code>services/snapshots/service.go</code>，插件类型为 <code>&quot;io.containerd.grpc.v1&quot;</code> ID 为 <code>snapshots</code>，该插件依赖：</li>
<li>注册于 <code>services/snapshots/snapshotters.go</code>，类型为 <code>&quot;io.containerd.service.v1&quot;</code> ID 为 <code>snapshots-service</code> 的插件，instance 类型为 <code>map[string]snapshots.Snapshotter</code>，该插件依赖：</li>
<li>注册于 <code>metadata/plugin/plugin.go</code>，类型为 <code>&quot;io.containerd.metadata.v1&quot;</code> ID 为 <code>bolt</code> 的插件，该插件依赖：</li>
<li>类型为 <code>&quot;io.containerd.snapshotter.v1&quot;</code> 的插件，有多个，注册于：

<ul>
<li><code>vendor/github.com/containerd/aufs/plugin/plugin.go</code></li>
<li><code>snapshots/btrfs/plugin/plugin.go</code></li>
<li><code>snapshots/native/plugin/plugin.go</code></li>
<li><code>snapshots/overlay/plugin/plugin.go</code></li>
<li><code>services/server/server.go@LoadPlugins</code> 配置于 <code>/etc/containerd/config.toml</code> 的 <code>[proxy_plugins]</code> 段，类型为 <code>snapshot</code> 的 snapshotter。</li>
</ul></li>
</ul></li>
<li>初始化：

<ul>
<li><code>services/snapshots/service.go</code> 初始化阶段获取 <code>services/snapshots/snapshotters.go</code> 返回的 <code>map[string]snapshots.Snapshotter</code>。</li>
<li><code>services/snapshots/snapshotters.go</code> 初始化阶段调用 <code>metadata/plugin/plugin.go</code> 的 <code>Snapshotters()</code> 函数。</li>
<li><code>metadata/plugin/plugin.go</code> 获取到所有的 <code>&quot;io.containerd.snapshotter.v1&quot;</code> 插件，并记录到 <code>map[string]snapshots.Snapshotter</code> 中。</li>
</ul></li>
<li><code>services/snapshots/service.go</code> 所有的函数流程均如下：

<ul>
<li>从 req 中获取到 <code>Snapshotter</code> 字段，从 <code>map[string]snapshots.Snapshotter</code> 中获取到对应的 <code>&quot;io.containerd.snapshotter.v1&quot;</code> 插件。</li>
<li>根据 req 构造 <code>snapshots.Snapshotter</code> 中对应的函数的参数并调用。</li>
<li>将 resp 转换为 grpc 的 resp，并返回。</li>
</ul></li>
</ul></li>
</ul>
]]></description></item><item><title>Containerd 详解（二） Client 核心流程</title><link>https://www.rectcircle.cn/posts/containerd-2-client-core-process/</link><pubDate>Wed, 24 May 2023 00:11:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/containerd-2-client-core-process/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://github.com/containerd/containerd/tree/v1.7.0">v1.7.0</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>本部分将使用 Go 语言实现一个简单的示例程序，该程序通过对 containerd go client 调用：</p>

<ul>
<li>拉取 <code>&quot;docker.io/library/busybox:1.36&quot;</code> 镜像。</li>
<li>使用 busybox 启动一个 container，运行 <code>sleep infinity</code> 命令。</li>
<li>（可选） 睡眠 60 秒，在这期间，使用 ctr 命令查看镜像，container 以及 task。</li>
<li>程序退出前，删除所有资源。</li>
</ul>

<p>本文将分析基于这个 client 代码，分析 containerd go client 的核心流程。</p>

<p>本系列所有代码位于： github <a href="https://github.com/rectcircle/learn-contaierd-experiment">rectcircle/learn-contaierd-experiment</a>。</p>

<h2 id="示例程序">示例程序</h2>

<p><code>01-core-process/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// go build ./01-core-process/main.go &amp;&amp; sudo ./main
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#75715e">// &#34;time&#34;
</span><span style="color:#75715e"></span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/namespaces&#34;</span>
	<span style="color:#e6db74">&#34;github.com/containerd/containerd/oci&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">containerExample</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">containerExample</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;/run/containerd/containerd.sock&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Close</span>()

	<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">namespaces</span>.<span style="color:#a6e22e">WithNamespace</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;default&#34;</span>)
	<span style="color:#75715e">// image, err := client.Pull(ctx, &#34;docker.io/library/golang:1.20&#34;, containerd.WithPullUnpack)
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">image</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Pull</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;docker.io/library/busybox:1.36&#34;</span>, <span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithPullUnpack</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Successfully pulled %s image\n&#34;</span>, <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">Name</span>())

	<span style="color:#a6e22e">container</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">NewContainer</span>(
		<span style="color:#a6e22e">ctx</span>,
		<span style="color:#e6db74">&#34;busybox&#34;</span>,
		<span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithNewSnapshot</span>(<span style="color:#e6db74">&#34;busybox&#34;</span>, <span style="color:#a6e22e">image</span>),
		<span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithNewSpec</span>(
			<span style="color:#a6e22e">oci</span>.<span style="color:#a6e22e">WithImageConfig</span>(<span style="color:#a6e22e">image</span>),
			<span style="color:#a6e22e">oci</span>.<span style="color:#a6e22e">WithProcessArgs</span>(<span style="color:#e6db74">&#34;sleep&#34;</span>, <span style="color:#e6db74">&#34;infinity&#34;</span>),
		),
	)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">container</span>.<span style="color:#a6e22e">Delete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithSnapshotCleanup</span>)
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Successfully created container with ID %s and snapshot with ID busybox&#34;</span>, <span style="color:#a6e22e">container</span>.<span style="color:#a6e22e">ID</span>())
	<span style="color:#75715e">// time.Sleep(60 * time.Second)
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<h2 id="源码分析">源码分析</h2>

<p>使用 VSCode 打开示例代码库，安装 Go 扩展，打开调试视图，选择 <code>Launch: 01-core-process</code>，启动调试，在终端输入 root 密码，单步分析执行过程。</p>

<h3 id="创建-client">创建 client</h3>

<p><code>client, err = containerd.New(&quot;/run/containerd/containerd.sock&quot;)</code></p>

<ul>
<li>构建一个 containerd 的 client。</li>
<li>第一个参数为 containerd 的 sock 文件。</li>
<li>可选的一些 client 选项，一些默认值如下：

<ul>
<li><code>timeout</code> 超时时间，为 <code>10</code> 秒。</li>
<li><code>runtime</code> 运行时，为 <code>io.containerd.runc.v2</code>。
<br /></li>
</ul></li>
</ul>

<h3 id="创建-namespace">创建 namespace</h3>

<p>建立在 containerd 的上层应用有很多，如 Kubernetes、Docker。为了支持同一个机器可以同时安装 Kubernetes、Docker 以及基于 containerd 的其他应用。</p>

<p>containerd 提供了 namespace 的概念对这些上层应用进行隔离。也就是说，docker 是 containerd 中是一个 namespace、Kubernetes 也是一个 namespace。</p>

<p>这两者相互之间无法看到对方创建的 Container。</p>

<p>在 Client 层面，namespace 使用 ctx 传递给 client 的所有接口，最终作为 GRPC 的 Header 透传到 containerd 的 deamon。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">namespaces</span>.<span style="color:#a6e22e">WithNamespace</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;default&#34;</span>)</code></pre></div>
<h3 id="拉取镜像">拉取镜像</h3>

<p><code>image, err := client.Pull(ctx, &quot;docker.io/library/golang:1.20&quot;, containerd.WithPullUnpack)</code></p>

<p>参数说明：</p>

<ul>
<li>第一个参数 ctx 上下文，可以配置 namespace、租约等。</li>
<li>第二个参数为拉取的镜像应用字符串。</li>
<li>第三个参数为一个选项，表示下载下来镜像后，使用 snapshotter 进行解包。</li>
</ul>

<p>客户端流程出下：</p>

<ul>
<li>获取一个 pullCtx，类型为 <code>RemoteContext</code>，包含一个镜像 Resolver，默认为 Docker Registry Resolver，用来对接具体的镜像仓库的实现。</li>
<li>ctx 如果没有，申请一个租约，有效期为 24 小时。租约是一种对资源处于使用状态的一种标记。在有效期内的资源都不会被垃圾回收。更多参见： <a href="https://github.com/containerd/containerd/blob/main/docs/garbage-collection.md#what-is-a-lease">docs</a>。</li>
<li>配置了 <code>WithPullUnpack</code>，因此需要获取 snapshotter，默认为 overlayfs。并构造一个 unpacker。</li>
<li>调用 fetch 函数，下载镜像。

<ul>
<li>获取 ContentStore</li>
<li>使用 pullCtx 的 Resolver 从镜像仓库获取要拉取的镜像的 <code>index.json</code> （docker 中媒体类型为 <code>&quot;application/vnd.docker.distribution.manifest.list.v2+json&quot;</code>）。</li>
<li>构造一个处理函数链，在这个函数链中根据配置下载镜像内容。</li>
<li>通过 <code>images.Dispatch</code> 调用这个处理函数链，下载到 <code>/var/lib/containerd/io.containerd.content.v1.content</code>。</li>
<li>在一个协程中，进行 unpack，参见下文。</li>
<li>需要特别说明的是：

<ul>
<li>获取 <code>index.json</code> 以及后续所有内容的下载完全发生在客户端。</li>
<li>客户端获取到文件流后，通过 GRPC 调用 containerd deamon 的 ContentStore 服务将文件内容写到指定位置中。</li>
</ul></li>
</ul></li>
<li>调用 unpacker 的 wait 等待 unpack 执行完成，unpack 逻辑如下，针对镜像的每一层：

<ul>
<li>调用 <code>Snapshotter.Stat</code>，检查该层是否已经就绪，如果已经就绪，则什么都不做。否则执行后续操作。</li>
<li>调用 <code>Snapshotter.Prepare</code>，创建构建到该层的文件系统的 Mount 参数列表（类型 <code>[]Mount</code>），overlayfs 的 Snapshotter 返回一个元素：

<ul>
<li>参数 key 格式为 <code>&quot;extract-&lt;随机数&gt; &lt;parentChainID&gt;&quot;</code></li>
<li>如果是第一层，返回 <code>Type=bind</code>，<code>Source=&quot;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/26/fs&quot;</code>，<code>Options</code> 为：

<ul>
<li><code>&quot;rw&quot;</code></li>
<li><code>&quot;rbind&quot;</code></li>
</ul></li>
<li>其他层，返回，返回 <code>Type=&quot;overlay&quot;</code>，<code>Type=&quot;overlay&quot;</code>，<code>Options</code> 为：

<ul>
<li><code>&quot;index=off&quot;</code></li>
<li><code>&quot;workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/27/work&quot;</code></li>
<li><code>&quot;upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/27/fs&quot;</code></li>
<li><code>&quot;lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/26/fs&quot;</code></li>
</ul></li>
</ul></li>
<li>通过 GRPC 调用 containerd deamon 的 diff api 的 Apply 函数，该函数应该会将调用 mount 系统调用，mount 到一个临时目录，并将层的内容写入该目录（该部分细节参见：本系列第四篇）。</li>
<li>调用 <code>Snapshotter.Commit</code>，提交该层的 snapshot，完成后该层的 Snapshot 创建完成。</li>
</ul></li>
<li>调用镜像服务，创建该镜像，如果镜像存在则更新该镜像。</li>
</ul>

<h3 id="启动容器">启动容器</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">container</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">NewContainer</span>(
		<span style="color:#a6e22e">ctx</span>,
		<span style="color:#e6db74">&#34;busybox&#34;</span>,
		<span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithNewSnapshot</span>(<span style="color:#e6db74">&#34;busybox&#34;</span>, <span style="color:#a6e22e">image</span>),
		<span style="color:#a6e22e">containerd</span>.<span style="color:#a6e22e">WithNewSpec</span>(
			<span style="color:#a6e22e">oci</span>.<span style="color:#a6e22e">WithImageConfig</span>(<span style="color:#a6e22e">image</span>),
			<span style="color:#a6e22e">oci</span>.<span style="color:#a6e22e">WithProcessArgs</span>(<span style="color:#e6db74">&#34;sleep&#34;</span>, <span style="color:#e6db74">&#34;infinity&#34;</span>),
		),
	)</code></pre></div>
<ul>
<li>ctx 如果没有，申请一个租约，有效期为 24 小时。</li>
<li>构造 <code>containers.Container</code> 结构体。</li>
<li>将 Options 应用到 <code>containers.Container</code> 结构体 <code>c</code>。

<ul>
<li><code>containerd.WithNewSnapshot</code>：获取 Snapshotter，并调用 <code>Prepare</code> （key 为 <code>&lt;snapshotID&gt;</code>，本例中为 <code>busybox</code>），并配置 <code>c.SnapshotKey</code> 和 <code>c.Image</code>。</li>
<li><code>containerd.WithNewSpec</code> 配置 <code>c.Spec</code>，这个 Spec 为 OCI 的 <a href="https://github.com/opencontainers/runtime-spec">runtime spec</a>。</li>
</ul></li>
<li>通过 GRPC 调用 containerd deamon 的 container service api 的 Create 创建这个 Container。</li>
</ul>

<h3 id="启动任务">启动任务</h3>

<ul>
<li><code>container.NewTask(ctx, cio.NewCreator(cio.WithStdio))</code> 使用上述容器配置的进程，创建任务。

<ul>
<li>创建 <code>/run/containerd/fifo/&lt;随机数&gt;/&lt;taskID&gt;-stdin,stdout,stderr</code> fifo 文件，并对接当前进程的 stdin、stdout、stderr。</li>
<li>创建请求参数。</li>
<li>获取 Snapshotter，调用 Mounts 函数（key 为 <code>&lt;snapshotID&gt;</code>，本例中为 <code>busybox</code>），获取 mount 参数 （Type=overlay），将这个 mount 设置为 request 的 Rootfs。</li>
<li>通过 GRPC 调用 containerd deamon 的 task service api 的 Create 创建这个 Task。</li>
<li>此时 <code>runc init</code> 执行完成，等待发送 start 信号。</li>
</ul></li>
<li><code>task.Start(ctx)</code> 通过 GRPC 调用 containerd deamon 的 task service api 的 Start 启动这个 Task，此时 runc init 进程收到消息，exec 子进程。</li>
</ul>
]]></description></item><item><title>Containerd 详解（一） 快速开始</title><link>https://www.rectcircle.cn/posts/containerd-1-quickstart/</link><pubDate>Fri, 05 May 2023 16:10:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/containerd-1-quickstart/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://github.com/containerd/containerd/tree/v1.7.0">v1.7.0</a></p>
</blockquote>

<h2 id="简述">简述</h2>

<p>Containerd 是一个云原生（容器）领域行业标准容器运行时。以操作系统守护进程方式提供服务，管理一台机器上每个容器生命周期，包括：</p>

<ul>
<li>镜像下载和存储。</li>
<li>容器 rootfs （根文件系统）的生成。</li>
<li>容器的启动和守护。</li>
<li>容器的低级存储和附加网络。</li>
</ul>

<p>Containerd 是 CNCF 毕业项目，是 Kubernetes 和 Docker 的默认容器运行时。</p>

<p>Containerd 守护进程默认提供了两套 API：</p>

<ul>
<li>Containerd 原生 GRPC API （<a href="https://github.com/containerd/containerd/blob/v1.7.0/api/README.md">源码</a>），并提供了 Go SDK （参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/client.go">源码</a>），Docker 以该方式集成 Containerd。</li>
<li>Kubernetes 的 CRI GRPC API（<a href="https://github.com/containerd/containerd/blob/main/pkg/cri/cri.go">源码</a>），形态上通过 Containerd Plugin 的方式提供服务（原生插件，打包到了 Containerd 二进制中），架构参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/docs/cri/architecture.md">docs</a>。Kubernetes 以该方式集成 Containerd。</li>
</ul>

<p>在底层容器运行时方面，Containerd 采用 <a href="https://github.com/opencontainers/runtime-spec">OCI-runtime</a> 标准，默认使用 runc 作为运行时。</p>

<p>简单概述典型场景中 Kubernetes、Containerd、Runc 的 层级关系如下：</p>

<ul>
<li>Kubernetes 负责集群（多节点）的调度和管理，在单个节点，通过 Kubelet 组件通过 CRI GRPC 接口调用 Containerd。</li>
<li>Containerd 提供单个节点的容器生命周期管理，包括镜像、存储、rootfs、网络，启动容器是 Containerd 通过 <a href="https://github.com/opencontainers/runtime-spec">OCI-runtime</a> 标准调用 runc。</li>
<li>Runc 容器引导器，负责根据一个容器的具体配置，在指定 rootfs 上引导启动一个容器进程。</li>
</ul>

<h2 id="安装">安装</h2>

<blockquote>
<p><a href="https://github.com/containerd/containerd/blob/v1.7.0/docs/getting-started.md#installing-containerd">Getting started with containerd - Installing containerd
</a></p>
</blockquote>

<p>官方提供了三种安装方式，本文只介绍第一种：从官方二进制方式安装。</p>

<ul>
<li><p>下载安装 containerd 二进制可执行文件（假设系统为 x86_64 使用 glibc 的现代 Linux）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-linux-amd64.tar.gz
sudo tar Cxzvf /usr/local containerd-1.7.0-linux-amd64.tar.gz
<span style="color:#75715e"># 安装内容如下：</span>
<span style="color:#75715e"># bin/</span>
<span style="color:#75715e"># bin/containerd-shim-runc-v1</span>
<span style="color:#75715e"># bin/containerd</span>
<span style="color:#75715e"># bin/containerd-shim-runc-v2</span>
<span style="color:#75715e"># bin/containerd-shim</span>
<span style="color:#75715e"># bin/ctr</span>
<span style="color:#75715e"># bin/containerd-stress</span></code></pre></div></li>

<li><p>安装 runc，前往 <a href="https://github.com/opencontainers/runc/releases">runc release 页</a>，下载安装。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://github.com/opencontainers/runc/releases/download/v1.1.7/runc.amd64
sudo install -m <span style="color:#ae81ff">755</span> runc.amd64 /usr/local/sbin/runc</code></pre></div></li>

<li><p>安装 CNI plugins，前往 <a href="https://github.com/containernetworking/plugins/releases">CNI plugins release 页</a> 下载安装。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://github.com/containernetworking/plugins/releases/download/v1.2.0/cni-plugins-linux-amd64-v1.2.0.tgz
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.2.0.tgz</code></pre></div></li>
</ul>

<h2 id="配置-service">配置 service</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://raw.githubusercontent.com/containerd/containerd/v1.7.0/containerd.service
sudo mv containerd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now containerd</code></pre></div>
<h2 id="启动容器">启动容器</h2>

<p>注意：这里使用了 ctr 命令行工具，该命令行工具仅用于调试使用，官方不建议在生产环境使用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ctr --help
sudo ctr info
sudo ctr images pull docker.io/library/busybox:1.36
sudo ctr run -d docker.io/library/busybox:1.36 busybox sleep infinity
<span style="color:#75715e"># sudo ctr task kill -s 9 busybox  # 停止 task</span>
<span style="color:#75715e"># sudo ctr container rm busybox    # 删除 container</span>
<span style="color:#75715e"># sudo ctr snapshot delete busybox # 删除 snapshot</span></code></pre></div>
<h2 id="进程分析">进程分析</h2>

<p>执行 <code>ps xao pid,ppid,uid,cmd</code> 与 containerd 有关的进程输出入如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    PID    PPID   UID CMD
 934457       1     0 /usr/local/bin/containerd
 935161       1     0 /usr/local/bin/containerd-shim-runc-v2 -namespace default -id busybox -address /run/containerd/containerd.sock
 935181  935161     0 sleep infinity</pre></div>
<ul>
<li><code>/usr/local/bin/containerd</code> containerd 守护进程，通过 <code>/run/containerd/containerd.sock</code> 对外提供 gRPC 接口。</li>

<li><p><code>/usr/local/bin/containerd-shim-runc-v2</code> 每个容器对应一个，shim （垫片）进程，负责管理容器（主进程）的生命周期。本例中为上文 busybox 容器的 shim 进程（详见：<a href="https://container42.com/2022/01/10/shim-shiminey-shim-shiminey/">博客</a>）。</p>

<ul>
<li><p>shim 需实现如下两个层面的接口：</p>

<ul>
<li><p>命令行接口：</p>

<ul>
<li><code>start</code> 子命令：containerd 会按照给定标准，调用该 <code>start</code> 子命令，在该退出之前，必须通过将 shim grpc server 的 unix socket 地址写入 stdout，这个 unix socket 位于 <code>/run/containerd/s/</code> 目录。（可通过 <code>sudo lsof -p 935161 | grep unix</code> 查看，<a href="https://github.com/containerd/containerd/blob/v1.7.0/runtime/v2/shim/util_unix.go#L68">源码</a>），在 <code>containerd-shim-runc-v2</code> 的实现为：

<ul>
<li>调用 <a href="https://github.com/containerd/containerd/blob/v1.7.0/runtime/v2/shim/util_unix.go#L68"><code>shim.SocketAddress</code></a> 生成用于提供 shim grpc server 服务的 unix socket addr，并监听该 socket。</li>
<li>通过 <code>cmd.ExtraFiles</code> 将这个 socket 传递给子进程，然后将这个 addr 通过 stdout 告知 containerd 进程，start 命令退出。</li>
<li>启动 shim grpc server 进程，该进程会获取到上文传递的 socket 文件描述符，参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/runtime/v2/shim/shim_unix.go#L58">源码</a>。</li>
</ul></li>
<li><code>delete</code> 子命令，略。</li>
</ul>

<p>更多参见： <a href="https://github.com/containerd/containerd/blob/v1.7.0/runtime/v2/README.md">runtime v2 - README - Shim Authoring</a>。</p></li>

<li><p>shim grpc server 接口的实现，接口定义参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/api/runtime/task/v2/shim.proto">shim.proto</a>。</p></li>
</ul></li>

<li><p>主要职责为：</p>

<ul>
<li>执行 runC 命令启动容器；</li>
<li>监控容器进程状态，当容器执行完成后，通过 exit fifo 文件报告容器进程结束状态；</li>
<li>当容器 1 号进程被杀死后，reaper 掉其所有其子进程。该职责通过 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl</code> 系统调用</a> 和 <code>PR_SET_CHILD_SUBREAPER</code> 选项实现。</li>
</ul></li>
</ul></li>

<li><p><code>sleep infinity</code> 容器主进程，由 <code>runc</code> 引导启动。本例中为上文 busybox 容器的 1 号进程。</p></li>
</ul>

<h2 id="存储分析">存储分析</h2>

<blockquote>
<p>参考： <a href="https://github.com/containerd/containerd/blob/v1.7.0/docs/ops.md">ops.md</a> | <a href="https://github.com/containerd/containerd/blob/v1.7.0/docs/content-flow.md">content-flow.md</a></p>
</blockquote>

<h3 id="var-lib-containerd">/var/lib/containerd/</h3>

<p>containerd <code>root</code> 目录。用于存储持久化的数据，默认为 <code>/var/lib/containerd</code>，可以通过 <code>--root</code> 选项配置。</p>

<p>containerd 本身是插件化的，因此 containerd 自身并不会在该目录存储任何内容。该目录的内容都是由 containerd 插件创建和维护的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/var/lib/containerd/
├── io.containerd.content.v1.content
│   ├── blobs
│   └── ingest
├── io.containerd.metadata.v1.bolt
│   └── meta.db
├── io.containerd.runtime.v2.task
│   ├── default
│   └── example
├── io.containerd.snapshotter.v1.btrfs
└── io.containerd.snapshotter.v1.overlayfs
    ├── metadata.db
    └── snapshots</pre></div>
<ul>
<li><code>io.containerd.content.v1.content</code> 目录 OCI image （即 docker 镜像） 内存存储，更多参见：<a href="/posts/oci-image-spec/">oci image spec</a>。</li>
<li><code>io.containerd.metadata.v1.bolt</code> 存储 containerd 管理的镜像、容器、快照的元数据，存储的内容参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/metadata/buckets.go">源码</a>。</li>
<li><code>io.containerd.snapshotter.v1.&lt;type&gt;</code> Snapshotter 快照目录，参见：<a href="https://github.com/containerd/containerd/blob/v1.7.0/docs/snapshotters/README.md">Snapshotters 文档</a>。

<ul>
<li><code>io.containerd.snapshotter.v1.btrfs</code> 使用 btrfs 文件系统创建容器快照的目录，目前仍处于早期阶段，默认不启用。</li>
<li><code>io.containerd.snapshotter.v1.overlayfs</code> 默认的 snapshotter。采用 overlayfs2 创建快照。</li>
</ul></li>
</ul>

<p>上文我们多次提到了 snapshotter 、快照之类的概念。containerd 的主要职责就是，从一个镜像加上运行配置，最终启动一个容器。我们知道，容器的是有独立于宿主机的根文件系统的（rootfs）。containerd 将镜像转换为一个 rootfs 的语义抽象为一种插件： snapshotter 。开发者可以自由的利用不同的底层技术，来构造 rootfs。</p>

<p>containerd 默认提供了多种 snapshotter 实现，目前广泛使用的是 overlayfs。而 <code>io.containerd.snapshotter.v1.overlayfs</code> 目录就是 overlayfs snapshotter 的数据目录，这里重点介绍一下其结构和原理。</p>

<p>执行 <code>mount | grep busybox</code> 观察 busybox 的 rootfs：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">overlay on /run/containerd/io.containerd.runtime.v2.task/default/busybox/rootfs type overlay (rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/work)</pre></div>
<p>可以看出关键信息如下：</p>

<ul>
<li><code>lowerdir</code> 为 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs</code>。</li>
<li><code>upperdir</code> 为 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/fs</code>。</li>
<li><code>workdir</code> 为 <code>/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/work</code>。</li>
<li>挂载点为 <code>/run/containerd/io.containerd.runtime.v2.task/default/busybox/rootfs</code></li>
</ul>

<p>因此 <code>overlayfs</code> snapshotter 插件的准备一个容器的 rootfs 的执行过程如下：</p>

<ul>
<li>将 <code>io.containerd.content.v1.content</code> 目录的 layer blobs 解压到 <code>io.containerd.snapshotter.v1.overlayfs/snapshots/&lt;id&gt;/fs</code> 中，并创建 <code>work</code> 目录，并记录到 <code>metadata.db</code> 中。</li>
<li>创建一个 <code>upperdir</code> 目录，存储到 <code>io.containerd.snapshotter.v1.overlayfs/snapshots/&lt;id&gt;/fs</code> 并创建 <code>work</code> 目录，并记录到  <code>metadata.db</code> 中。</li>
<li>调用构造 mount 命令参数，创建 rootfs 挂载到 <code>/run/containerd/io.containerd.runtime.v2.task/&lt;namespace&gt;/&lt;name&gt;/rootfs</code></li>
</ul>

<h3 id="run-containerd">/run/containerd/</h3>

<p>containerd <code>state</code> 目录。用于存储临时数据，默认为 <code>/run/containerd</code>，可以通过 <code>--state</code> 选项配置。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/run/containerd
├── containerd.sock
├── containerd.sock.ttrpc
├── fifo
│   └── 2043724491
│       ├── busybox-stderr
│       ├── busybox-stdin
│       └── busybox-stdout
├── io.containerd.runtime.v1.linux
├── io.containerd.runtime.v2.task
│   └── default
│       └── busybox
│           ├── address
│           ├── config.json
│           ├── init.pid
│           ├── log
│           ├── log.json
│           ├── options.json
│           ├── rootfs
│           ├── runtime
│           ├── shim-binary-path
│           └── work -&gt; /var/lib/containerd/io.containerd.runtime.v2.task/default/busybox
├── runc
│   └── default
│       └── busybox
│           └── state.json
└── s
    └── 3646bf529360b2d2555bc0d946ef2fb07e38596749e16cccbd778773b61a6f3c</pre></div>
<ul>
<li><code>containerd.sock</code> containerd 主服务，GRPC 服务。</li>
<li><code>containerd.sock.ttrpc</code> 用于低内存环境 GRPC 服务。</li>
<li><code>fifo</code> 容器进程（task）的 stdin、stdout、stderr 对接到目录的 fifo 文件中。kubectl attach 等命令原理就是对接到这个目录下的 fifo 文件。</li>
<li><code>io.containerd.runtime.v1.linux</code> ??</li>
<li><code>io.containerd.runtime.v2.task/&lt;namespace&gt;/&lt;name&gt;</code> 容器数据。

<ul>
<li><code>address</code> 连接到 shim 进程的地址，本例中文件内容为 <code>unix:///run/containerd/s/3646bf529360b2d2555bc0d946ef2fb07e38596749e16cccbd778773b61a6f3c</code>。</li>
<li><code>config.json</code> oci runtime spec 配置文件 (runc 配置)。</li>
<li><code>init.pid</code> 容器 1 号进程在宿主机名字空间的 pid。</li>
<li><code>log</code> 日志??</li>
<li><code>log.json</code> 日志??</li>
<li><code>options.json</code> 选项??</li>
<li><code>rootfs</code> 容器 rootfs，overlayfs 挂载点。</li>
<li><code>runtime</code> ??</li>
<li><code>shim-binary-path</code> shim 可执行文件路径。</li>
<li><code>work</code> 指向 <code>/var/lib/containerd/io.containerd.runtime.v2.task/default/busybox</code></li>
</ul></li>
<li><code>runc/&lt;default&gt;/&lt;name&gt;/state.json</code> runc 容器状态文件。</li>
<li><code>s/xxx</code> 与 shim 通讯的 domain socket 文件。</li>
</ul>
]]></description></item><item><title>扩展 Kubernetes （一） k3s 测试环境搭建</title><link>https://www.rectcircle.cn/posts/extend-kubernetes-01-k3s-testing-env/</link><pubDate>Sat, 22 Apr 2023 20:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/extend-kubernetes-01-k3s-testing-env/</guid><description type="html"><![CDATA[

<h2 id="系列概述">系列概述</h2>

<p>Kubernetes 是高度可扩展的。即 Kubernetes 可以在不修改 Kubernetes 源码的情况下，提供 Kubernetes 原生不具备的能力。</p>

<p>Kubernetes 扩展点众多，本系列无法一一枚举，因此本系列将从具体场景和实践出发，介绍 Kubernetes 的部分扩展点。</p>

<p>本系列未提到的 Kubernetes 扩展能力，可以参见 Kubernetes 官方文档：<a href="https://kubernetes.io/zh-cn/docs/concepts/extend-kubernetes/">扩展 Kubernetes</a>。</p>

<p>本系列假设读者了解 Kubernetes 的基本使用，理解 Kubernetes 的基本概念，如 Pod、Deployment、PV、PVC 等。</p>

<h2 id="k3s-简述">k3s 简述</h2>

<p>要对 Kubernetes 进行扩展开发，需要搭建一个 Kubernetes 测试环境，本系列将在一台 Linux 虚拟机上使用 k3s 搭建一个 Kubernetes 测试集群。</p>

<p>选择 k3s 的原因是：</p>

<ul>
<li>完全兼容 Kubernetes。</li>
<li>生产就绪，轻量级。</li>
<li>CNCF Sandbox 项目。</li>
<li>维护者是 Rancher Labs （创始人为华人 <a href="https://2d2d.io/s1/rancher/">Sheng Liang（梁胜）</a>），2020 被 SUSE （号称全球最大的独立开源公司）收购。</li>
</ul>

<p>关于 k3s 的简述，参见： <a href="https://docs.k3s.io/zh/">K3s - 轻量级 Kubernetes</a>。</p>

<p>k3s 除了支持单机部署一个集群外，还支持如下场景，比如：</p>

<ul>
<li>高可用部署：

<ul>
<li><a href="https://docs.k3s.io/zh/architecture#%E5%85%B7%E6%9C%89%E5%A4%96%E9%83%A8%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E9%AB%98%E5%8F%AF%E7%94%A8-k3s-server">多 server 并使用外部数据库</a></li>
<li><a href="https://docs.k3s.io/zh/advanced#%E8%BF%90%E8%A1%8C%E6%97%A0-agent-%E7%9A%84-server%E5%AE%9E%E9%AA%8C%E6%80%A7">server 禁用 agent</a></li>
<li>为多个 server 配置一个 4 层 LB。</li>
<li>agent 独立部署，配置 server 为上一步的 LB 地址。</li>
</ul></li>
<li>混合云：<a href="https://docs.k3s.io/zh/installation/network-options#%E5%88%86%E5%B8%83%E5%BC%8F%E6%B7%B7%E5%90%88%E6%88%96%E5%A4%9A%E4%BA%91%E9%9B%86%E7%BE%A4">支持 agent 部署在不同的内网中</a>。</li>
<li><a href="https://docs.k3s.io/zh/installation/network-options#%E5%8F%8C%E6%A0%88-ipv4--ipv6-%E7%BD%91%E7%BB%9C">IPv4、IPv6 双栈</a>。</li>
</ul>

<p>更多参见：<a href="https://docs.k3s.io/zh/">官方文档</a>。</p>

<h2 id="安装-k3s">安装 k3s</h2>

<blockquote>
<p>version: <a href="https://github.com/k3s-io/k3s/tree/v1.26.3+k3s1">v1.26.3+k3s1</a></p>
</blockquote>

<p>在使用 systemd 或 openrc 的 Linux x86_64 或 amd 操作系统 （kernal version &gt;= 5.1） 中执行如下安装命令（详细要求参见：<a href="https://docs.k3s.io/zh/installation/requirements">官方文档 - 要求</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># curl -sfL https://get.k3s.io | sh -</span>
<span style="color:#75715e"># 中国大陆</span>
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR<span style="color:#f92672">=</span>cn sh -
sudo chmod <span style="color:#ae81ff">666</span> /etc/rancher/k3s/k3s.yaml <span style="color:#75715e"># 仅测试，让当前机器其他用户可以直接通过 kubectl 操作集群。</span></code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[INFO]  Finding release for channel stable
[INFO]  Using v1.26.3+k3s1 as release
[INFO]  Downloading hash rancher-mirror.rancher.cn/k3s/v1.26.3-k3s1/sha256sum-amd64.txt
[INFO]  Downloading binary rancher-mirror.rancher.cn/k3s/v1.26.3-k3s1/k3s
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
[INFO]  systemd: Starting k3s</pre></div>
<p>脚本主要行为 （分析 get.k3s.io 脚本）：</p>

<ul>
<li>校验系统：确保当前系统存在 systemd 或 openrc。</li>
<li>下载安装 k3s 二进制文件到 <code>/usr/local/bin/</code> 目录。</li>
<li>在 <code>/usr/local/bin</code> 创建软链 kubectl crictl ctr 指向 k3s 二进制文件。</li>
<li>在 <code>/usr/local/bin</code> 创建 <code>k3s-killall.sh</code>、<code>k3s-uninstall.sh</code> 脚本。</li>
<li>创建环境变量文件 <code>/etc/systemd/system/k3s.service.env</code>。</li>
<li>安装 systemd service 到 <code>/etc/systemd/system/k3s.service</code>，并启动该 service。</li>
</ul>

<p>在安装阶段，安装脚本安装的文件如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/local/bin/
  k3s                # 唯一的可执行文件
  kubectl -&gt; k3s
  crictl -&gt; k3s
  ctr -&gt; k3s
  k3s-killall.sh     # 停止服务的脚本
  k3s-uninstall.sh   # 卸载 k3s 的脚本

/etc/systemd/system/
  k3s.service.env    # 环境变量
  k3s.service</pre></div>
<h2 id="运行分析">运行分析</h2>

<h3 id="配置和数据目录">配置和数据目录</h3>

<p>在运行，k3s 会产生如下目录和文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/etc/rancher/
  k3s/k3s.yaml           # kubectl
  node/password          # 节点秘钥
/run/
  k3s/containerd/        # containerd 相关文件目录
  flannel/subnet.env     # flannel 网络配置
/var/lib/
  rancher/k3s/           # k3s 数据目录
  kubelet/               # kubelet 数据目录</pre></div>
<h3 id="进程和架构分析">进程和架构分析</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ps -ef -w w</code></pre></div>
<p><img src="/image/how-it-works-k3s-revised-9c025ef482404bca2e53a89a0ba7a3c5.svg" alt="image" /></p>

<ul>
<li><p><code>/usr/local/bin/k3s server</code> k3s server 进程，包含：</p>

<ul>
<li><code>supervisor</code> 和 <code>tunnel proxy</code> kubernetes apiserver 和 kubelet 之间在边缘计算场景单向网络的问题（<a href="https://kubernetes.io/zh-cn/docs/concepts/architecture/control-plane-node-communication/#api-server-to-kubelet">apiserver -&gt; kubelet 方向</a>，在边缘场景无法通讯问题）。</li>
<li>默认网络组件 <code>Flunnel</code>。</li>
<li>多个 <a href="https://kubernetes.io/zh-cn/docs/concepts/overview/components/#control-plane-components">Kubernetes 控制面组件</a>：

<ul>
<li>apiserver</li>
<li>sqlite (替代 etcd)</li>
<li>scheduler</li>
<li>controller manager</li>
</ul></li>
<li>多个 <a href="https://kubernetes.io/zh-cn/docs/concepts/overview/components/#node-components">Kubernetes Node 组件</a>：

<ul>
<li>kubelet</li>
<li>kube-proxy</li>
</ul></li>
</ul>

<p>可以看出，all in one，这就是 k3s 和标准 Kubernetes 的核心区别</p></li>

<li><p><code>containerd -c /var/lib/rancher/k3s/agent/etc/containerd/config.toml -a /run/k3s/containerd/containerd.sock --state /run/k3s/containerd --root /var/lib/rancher/k3s/agent/containerd</code> containerd 进程，默认会启动如下容器（k3s 称为封装的<a href="https://docs.k3s.io/zh/installation/packaged-components#%E5%B0%81%E8%A3%85%E7%9A%84%E7%BB%84%E4%BB%B6">组件</a>）：</p>

<ul>
<li><code>traefik</code> 默认 ingress controller。</li>
<li><code>metrics-server</code> 容器。</li>
<li><code>local-path-provisioner</code> 利用本地磁盘实现 pvc 的一个存储类。</li>
<li><code>coredns</code> core dns。</li>
</ul></li>
</ul>

<h2 id="安装-kubernetes-仪表板">安装 Kubernetes 仪表板</h2>

<p>参见：<a href="https://docs.k3s.io/zh/installation/kube-dashboard">官方文档</a>。</p>

<p>注意，如果浏览器和 k3s server 不在同一设备执行如下命令：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># k3s 所在设备执行</span>
kubectl proxy
<span style="color:#75715e"># 浏览器所在设备执行</span>
ssh -L localhost:8001:localhost:8001 -NT dev <span style="color:#75715e"># dev 为 k3s 所在设备的 host</span></code></pre></div>
<p>打开浏览器，访问：  <a href="http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/">http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/</a> 。</p>

<p>token 通过如下方式获取：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kubectl -n kubernetes-dashboard create token admin-user</code></pre></div>
<h2 id="k3s-集群操作">k3s 集群操作</h2>

<h3 id="停止和启动">停止和启动</h3>

<p>k3s 提供了两种停止 k3s 集群的方式：</p>

<ul>
<li><code>sudo systemctl stop k3s.service</code> 仅停止 k3s server 服务，已运行的容器仍然运行。</li>
<li><code>sudo k3s-killall.sh</code> 停止 k3s server 服务，同时停止所有运行中的容器、网络、iptables、并重置 containerd 的状态。集群数据不会删除，在启动后，k3s 会重新拉起对应的 Pod。</li>
</ul>

<p>针对以上两种，重新启动的方式都是一样的：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo systemctl start k3s.service</code></pre></div>
<h3 id="卸载">卸载</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">k3s-uninstall.sh</code></pre></div>]]></description></item><item><title>NixOS 指南</title><link>https://www.rectcircle.cn/posts/nixos/</link><pubDate>Sun, 16 Apr 2023 00:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nixos/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: <a href="https://nixos.org/manual/nixos/stable/release-notes.html#sec-release-22.11">22.11</a></p>
</blockquote>

<h2 id="安装">安装</h2>

<p>前往 NixOS ISO <a href="https://nixos.org/download.html#nixos-iso">下载页</a>，建议下载 Graphical ISO image（该镜像也可以选择最小化安装）。</p>

<p>参考网络上制作 Linux USB 启动盘的教程制作 USB 启动盘。然后使用该 ISO 引导启动需要安装 NixOS 的设备。然后按照图形化安装程序的步骤一键安装即可，注意：</p>

<ul>
<li>键盘布局建议选择 English (US) - Default。</li>
<li>可以选择不安装桌面环境。</li>
</ul>

<p>安装完成后，重启系统即可进入 NixOS。</p>

<h2 id="快速配置">快速配置</h2>

<p>假设按照章节说明，选了 Minimal Installation （不安装桌面环境），需要进行一些配置才能更好的使用，大概配置如下内容。</p>

<ul>
<li>使用清华源二进制缓存加速下载</li>
<li>最小化安装的 NixOS 的只提供了 nano 编辑器，Linux 用户应该更熟悉 vim，因此，安装 vim。</li>
<li>NixOS 图形化安装程序并没有对 hostname 配置，在这里配置一下。</li>
<li>最小化安装的 NixOS 默认并没有开启 SSH，为了更方便的使用 NixOS，下面来开启 SSH。</li>
</ul>

<p>和其他 Linux 发行版不同，NixOS 通过 <code>/etc/nixos/configuration.nix</code> 配置文件来配置系统的一切，使用 <code>sudo nano /etc/nixos/configuration.nix</code> 来修改该配置文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">  <span style="color:#75715e"># 在配置文件花括号里面，添加如下行</span>
  nix<span style="color:#f92672">.</span>settings<span style="color:#f92672">.</span>substituters <span style="color:#960050;background-color:#1e0010">=</span> [ <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store&#34;</span> ];

  <span style="color:#75715e"># 如下部分默认配置文件均有对应的说明，搜索即可</span>
  networking<span style="color:#f92672">.</span>hostName <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#e6db74">&#34;pve-vm-nixos&#34;</span>; <span style="color:#75715e"># Define your hostname.</span>
  environment<span style="color:#f92672">.</span>systemPackages <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#66d9ef">with</span> pkgs; [
    vim <span style="color:#75715e"># Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.</span>
    wget
  ];
  services<span style="color:#f92672">.</span>openssh<span style="color:#f92672">.</span>enable <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#66d9ef">true</span>;</code></pre></div>
<p>大陆地区，从清华源获取 Channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/nixos-22.11 nixos
sudo nix-channel --update</code></pre></div>
<p>将 <code>/etc/nixos/configuration.nix</code> 应用到系统中。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo nixos-rebuild switch</code></pre></div>
<p>至此，一个最小化的 NixOS 配置完成，可通过 ssh 远程登录 NixOS。</p>

<h2 id="nixos-初探">NixOS 初探</h2>

<p>常规的 Linux 发行版，配置一个 Linux 系统的方式是：</p>

<ul>
<li>安装一个 Linux 标准的发行版。</li>
<li>执行一个 shell 脚本：

<ul>
<li>利用该发行版的包管理工具安装依赖软件包（如 apt、yum）。</li>
<li>修改配置文件（service、shell profile、软件包配置等）。</li>
<li>清理安装过程产生的垃圾。</li>
</ul></li>
<li>如果想将该系统分享给其他人，需要还需制作一个镜像，只能以镜像的方式来分享该环境。</li>
</ul>

<p>在容器场景，如上流程仍然没有什么变化，只是如上这些通过 Dockerfile 来描述。</p>

<p>而 NixOS 基于 Nix 包管理器，整个 NixOS 的完全可以通过 <code>/etc/nixos/configuration.nix</code> 配置文件，自动生成。也就是说，NixOS，通过分享 <code>/etc/nixos/configuration.nix</code> 配置文件，即可在任意设备上重现当前 NixOS 的整个系统的环境，而不需制作镜像。</p>

<p>这就是 NixOS 最重要的特性：可重现性。</p>

<p>下面初步通过观察文件系统、shell、环境变量等，来介绍 NixOS 的结构，阐述其如何实现可重现性。</p>

<h3 id="目录结构">目录结构</h3>

<p>NixOS 作为 Linux 发行版，其目录结构整体上是符合 <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard"><code>FHS</code></a> 的。</p>

<p>为了支持可重现，系统的软件包和配置均通过 <code>/etc/nixos/configuration.nix</code> 配置文件来定义，通过 nix-rebuild 命令：</p>

<ul>
<li>通过 nix 能力，将软件包下载或编译到 <code>/nix/store</code> 目录下。</li>
<li>生成 Linux 系统配置，通过 nix 能力，存储到 <code>/nix/store</code> 目录中。</li>
<li>根据 FHS 规范，修改对应位置软链的指向正确的 <code>/nix/store</code> 子目录或文件。</li>
</ul>

<p>因此 NixOS 的目录结构：</p>

<ul>
<li>整体符合 FHS 规范。</li>

<li><p>配置文件、软件包通过软链指向 <code>/nix/store</code> 子目录或文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/
├── bin
│   └── sh -&gt; /nix/store/gmz9kyy7m7dvbp34wjpmqjyir58z0xch-bash-interactive-5.1-p16/bin/sh
├── boot
│   ├── background.png
│   ├── converted-font.pf2
│   └── grub
├── dev
├── etc
│   ├── bashrc -&gt; /etc/static/bashrc
│   ├── profile -&gt; /etc/static/profile
│   ├── services -&gt; /etc/static/services
│   ├── set-environment -&gt; /etc/static/set-environment
│   ├── static -&gt; /nix/store/x4837ykhq2wvvvdgf16vyp1aac106slz-etc/etc
│   └── ...
├── home
│   └── rectcircle
│       ├── .bash_history
│       ├── .lesshst
│       ├── .ssh
│       └── .viminfo
├── lost+found
├── nix
│   ├── store
│   └── var
├── proc
├── root
│   ├── .bash_history
│   ├── .cache
│   ├── .lesshst
│   ├── .nix-channels
│   ├── .nix-defexpr
│   ├── .nix-profile -&gt; /nix/var/nix/profiles/default
│   ├── result -&gt; /nix/store/pwj4sf18l55bx8s1b8f9mjssnz9z1ffj-nixos-system-pve-vm-nixos-22.11.3567.0040164e473
│   └── sh
├── run
├── srv
├── sys
├── tmp
├── usr
│   └── bin
│       └── env -&gt; /nix/store/98rnm10cy6liayss4gbhksmpvmykl6kd-coreutils-9.1/bin/env
└── var</pre></div></li>
</ul>

<h3 id="etc-目录">/etc 目录</h3>

<p>本小结重点介绍 NixOS 的配置文件结构，从上面目录结构可以看出：</p>

<ul>
<li><code>/etc</code> 下的文件或目录（如 <code>profile</code>、<code>hostname</code>）多数都是软链，指向 <code>/etc/static</code> 目录下的同名项目。</li>
<li><code>/etc/static</code> 也是一个软链，是一个指向 <code>/nix/store/x4837ykhq2wvvvdgf16vyp1aac106slz-etc/etc</code> 的软链。</li>
<li><code>/nix/store/x4837ykhq2wvvvdgf16vyp1aac106slz-etc/etc</code> 目录下包含的就是操作系统的会用到的配置文件，这些配置就是根据 <code>/etc/nixos/configuration.nix</code> 生成的。</li>
</ul>

<h3 id="shell-环境">shell 环境</h3>

<p>举个具体的例子，bash 初始化流程：执行 <code>/etc/profile</code> (<code>/nix/store/x4837ykhq2wvvvdgf16vyp1aac106slz-etc/etc/profile</code>)，改文件里会 source <code>/etc/bashrc</code> (<code>/nix/store/x4837ykhq2wvvvdgf16vyp1aac106slz-etc/etc/bashrc</code>)。而 <code>/etc/bashrc</code> 会进行 bash 的初始化。在上述文件开头的注释说明了，不要修改：<code>DO NOT EDIT -- this file has been generated automatically.</code>。</p>

<p>最终在 shell 中执行 <code>echo $PATH</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/run/wrappers/bin:/home/rectcircle/.nix-profile/bin:/etc/profiles/per-user/rectcircle/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin</pre></div>
<p>这里可以看出，NixOS 的 可执行文件查找目录和常规的 Linux 发行版不同，<code>ls</code> 观察下这些目录，这些目录的项目均是指向 <code>/nix/store/xxx/bin/xxx</code> 的软链。</p>

<p>下面介绍这几个目录的不同之处：</p>

<ul>
<li><code>/nix/var/nix/profiles/default/bin</code> 是由 <code>/etc/nixos/configuration.nix</code> 配置文件生成的。</li>
<li><code>/run/current-system/sw/bin</code> 是 root 用户手动通过 <code>nix-env -iA xxx</code> 手动安装保存的位置，注意这种方式安装的包不受配置文件管理。</li>
<li><code>/home/rectcircle/.nix-profile/bin</code> 和 <code>/etc/profiles/per-user/rectcircle/bin</code> 是当前用户安装的包的位置。</li>
<li><code>/run/wrappers/bin</code> 包含一些需要特殊权限（如设置用户 id 位）的软件包，如 <code>sudo</code> 的可执行文件。</li>
</ul>

<p>观察 <code>ls /bin /usr/bin</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/bin:
sh

/usr/bin:
env</pre></div>
<p>可以看出，某些软件包，包含的 shell 脚本使用的 shebang 是直接指向解释器的，可能会失败，需要做适配才能在 NixOS 中运行，如： <code>#!/bin/bash</code>，此类改造为 <code>#!/usr/bin/env bash</code> 即可。（NixOS 为了保证系统的整洁性，至今仍没有将 <code>/bin/bash</code> 添加其系统中，参见：<a href="https://discourse.nixos.org/t/add-bin-bash-to-avoid-unnecessary-pain/5673">讨论</a>）。</p>

<h2 id="配置文件说明">配置文件说明</h2>

<p>NixOS 是通过 <code>/etc/nixos/configuration.nix</code> 配置文件配置的。</p>

<p>本部分将介绍如果利用该配置文件，在其他 Linux 系统中需要执行一堆命令才能完成的系统配置。</p>

<p>由于 <code>/etc/nixos/configuration.nix</code> 配置项过多，本部分仅介绍一些常见的场景。对于其他场景以及全部配置项，请查阅：</p>

<ul>
<li><a href="https://nixos.org/manual/nixos/stable/index.html#ch-configuration">NixOS Manual - II. Configuration</a>。</li>
<li><a href="https://nixos.org/manual/nixos/stable/options.html">NixOS Manual - Appendix A. Configuration Options</a>。</li>
</ul>

<h3 id="配置语法">配置语法</h3>

<p><code>/etc/nixos/configuration.nix</code> 配置文件是一个 nix 表达式，可以使用 nix 语言的所有特性，关于 nix 语言，参见本系列：<a href="/posts/nix-3-nix-dsl/">Nix 详解（三） nix 领域特定语言</a>。</p>

<p>该表达式必须是一个 nix 函数结构如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{ config<span style="color:#f92672">,</span> pkgs<span style="color:#f92672">,</span> <span style="color:#f92672">...</span> }:
{
  <span style="color:#75715e"># option definitions</span>
}</code></pre></div>
<p>下文具体场景配置的位置上如无说明，均位于 <code># option definitions</code> 附近。</p>

<h3 id="nix-配置">Nix 配置</h3>

<p>如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">  nix.settings.substituters = [ &#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store&#34; ];</pre></div>
<p>更多参见：<a href="https://search.nixos.org/options?channel=22.11&amp;show=nix.settings.substituters&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=nix.settings">NixOS options search</a>。</p>

<h3 id="包安装">包安装</h3>

<p>所有可用包，前往 <a href="https://search.nixos.org/packages">https://search.nixos.org/packages</a> 搜索，并配置到配置文件，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">  environment<span style="color:#f92672">.</span>systemPackages <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#66d9ef">with</span> pkgs; [
    vim
    wget
  ];</code></pre></div>
<h3 id="shell-profile">Shell Profile</h3>

<p>很多时候，我们想给 shell 配置一些 alias 导出一些自己的环境变量，此时可以通过如下方式配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">  environment<span style="color:#f92672">.</span>interactiveShellInit <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    alias l=&#39;ls -alh&#39;
</span><span style="color:#e6db74">    alias k=&#39;kubectl&#39;
</span><span style="color:#e6db74">    alias ll=&#39;ls -l&#39;
</span><span style="color:#e6db74">  &#39;&#39;</span>;
  environment<span style="color:#f92672">.</span>variables <span style="color:#960050;background-color:#1e0010">=</span> {
    A <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>;
  };</code></pre></div>
<p>更多参见：<a href="https://search.nixos.org/options?channel=22.11&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=interactiveShellInit">NixOS options search</a>。</p>

<h3 id="用户配置">用户配置</h3>

<p>可以为某个用户配置用户粒度的包、环境变量等。</p>

<p>前往 <a href="https://search.nixos.org/options">NixOS options search</a>，搜索 <code>users.users.&lt;name&gt;</code>。</p>

<h3 id="安装服务">安装服务</h3>

<p>如果想安装一些服务，如 MySQL，Redis 等，可以前往 <a href="https://search.nixos.org/options">NixOS options search</a>，搜索 <code>services.xxx</code> 搜索配置项，按照说明配置即可。</p>

<h3 id="配置原理">配置原理</h3>

<p>上文介绍了 <code>/etc/nixos/configuration.nix</code> 配置文件的基本结构和常见场景的用法。这里介绍下 NixOS 是如何根据这个配置文件生成这个系统的最终配置项，最终应用到系统中的过程。</p>

<p>回顾一下 <code>/etc/nixos/configuration.nix</code> 语法，其本质上是一个 nix 函数，这类函数在 NixOS 中被称为 NixOS Module。</p>

<ul>
<li>该函数接收包含至少包含 <code>config</code>、<code>pkgs</code> 属性的属性集。可以通过 <code>nix repl --expr  '{ nixos = import &lt;nixpkgs/nixos&gt; { configuration = {}; }; }'</code> 命令，进入 nix repl，输入 nixos 即可查看该属性集的属性，注意该命令只能在 NixOS 系统中执行，不能在只装了 nix 的其他 Linux 发行版中运行：

<ul>
<li><code>config</code> 属性集，属性为 NixOS 模块的选项的配置，据官网称，超过 10000 个，可以通过 <a href="https://search.nixos.org/options">官方搜索站点</a> 或 <a href="https://nixos.org/manual/nixos/stable/options.html">NixOS Manual - Appendix A. Configuration Options</a> 查找包含属性和默认值。也可以通过在上述 repl 中输入 <code>nixos.config.x</code> 按 Tab 可以看到包含的属性。</li>
<li><code>pkgs</code> 属性集，即 nixpkgs 声明的 nix package，据官网称，超过 80000 个，可以通过 <a href="https://search.nixos.org/packages">官方搜索站点</a> 查找。也可以通过在上述 repl 中输入 <code>nixos.pkgs.x</code> 按 Tab 查看。</li>
<li><code>options</code> 是对 <code>config</code> 选项的定义，包括数据类型，数据校验，默认值，描述说明等。</li>
<li><code>system</code>  ???</li>
<li><code>vm</code> ???</li>
<li><code>vmWithBootLoader</code> ???</li>
</ul></li>
<li>该函数返回 NixOS Module 配置的属性集，有两个选择：

<ul>
<li>速记模式语法，即 <code>/etc/nixos/configuration.nix</code> 使用的模式，这个属性集本身代表就是一个 <code>config</code>，这种模式，在实现中会转换为标准语法，参见：<a href="https://github.com/NixOS/nixpkgs/blob/22.11/lib/modules.nix#L418">源码</a>。</li>
<li>标准语法，一般在定义 Module 时使用，返回一个包含 <code>config</code>、<code>options</code>、<code>imports</code> 等属性的属性集，例如： <a href="https://github.com/NixOS/nixpkgs/blob/22.11/nixos/modules/services/networking/ssh/sshd.nix">sshd 服务模块源码</a>。</li>
</ul></li>
</ul>

<p>NixOS 配置相关源码也位于 <a href="https://github.com/NixOS/nixpkgs"><code>NixOS/nixpkgs</code></a> 代码库，结合代码可以得知 NixOS 加载配置的过程如下：</p>

<ul>
<li>读取 <code>NIXOS_CONFIG</code> 环境变量指向的文件，或 <code>&lt;nixos-config&gt;</code> 文件（<code>/etc/nixos/configuration.nix</code>），获取用户配置的 NixOS Module （<a href="https://github.com/NixOS/nixpkgs/blob/22.11/nixos/default.nix">nixpkgs/nixos/default.nix</a>）。</li>
<li>加载所有的 nixpkgs 所有的 NixOS Module (官方 Module) （<a href="https://github.com/NixOS/nixpkgs/blob/22.11/nixos/modules/module-list.nix">nixpkgs/nixos/modules/module-list.nix</a>）。</li>
<li>根据如上两步获取的 Module 列表对 config 的配置，options 中声明的默认值，imports 中声明的依赖关系，生成最终的 <code>config</code> （<a href="https://github.com/NixOS/nixpkgs/blob/22.11/lib/modules.nix"><code>nixpkgs/lib/modules.nix</code></a>）。</li>
</ul>

<p>最后 nixos-rebuild 会根据最终的 <code>config</code> 配置，配置操作系统，该步骤参见： <a href="https://nixos.org/manual/nixos/stable/index.html#sec-switching-systems">NixOS Manual - Chapter 69. What happens during a system switch?</a>。</p>

<p>更多关于 NixOS Module 参见： <a href="https://nixos.wiki/wiki/NixOS_modules">NixOS Wiki</a>。</p>

<h2 id="nixos-rebuild-使用说明">nixos-rebuild 使用说明</h2>

<p>nixos-rebuild 用于根据 <code>/etc/nixos/configuration.nix</code> 配置文件，应用到系统，常用的子命令如下：</p>

<ul>
<li><code>switch</code> 使用最新的配置应用到系统中，并保证重启后保持。</li>
<li><code>test</code> 对配置进行构建，并立即应用到当前系统，下次启动后将回滚到之前的状态。</li>
<li><code>boot</code> 对配置进行构建，但是不立即应用到系统中，下次启动后再生效。</li>
<li><code>build</code> 只进行构建，并产生一个 result 软链指向最新的构建，但是不会对当前系统造成任何影响。</li>
</ul>

<p>回滚通过如下选项触发：</p>

<ul>
<li><code>--rollback</code> 如 <code>nixos-rebuild --rollback switch</code>。</li>
</ul>

<p>如果想切换到任意版本，步骤如下：</p>

<ul>
<li>使用 <code>sudo nix-env --list-generations --profile /nix/var/nix/profiles/system</code> 列出系统的所有版本。</li>

<li><p>使用如下命令切换版本（参见： <a href="https://github.com/NixOS/nixpkgs/issues/24374">issue</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo nix-env --switch-generation <span style="color:#ae81ff">12345</span> -p /nix/var/nix/profiles/system
sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch</code></pre></div></li>
</ul>

<p>软件和系统更新：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 只更新软件包。</span>
sudo nixos-rebuild switch --upgrade
<span style="color:#75715e"># 更新大版本：如有新版本替换如下链接（注意如果 Nix 数据库架构变更，可能升级后无法轻易撤销）。</span>
sudo nix-channel --add https://nixos.org/channels/nixos-22.11 nixos
sudo nixos-rebuild switch --upgrade
<span style="color:#75715e"># 自动更新配置</span>
<span style="color:#75715e"># system.autoUpgrade.enable = true;</span>
<span style="color:#75715e"># system.autoUpgrade.allowReboot = true;</span>
<span style="color:#75715e"># system.autoUpgrade.channel = https://nixos.org/channels/nixos-22.11;</span></code></pre></div>
<h2 id="相关站点">相关站点</h2>

<ul>
<li><a href="https://nixos.org/manual/nixos/stable/index.html">NixOS Manual</a></li>
<li><a href="https://search.nixos.org/options">NixOS Options Search</a></li>
<li><a href="https://nixos.wiki/wiki/NixOS_modules">NixOS Wiki - NixOS modules</a></li>
<li><a href="https://nixos.wiki/wiki/Nixos-rebuild">NixOS Wiki - nixos-rebuild</a></li>
<li><a href="https://github.com/NixOS/nixpkgs/tree/master/nixos">github NixOS/nixpkgs -  nixpkgs/nixos</a></li>
</ul>
]]></description></item><item><title>Linux 网络虚拟化技术（六） Wireguard VPN</title><link>https://www.rectcircle.cn/posts/linux-net-virual-06-wireguard/</link><pubDate>Sun, 09 Apr 2023 01:50:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-06-wireguard/</guid><description type="html"><![CDATA[

<blockquote>
<p>声明：在中文语境下，VPN 被特化为突破互联网管控手段的代名词。本文介绍的 VPN 并非这个含义，而是其原意，面向组织的虚拟私有网络。</p>
</blockquote>

<h2 id="概述">概述</h2>

<p>本系列上一篇文章，我们介绍了使用 tun/tap 实现一个简单的 vpn 的示例。在工业界，有很多成熟的商业或开源的 VPN 协议和实现。</p>

<p>传统的 VPN 协议有 OpenVPN、IPSec/L2TP、PPTP，但是这些 VPN 协议相对笨重复杂。</p>

<p>而本文介绍的 Wireguard 是极简、快速、现代的 VPN。据其官网称，其比 IPsec 更快、更简单、更精简和更有用，比 OpenVPN 具有更高的性能。可以运行在嵌入式设备和超级计算机、跨平台支持 Linux、Windows、macOS、BSD、iOS、Android。</p>

<p>选择 Wireguard 作为 VPN 的代表来介绍的另外一个重要原因是，Wireguard 已于 Linux 5.6 (2020) 进入 Linux 内核。</p>

<p>Wireguard 一些关键技术点如下：</p>

<ul>
<li>极简，专注于 VPN 的安全和路由，C 代码据称只有 4000 行 (2020 年)。配置分发管理（秘钥、IP） Wireguard 不关注。</li>
<li>工作在 IP 层，IP 数据包加密后通过 UDP 隧道在节点间传输。</li>
<li>安全上 Wireguard 在这个流程上每个环节有且只有一种算法，这极大的简化了代码量，符合 Unix 极简哲学，使用者无需关心复杂的安全算法的选择。加密流程类似于 SSH。</li>
</ul>

<h2 id="模型">模型</h2>

<p>Wireguard 搭建了一个是由 interface 组成的虚拟网络。在这个网络中所有 interface 的都是对等，对数据包采用相同的处理逻辑。</p>

<p>interface 通过一个 <code>ini</code> 格式的配置文件定义，包含：</p>

<ul>
<li>一个 <code>[interface]</code> 配置，即当前 interface 自身的属性。</li>
<li>多个 <code>[peer]</code> 配置，即当前 interface 可以直接感知到的其他 interface。</li>
</ul>

<p>可以将 Wireguard 网络可视化为一个有向图，则一个 <code>interface</code> 配置的 <code>[interface]</code> 定义了这个有向图的 node，<code>[peer]</code> 定义了这个有向图的边。</p>

<p><img src="/image/wireguard-vpn-base-model.svg" alt="image" /></p>

<h2 id="配置">配置</h2>

<p>在 Linux 中，Wireguard 配置文件位于 <code>/etc/wireguard</code> 目录。配置文件为 <code>&lt;interface-name&gt;.conf</code>，如 <code>/etc/wireguard/wg0.conf</code>。</p>

<p>正如上文所述，一个 interface 的配置包含两类，<code>[interface]</code> 和 <code>[peer]</code>。</p>

<p><code>[interface]</code> 核心字段如下：</p>

<ul>
<li><code>Address</code>，格式为带有子网后缀的 IP 地址，如 <code>192.168.96.1/24</code>，表示当前 interface 绑定的 IP 以及子网。这个字段是 <code>wg-quick</code> 识别的字段，用于生成 Linux interface，并配置一个默认路由。</li>
<li><code>PrivateKey</code>，当前 interface 的私钥，格式为一个 base64 字符串，由 <code>wg genkey</code> 命令生成。</li>
<li><code>ListenPort</code>，默认为 <code>51820</code>，监听的 UDP 端口，用于其他的 interface 和当前 interface 建立用于数据交换的 Tunnel。</li>
<li>其他字段参见： <a href="https://man7.org/linux/man-pages/man8/wg-quick.8.html">wg-quick(8) — Linux manual page</a> | <a href="https://icloudnative.io/posts/wireguard-docs-practice/#interface">WireGuard 教程：WireGuard 的搭建使用与配置详解 - 2、配置详解 - interface</a></li>
</ul>

<p><code>[peer]</code> 核心字段如下：</p>

<ul>
<li><code>PublicKey</code>，指向 interface 的公钥，格式为一个 base64 字符串，由 <code>wg pubkey &lt; /path/to/pri-key</code> 命令生成。</li>
<li><code>AllowedIPs</code>，指向 interface 允许的 IP，网段格式，多个用逗号分隔，如 <code>192.168.96.2/32,192.168.31.0/24</code>。用来实现路由和流量过滤。即：

<ul>
<li>出流量：经过本 interface 的出数据包的目标 ip 是否在该 IP 列表中，如果在，则出数据包将发到到该 peer 对应的远端 interface 中。</li>
<li>入流量：来自该 peer 对应的 interface 的入数据包 的源 ip 是否在 IP 列表中，如果不在，将丢弃。</li>
</ul></li>
<li><code>Endpoint</code> 可选，指向 interface 的隧道地址，格式为 <code>ip:port</code> 或 <code>domain:port</code>，域名解析发生在该接口启动的时刻。<code>port</code> 为指向 interface 的 <code>ListenPort</code> 字段值。</li>
<li><code>PersistentKeepalive</code> 配置了 <code>Endpoint</code> 时，可选配置，表示和 <code>Endpoint</code> 心跳间隔。</li>
<li>其他字段参见： <a href="https://man7.org/linux/man-pages/man8/wg.8.html">wg(8) — Linux manual page</a> | <a href="https://icloudnative.io/posts/wireguard-docs-practice/#peer">WireGuard 教程：WireGuard 的搭建使用与配置详解 - 2、配置详解 - peer</a></li>
</ul>

<h2 id="流程">流程</h2>

<p>将上方模型图，扩充成一个目标为在网络的笔记本和手机可以访问家庭内网任意 ip 的场景：</p>

<ul>
<li>家庭内网 <code>192.168.31.0/32</code>，网关 <code>openwrt</code>，没有公网 IP。</li>
<li>部署一个 VPN 网段 <code>192.168.96.0/24</code>。</li>
<li>具有公网 ip 的云服务器 <code>huawei</code></li>
<li>两台移动设备，分别为笔记本电脑 <code>mac</code>，手机 <code>mi11</code>。</li>
</ul>

<p>此时模型图变为：</p>

<p><img src="/image/wireguard-vpn-process.svg" alt="image" /></p>

<p>说明：</p>

<ul>
<li>方块代表 interface， 红色字为 interface 的 Address 字段。</li>
<li>从方块出发的线表示该 interface 的 peer 配置，线上的字为 peer 的 <code>AllowedIPs</code>，即路由。</li>
<li>实线表示配置该 peer 配置了 <code>Endpoint</code>，换句话说，实线代表一个 tunnel，这些 interface 一旦 up，这些 tunnel 将根据双方的公私钥自动建立起安全 tunnel。</li>
<li>两个 interface 之间可以连通的充分必要条件是：

<ul>
<li>存在两个相互指向的线（即，两个 interface 需要分别配置指向对方的 peer，目的是双方都配置好公私钥）。</li>
<li>这两个线至少有一个实线（即，网络至少单向可通，需要建立一个 tunnel，用来传输数据）。</li>
</ul></li>
<li>因为 huawei 设备具有公网 ip，所以 mi11、mac、openwrt 才能有一个实线指向 huawei。</li>
</ul>

<p>下面分析如下几个场景的数据流：</p>

<ul>
<li>mi11 访问 openwrt，sip 为 192.168.96.3，dip 为 192.168.96.2。

<ul>
<li>ip 包从应用到达 <code>mi11</code> 的 wireguard interface，该 interface 查询 peer 列表，发现第一个 peer 的 AllowedIPs 的 <code>192.168.96.0/24</code> 能匹配上该数据包的 dip。因此数据通过 <code>m11 -&gt; huawei</code> 的 UDP tunnel，发送到 <code>huawei</code> 的 wireguard interface。</li>
<li>ip 包到达 <code>huawei</code> 的 wireguard interface，该 interface 查询 peer 列表，发现第一个 peer 的 AllowedIPs 的 <code>192.168.96.2/32</code> 能匹配上该数据包的 dip。因此数据通过 <code>openwrt -&gt; huawei</code> 的 UDP tunnel，发送到 <code>openwrt</code> 的 wireguard interface。这里值得提一下的是，在 tunnel 视角，这个 tunnel 的方向是 <code>openwrt -&gt; huawei</code>，即 openwrt 侧发起建立的，但是 <code>huawei -&gt; openwrt</code> 的 ip 数据包，是可以通过该 tunnel 从 <code>huawei</code> 发送到 <code>openwrt</code> 的。</li>
<li>ip 包到达 <code>openwrt</code> 的 wireguard interface，该 interface 发现目标 ip 就是当前 interface ip，说明该数据包的目标就是当前设备。于是，该数据包将传送到应用层。</li>
<li>流程结束。</li>
</ul></li>
<li>mi11 访问 nas，sip 为 192.168.96.3，dip 为 192.168.31.5。

<ul>
<li>前面的流程和第一个场景的 1、2 步一致，只是参数不同。</li>
<li>ip 包到达 <code>openwrt</code> 后，会查询当前设备的路由表，发现 dip 在当前 lan 局域网内，于是将 ip 包发送到 nas 设备。这里值得提一下的是，该流程和 wiredguard 没有关系，是 openwrt 自身对路由表的处理逻辑了。</li>
</ul></li>
<li>pc 访问 mac。sip 为 192.168.31.2，dip 为 192.168.96.4。

<ul>
<li>ip 包从应用到达 <code>pc</code> 的内核，内核通过默认路由发送到网关 <code>openwrt</code>，这里值得提一下的是，该流程和 wiredguard 没有关系，是局域网的自身配置决定的。</li>
<li>后续的流程和第一个场景的 1、2 步一致，只是参数不同。</li>
</ul></li>
</ul>

<h2 id="实施细节">实施细节</h2>

<p>本部分将上文流程部分示例进行落地实施，这个过程有很多细节值得关注。</p>

<h3 id="生成-key">生成 key</h3>

<p>建议在 Linux 系统安装 wireguard 工具集（安装参见：<a href="#配置-huawei">下文</a>），通过命令行生成所有设备 wireguard 配置 interface 需要的公私钥。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd /etc/wireguard/
umask <span style="color:#ae81ff">0077</span>
wg genkey &gt; huawei.key
wg genkey &gt; openwrt.key
wg genkey &gt; mi11.key
wg genkey &gt; mac.key
umask <span style="color:#ae81ff">0022</span>
wg pubkey &lt; huawei.key &gt; huawei.key.pub
wg pubkey &lt; openwrt.key &gt; openwrt.key.pub
wg pubkey &lt; mi11.key &gt; mi11.key.pub
wg pubkey &lt; mac.key &gt; mac.key.pub</code></pre></div>
<h3 id="配置-huawei">配置 huawei</h3>

<p>该设备为 Ubuntu 22.04，内核版本为  5.15，因此不需要升级内核（如果内核小于 5.6 需先升级内核），可以直接安装（参考：<a href="https://www.wireguard.com/install/#ubuntu-module-tools">官方文档</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt update
sudo apt install wireguard</code></pre></div>
<p>配置文件 <code>vim /etc/wireguard/wg0.conf</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[Interface]</span>
<span style="color:#75715e"># Name = huawei</span>
<span style="color:#a6e22e">Address</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.1/24</span>
<span style="color:#a6e22e">PrivateKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat huawei.key 获取</span>
<span style="color:#a6e22e">ListenPort</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">51820</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = openwrt</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.2/32,192.168.31.0/24</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat openwrt.key.pub 获取</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = mi11</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.3/32</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat mi11.key.pub 获取</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = mac</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.4/32</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat mac.key.pub 获取</span></code></pre></div>
<p>启动 wg0 接口 <code>wg-quick up wg0</code>。</p>

<p>内核需开启 forward 等内核参数：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># ipv4 包转发</span>
echo <span style="color:#e6db74">&#34;net.ipv4.ip_forward = 1&#34;</span> &gt;&gt; /etc/sysctl.conf
<span style="color:#75715e"># arp 代理</span>
echo <span style="color:#e6db74">&#34;net.ipv4.conf.all.proxy_arp = 1&#34;</span> &gt;&gt; /etc/sysctl.conf
<span style="color:#75715e"># ipv6 包转发</span>
echo <span style="color:#e6db74">&#34;net.ipv6.conf.all.forwarding = 1&#34;</span> &gt;&gt; /etc/sysctl.conf
<span style="color:#75715e"># 应用配置</span>
sysctl -p /etc/sysctl.conf</code></pre></div>
<p>如果需要通过该节点转发外网访问流程，需配置 iptables 开启 NAT。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i wg0 -o wg0 -m conntrack --ctstate NEW -j ACCEPT
<span style="color:#75715e"># 192.168.96.0/24 为 VPN 网段，eth0 为公网出口网卡设备。</span>
iptables -t nat -A POSTROUTING -s <span style="color:#ae81ff">192</span>.168.96.0/24 -o eth0 -j MASQUERADE</code></pre></div>
<p>测试路由是否正常：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ip route get <span style="color:#ae81ff">192</span>.168.96.2
ip route get <span style="color:#ae81ff">192</span>.168.31.1</code></pre></div>
<p>最后，需要到云服务器管理后台开放 UDP 51820 端口入流量。</p>

<h3 id="配置-openwrt">配置 openwrt</h3>

<blockquote>
<p>openwrt 各种定制版层出不穷，本文介绍的官方编译版本，版本号为： 22.03 x86_64。</p>
</blockquote>

<p>openwrt 提供了 GUI 方式配置 wiredguard 的包，打开 WebUI -&gt; 系统 -&gt; Sofeware。搜索安装： <code>luci-app-wireguard</code>、<code>wireguard-tools</code>、<code>luci-i18n-wireguard-zh-cn</code>，然后重启 Openwrt。</p>

<p>配置文件如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[Interface]</span>
<span style="color:#75715e"># Name = openwrt</span>
<span style="color:#a6e22e">Address</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.2/32</span>
<span style="color:#a6e22e">PrivateKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat openwrt.key 获取</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = huawei</span>
<span style="color:#a6e22e">Endpoint</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&lt;huawei 公网IP&gt;:51820</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.0/24</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat huawei.key.pub 获取</span>
<span style="color:#a6e22e">PersistentKeepalive</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">15</span></code></pre></div>
<p>打开 WebUI -&gt; 网络 -&gt; 接口 -&gt; 添加新接口，填写如下信息，点击创建接口。</p>

<ul>
<li>名称： wg0</li>
<li>协议：WireGuard VPN</li>
</ul>

<p>常规设置，导入配置文件，点击加载配置文件，将上面配置文件粘贴进来，点击导入配置。然后进行一些额外的配置</p>

<ul>
<li>对端 -&gt; 第一个条目 -&gt; 编辑， 勾选路由允许的 IP。添加对 AllowedIPs 路由。这样才能实现局域网内的设备通过 VPN IP 访问其他设备。</li>
</ul>

<p>然后，一直点保存、保存并应用。</p>

<p>目前，还有个问题，即在 <code>huawei</code> 上无法访问 <code>192.168.31.0/24</code> 上的 IP。原因是没有给 OpenWrt  wg0 配置防火墙，配置方法如下：</p>

<ul>
<li>打开 OpenWrt WebUI -&gt; 网络 -&gt; 防火墙 -&gt; 常规配置 -&gt; Zone，点击添加，填写如下内容（该部分是试出来的，并不太理解）：

<ul>
<li>名称： wg0</li>
<li>Input、Output、Forward： accept</li>
<li>Masquerading： 勾选</li>
<li>Covered network：选择 wg0、lan （如果没有 wg0 可以先创建出来后面再编辑）</li>
<li>Allow forward to destination zones： lan</li>
<li>Allow forward from source zones： lan</li>
</ul></li>
<li>保存并应用后，重新配置 wg0 接口。

<ul>
<li>WebUI -&gt; 网络 -&gt; 接口 -&gt; wg0，编辑。</li>
<li>防火墙设置，创建分配防火墙区域，选择 wg0。</li>
</ul></li>
<li>保存并应用即可。</li>
</ul>

<p>此时可以进行如下测试：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># huawei 上执行</span>
ping <span style="color:#ae81ff">192</span>.168.96.2
ping <span style="color:#ae81ff">192</span>.168.31.1
<span style="color:#75715e"># openwrt 上执行</span>
ping <span style="color:#ae81ff">192</span>.168.96.1
<span style="color:#75715e"># pc 上执行</span>
ping <span style="color:#ae81ff">192</span>.168.96.1</code></pre></div>
<p>如上均可以 ping 通，说明配置正确。</p>

<h3 id="配置-mi">配置 mi</h3>

<p>前往 <a href="https://f-droid.org/en/packages/com.wireguard.android/">f-droid</a> 下载安装最新版安卓客户端。</p>

<p>配置文件如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[Interface]</span>
<span style="color:#75715e"># Name = mi11</span>
<span style="color:#a6e22e">Address</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.3/32</span>
<span style="color:#a6e22e">PrivateKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat mi11.key 获取</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = huawei</span>
<span style="color:#a6e22e">Endpoint</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&lt;huawei 公网IP&gt;:51820</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.0/24,192.168.31.0/24</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat huawei.key.pub 获取</span>
<span style="color:#a6e22e">PersistentKeepalive</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">15</span></code></pre></div>
<p>打开 Android App，点击加号，导入配置，或者手动填写配置，保存后，点击开关开启 VPN。</p>

<p>验证方法为：</p>

<ul>
<li>在 <code>huawei</code>，<code>ping 192.168.96.3</code> 是否可以 ping 通</li>
<li>在 <code>mi11</code>，关闭 WIFI，使用数据流量，打开 VPN：

<ul>
<li>打开浏览器，访问 OpenWrt 的 WebUI 的内网地址，本例中为 <code>192.168.31.254</code>，是否可以正常打开。</li>
<li>打开 ES 文件浏览器，是否可以连接到 NAS 上的 Samba 服务器。</li>
</ul></li>
</ul>

<h3 id="配置-mac">配置 mac</h3>

<p>在中国大陆地区无法从 App Store 下载 <a href="https://apps.apple.com/us/app/wireguard/id1451685025">GUI 版本 wireguard</a>，因此本部分将介绍 cli 方式。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install wireguard-tools</code></pre></div>
<p>配置文件 <code>vim /etc/wireguard/wg0.conf</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[Interface]</span>
<span style="color:#75715e"># Name = mac</span>
<span style="color:#a6e22e">Address</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.4/32</span>
<span style="color:#a6e22e">PrivateKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat mac.key 获取</span>

<span style="color:#66d9ef">[Peer]</span>
<span style="color:#75715e"># Name = huawei</span>
<span style="color:#a6e22e">Endpoint</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&lt;huawei 公网IP&gt;:51820</span>
<span style="color:#a6e22e">AllowedIPs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">192.168.96.0/24,192.168.31.0/24</span>
<span style="color:#a6e22e">PublicKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">xxx # cat huawei.key.pub 获取</span>
<span style="color:#a6e22e">PersistentKeepalive</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">15</span></code></pre></div>
<p>启动 wg0 接口 <code>wg-quick up wg0</code>。</p>

<h2 id="用户态实现">用户态实现</h2>

<p>在某些情况下，无法将内核升级到 5.6 之上，此时可以选择官方用户态实现（<a href="https://www.wireguard.com/repositories/">官方代码仓库</a> 列表）：<a href="https://github.com/WireGuard/wireguard-go">wireguard-go</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 依赖</span>
apt update
apt install -y git libmnl-dev libelf-dev build-essential pkg-config
<span style="color:#75715e"># 安装 Go</span>
wget https://go.dev/dl/go1.20.3.linux-amd64.tar.gz
tar -zxvf go1.20.3.linux-amd64.tar.gz
rm -rf go1.20.3.linux-amd64.tar.gz
mv go /usr/local/
echo <span style="color:#e6db74">&#39;export PATH=/usr/local/go/bin:$PATH&#39;</span> &gt;&gt; /etc/profile
export PATH<span style="color:#f92672">=</span>$PATH:/usr/local/go/bin
<span style="color:#75715e"># 编译安装 wireguard-go</span>
git clone https://git.zx2c4.com/wireguard-go
cd wireguard-go
<span style="color:#75715e"># 最新版似乎有 bug，卡在 [#] wg setconf utun2 /dev/fd/63</span>
<span style="color:#75715e"># https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=253537</span>
git checkout <span style="color:#ae81ff">0</span>.0.20201118
go build -v -o <span style="color:#e6db74">&#34;wireguard-go&#34;</span>
mv wireguard-go /usr/sbin/wireguard-go
cd ../
rm -rf wireguard-go
<span style="color:#75715e"># 编译安装 WireGuard</span>
git clone https://git.zx2c4.com/WireGuard
cd WireGuard/src/tools
make <span style="color:#f92672">&amp;&amp;</span> make install
cd ../../../
rm -rf WireGuard</code></pre></div>
<p>注意：该实现依赖 tun 内核模块，需开启，才能正常工作。</p>

<p>后续步骤和上文<a href="#配置-huawei">配置 huawei</a> 一致。</p>

<p>设置开启自动启动：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">systemctl enable wg-quick@wg0</code></pre></div>
<h2 id="基于-wireguard-vpn-的应用">基于 Wireguard VPN 的应用</h2>

<p>从上面可以看出 Wireguard VPN 专注于 VPN 核心问题，解决流量加密和 Tunnel 问题。直接使用配置起来非常麻烦。因此，业界有基于 Wireguard 的面相普通用户更友好的商业产品： <a href="https://tailscale.com/">tailscale</a> 比较适合小型组织。该产品开源届也实现了 tailscale 的开源替代 <a href="https://github.com/juanfont/headscale">headscale</a>。当然面向大型组织，可以基于 Wireguard VPN 根据自身需求自研自己的 VPN 服务。</p>

<p>除了面向组织的 VPN 场景，Wireguard VPN 的另一个重要场景就是云原生领域。基于 Wireguard VPN 的 k8s 网络插件，实现搭建跨内网，跨云的 k8s 集群。这个网络插件 <a href="https://kilo.squat.ai/">Kilo</a>。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://www.wireguard.com/">官网</a></li>
<li><a href="https://man7.org/linux/man-pages/man8/wg-quick.8.html">wg-quick(8) — Linux manual page</a></li>
<li><a href="https://man7.org/linux/man-pages/man8/wg.8.html">wg(8) — Linux manual page</a></li>
<li><a href="https://icloudnative.io/tags/wireguard/page/2/">云原生实验室 #WireGuard#</a></li>
</ul>
]]></description></item><item><title>Nix 详解（六） 备忘单</title><link>https://www.rectcircle.cn/posts/nix-6-cheat-sheet/</link><pubDate>Mon, 27 Mar 2023 02:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-6-cheat-sheet/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1</p>
</blockquote>

<h2 id="命令">命令</h2>

<p>本部分仅记录本系列使用过的，以及一些常用的命令，更多细节参见：<a href="https://nixos.org/manual/nix/stable/command-ref/command-ref.html">官方手册 - 命令参考</a>。</p>

<h3 id="安装卸载">安装卸载</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">### 单用户安装（不配置 channel）</span>
<span style="color:#75715e"># 国内镜像</span>
sh &lt;<span style="color:#f92672">(</span>curl -L https://mirrors.tuna.tsinghua.edu.cn/nix/latest/install<span style="color:#f92672">)</span> --no-daemon --no-channel-add
<span style="color:#75715e"># 官方</span>
sh &lt;<span style="color:#f92672">(</span>curl -L https://nixos.org/nix/install<span style="color:#f92672">)</span> --no-daemon --no-channel-add
<span style="color:#75715e"># 官方，另一种语法</span>
curl -sSL https://nixos.org/nix/install | bash -s -- --no-daemon --no-channel-add
<span style="color:#75715e"># 安装固定版本</span>
curl -sSL https://releases.nixos.org/nix/nix-2.22.1/install | bash -s -- --no-daemon --no-channel-add


<span style="color:#75715e">### 单用户卸载</span>
sudo rm -rf /nix ~/.nix-* ~/.local/state/nix</code></pre></div>
<h3 id="nix-channel">nix-channel</h3>

<p>Channel 管理。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 添加 Channel （官方）</span>
nix-channel --add https://nixos.org/channels/nixpkgs-unstable
<span style="color:#75715e"># 添加 Channel （清华 mirror）</span>
nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/nixpkgs-unstable nixpkgs
<span style="color:#75715e"># 下载 Channel 内容</span>
nix-channel --update
<span style="color:#75715e"># 删除 Channel</span>
nix-channel --remove nixpkgs</code></pre></div>
<h3 id="nix-env">nix-env</h3>

<p>包管理命令，常见用法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 列出已配置 channel 中，所有可用的包</span>
nix-env -qaP
<span style="color:#75715e"># 列出 /path/to/nixpkgs channel 中，所有可安装的包</span>
nix-env -qaPf /path/to/nixpkgs
<span style="color:#75715e"># 按关键字查询</span>
nix-env -qaP firefox
nix-env -qaP <span style="color:#e6db74">&#39;firefox.*&#39;</span> <span style="color:#75715e"># 支持正则</span>
<span style="color:#75715e"># 列出已配置 channel 中，所有可安装的包，以及其状态。</span>
nix-env -qaPs
<span style="color:#75715e"># -PS  nixpkgs.bash                bash-3.0</span>
<span style="color:#75715e"># --S  nixpkgs.binutils            binutils-2.15</span>
<span style="color:#75715e"># IPS  nixpkgs.bison               bison-1.875d</span>
<span style="color:#75715e"># I 表示已应用到当前环境，P 表示已经安装到 /nix/store 中了，S 表示在缓存 server 是否存在二进制缓存。</span>

<span style="color:#75715e"># 列出已安装的包的各种信息（json 格式）</span>
nix-env -q --installed --json --out-path --drv-path --description --meta

<span style="color:#75715e"># 安装包</span>
nix-env -iA nixpkgs.go
<span style="color:#75715e"># 升级包</span>
nix-env -uA nixpkgs.go
<span style="color:#75715e"># 升级所有包</span>
nix-env -u
<span style="color:#75715e"># 仅打印可以升级的包</span>
nix-env -u --dry-run
<span style="color:#75715e"># 卸载包（磁盘空间未释放，如需释放，参见垃圾回收）</span>
nix-env -e go
<span style="color:#75715e"># 安装旧版包 https://lazamar.co.uk/nix-versions/</span>
nix-env -iA go -f https://github.com/NixOS/nixpkgs/archive/d1c3fea7ecbed758168787fe4e4a3157e52bc808.tar.gz

<span style="color:#75715e"># nix 特色的环境版本管理（每次安装、升级、写在都会生成一个版本）</span>
<span style="color:#75715e"># 列出所有环境版本</span>
nix-env --list-generations
<span style="color:#75715e"># 回滚到上一个版本</span>
nix-env --rollback 
<span style="color:#75715e"># 回滚到指定版本</span>
nix-env --switch-generation <span style="color:#ae81ff">43</span></code></pre></div>
<p>常见选项说明：</p>

<ul>
<li><code>-q</code> 查询操作。</li>
<li><code>-a</code> 只列出可以安装，但还未安装的包。</li>
<li><code>-P</code> 打印属性路径（唯一标识）。</li>
<li><code>-s</code> 获取软件包的状态。</li>
<li><code>-i</code> 安装软件包。</li>
<li><code>-A</code> 表示使用包属性名定位安装包</li>
<li><code>-f</code> 从指定 git commit 的 channel 中安装包（commit id 前往 <a href="https://github.com/NixOS/nixpkgs/commits/master">https://github.com/NixOS/nixpkgs/commits/master</a> 查询），支持 url、本地目录。中国大陆网络问题：

<ul>
<li>方案1：前往，<a href="https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/?C=M&amp;O=D">清华源 Release 目录</a>， 搜索最新的<code>nixpkgs-unstable@nixpkgs</code> 开头的最新，复制 nixexprs.tar.xz 路径。</li>
<li>方案2：先通过科学上网，clone 下整个 <a href="https://github.com/NixOS/nixpkgs">https://github.com/NixOS/nixpkgs</a> 仓库（几个 G 大小），然后 checkout 到指定版本，然后在通过 <code>nix-env -f</code> 指定到 nixpkgs 根目录目录，这样后续就不用处理网络问题了。</li>
</ul></li>
</ul>

<h3 id="nix-collect-garbage">nix-collect-garbage</h3>

<p>释放磁盘空间</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 垃圾回收</span>
<span style="color:#75715e"># 真正删除没有被使用的软件包</span>
nix-collect-garbage -d</code></pre></div>
<h3 id="nix-shell">nix-shell</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 打开一个 shell，并安装 `go_1_19`、`jq`、`curl`。</span>
nix-shell -p go_1_19 jq curl
<span style="color:#75715e"># 安装包的同时，不继承环境变量，使用固定版本的 nixpkgs 的配置。</span>
nix-shell -p go_1_19 jq curl --pure -I nixpkgs<span style="color:#f92672">=</span>https://github.com/NixOS/nixpkgs/archive/794f34657e066a5e8cc4bb34491fee02240c6ac4.tar.gz
<span style="color:#75715e"># nix-shell -p go_1_19 jq curl --pure -I nixpkgs=https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz</span>


<span style="color:#75715e"># nix-shell 实现可重现脚本</span>
<span style="color:#75715e">#!/usr/bin/env nix-shell</span>
<span style="color:#75715e">#! nix-shell -i bash --pure</span>
<span style="color:#75715e">#! nix-shell -p bash go_1_19 jq curl which</span>
<span style="color:#75715e">#! nix-shell -I nixpkgs=https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz</span>

<span style="color:#75715e"># nix-shell 可重现脚本使用 shell.nix 的配置</span>
<span style="color:#75715e">#!/usr/bin/env nix-shell</span>
<span style="color:#75715e">#! nix-shell -i bash --pure shell.nix</span></code></pre></div>
<ul>
<li><code>-p</code> 从 nixpkgs 中，指定安装的 package。</li>
<li><code>--pure</code> 指，不继承当前进程的环境变量。</li>
<li><code>-I</code> 从指定 git commit 的 channel 中安装包（commit id 前往 <a href="https://github.com/NixOS/nixpkgs/commits/master">https://github.com/NixOS/nixpkgs/commits/master</a> 查询），支持 url、本地目录。中国大陆网络问题：

<ul>
<li>方案1：前往，<a href="https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/?C=M&amp;O=D">清华源 Release 目录</a>， 搜索最新的<code>nixpkgs-unstable@nixpkgs</code> 开头的最新，复制 nixexprs.tar.xz 路径。</li>
<li>方案2：先通过科学上网，clone 下整个 <a href="https://github.com/NixOS/nixpkgs">https://github.com/NixOS/nixpkgs</a> 仓库（几个 G 大小），然后 checkout 到指定版本，然后在通过 <code>nix-env -f</code> 指定到 nixpkgs 根目录目录，这样后续就不用处理网络问题了。</li>
</ul></li>
<li><code>-i</code> 指定 shell 交互式解释器。</li>
<li><code>&lt;path&gt;</code> 最后一个参数为一个 <code>.nix</code> 文件，默认为当前目录的 <code>shell.nix</code> 如果 <code>shell.nix</code> 不存在，则当前目录的 <code>default.nix</code>。注意，如果指定且写在 shell 的 shebang 中，则当前路径为为脚本所在目录，而不是 work dir。</li>
</ul>

<h3 id="nix-repl">nix repl</h3>

<p>启动一个 nix 语言的交互式环境。</p>

<h3 id="nix-instantiate">nix-instantiate</h3>

<p>执行一个 <code>.nix</code> 文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 执行一个 nix 文件，并不会递归求值。</span>
nix-instantiate --eval nix-lang-demo/01-hello.nix
<span style="color:#75715e"># 执行一个 nix 文件，并递归求值，并将值以 json 格式打印，注意如果包含函数这种没法序列化为 json 的将报错。</span>
nix-instantiate --eval nix-lang-demo/02-primitives-data-type.nix --strict --json
<span style="color:#75715e"># 执行一个 nix 文件，要求该函数返回一个 derivation 类型，或者返回值为 derivation 类型的函数。</span>
<span style="color:#75715e"># 然后将 derivation 序列化到 /nix/store/$hash-$name.drv 文件。</span>
nix-instantiate nix-lang-demo/12-derivation.nix</code></pre></div>
<h3 id="nix-show-derivation">nix show-derivation</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 将一个 /nix/store/$hash-$name.drv 转换为 json 格式，并输出。</span>
nix --extra-experimental-features nix-command show-derivation <span style="color:#66d9ef">$(</span>nix-instantiate path/to/file.nix<span style="color:#66d9ef">)</span></code></pre></div>
<h3 id="nix-build">nix-build</h3>

<p>构建 derivation 并在当前目录创建指向 /nix/store 中 out 的软链 <code>result</code>。</p>

<p>该命令是 <code>nix-instantiate</code> 和 <code>nix-store --realise</code> 的一个包装器。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 构建 nixpkgs 中属性为名为 go_1_19 的 derivation</span>
nix-build <span style="color:#e6db74">&#39;&lt;nixpkgs&gt;&#39;</span> -A go_1_19
<span style="color:#75715e"># ls -l result</span>
<span style="color:#75715e"># lrwxrwxrwx  ...  result -&gt; /nix/store/d18hyl92g30l...-go-1.19.3</span>

<span style="color:#75715e"># nix 表达式可以通过 -E 直接给出</span>
nix-build -E <span style="color:#e6db74">&#39;with import &lt;nixpkgs&gt; { }; runCommand &#34;foo&#34; { } &#34;echo bar &gt; $out&#34;&#39;</span>

<span style="color:#75715e"># 可以从指定 url 的 channel 构建</span>
nix-build https://github.com/NixOS/nixpkgs/archive/master.tar.gz -A hello</code></pre></div>
<h3 id="nix-store">nix-store</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 构建一个 /nix/store/$hash-$name.drv 文件。</span>
nix-store -r <span style="color:#66d9ef">$(</span>nix-instantiate path/to/file.nix<span style="color:#66d9ef">)</span>
<span style="color:#75715e"># 打印 /nix/store/$hash-$name.drv 构建过程日志。</span>
nix-store --read-log <span style="color:#66d9ef">$(</span>nix-instantiate path/to/file.nix<span style="color:#66d9ef">)</span>

<span style="color:#75715e"># 将一个 derivation 输出打包成 nar 包</span>
nix-store --dump /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1 &gt; test.nar
<span style="color:#75715e"># 将 test.nar 压缩为 test.nar.xz</span>
<span style="color:#75715e"># xz test.nar</span>
<span style="color:#75715e"># 将一个 derivation 的 nar 包导入当前机器</span>
nix-store --import v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.nar
<span style="color:#75715e"># 查询一个 derivation 输出的 nar 包的 hash</span>
nix-store -q --hash /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
<span style="color:#75715e"># 查询一个 derivation 输出的 nar 包的 size</span>
nix-store -q --size /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
<span style="color:#75715e"># 查询一个 derivation 输出的直接依赖</span>
nix-store -q --references /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
<span style="color:#75715e"># 查询一个 derivation 输出的所有依赖（闭包）</span>
nix-store -q --requisites /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
<span style="color:#75715e"># 查询一个 derivation 输出对应的 drv文件</span>
nix-store -q --deriver /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
<span style="color:#75715e"># 生成签名用的 key</span>
nix-store --generate-binary-cache-key binarycache.example.com cache-priv-key.pem cache-pub-key.pem

<span style="color:#75715e"># 将一个 derivation 输出及其所有依赖导出到文件</span>
nix-store --export <span style="color:#66d9ef">$(</span>nix-store -qR /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1<span style="color:#66d9ef">)</span> &gt; hello.closure
<span style="color:#75715e"># 将一个 derivation 输出及其所有依赖导入到当前机器</span>
nix-store --import &lt; hello.closure</code></pre></div>
<h3 id="nix-hash">nix-hash</h3>

<p>生成 <code>nar</code> 和 <code>nar.xz</code> 等文件的 hash。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-hash --type sha256 --flat --base32 test.nar</code></pre></div>
<h3 id="nix-copy-closure">nix-copy-closure</h3>

<p>将某个 <code>/nix/store/xxx</code> 目录及其依赖通过 ssh 协议拷贝到另一台机器。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-copy-closure --to alice@itchy.example.org /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code></pre></div>
<h2 id="环境变量">环境变量</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 禁用分页器 (END) 这种全屏显示</span>
PAGER<span style="color:#f92672">=</span></code></pre></div>
<h2 id="配置文件">配置文件</h2>

<p>单用户安装，路径 ~/.config/nix/nix.conf ，详见：<a href="https://nixos.org/manual/nix/stable/command-ref/conf-file.html">官方手册 - 配置文件</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># 二进制缓存服务
substituters = https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store https://cache.nixos.org/</pre></div>
<h2 id="实验性特性">实验性特性</h2>

<p>目前 (2023-04) nix 有一些实现性特性，本系列暂不介绍，仅在此列出，后续正式推出后再视情况整理介绍：</p>

<ul>
<li><code>nix</code> 命令集，用来替代 <code>nix-xxx</code> 的命令，详见：<a href="https://nixos.org/manual/nix/stable/command-ref/new-cli/nix.html">Nix manual - Nix</a></li>
<li>Flakes 以声明的方式指定位于代码仓库的依赖，详见：<a href="https://nixos.wiki/wiki/Flakes">NixOS Wiki - Flakes</a></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://nix-community.github.io/awesome-nix/">Awesome Nix</a></li>
<li><a href="https://nixos.org/guides/nix-pills">Nix Pills</a></li>
<li><a href="https://nix.dev/">nix.dev</a></li>
<li><a href="https://nixos.org/manual/nix/stable/">Nix 手册</a></li>
<li><a href="https://github.com/NixOS/nix">Nix github</a></li>
<li><a href="https://nixos.wiki/">NixOS wiki</a>

<ul>
<li><a href="https://nixos.wiki/wiki/Development_environment_with_nix-shell">Development environment with nix-shell</a></li>
</ul></li>
<li><a href="https://nixos.org/learn.html">NixOS Learn</a></li>
<li><a href="https://mirrors.tuna.tsinghua.edu.cn/help/nix/">清华大学 Nix 源</a></li>
<li>nixpkgs 相关

<ul>
<li><a href="https://github.com/NixOS/nixpkgs">nixpkgs github</a></li>
<li><a href="https://nixos.org/manual/nixpkgs/stable/">nixpkgs 手册</a></li>
</ul></li>
<li>安装旧版本相关

<ul>
<li><a href="https://lazamar.co.uk/nix-versions/">Nix package versions</a></li>
<li><a href="https://lazamar.github.io/download-specific-package-version-with-nix/">Searching and installing old versions of Nix packages</a>。</li>
<li><a href="https://github.com/lazamar/nix-package-versions">github lazamar/nix-package-versions</a></li>
<li><a href="https://github.com/NixOS/nixpkgs/issues/9682">No way to install/use a specific package version? #9682</a></li>
</ul></li>
<li>其他

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=arrterian.nix-env-selector">VSCode 扩展 Nix Environment Selector</a> | <a href="https://github.com/arrterian/nix-env-selector/issues/66">issue</a> | <a href="https://github.com/microsoft/vscode/issues/152806">issue</a></li>
<li><a href="https://grass.show/post/create-environment-with-nix-and-direnv">使用Nix+direnv快速构建不同软件版本的开发环境</a></li>
<li><a href="https://devpress.csdn.net/cicd/62ee0a19c6770329307f3202.html#devmenu9">如何使用 Nix 轻松获取依赖项</a></li>
</ul></li>
<li>二进制缓存

<ul>
<li><a href="https://nixos.wiki/wiki/Binary_Cache">Nix Wiki - Binary Cache</a></li>
<li><a href="https://nixos.org/manual/nix/stable/package-management/sharing-packages.html">Nix Reference Manual - Sharing Packages Between Machines</a></li>
</ul></li>
<li>论文

<ul>
<li><a href="https://edolstra.github.io/pubs/phd-thesis.pdf">The Purely Functional Software Deployment Model</a></li>
<li><a href="https://edolstra.github.io/pubs/nixos-icfp2008-final.pdf">NixOS: A Purely Functional Linux Distribution</a></li>
</ul></li>
</ul>
]]></description></item><item><title>Nix 详解（五）在研发团队中落地设计</title><link>https://www.rectcircle.cn/posts/nix-5-how-to-use-in-rd-group/</link><pubDate>Mon, 27 Mar 2023 00:44:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-5-how-to-use-in-rd-group/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1</p>
</blockquote>

<h2 id="场景推演">场景推演</h2>

<p>从代码角度来看，一个软件项目的需求开发的工作流大致为：代码设计、代码开发环境搭建、代码编写、代码测试、代码编译、代码部署。</p>

<p>根据前面几篇文章的说明，推荐将 Nix 应用在研发团队的工作流程如下场景：</p>

<p><strong>场景一：实现声明式的开发、测试、编译环境。</strong></p>

<p>Nix 相较其他包管理工具，最大的优势是声明式的和可重现的。因此，自然的可以使用 Nix 来声明一个项目的开发、测试、编译环境。 开发、测试、编译环境的依赖一般情况下是一致的，因此针对该场景：</p>

<ul>
<li>需为项目添加一个 <code>shell.nix</code> 文件，声明项目的开发、测试、编译依赖。</li>

<li><p>开发、测试、编译相关的行为通过 shell 脚本实现，通过 shebang 注释应用上面的声明。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env nix-shell
</span><span style="color:#75715e"></span><span style="color:#75715e">#! nix-shell -i bash --pure shell.nix</span></code></pre></div></li>
</ul>

<p><strong>场景二：研发团队内部自研面向内部成员的 CLI 工具发布平台。</strong></p>

<p>针对 Unix 平台的 CLI 工具，可以使用 Nix 来构建和发布 CLI。针对该场景：</p>

<ul>
<li>类似于 nixpkgs，在研发团队内建设一个类似的代码仓库，管理所有需要发布 CLI 工具的 derivation 声明。</li>
<li>研发团队内 CI/CD 平台，提供构建能力，并将构建产物同步到二进制缓存服务。</li>
</ul>

<p><strong>场景三：使用 Nix 构建后端服务的部署镜像（环境）？</strong></p>

<p>目前，Nix 函数式可重现的特性存在一个比较严重的问题，即没有区分构建和运行依赖。</p>

<p>也就是说，一个软件包，即使其已经预构建好并存储在了二进制缓存服务中，但是在安装时，仍然会安装其编译依赖如编译器。</p>

<p>这就导致了，安装一个软件包的磁盘占用会异常的大。在云原生时代，一般部署环境对应一个镜像。</p>

<p>如果使用 Nix 来构建一个镜像，这就导致镜像大小比较大，且包含了没有必要的编译依赖。如果可以接受这一点，则即可使用 Nix 来构建部署镜像。</p>

<p>该问题，参见： <a href="https://github.com/NixOS/nix/issues/8107">Issue</a>。</p>

<h2 id="架构图">架构图</h2>

<p><img src="/image/nix-in-org-arch.svg" alt="image" /></p>

<h2 id="channel-聚合服务">Channel 聚合服务</h2>

<p>该服务主要解决如下几个问题：</p>

<ul>
<li>由于 nixpkgs channel 位于 github 网络不稳定，这里提供对于该 channel 每个 commit 的代理和缓存服务。</li>
<li>提供托管在研发团队内部代码 channel 的注册，并向外暴露唯一的 channel 包。研发团队内用户，如果想将自己的项目发布给研发团队内其他成员。通过该服务的一个管理页面，注册自己的项目，如 <code>myAbcPkg</code>。该服务会提供一个包含 <code>default.nix</code> 的 <code>tar.xz</code> 的 URL。这个 <code>default.nix</code> 最终返回的是一个属性集，这个属性集包含了所有注册的项目。</li>
</ul>

<p>最终，用户的配置为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nix-channel --add https://example.com/channel/nixpkgs-unstable nixpkgs
nix-channel --add https://example.com/channel/rdgrouppkgs rdgrouppkgs
nix-channel --update</pre></div>
<p>这样，用户即可通过 <code>nix-env -iA nixpkgs.go</code> 安装 nixpkgs 的包；通过 <code>nix-env -iA rdgrouppkgs.myAbcPkg.xxx</code> 安装私有包。</p>

<h2 id="nix-缓存聚合器">Nix 缓存聚合器</h2>

<p>nix 缓存聚合器提供 Nix 构建产物的缓存，主要有如下能力：</p>

<ul>
<li>提供对 nix 安装包、安装脚本的缓存。</li>
<li>提供对 nixpkgs 缓存的代理和缓存能力。</li>
<li>提供对研发团队内私有包的缓存上传缓存能力。</li>
</ul>

<p>使用上：</p>

<ul>
<li>用户使用该服务提供的私有安装脚本，即可快速的安装 nix。</li>
<li>用户只需，在 <code>~/.config/nix/nix.conf</code> 配置该缓存聚合器。则可以利用该缓存的快速安装包。</li>
<li>用户如想发布一个私有包，在研发团队内部现有的 CI/CD 流水线中，使用 <code>nix-build</code> 构建好私有包后，即可通过该服务提供的上传 URL 即可将构建缓存发布到该缓存聚合器中。</li>
</ul>
]]></description></item><item><title>Nix 详解（四） HTTP 二进制缓存详解</title><link>https://www.rectcircle.cn/posts/nix-4-http-binary-cache/</link><pubDate>Mon, 20 Mar 2023 01:50:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-4-http-binary-cache/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1 | <a href="https://github.com/rectcircle/learn-nix-demo">示例代码库</a></p>
</blockquote>

<h2 id="简述">简述</h2>

<p>nixpkgs 作为 nix 官方的 channel，定义了 80000+ 个包的构建过程。用户在使用这些包时，如果都需要现场编译的话，那么包的安装速度会非常慢。</p>

<p>从前面几篇文章可以看出，nix 利用函数式的 nix 语言配合 <code>/nix/store</code> 存储机制，可以做到 nix 包的可重现。</p>

<p>基于此，自然的想法是， nixpkgs 在后台，将 nixpkgs 的所有的包够构建出来，并存储在 <code>/nix/store</code> 目录，并通过 HTTP 服务将 <code>/nix/store</code> 托管在互联网上。</p>

<p>此时，用户只需配置这个 HTTP 服务的地址，在安装 nixpkgs 包时，从该 HTTP 服务中下载包构建产物即可，这样将跳过编译过程，可以大大加快 nix 包的安装过程。</p>

<p>这个 HTTP 服务就被称为二进制缓存服务。本文将围绕这个二进制缓存服务介绍：</p>

<ul>
<li>通过 Go 实现一个 HTTP 反向代理，介绍二进制缓存服务的接口规范（官方暂无详细说明）。</li>
<li>如何将任意一台机器的 <code>/nix/store</code> 部署成一个二进制缓存服务，并介绍其原理。</li>
<li>如何将存储在 <code>/nix/store</code> 的一个包及其依赖导出到文件，以及如何将该文件导入到 <code>/nix/store</code> 中。</li>
</ul>

<p>关于二进制缓存服务官方的文档，主要有：</p>

<ul>
<li><a href="https://nixos.wiki/wiki/Binary_Cache">Nix Wiki - Binary Cache</a></li>
<li><a href="https://nixos.org/manual/nix/stable/package-management/sharing-packages.html">Nix Reference Manual - Sharing Packages Between Machines</a></li>
</ul>

<h2 id="二进制缓存服务相关接口分析">二进制缓存服务相关接口分析</h2>

<p>本部分将简单实现一个 http 反向代理，并打印相关日志，探索 nix 包安装过程中的 http 请求，探索包安装相关的接口规范。</p>

<h3 id="实现并运行一个简单的-http-代理">实现并运行一个简单的 http 代理</h3>

<p><code>nix-binary-cache-http-proxy/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;net/url&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">upstream</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">URL</span>      <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">UseProxy</span> <span style="color:#66d9ef">bool</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newHandle</span>(<span style="color:#a6e22e">httpProxy</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">upstreams</span> []<span style="color:#a6e22e">upstream</span>) (<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>), <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultClient</span>
	<span style="color:#a6e22e">proxyClient</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultClient</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">httpProxy</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
		<span style="color:#a6e22e">httpProxyURL</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">httpProxy</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
		}
		<span style="color:#a6e22e">proxyClient</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{<span style="color:#a6e22e">Transport</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Transport</span>{<span style="color:#a6e22e">Proxy</span>: <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ProxyURL</span>(<span style="color:#a6e22e">httpProxyURL</span>)}}
	}

	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
		<span style="color:#75715e">// 打印访问日志
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;path: %s&#34;</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Path</span>)
		<span style="color:#75715e">// TODO 优先从缓存中读取。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 尝试从 upstream 中获取
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">resp</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Response</span>
		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">u</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">upstreams</span> {
			<span style="color:#a6e22e">targetPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(<span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">URL</span>, <span style="color:#e6db74">&#34;/&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Path</span>
			<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">UseProxy</span> {
				<span style="color:#a6e22e">c</span> = <span style="color:#a6e22e">proxyClient</span>
			}
			<span style="color:#a6e22e">_resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">targetPath</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;  try upstream[%d]: %s, error: %s&#34;</span>, <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">targetPath</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#66d9ef">continue</span>
			}
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_resp</span>.<span style="color:#a6e22e">StatusCode</span> <span style="color:#f92672">!=</span> <span style="color:#ae81ff">200</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;  try upstream[%d]: %s, status code is: %d&#34;</span>, <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">targetPath</span>, <span style="color:#a6e22e">_resp</span>.<span style="color:#a6e22e">StatusCode</span>)
				<span style="color:#a6e22e">_resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
				<span style="color:#66d9ef">continue</span>
			}
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;  try upstream[%d]: %s, success&#34;</span>, <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">targetPath</span>)
			<span style="color:#a6e22e">resp</span> = <span style="color:#a6e22e">_resp</span>
			<span style="color:#66d9ef">break</span>
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;  all upstream not found&#34;</span>)
			<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#ae81ff">404</span>)
			<span style="color:#66d9ef">return</span>
		}
		<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
		<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StatusCode</span>)
		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
		<span style="color:#75715e">// TODO 起一个协程写入缓存中。
</span><span style="color:#75715e"></span>	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">upstreams</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">upstream</span>{
		{
			<span style="color:#a6e22e">URL</span>:      <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store&#34;</span>,
			<span style="color:#a6e22e">UseProxy</span>: <span style="color:#66d9ef">false</span>,
		},
		{
			<span style="color:#a6e22e">URL</span>:      <span style="color:#e6db74">&#34;https://cache.nixos.org&#34;</span>,
			<span style="color:#a6e22e">UseProxy</span>: <span style="color:#66d9ef">true</span>,
		},
	}
	<span style="color:#a6e22e">httpProxy</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;HTTP_PROXY&#34;</span>)
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;HTTP_PROXY&#34;</span>)
	<span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newHandle</span>(<span style="color:#a6e22e">httpProxy</span>, <span style="color:#a6e22e">upstreams</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#a6e22e">handle</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ListenAndServe</span>(<span style="color:#e6db74">&#34;:8000&#34;</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}</code></pre></div>
<p>代码说明（来自 chatgpt）：</p>

<p>这是一段 Go 语言编写的简单的 HTTP 反向代理服务器。它接收来自客户端的请求并尝试从多个上游（即 upstreams）中获取响应数据，然后将响应数据返回给客户端。如果所有的上游都无法提供响应数据，则返回 404 错误。</p>

<p>在代码中，我们可以看到以下函数和变量的定义：</p>

<ul>
<li>upstream 结构体：包含一个 URL 字段和一个 UseProxy 字段。URL 字段表示上游 URL，UseProxy 字段表示是否使用代理。</li>
<li>newHandle 函数：返回一个处理 HTTP 请求的函数，该函数根据请求的 URL 路径尝试从上游中获取响应数据并返回给客户端。如果所有上游都无法提供响应数据，则返回 404 错误。</li>
<li>main 函数：该函数创建一个 HTTP 服务器并监听端口 8000，然后将所有请求路由到 newHandle 函数返回的处理函数中进行处理。</li>
</ul>

<p>该代理服务器的工作流程如下：</p>

<ul>
<li>获取客户端请求的 URL 路径。</li>
<li>对于每个上游，将上游 URL 和请求路径拼接为一个新的 URL，然后使用 HTTP 客户端发出 GET 请求。</li>
<li>如果请求成功，将响应数据返回给客户端。</li>
<li>如果请求失败或响应状态码不是 200，则跳过该上游，继续向下一个上游发起请求。</li>
<li>如果所有上游都无法提供响应数据，则返回 404 错误。</li>
</ul>

<p>需要注意的是，这段代码只是一个简单的反向代理服务器示例，实际生产环境中需要进行更多的安全和性能优化。例如，可以添加访问控制、缓存、日志记录等功能来提高系统的可用性和稳定性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd nix-binary-cache-http-proxy
HTTP_PROXY<span style="color:#f92672">=</span>http://192.168.31.254:1082 go run ./</code></pre></div>
<p>这里的 <code>http://192.168.31.254:1082</code> 是一个 HTTP 代理，这个 HTTP 代理有一个连接海外的专线。</p>

<h3 id="使用-http-代理安装-nix-包">使用 http 代理安装 nix 包</h3>

<p>使用 <code>nix-env -e hello &amp;&amp; nix-collect-garbage -d  &amp;&amp; nix-env -iA nixpkgs.hello --option substituters http://127.0.0.1:8000</code> 命令（或修改 <code>~/.config/nix/nix.conf</code> 的 substituters 字段）。</p>

<p>观察上文 <code>go run ./</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">2023/03/19 21:59:53 path: /nix-cache-info
2023/03/19 21:59:53   try upstream[0]: https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store/nix-cache-info, success
2023/03/19 21:59:53 path: /v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.narinfo
2023/03/19 21:59:53   try upstream[0]: https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.narinfo, success
2023/03/19 21:59:53 path: /nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz
2023/03/19 21:59:53   try upstream[0]: https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store/nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz, success</pre></div>
<h3 id="结果分析">结果分析</h3>

<p>上文，可以看出，一共有三类路径，分别是：</p>

<ul>
<li><code>/nix-cache-info</code> 这个缓存服务的基础信息。</li>
<li><code>/${pkg_hash}.narinfo</code> 待下载的文件（nar、Nix 归档文件，参见：<a href="https://edolstra.github.io/pubs/phd-thesis.pdf">论文</a> Figure 5.2 ）的元信息。</li>
<li><code>/nar/${file_hash}.nar.xz</code> nar 文件的下载路径。</li>
</ul>

<p>执行 <code>curl http://127.0.0.1:8000/nix-cache-info</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">StoreDir: /nix/store
WantMassQuery: 1
Priority: 40</pre></div>
<ul>
<li><code>StoreDir</code> 该缓存服务的 nix store 存储路径。</li>
<li><code>WantMassQuery</code> 该缓存服务是否可以并发请求（说明来自 chatgpt）。</li>
<li><code>Priority</code> 该二进制缓存的优先级，客户端配置多个时，会按照该字段排序进行下载，数值越小优先级越高。</li>
</ul>

<p><code>curl http://127.0.0.1:8000/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.narinfo</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">StorePath: /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
URL: nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz
Compression: xz
FileHash: sha256:0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5
FileSize: 50160
NarHash: sha256:1bkbsk4wkk92syg4s7wafy5cxrsprlinax35zgp54y9r0f7a44jz
NarSize: 226504
References: 76l4v99sk83ylfwkz8wmwrm4s8h73rhd-glibc-2.35-224 v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1
Deriver: 25i5yk3xxr0g54rab62jfmi2hpmcapiw-hello-2.12.1.drv
Sig: cache.nixos.org-1:wNCGXAt+CyxXwRFKCama8lAYXI+nz0ON4AWKZ7wCL7ccoJ8UTf1FtQzFi5MXZ7DuebGr90POlbotF7NfcS+iCw==</pre></div>
<ul>
<li><code>StorePath</code> 该包的存储路径。</li>
<li><code>URL</code> 该包的下载路径。</li>
<li><code>Compression</code> 压缩格式。</li>
<li><code>FileHash</code> 文件 hash（<code>.nar.xz</code> 压缩文件）。</li>
<li><code>FileSize</code> 文件大小（<code>.nar.xz</code> 压缩文件）。</li>
<li><code>NarHash</code> nar 文件 hash（解压后）。</li>
<li><code>NarSize</code> nar 文件大小（解压后）。</li>
<li><code>References</code> 直接依赖的其他包。</li>
<li><code>Deriver</code> 产生该包的 <code>deriver</code>。</li>
<li><code>Sig</code> 签名。</li>
</ul>

<p><code>wget http://127.0.0.1:8000/nar/0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz &amp;&amp; ls -al 0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz &amp;&amp; rm -rf 0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">-rw-rw-r-- 1 rectcircle users 50160  3月 19 22:44 0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5.nar.xz</pre></div>
<p>可以看出，该文件的 size 和 <code>narinfo</code> 的 <code>FileSize</code> 相同。</p>

<h2 id="二进制缓存服务-nar-详解">二进制缓存服务 nar 详解</h2>

<p>二进制缓存服务就是根据设备上 <code>/nix/store</code> 以及 <code>/nix/var/nix</code> 相关元数据，生成 <code>.narinfo</code> 以及 <code>.nar.xz</code> 文件的下载服务。</p>

<h3 id="nar-文件生成">nar 文件生成</h3>

<p>nar 文件是一种 Nix 软件包存档文件格式，用于在不同的计算机系统之间传递和安装 Nix 软件包。NAR代表 <code>&quot;Nix Archive&quot;</code>，它是一种可扩展的归档格式，其中包含了 Nix 软件包的所有文件和元数据（来自 chatgpt）。</p>

<p>通过 <code>nix-store --dump /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1 &gt; test.nar</code> 命令，可以生成一个 nar 文件。</p>

<p>观察，该文件：</p>

<ul>
<li><code>ls -al test.nar</code>。可以看出，该文件的大小为： <code>226504</code>，对应上文的 <code>NarSize</code>。</li>
<li><code>nix-hash --type sha256 --flat --base32 test.nar</code>。可以看出，该文件的 hash 是 <code>1bkbsk4wkk92syg4s7wafy5cxrsprlinax35zgp54y9r0f7a44jz</code>，对应上文的 <code>NarHash</code>。</li>
</ul>

<p>通过 <code>xz test.nar</code> 命令，可以生成一个 <code>test.nar.xz</code> 文件。</p>

<p>观察，该文件：</p>

<ul>
<li><code>ls -al test.nar.xz</code>。可以看出，该文件的大小为： <code>50160</code>，对应上文的 <code>FileSize</code>。</li>
<li><code>nix-hash --type sha256 --flat --base32 test.nar.xz</code>。可以看出，该文件的 hash 是 <code>0qjw94x5c54sk397xhz4l134mk4cvyiakvdbczmal08rgd975sp5</code>，对应上文的 <code>FileHash</code>。</li>
</ul>

<p>注意，生成 hash，请使用 nix-hash 命令，而非 shasum + base32。</p>

<h3 id="narinfo-文件生成">narinfo 文件生成</h3>

<ul>
<li><code>StorePath</code> 该包的存储路径。</li>
<li><code>URL</code> 该包的下载路径，取决于路由格式，一般为 <code>nar/$FileHash.nar.xz</code>。</li>
<li><code>Compression</code> 压缩格式，一般为 <code>xz</code>。</li>
<li><code>FileHash</code> 参见上文。</li>
<li><code>FileSize</code> 参见上文。</li>
<li><code>NarHash</code> 除了上文方式外，还可以通过 <code>nix-store -q --hash /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code> 命令可以查看，该命令读取的是 <code>/nix/var/nix/db</code> 数据库中的，不是实时计算的。</li>
<li><code>NarSize</code> 除了上文方式外，还可以通过 <code>nix-store -q --size /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code> 命令可以查看，该命令读取的是 <code>/nix/var/nix/db</code> 数据库中的，不是实时计算的。</li>
<li><code>References</code> 通过 <code>nix-store -q --references /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code> 命令可以查看。</li>
<li><code>Deriver</code> 通过 <code>nix-store -q --deriver /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code> 命令可以查看。</li>

<li><p><code>Sig</code> 通过 <code>nix-store --generate-binary-cache-key binarycache.example.com cache-priv-key.pem cache-pub-key.pem</code> 生成一个秘钥对。签名算法，参见： <a href="https://github.com/edolstra/nix-serve/blob/master/nix-serve.psgi#L40">edolstra/nix-serve/nix-serve.psgi#L40</a>。使用 ChatGPT 将该签名算法转换为了 Go 的写法，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">import</span> (
    <span style="color:#e6db74">&#34;io/ioutil&#34;</span>
    <span style="color:#e6db74">&#34;crypto/sha256&#34;</span>
    <span style="color:#e6db74">&#34;encoding/hex&#34;</span>
    <span style="color:#e6db74">&#34;golang.org/x/crypto/openpgp&#34;</span>
    <span style="color:#e6db74">&#34;golang.org/x/crypto/openpgp/armor&#34;</span>
    <span style="color:#e6db74">&#34;golang.org/x/crypto/openpgp/packet&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">signNixStorePath</span>(<span style="color:#a6e22e">storePath</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">narHash</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">narSize</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">refs</span> []<span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">secretKeyFile</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
    <span style="color:#75715e">// 读取秘钥
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">secretKey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">secretKeyFile</span>)
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
    }
    <span style="color:#a6e22e">secretKey</span> = <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">secretKey</span>)

    <span style="color:#75715e">// 计算指纹
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">hash</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sha256</span>.<span style="color:#a6e22e">New</span>()
    <span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#a6e22e">storePath</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">narHash</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">narSize</span>))
    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">ref</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">refs</span> {
        <span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#a6e22e">ref</span>))
    }
    <span style="color:#a6e22e">fingerprint</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hex</span>.<span style="color:#a6e22e">EncodeToString</span>(<span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Sum</span>(<span style="color:#66d9ef">nil</span>))

    <span style="color:#75715e">// 对指纹进行数字签名
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">signer</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">openpgp</span>.<span style="color:#a6e22e">ReadArmoredKeyRing</span>(<span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">secretKey</span>))
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
    }
    <span style="color:#a6e22e">entity</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">signer</span>[<span style="color:#ae81ff">0</span>]
    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sigBuf</span> <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>
    <span style="color:#a6e22e">writer</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">armor</span>.<span style="color:#a6e22e">Encode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">sigBuf</span>, <span style="color:#e6db74">&#34;PGP SIGNATURE&#34;</span>, <span style="color:#66d9ef">nil</span>)
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
    }
    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">writer</span>.<span style="color:#a6e22e">Close</span>()
    <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">entity</span>.<span style="color:#a6e22e">PrivateKey</span>.<span style="color:#a6e22e">Sign</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">sigBuf</span>, <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">NewReader</span>([]byte(<span style="color:#a6e22e">fingerprint</span>)), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">packet</span>.<span style="color:#a6e22e">Config</span>{})
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
    }

    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sigBuf</span>.<span style="color:#a6e22e">String</span>(), <span style="color:#66d9ef">nil</span>
}</code></pre></div></li>
</ul>

<h2 id="搭建二进制缓存服务">搭建二进制缓存服务</h2>

<h3 id="安装">安装</h3>

<p>社区有几个 nix 二进制缓存服务，这里介绍两个分别是：</p>

<ul>
<li><a href="https://github.com/edolstra/nix-serve"><code>edolstra/nix-serve</code></a>， 官方 <a href="https://nixos.wiki/wiki/Binary_Cache">wiki</a> 和 <a href="https://nixos.org/manual/nix/stable/package-management/sharing-packages.html">手册</a> 介绍的就是这个。</li>
<li><a href="https://github.com/aristanetworks/nix-serve-ng"><code>aristanetworks/nix-serve-ng</code></a>，自称性能最好的 nix 二进制缓存服务。</li>
</ul>

<p>本文将安装 nix-serve-ng。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># nix-serve 和 nix-serve-ng 都有 bug，因此需要使用旧版 nix。</span>
<span style="color:#75715e"># https://github.com/aristanetworks/nix-serve-ng/issues/22</span>
<span style="color:#75715e"># https://github.com/NixOS/nix/issues/7704</span>
<span style="color:#75715e"># nix-env -iA nixpkgs.nix-serve-ng</span>
nix-env -E <span style="color:#e6db74">&#39;_: let pkgs = import &lt;nixpkgs&gt; {}; in pkgs.nix-serve-ng.override { nix = pkgs.nixVersions.nix_2_12; }&#39;</span> -i --option substituters http://127.0.0.1:8000 </code></pre></div>
<h3 id="运行验证">运行验证</h3>

<p>运行</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 默认绑定 5000 端口</span>
nix-serve</code></pre></div>
<p>验证</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl http://127.0.0.1:5000/nix-cache-info
<span style="color:#75715e"># StoreDir: /nix/store</span>
<span style="color:#75715e"># WantMassQuery: 1</span>
<span style="color:#75715e"># Priority: 30</span>

curl http://127.0.0.1:5000/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.narinfo
<span style="color:#75715e"># StorePath: /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</span>
<span style="color:#75715e"># URL: nar/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-1bkbsk4wkk92syg4s7wafy5cxrsprlinax35zgp54y9r0f7a44jz.nar</span>
<span style="color:#75715e"># Compression: none</span>
<span style="color:#75715e"># NarHash: sha256:1bkbsk4wkk92syg4s7wafy5cxrsprlinax35zgp54y9r0f7a44jz</span>
<span style="color:#75715e"># NarSize: 226504</span>
<span style="color:#75715e"># References: 76l4v99sk83ylfwkz8wmwrm4s8h73rhd-glibc-2.35-224 v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</span>
<span style="color:#75715e"># Deriver: 25i5yk3xxr0g54rab62jfmi2hpmcapiw-hello-2.12.1.drv</span>
<span style="color:#75715e"># Sig: cache.nixos.org-1:wNCGXAt+CyxXwRFKCama8lAYXI+nz0ON4AWKZ7wCL7ccoJ8UTf1FtQzFi5MXZ7DuebGr90POlbotF7NfcS+iCw==</span></code></pre></div>
<p>可以看出，为了性能 <code>nix-serve-ng</code> 并未启用压缩模式，比较适合高速内网使用，如果在公网提供服务，该服务不能满足需求。</p>

<h2 id="nix-store-导入导出">/nix/store 导入导出</h2>

<p>上文的 <code>nix-serve-ng</code> 服务的是基于 nix-store 命令同款库实现的。</p>

<p>如果我们需要部署一个二进制缓存服务，这台机器应该只作为 HTTP Server，提供二进制缓存服务。不可能在这台机器进行构建。</p>

<p>最好的做法是，将 <code>/nix</code> 目录存储在 NAS 中，并有多台二进制缓存和构建节点，这些节点，都挂载 NAS 到 <code>/nix</code> 目录。构建节点负责调用 <code>nix-build</code> 构建不存在的包，二进制缓存节点负责对外提供 HTTP Server（本方案，未测试，重点关注 <code>/nix</code> 并发访问是否有问题），该方案本文将不多赘述。</p>

<p>另一种简单的做法，是单台节点提供二进制缓存服务，其他多台构建节点负责构建，并将构建产物同步到 二进制缓存服务节点。</p>

<p>注意，直接 scp 或 rsync <code>/nix/store</code> 是不行的，原因在于 nix-store 还有一些元数据存储在 sqlite 数据库中，位于 <code>/nix/var/nix</code> 目录。</p>

<p>因此，这就要求 nix 提供构建产物导入导出的机制。nix 提供了相关能力。</p>

<h3 id="拷贝-closure">拷贝 Closure</h3>

<blockquote>
<p><a href="https://nixos.org/manual/nix/stable/package-management/copy-closure.html">Copying Closures via SSH</a></p>
</blockquote>

<p>Closure 指包含了一个包自身及其所有依赖的文件，即一个 nar 文件列表。 nix 提供了一个命令可以将某个 <code>/nix/store/xxx</code> 目录及其依赖通过 scp 拷贝到另一台机器的能力。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-copy-closure --to alice@itchy.example.org /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1</code></pre></div>
<p>注意，如果依赖或包自身远端已经存在了，该方式将不会重复 copy。</p>

<p>除了以上方式外，nix 还提供了将 Closure 导出到文件命令，以及将 Closure 导入到 <code>/nix/store</code> 的命令。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在一台机器上导出</span>
nix-store --export <span style="color:#66d9ef">$(</span>nix-store -qR /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1<span style="color:#66d9ef">)</span> &gt; hello.closure
<span style="color:#75715e"># 在另一台机器上导入</span>
nix-store --import &lt; hello.closure</code></pre></div>
<p>注意，该方式可以灵活的通过各种网络协议传输文件，文件内容的裁剪需要通过缓存服务 API 自助实现。</p>

<h3 id="拷贝-nar">拷贝 nar</h3>

<p>除了上文拷贝 Closure 外，nix 还提供了导入导出 nar 的能力，上文已经说明，本部分仅做记录。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 在一台机器上导出</span>
nix-store --dump /nix/store/v02pl5dhayp8jnz8ahdvg5vi71s8xc6g-hello-2.12.1 &gt; v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.nar
<span style="color:#75715e"># 在另一台机器上导入</span>
nix-store --import v02pl5dhayp8jnz8ahdvg5vi71s8xc6g.nar</code></pre></div>
<h3 id="自定义-channel-缓存">自定义 Channel 缓存</h3>

<p>对于自定义 Channel 构建和缓存服务，nix 生态中已有类似的开源软件或商业产品，这里简单列一下：</p>

<ul>
<li><a href="https://github.com/NixOS/hydra"><code>NixOS/hydra</code></a> 一个基于 Nix 的持续集成平台，NixOS 和 nixpkgs 就是使用该开源软件构建的。</li>
<li><a href="https://www.cachix.org/"><code>cachix</code></a> 基于 nix 的付费的商业版缓存服务，对于开源项目有 5G 免费存储额度。</li>
</ul>

<p>在企业场景，基于以上技术，完全可以基于企业已有基础设施，较低成本的实现一套自定义 Channel 包的构建和缓存服务。</p>
]]></description></item><item><title>Nix 详解（三） nix 领域特定语言</title><link>https://www.rectcircle.cn/posts/nix-3-nix-dsl/</link><pubDate>Sun, 12 Mar 2023 22:52:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-3-nix-dsl/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1 | <a href="https://github.com/rectcircle/learn-nix-demo">示例代码库</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>为了更好的描述一个包，从源码到制品的过程，nix 设计了一套领域特定语言（DSL），来声明一个包。这个语言就叫做 nix 语言。</p>

<p>nix 是一种特定领域的、纯函数式的、惰性求值的、动态类型的编程语言。</p>

<p>该语言主要的应用场景为：</p>

<ul>
<li>定义一个 nix channel，之前文章多次提到的 nixpkgs 收录的超过 8 万个包，就是通过 nix 语言声明的。</li>
<li>在 <code>shell.nix</code> 中使用，正如之前文章所讲，其可以为一个项目定义一个可重现的隔离的开发环境。</li>
<li>在 NixOS 中，来定义操作系统环境，本系列不多赘述。</li>
</ul>

<h2 id="hello-world">Hello World</h2>

<p><code>nix-lang-demo/01-hello.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hello world&#34;</span>;
<span style="color:#66d9ef">in</span> msg</code></pre></div>
<p>运行代码，<code>nix-instantiate --eval nix-lang-demo/01-hello.nix</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&#34;hello world&#34;</pre></div>
<p>除了直接运行一个 <code>.nix</code> 代码文件外。通过实验性的 <code>nix repl</code> 命令，可以打开一个 nix 交互式 shell，来交互式的执行 nix 表达式。</p>

<p>关于 <code>let in</code> 参见下文：<a href="#局部变量">局部变量</a>。</p>

<h2 id="程序结构">程序结构</h2>

<p>和常规的命令式通用编程语言不同，nix 是一种声明式的表达式语言。</p>

<p>常规的 Go、Java、C 等编程语言，一个程序的入口是一个 main 函数。</p>

<p>在 nix 中，没有一个 main 函数。一个 nix 的程序就是 nix 提供的几种基本结构组合而成的表达式。</p>

<p>在执行一个正确的 nix 程序时，解释器最终会推导出一个且必须推导出一个值出来。这个值，必须是 nix 支持的几种数据类型之一，参见下文。</p>

<h2 id="数据类型">数据类型</h2>

<p>nix 的数据类型类似于 JSON，可以分为基本数据类型、列表和属性集。</p>

<h3 id="基本数据类型">基本数据类型</h3>

<ul>
<li><p>字符串，支持多种表达方式。</p>

<ul>
<li><code>&quot;string&quot;</code> 双引号包裹的字符串，对于特殊字符需使用 <code>\</code> 转移，如： <code>\&quot;</code>、<code>\$</code>、<code>\n</code>、<code>\r</code>、<code>\t</code>。该类字符串支持使用 <code>${}</code> 进行插值。和其他语言的 <code>&quot;&quot;</code> 相比，在 nix 中，该类型字符串支持多行的写法。</li>

<li><p><code>''string''</code> 两个单引号包裹的字符串，支持多行，该类字符串会自动删除每一行相同数目（这个数目为所有行中前导空格数最小的数目）的前导空格。比如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">  This is the first line.
</span><span style="color:#e6db74">  This is the second line.
</span><span style="color:#e6db74">    This is the third line.
</span><span style="color:#e6db74">&#39;&#39;</span></code></pre></div>
<p>等价于</p>

<p><code>&quot;This is the first line.\nThis is the second line.\n  This is the third line.\n&quot;</code></p>

<p>该类型字符串也支持 <code>${}</code>进行字符串插值。对于特殊字符需使用 <code>''</code> 转移，如：</p>

<ul>
<li><code>'''</code> 等价于 <code>&quot;''&quot;</code></li>
<li><code>''$</code> 等价于 <code>&quot;$&quot;</code></li>
<li><code>\n</code> 等价于 <code>&quot;\\n&quot;</code>，<code>''\n</code> 等价于 <code>&quot;\n&quot;</code></li>
<li><code>\r</code> 等价于 <code>&quot;\\r&quot;</code>，<code>''\r</code> 等价于 <code>&quot;\r&quot;</code></li>
<li><code>\t</code> 等价于 <code>&quot;\\t&quot;</code>，<code>''\t</code> 等价于 <code>&quot;\t&quot;</code></li>
</ul></li>

<li><p>双单引号字符串和双引号字符串相比，有更少的引用，且，在书写多行字符串时，代码格式化的缩进会自动去除，且，有更少的转移字符。因此，在写多行字符串时，建议使用双单引号格式。</p></li>

<li><p>最后，符合 <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a> 的 URL 可以不适用引号包裹，可以直接使用。</p></li>
</ul></li>

<li><p>数字，支持且不区分整型和浮点型，格式如 <code>123</code>、<code>123.43</code>、<code>.27e13</code></p></li>

<li><p>路径，如 <code>/bin/sh</code>、<code>./abc</code>、<code>abc/123</code>，包含一个斜杠的会被识别为路径类型。nix 会把这些路径都转换为绝对路径，注意 nix 中的相对路径都是相对于 <code>.nix</code> 源代码文件的。</p>

<p>nix 也支持 <code>~/abc</code> 这种写法。</p>

<p>nix 还支持一种特殊写法，如 <code>&lt;nixpkgs&gt;</code>，nix 在 <code>NIX_PATH</code> 环境变量中查找指定名字的路径，当 <code>NIX_PATH</code> 不存在时，会在 <code>~/.nix-defexpr/channels</code> 中查找。</p>

<p>路径可以作为字符串插值的符号，如 <code>&quot;${./foo.txt}&quot;</code>，针对这种情况，nix 会将路径对应文件或目录复制到 <code>&quot;/nix/store/&lt;hash&gt;-foo.txt&quot;</code> 中。（Nix 语言假定在计算 Nix 表达式时所有输入文件都将保持不变。例如，假设您在 nix repl 会话期间使用了内插字符串中的文件路径。稍后在同一会话中，更改文件内容后，再次使用文件路径评估内插字符串可能不会返回新的存储路径，因为 Nix 可能不会重新读取文件内容）</p>

<p>除了 <code>&lt;&gt;</code> 语法外，路径也支持插值，注意，至少要有一个 <code>/</code> 出现在插值之前，才会被识别为路径。例如：<code>a.${foo}/b.${bar}</code> 会被识别为除法运算而不是路径，因此需要改为 <code>./a.${foo}/b.${bar}</code>。</p>

<p>注意，通过 <code>nix-instantiate --eval</code> 执行文件时，如果使用 <code>--strict</code> 启用严格模式，则需要保证所有的 PATH 都必须存在，且 nix 会将这些文件或目录复制到 <code>/nix/store</code> 中，路径变量的值将变为 <code>/nix/store/$hash-$name</code>。</p></li>

<li><p>bool，可选值为 true 或 false。</p></li>

<li><p>null，空值，表示 null。</p></li>
</ul>

<p>完整示例 (<code>nix-lang-demo/02-primitives-data-type.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/02-primitives-data-type.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>;
<span style="color:#66d9ef">in</span> {
  demo_01_str_double_quotes <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;foo bar </span><span style="color:#ae81ff">\r</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">\t</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">\\</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">\$</span><span style="color:#e6db74">{&#34;</span>;
  demo_02_str_with_string_interpolation <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;a: </span><span style="color:#e6db74">${</span>a<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>;
  demo_03_str_two_single_quotes <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    line1
</span><span style="color:#e6db74">    line2
</span><span style="color:#e6db74">    \r \n \t \
</span><span style="color:#e6db74">    </span><span style="color:#ae81ff">&#39;&#39;\r</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">&#39;&#39;\t</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">&#39;&#39;\n</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">&#39;&#39;&#39;</span><span style="color:#e6db74"> </span><span style="color:#ae81ff">&#39;&#39;$</span><span style="color:#e6db74">{
</span><span style="color:#e6db74">    a: </span><span style="color:#e6db74">${</span>a<span style="color:#e6db74">}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">    &#39;&#39;</span>;

  demo_04_str_url <span style="color:#f92672">=</span> <span style="color:#e6db74">https://rectcircle.cn</span>;
  demo_05_num_int <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  demo_06_num_float <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span><span style="color:#ae81ff">.1</span>;
  demo_07_num_e <span style="color:#f92672">=</span> <span style="color:#ae81ff">.27e13</span>;

  demo_08_path_abs_path <span style="color:#f92672">=</span> <span style="color:#e6db74">/bin/sh</span>;
  demo_09_path_rel_path1 <span style="color:#f92672">=</span> <span style="color:#e6db74">./demopath/a</span>;
  demo_10_path_rel_path2 <span style="color:#f92672">=</span> <span style="color:#e6db74">demopath/a</span>;
  demo_11_path_home_path <span style="color:#f92672">=</span> <span style="color:#e6db74">~/.bashrc</span>;

  demo_12_bool_true <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  demo_13_bool_false <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;

  demo_14_null <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/02-primitives-data-type.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_str_double_quotes&#34;</span>: <span style="color:#e6db74">&#34;foo bar \r \t \n \\ ${&#34;</span>,
  <span style="color:#f92672">&#34;demo_02_str_with_string_interpolation&#34;</span>: <span style="color:#e6db74">&#34;a: 1&#34;</span>,
  <span style="color:#f92672">&#34;demo_03_str_two_single_quotes&#34;</span>: <span style="color:#e6db74">&#34;line1\nline2\n\\r \\n \\t \\\n\r \t \n&#39;&#39; ${\na: 1\n&#34;</span>,
  <span style="color:#f92672">&#34;demo_04_str_url&#34;</span>: <span style="color:#e6db74">&#34;https://rectcircle.cn&#34;</span>,
  <span style="color:#f92672">&#34;demo_05_num_int&#34;</span>: <span style="color:#ae81ff">1</span>,
  <span style="color:#f92672">&#34;demo_06_num_float&#34;</span>: <span style="color:#ae81ff">1.1</span>,
  <span style="color:#f92672">&#34;demo_07_num_e&#34;</span>: <span style="color:#ae81ff">2700000000000</span>,
  <span style="color:#f92672">&#34;demo_08_path_abs_path&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/9wk86jmq024g8yb40wh4y5znkh1dix8y-sh&#34;</span>,
  <span style="color:#f92672">&#34;demo_09_path_rel_path1&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a&#34;</span>,
  <span style="color:#f92672">&#34;demo_10_path_rel_path2&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a&#34;</span>,
  <span style="color:#f92672">&#34;demo_11_path_home_path&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/x1znix2cdfg9fnmgvkdda19n28jphdm7-.bashrc&#34;</span>,
  <span style="color:#f92672">&#34;demo_12_bool_true&#34;</span>: <span style="color:#66d9ef">true</span>,
  <span style="color:#f92672">&#34;demo_13_bool_false&#34;</span>: <span style="color:#66d9ef">false</span>,
  <span style="color:#f92672">&#34;demo_14_null&#34;</span>: <span style="color:#66d9ef">null</span>
}</code></pre></div>
<h3 id="函数类型">函数类型</h3>

<p>nix 语言是函数式的，其函数也是一种数据类型，也就是说 nix 的函数可以作为返回值，也可以作为函数参数、可以赋值给变量。</p>

<p>因为函数可以在列表、属性集中使用，因此先介绍函数。</p>

<p>nix 函数的定义语法为: <code>函数参数: 函数体</code>，语义为：接收一个值作为一个参数，并返回值。函数调用方式为 <code>函数名 函数参数值</code>。例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span> 
  addOne <span style="color:#f92672">=</span> x: x<span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>;
<span style="color:#66d9ef">in</span> addOne <span style="color:#ae81ff">1</span> <span style="color:#75715e"># 返回 2</span></code></pre></div>
<p>可以说，上面这句话，这就是 nix 函数的全部。但是基于此 nix 提供了一些和 Python 差不多强大的函数能力。</p>

<ul>
<li>多参数函数。如：函数 f <code>x: y: x + y</code>，其实等价于 <code>x: (y: x + y)</code>，可以理解为，参数为 <code>x</code> 的函数返回了一个参数为 <code>y</code> 的函数，这个参数为 <code>y</code> 的函数返回 <code>x + y</code> 的值。调用方式为 <code>f 1 2</code>，其实等价于 <code>(f 1) 2</code>。</li>
<li>命名参数函数。示例如下：

<ul>
<li>简单场景，函数 f <code>{a, b}: a + b</code>，本质上是一种语法糖，节本等价于 <code>x: x.a + x.b</code>。调用方式为 <code>f {a = 1; b = 2; }</code>，但是需要注意的是这种方式 nix 会对参数进行属性是否存在校验。也就是说调用时缺少（<code>f {a = 1;}</code>）或者多余（<code>f {a = 1; b = 2; c= 3;}</code> ）属性均会报错。</li>
<li>属性默认值，函数 f <code>{a, b ? 0}: a + b</code>，<code>b ? 0</code>表示 b 的默认值为 0，调用时可以不传 b，如 <code>f {a = 1;}</code> 将返回 1。</li>
<li>其他属性和命名属性，函数 f <code>args@{ a, b, ... }: a + b + args.b + args.c</code> 或 <code>{ a, b, ... }@args: a + b + args.b + args.c</code>。<code>...</code> 该函数调用时，允许传递了除了 a, b 之外的属性。<code>@args</code> 表示将整个属性集赋值给变量 <code>args</code>，在函数体中可以使用 args 访问整个属性集。<code>...</code> 和 <code>@</code> 一般同时出现，但这不是强制的。如下方式调用：

<ul>
<li><code>f {a = 1; b = 2;}</code> 报错。</li>
<li><code>f {a = 1; b = 2; c = 3;}</code> 返回 8。</li>
<li><code>f {a = 1; b = 2; c = 3; d = 4;}</code> 返回 8。</li>
</ul></li>
</ul></li>
</ul>

<p>完整示例 (<code>nix-lang-demo/03-func-data-type.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  addOne <span style="color:#f92672">=</span> x: x<span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>;
  add <span style="color:#f92672">=</span> x: y: x <span style="color:#f92672">+</span> y;
  addTwo <span style="color:#f92672">=</span> add <span style="color:#ae81ff">2</span>;
  addAttrs <span style="color:#f92672">=</span> {x<span style="color:#f92672">,</span> y}: x <span style="color:#f92672">+</span> y;
  addAttrsYDefault2 <span style="color:#f92672">=</span> {x<span style="color:#f92672">,</span> y<span style="color:#f92672">?</span><span style="color:#ae81ff">2</span>}: x <span style="color:#f92672">+</span> y;
  addAttrsAtAndRemaining <span style="color:#f92672">=</span> attrs<span style="color:#f92672">@</span>{x<span style="color:#f92672">,</span> y<span style="color:#f92672">,</span> <span style="color:#f92672">...</span>}: x <span style="color:#f92672">+</span> attrs<span style="color:#f92672">.</span>y <span style="color:#f92672">+</span> attrs<span style="color:#f92672">.</span>z;
<span style="color:#66d9ef">in</span> {
  demo_01_add_one_2 <span style="color:#f92672">=</span> addOne <span style="color:#ae81ff">2</span>;
  demo_02_add_1_2 <span style="color:#f92672">=</span> add <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">2</span>;
  demo_03_add_two_1 <span style="color:#f92672">=</span> addTwo <span style="color:#ae81ff">1</span>;
  demo_04_add_attrs_x1_y2 <span style="color:#f92672">=</span> addAttrs { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; y <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>; };
  demo_05_add_attrs_y_default2_x1 <span style="color:#f92672">=</span> addAttrsYDefault2 { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; };
  demo_06_add_attrs_at_and_remaining_x_1_y_1_z_1_q_3 <span style="color:#f92672">=</span> addAttrsAtAndRemaining { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; y <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; z <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; q <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>; };
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_add_one_2&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_02_add_1_2&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_03_add_two_1&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_04_add_attrs_x1_y2&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_05_add_attrs_y_default2_x1&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_06_add_attrs_at_and_remaining_x_1_y_1_z_1_q_3&#34;</span>: <span style="color:#ae81ff">3</span>
}</code></pre></div>
<h3 id="列表">列表</h3>

<p>nix 通过方括号 <code>[]</code> 定义一个列表。和其他语言不同，列表中的元素通过空格而不是分割。</p>

<p>如： <code>[ 123 ./foo.nix &quot;abc&quot; (f { x = y; }) ]</code>，这个列表包含 4 个元素。第一个元素为数字、第二个元素为路径、第三个元素为字符串、第四个元素为调用函数 <code>f</code> 并获取结果（使用了小括号包裹）。</p>

<p>而对于 <code>[ 123 ./foo.nix &quot;abc&quot; f { x = y; } ]</code> 列表，包含 5 个元素。第四个元素为一个函数、第五个元素为属性集。</p>

<p>注意：数组的求值是惰性的，且是严格长度的。</p>

<p>完整示例 (<code>nix-lang-demo/04-list-data-type.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/04-list-data-type.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  addAttrs <span style="color:#f92672">=</span> { x<span style="color:#f92672">,</span> y }: x <span style="color:#f92672">+</span> y;
  demo_01_list_1 <span style="color:#f92672">=</span> [ <span style="color:#ae81ff">123</span> <span style="color:#e6db74">demopath/a</span> <span style="color:#e6db74">&#34;abc&#34;</span> (addAttrs { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; y <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>; }) ];
  demo_01_list_2 <span style="color:#f92672">=</span> [ <span style="color:#ae81ff">123</span> <span style="color:#e6db74">demopath/a</span> <span style="color:#e6db74">&#34;abc&#34;</span> addAttrs { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; y <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>; } ];
<span style="color:#66d9ef">in</span>
{
  demo_01_list_1 <span style="color:#f92672">=</span> demo_01_list_1;
  demo_01_list_2_len <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>length demo_01_list_2;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_list_1&#34;</span>: [
    <span style="color:#ae81ff">123</span>,
    <span style="color:#e6db74">&#34;/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a&#34;</span>,
    <span style="color:#e6db74">&#34;abc&#34;</span>,
    <span style="color:#ae81ff">3</span>
  ],
  <span style="color:#f92672">&#34;demo_01_list_2_len&#34;</span>: <span style="color:#ae81ff">5</span>
}</code></pre></div>
<h3 id="属性集">属性集</h3>

<p>nix 通过花括号 <code>{}</code> 定义一个属性集。属性集的每个元素（属性）为一个键值对，key 和 value 使用 <code>=</code> 分割，以 <code>;</code> 结尾。</p>

<p>如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{
  x <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
  text <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Hello&#34;</span>;
  y <span style="color:#f92672">=</span> f { bla <span style="color:#f92672">=</span> <span style="color:#ae81ff">456</span>; };
}</code></pre></div>
<p>该示例包含 3 个属性，分别是：值为数字的 x、值为字符串的 text、值为 f 函数返回值的 y。</p>

<p>属性集的属性通过点号 <code>.</code> 方式访问，如：<code>{ a = &quot;Foo&quot;; b = &quot;Bar&quot;; }.a</code>。</p>

<p>如果访问属性不存在时，取默认值，可以通过 <code>or</code> 实现，如：<code>{ a = &quot;Foo&quot;; b = &quot;Bar&quot;; }.c or &quot;Xyzzy&quot;</code>。</p>

<p>属性集的 Name 可以是任意字符串，如果是包含特殊字符可以使用 <code>.&quot;xxx&quot;</code> 的方式访问，如：<code>{ &quot;$!@#?&quot; = 123; }.&quot;$!@#?&quot;</code>。</p>

<p>属性的访问也支持插值，如：<code>let bar = &quot;foo&quot;; in { foo = 123; }.${bar}</code>，等价于 <code>{ foo = 123; }.foo</code>。</p>

<p>属性定义时其名字也支持插值，如：<code>let bar = &quot;foo&quot;; in { ${bar} = 123; }.foo</code>，等价于 <code>{ foo = 123; }.foo</code>。</p>

<p>属性定义是如果其名字插值的是一个 null，则不会将该属性添加到该属性即中（因为 null 无法转换为一个字符串），如：<code>{ ${null} = true; }</code> 等价于 <code>{}</code>。</p>

<p>属性集可以通过 <code>__functor</code> 属性名，将该属性集定义成一个函数，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span> add <span style="color:#f92672">=</span> { __functor <span style="color:#f92672">=</span> self: x: x <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>x; };
    inc <span style="color:#f92672">=</span> add <span style="color:#f92672">//</span> { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; };
<span style="color:#66d9ef">in</span> add <span style="color:#ae81ff">1</span></code></pre></div>
<ul>
<li>第一行，定义了一个 add 属性集，其 <code>__functor</code> 是属性是一个函数，该函数参数为 self 和 x，函数体为 <code>self.x + x</code></li>
<li>第二行，使用 <code>{ x = 1; }</code> 更新（<code>//</code> 语法） add 属性集，其返回，赋值给变量 inc（注意这里的更新并不会影响 add 值自身，因为 nix 的值都是不可变的）。</li>
<li>第三行，将 inc 作为函数调用，参数为 1。此时，实际上调用了 <code>__functor</code> 函数。</li>
<li>利用该特性可以实现类似面向对象的效果。</li>
</ul>

<p>默认情况下，定义一个属性集，属性之间是不能相互引用，如下将报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{
  y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
  x <span style="color:#f92672">=</span> y;
}</code></pre></div>
<p>通过，在花括号前添加 <code>rec</code>，表示声明一个递归属性集。此时，同一属性集内部的属性可以相互引用，如下不会报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">rec</span> {
  y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
  x <span style="color:#f92672">=</span> y;
}</code></pre></div>
<p>此外，递归属性集，属性的引用和顺序无关，如下不会报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">rec</span> {
  x <span style="color:#f92672">=</span> y;
  y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
}</code></pre></div>
<p>此外，在递归属性集中，如果引用的名字，在作用域内有同名的变量，且属性集内也有同名的属性，此时取属性集属性的值。如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span> y <span style="color:#f92672">=</span> <span style="color:#ae81ff">456</span>;
<span style="color:#66d9ef">in</span> <span style="color:#66d9ef">rec</span> {
  x <span style="color:#f92672">=</span> y;
  y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
}</code></pre></div>
<p>将返回： <code>{ x = 123; y = 123; }</code>。</p>

<p>完整示例 (<code>nix-lang-demo/05-attrs-data-type.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/05-attrs-data-type.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  bKey <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;b&#34;</span>;
  dKey <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;d&#34;</span>;
  demo_01_define <span style="color:#f92672">=</span> {
    a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
    b <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;b&#34;</span>;
    <span style="color:#e6db74">&#34;$!@#?&#34;</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
    <span style="color:#e6db74">${</span>dKey<span style="color:#e6db74">}</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;
    <span style="color:#e6db74">${</span><span style="color:#66d9ef">null</span><span style="color:#e6db74">}</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  };
  demo_02_access <span style="color:#f92672">=</span> {
    a <span style="color:#f92672">=</span> demo_01_define<span style="color:#f92672">.</span>a;
    b <span style="color:#f92672">=</span> demo_01_define<span style="color:#f92672">.</span><span style="color:#e6db74">${</span>bKey<span style="color:#e6db74">}</span>;
    c <span style="color:#f92672">=</span> demo_01_define<span style="color:#f92672">.</span>c or <span style="color:#e6db74">&#34;c not exist&#34;</span>;
    <span style="color:#e6db74">&#34;$!@#?&#34;</span> <span style="color:#f92672">=</span> demo_01_define<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;$!@#?&#34;</span>;
    d <span style="color:#f92672">=</span> demo_01_define<span style="color:#f92672">.</span>d;
  };

  callable_attr_define <span style="color:#f92672">=</span> { __functor <span style="color:#f92672">=</span> self: x: x <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>x; };
  demo_03_callable_attr_object <span style="color:#f92672">=</span> callable_attr_define <span style="color:#f92672">//</span> { x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; };

  demo_04_rec_attr1 <span style="color:#f92672">=</span> <span style="color:#66d9ef">rec</span> {
    y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
    x <span style="color:#f92672">=</span> y;
  };
  y <span style="color:#f92672">=</span> <span style="color:#ae81ff">456</span>;
  demo_05_rec_attr2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">rec</span> {
    x <span style="color:#f92672">=</span> y;
    y <span style="color:#f92672">=</span> <span style="color:#ae81ff">123</span>;
  };
<span style="color:#66d9ef">in</span>
{
  demo_01_define <span style="color:#f92672">=</span> demo_01_define;
  demo_02_access <span style="color:#f92672">=</span> demo_02_access;
  demo_03_call_attr <span style="color:#f92672">=</span> demo_03_callable_attr_object <span style="color:#ae81ff">2</span>;
  demo_04_rec_attr1 <span style="color:#f92672">=</span> demo_04_rec_attr1;
  demo_05_rec_attr2 <span style="color:#f92672">=</span> demo_05_rec_attr2;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/05-attrs-data-type.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_define&#34;</span>: {
    <span style="color:#f92672">&#34;$!@#?&#34;</span>: <span style="color:#ae81ff">123</span>,
    <span style="color:#f92672">&#34;a&#34;</span>: <span style="color:#ae81ff">1</span>,
    <span style="color:#f92672">&#34;b&#34;</span>: <span style="color:#e6db74">&#34;b&#34;</span>,
    <span style="color:#f92672">&#34;d&#34;</span>: <span style="color:#ae81ff">4</span>
  },
  <span style="color:#f92672">&#34;demo_02_access&#34;</span>: {
    <span style="color:#f92672">&#34;$!@#?&#34;</span>: <span style="color:#ae81ff">123</span>,
    <span style="color:#f92672">&#34;a&#34;</span>: <span style="color:#ae81ff">1</span>,
    <span style="color:#f92672">&#34;b&#34;</span>: <span style="color:#e6db74">&#34;b&#34;</span>,
    <span style="color:#f92672">&#34;c&#34;</span>: <span style="color:#e6db74">&#34;c not exist&#34;</span>,
    <span style="color:#f92672">&#34;d&#34;</span>: <span style="color:#ae81ff">4</span>
  },
  <span style="color:#f92672">&#34;demo_03_call_attr&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;demo_04_rec_attr1&#34;</span>: {
    <span style="color:#f92672">&#34;x&#34;</span>: <span style="color:#ae81ff">123</span>,
    <span style="color:#f92672">&#34;y&#34;</span>: <span style="color:#ae81ff">123</span>
  },
  <span style="color:#f92672">&#34;demo_05_rec_attr2&#34;</span>: {
    <span style="color:#f92672">&#34;x&#34;</span>: <span style="color:#ae81ff">123</span>,
    <span style="color:#f92672">&#34;y&#34;</span>: <span style="color:#ae81ff">123</span>
  }
}</code></pre></div>
<h2 id="变量">变量</h2>

<h3 id="局部变量">局部变量</h3>

<p>nix 通过 <code>let in</code> 来创建一个作用域，并定义一批变量，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">let
  a = 1;
  b = 2;
in
  a + b</pre></div>
<p>如上写法等价于： <code>1 + 2</code>。</p>

<h3 id="属性继承">属性继承</h3>

<p>当我们想构造一个属性集，并想将作用域中的某些属性作为该属性集的属性时，一般的写法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
<span style="color:#66d9ef">in</span> {
  a <span style="color:#f92672">=</span> a;
  b <span style="color:#f92672">=</span> b;
  c <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
}</code></pre></div>
<p>nix 提供继承语法糖，可以将上述简化为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
<span style="color:#66d9ef">in</span> {
  <span style="color:#66d9ef">inherit</span> a b;
  c <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
}</code></pre></div>
<p>inherit 还可以从一个属性集中继承其中的几个属性，示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  x <span style="color:#f92672">=</span> {
    b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
    c <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
  };
<span style="color:#66d9ef">in</span> {
  <span style="color:#66d9ef">inherit</span> a;
  <span style="color:#66d9ef">inherit</span> (x) b c;
}</code></pre></div>
<p>等价于：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  x <span style="color:#f92672">=</span> {
    b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
    c <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
  };
<span style="color:#66d9ef">in</span> {
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> x<span style="color:#f92672">.</span>b;
  c <span style="color:#f92672">=</span> x<span style="color:#f92672">.</span>c;
}</code></pre></div>
<h3 id="with-表达式">with 表达式</h3>

<p>类似于 python 的 with。通过 with 可以创建一个作用域，并将一个属性集中属性作为作用域中的变量。</p>

<p>示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">with</span> {
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
}; a <span style="color:#f92672">+</span> b</code></pre></div>
<p>等价于：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
<span style="color:#66d9ef">in</span> a <span style="color:#f92672">+</span> b</code></pre></div>
<p>等价于：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span> 
x <span style="color:#f92672">=</span> {
    a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
    b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>; 
  };
<span style="color:#66d9ef">in</span> <span style="color:#66d9ef">with</span> x;
a <span style="color:#f92672">+</span> b</code></pre></div>
<h3 id="示例">示例</h3>

<p><code>nix-lang-demo/06-var.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/06-var.nix --strict --json | jq</span>

<span style="color:#66d9ef">let</span> 
  a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
  attrs1 <span style="color:#f92672">=</span> {
    x <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
    y <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;
  };
  attrs2 <span style="color:#f92672">=</span> {
    m <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>;
    n <span style="color:#f92672">=</span> <span style="color:#ae81ff">6</span>;
  };
<span style="color:#66d9ef">in</span> <span style="color:#66d9ef">with</span> attrs2;
{
  <span style="color:#66d9ef">inherit</span> a b;
  <span style="color:#66d9ef">inherit</span> (attrs1) x y;
  m <span style="color:#f92672">=</span> m;
  <span style="color:#66d9ef">inherit</span> n;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/06-var.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;a&#34;</span>: <span style="color:#ae81ff">1</span>,
  <span style="color:#f92672">&#34;b&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#f92672">&#34;m&#34;</span>: <span style="color:#ae81ff">5</span>,
  <span style="color:#f92672">&#34;n&#34;</span>: <span style="color:#ae81ff">6</span>,
  <span style="color:#f92672">&#34;x&#34;</span>: <span style="color:#ae81ff">3</span>,
  <span style="color:#f92672">&#34;y&#34;</span>: <span style="color:#ae81ff">4</span>
}</code></pre></div>
<h2 id="流程控制">流程控制</h2>

<h3 id="条件表达式">条件表达式</h3>

<p>语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">if e1 then e2 else e3</pre></div>
<p>例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">let x = 1;
in if x &gt; 0 then &#34;x &gt; 0&#34; else &#34;x &lt;= 0&#34;</pre></div>
<p>返回，  &ldquo;x &gt; 0&rdquo;。</p>

<h3 id="循环">循环</h3>

<p>nix 是个无副作用的函数式的表达式语言。因此，nix 没有命令式编程语言的 while 或者 for 循环。</p>

<p>一般情况，需要循环场景，就是对列表或者属性集进行转换。nix 可以通过内置高阶函数，如 <code>builtins.filter</code>、 <code>builtins.map</code>，来达到类似的效果。</p>

<h3 id="示例-1">示例</h3>

<p><code>nix-lang-demo/07-flow-control.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/07-flow-control.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  l <span style="color:#f92672">=</span> [<span style="color:#ae81ff">1</span> <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">5</span> <span style="color:#ae81ff">6</span>];
  filter <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>filter;
  map <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>map;
<span style="color:#66d9ef">in</span> {
  demo_01_x_great_than_0 <span style="color:#f92672">=</span> <span style="color:#66d9ef">if</span> x <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">then</span> <span style="color:#e6db74">&#34;x &gt; 0&#34;</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;x &lt;= 0&#34;</span>;
  demo_02_l_filter_map <span style="color:#f92672">=</span> map (e: e <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span>) (filter (e: e<span style="color:#f92672">&lt;=</span><span style="color:#ae81ff">3</span>) l);
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/07-flow-control.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_x_great_than_0&#34;</span>: <span style="color:#e6db74">&#34;x &gt; 0&#34;</span>,
  <span style="color:#f92672">&#34;demo_02_l_filter_map&#34;</span>: [
    <span style="color:#ae81ff">2</span>,
    <span style="color:#ae81ff">4</span>,
    <span style="color:#ae81ff">6</span>
  ]
}</code></pre></div>
<h2 id="错误处理">错误处理</h2>

<h3 id="断言">断言</h3>

<p>通过 assert 可以检查某些条件是否成立，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">assert</span> e1; e2</code></pre></div>
<p>其中 e1 是一个可以计算为 bool 类型值的表达式。如果 e1 为 true，则返回 e2 的值，如果 e1 为 false，则停止计算，并打印调用栈信息。如：</p>

<ul>
<li><code>assert true; 1</code> 将返回 1。</li>
<li><code>assert false; 1</code> 将报错。</li>
</ul>

<h3 id="抛出错误">抛出错误</h3>

<p>nix 的错误抛出，由内置函数提供，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span><span style="color:#a6e22e">throw</span> s</code></pre></div>
<p>抛出错误，如果上层没有处理，解释器会打印消息 <code>s</code>，并停止运行（评估）。</p>

<h3 id="错误终止">错误终止</h3>

<p>nix 的错误终止，由内置函数提供，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>abort s</code></pre></div>
<p>上层无法捕捉该异常，解释器会打印消息 <code>s</code>，并停止运行（评估）。</p>

<h3 id="错误捕捉">错误捕捉</h3>

<p>nix 的错误捕捉，由内置函数提供，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>tryEval e</code></pre></div>
<ul>
<li>只能捕捉 <code>assert</code> 和 <code>builtins.throw</code> 产生的错误。</li>
<li>返回一个属性集，包含两个手机用：

<ul>
<li><code>success</code> bool 类型，是否成功，如果捕捉到错误，则该属性为 <code>false</code>。</li>
<li><code>value</code> 任意类型。

<ul>
<li>如果 <code>success = false</code>，则该参数为 <code>false</code>，注意，不是错误消息（参见：<a href="https://github.com/NixOS/nix/issues/356">issue</a>）。</li>
<li>否者该参数 e 的值。</li>
</ul></li>
</ul></li>
</ul>

<h3 id="示例-2">示例</h3>

<p><code>nix-lang-demo/08-err-handle.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/08-err-handle.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  divide <span style="color:#f92672">=</span> a: b: <span style="color:#66d9ef">assert</span> b <span style="color:#f92672">!=</span><span style="color:#ae81ff">0</span> ; a<span style="color:#f92672"> / </span>b;
  throw_err <span style="color:#f92672">=</span> msg: builtins<span style="color:#f92672">.</span><span style="color:#a6e22e">throw</span> msg;
  abort_err <span style="color:#f92672">=</span> msg: builtins<span style="color:#f92672">.</span>abort msg;
<span style="color:#66d9ef">in</span> {
  demo_01_4_div_2 <span style="color:#f92672">=</span> divide <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">2</span>;
  demo_02_try_4_div_0 <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>tryEval (divide <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">0</span>);
  demo_03_try_4_div_2 <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>tryEval (divide <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">2</span>);
  demo_04_try_throw_err <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>tryEval (throw_err <span style="color:#e6db74">&#34;err&#34;</span>);
  <span style="color:#75715e"># demo_05_try_abort_err = builtins.tryEval (abort_err &#34;err&#34;); # abort 无法捕捉</span>
  <span style="color:#75715e"># demo_06_try_builtins_4_div_0 = builtins.tryEval (4 / 0); # 除 0 无法捕捉</span>
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/08-err-handle.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_4_div_2&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#f92672">&#34;demo_02_try_4_div_0&#34;</span>: {
    <span style="color:#f92672">&#34;success&#34;</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#66d9ef">false</span>
  },
  <span style="color:#f92672">&#34;demo_03_try_4_div_2&#34;</span>: {
    <span style="color:#f92672">&#34;success&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#ae81ff">2</span>
  },
  <span style="color:#f92672">&#34;demo_04_try_throw_err&#34;</span>: {
    <span style="color:#f92672">&#34;success&#34;</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#66d9ef">false</span>
  }
}</code></pre></div>
<h2 id="操作">操作</h2>

<blockquote>
<p>参见：<a href="https://nixos.org/manual/nix/stable/language/operators.html#operators">Nix 手册 Operators</a></p>
</blockquote>

<p>Nix 操作符和 C 语言的类似，区别是：</p>

<ul>
<li>nix 不支持 <code>:?</code>，类似效果的是 <code>if then else</code>。</li>
<li>nix 不支持 <code>++</code>，<code>--</code>、<code>+=</code>、<code>-=</code> 等类似的涉及修改变量值的操作符。</li>
<li>nix 支持的一些 C 语言没有的操作符：

<ul>
<li><code>attrset ? attrpath</code>，返回 bool 值， 判断属性集中是否存在某个属性。attrpath 支持 <code>a.b.c</code> 格式。</li>
<li><code>list ++ list</code>，返回一个 list，两个 list 连接产生一个新的 list。</li>
<li><code>string + string</code>，返回一个 string，字符串拼接。</li>
<li><code>path + path</code>，返回一个 path，路径拼接（注意最终都会转换为绝对路径进行拼接，而不是路径 join）。</li>
<li><code>path + string</code>，返回一个 path，路径拼接（两者先转换为字符串，然后直接拼接到一起，然后转换为一个路径）。</li>
<li><code>string + path</code>，返回一个 string，路径拼接（path 路径必须存在，nix 会将该路径复制到 /nix/store 中，并将 string 和 <code>/nix/store/$hash-文件名</code> 拼接，并转换为字符串），比如 <code>&quot;/abc&quot; + ./README.md</code>，返回 <code>&quot;/abc/nix/store/qmj08qmd1bb89g6wami4v2fq5ma4f42c-README.md&quot;</code>。</li>
<li><code>attrset // attrset</code> 使用后一个属性集更新到前一个属性集中（存在则覆盖），返回这个更新后的属性集。</li>
<li><code>bool -&gt; bool</code> 一种特殊的逻辑运算符，等价于 <code>!b1 || b2</code>，参见：<a href="https://en.wikipedia.org/wiki/Truth_table#Logical_implication">wiki</a>。</li>
</ul></li>
</ul>

<p>完整示例 (<code>nix-lang-demo/09-operators.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/09-operators.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  attrs1 <span style="color:#f92672">=</span> {
    x <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
  };
  list1 <span style="color:#f92672">=</span> [<span style="color:#ae81ff">1</span> <span style="color:#ae81ff">2</span>];
  list2 <span style="color:#f92672">=</span> [<span style="color:#ae81ff">3</span> <span style="color:#ae81ff">4</span>];
<span style="color:#66d9ef">in</span>
{
  demo_01_attrs1_has_x <span style="color:#f92672">=</span> attrs1 <span style="color:#f92672">?</span> x;
  demo_02_attrs1_has_y <span style="color:#f92672">=</span> attrs1 <span style="color:#f92672">?</span> y;
  demo_03_attrs1_has_a_dot_b <span style="color:#f92672">=</span> attrs1 <span style="color:#f92672">?</span> a<span style="color:#f92672">.</span>b;

  demo_04_list1_concat_list2 <span style="color:#f92672">=</span> list1 <span style="color:#f92672">++</span> list2;

  demo_05_str1_concat_str2 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;abc&#34;</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;123&#34;</span>;
  <span style="color:#75715e"># demo_06_path1_concat_path2 = demopath/a + demopath/b; # 严格模式将报错，因为返回的路径不存在。</span>
  demo_07_path1_concat_str2 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;demopath/a&#34;</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">demopath/b</span>; 
  <span style="color:#75715e"># demo_08_str1_concat_path2 = demopath/a + &#34;demopath/b&#34;; # 严格模式将报错，因为返回的路径不存在。</span>

  demo_08_attrs <span style="color:#f92672">=</span> attrs1;
  demo_09_attrs1_merge_attrs2 <span style="color:#f92672">=</span> attrs1 <span style="color:#f92672">//</span> {y <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;};

  demo_10_implication <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span> <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">true</span>;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/09-operators.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_attrs1_has_x&#34;</span>: <span style="color:#66d9ef">true</span>,
  <span style="color:#f92672">&#34;demo_02_attrs1_has_y&#34;</span>: <span style="color:#66d9ef">false</span>,
  <span style="color:#f92672">&#34;demo_03_attrs1_has_a_dot_b&#34;</span>: <span style="color:#66d9ef">false</span>,
  <span style="color:#f92672">&#34;demo_04_list1_concat_list2&#34;</span>: [
    <span style="color:#ae81ff">1</span>,
    <span style="color:#ae81ff">2</span>,
    <span style="color:#ae81ff">3</span>,
    <span style="color:#ae81ff">4</span>
  ],
  <span style="color:#f92672">&#34;demo_05_str1_concat_str2&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
  <span style="color:#f92672">&#34;demo_07_path1_concat_str2&#34;</span>: <span style="color:#e6db74">&#34;demopath/a/nix/store/nqxj2if4v96ksj1mgsblgc375wcslf83-b&#34;</span>,
  <span style="color:#f92672">&#34;demo_08_attrs&#34;</span>: {
    <span style="color:#f92672">&#34;x&#34;</span>: <span style="color:#ae81ff">1</span>
  },
  <span style="color:#f92672">&#34;demo_09_attrs1_merge_attrs2&#34;</span>: {
    <span style="color:#f92672">&#34;x&#34;</span>: <span style="color:#ae81ff">1</span>,
    <span style="color:#f92672">&#34;y&#34;</span>: <span style="color:#ae81ff">2</span>
  },
  <span style="color:#f92672">&#34;demo_10_implication&#34;</span>: <span style="color:#66d9ef">true</span>
}</code></pre></div>
<h2 id="内置常量和内置函数">内置常量和内置函数</h2>

<ul>
<li>内置常量：

<ul>
<li><code>builtins</code>，包含内置函数的属性集。</li>
<li><code>builtins.currentSystem</code>，如 <code>&quot;i686-linux&quot;</code> or <code>&quot;x86_64-darwin&quot;</code>。</li>
</ul></li>
<li>已经添加到顶层作用域，无需通过 <code>builtins</code> 引用的内置函数：

<ul>
<li><code>abort</code>，参见上文错误处理。</li>
<li><code>baseNameOf s</code> 类似于 gnu 的 basename，去除路径的路径，返回文件名。</li>
<li><code>break</code> In debug mode (enabled using &ndash;debugger), pause Nix expression evaluation and enter the REPL. Otherwise, return the argument v.</li>
<li><code>derivation</code> nix 编译系统核心函数，参见下文：<a href="#推导-derivation">推导</a>。</li>
<li><code>derivationStrict</code> 没找到相关手册，只有一个相关 <a href="https://github.com/NixOS/nix/issues/7569">issue</a>。</li>
<li><code>dirOf s</code> 类似于 gnu 的 dirname，返回路径所在目录。</li>
<li><code>fetchGit</code>、<code>fetchMercurial</code>、<code>fetchTarball</code>、<code>fetchTree</code>，参见下文：<a href="#fetch-相关函数">fetch 相关函数</a>。</li>
<li><code>fromTOML</code> 未找到相关文档。</li>
<li><code>import</code> 参见下文：<a href="#模块系统">模块系统</a>。</li>
<li><code>isNull e</code> 判断是否是 null（此功能已弃用；使用 <code>e == null</code> 代替）。</li>
<li><code>map f list</code> 转换一个列表，函数式编程的 map 原语。</li>
<li><code>placeholder</code> 不太理解，参见：<a href="https://nixos.org/manual/nix/stable/language/builtins.html#builtins-placeholder">原文</a>。</li>
<li><code>removeAttrs set list</code> 从 set 中删除指定的属性。</li>
<li><code>scopedImport</code> 未找到相关文档。</li>
<li><code>throw</code> 参见上文错误处理。</li>
<li><code>toString</code> 将值转换为字符串，一个属性集可以通过特殊属性 <code>__toString = self: ...;</code> 自定义 toString 格式。</li>
</ul></li>
<li>其他内置函数，参见：<a href="https://nixos.org/manual/nix/stable/language/builtins.html">Nix 手册 - 内置函数</a>。</li>
</ul>

<h2 id="fetch-相关函数">fetch 相关函数</h2>

<p>nix 提供了一些从网络上下载文件的内置函数，执行这些函数，nix 会将这些文件下载下来，并存储到 <code>/nix/store</code> 中，并返回存储的路径。</p>

<ul>
<li><p><code>builtins.fetchurl</code> 下载 url。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span> fetchurl <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>fetchurl;
<span style="color:#66d9ef">in</span> fetchurl {
url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz&#34;</span>;
sha256 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465&#34;</span>;
}</code></pre></div></li>

<li><p><code>builtins.fetchGit args</code> 从 git 中下载文件。</p>

<ul>
<li><p>args 是一个属性集。</p>

<ul>
<li><code>url</code> 仓库地址。</li>
<li><code>name</code> 存储到 <code>/nix/store</code> 的名称，默认为 URL 的 basename。</li>
<li><code>rev</code> 要获取的 git 修订版。默认为 ref 指向的。</li>
<li><code>ref</code> 分支名或者标签名，如 <code>master</code>、<code>&quot;refs/heads/0.5-release&quot;</code>，默认为 <code>HEAD</code>。</li>
<li><code>submodules</code> 是否 checkout 子模块，默认为 false。</li>
<li><code>shallow</code> 是否浅克隆，默认为 false。</li>
<li><code>allRefs</code> 是否获取仓库的所有引用，默认为 false，即只获取 <code>ref</code> 参数配置的。</li>
</ul></li>

<li><p>示例：通过 ssh 从私有仓库获取。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;git@github.com:my-secret/repository.git&#34;</span>;
  ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
  rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;adab8b916a45068c044658c4158d81878f9ed1c3&#34;</span>;
}</code></pre></div></li>

<li><p>示例：配置引用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/NixOS/nix.git&#34;</span>;
  ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;refs/heads/0.5-release&#34;</span>;
}</code></pre></div></li>

<li><p>示例：下载指定分支的指定 commit（推荐配置 rev 来指定 commit，这样是可重现的，否则随着分支的提交，未来某个时刻获取到的和当前可能不一致）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/nixos/nix.git&#34;</span>;
  rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;841fcbd04755c7a2865c51c1e2d3b045976b7452&#34;</span>;
  ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1.11-maintenance&#34;</span>;
}</code></pre></div></li>

<li><p>示例：如果要查找的 commit 位于 git 存储库的默认分支中，您可以省略 ref 属性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/nixos/nix.git&#34;</span>;
  rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;841fcbd04755c7a2865c51c1e2d3b045976b7452&#34;</span>;
}</code></pre></div></li>

<li><p>示例：指定某个具体 tag。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/nixos/nix.git&#34;</span>;
  ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;refs/tags/1.9&#34;</span>;
}</code></pre></div></li>

<li><p>示例：获取最新版本。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">builtins<span style="color:#f92672">.</span>fetchGit {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh://git@github.com/nixos/nix.git&#34;</span>;
  ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
}</code></pre></div></li>
</ul></li>

<li><p><code>builtins.fetchTarball args</code>  从 url 中下载一个 tar 包（压缩格式必须是 gzip, bzip2 or xz 之一的）（缓存在 <code>~/.cache/nix/tarballs/</code> 路径），并解包到一个目录中。注意，tar 的顶层目录会被删除。然后将目录存储到 <code>/nix/store</code>，并返回该路径，该函数一般和 import 函数（参见下文）一起使用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">with</span> <span style="color:#f92672">import</span> (fetchTarball {
  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/NixOS/nixpkgs/archive/nixos-14.12.tar.gz&#34;</span>;
  sha256 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1jppksrfvbk5ypiqdz4cddxdl8z6zyzdb2srq8fcffr327ld5jj2&#34;</span>;
}) {};</code></pre></div></li>
</ul>

<p>完整示例 (<code>nix-lang-demo/10-builtins-fetch.nix</code>)。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/10-builtins-fetch.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  fetchurl <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>fetchurl;
  fetchGit <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>fetchGit;
  fetchTarball <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>fetchTarball;
<span style="color:#66d9ef">in</span>
{
  demo_01_fetchurl <span style="color:#f92672">=</span> fetchurl {
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz&#34;</span>;
    sha256 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465&#34;</span>;
  };
  demo_02_fetchGit <span style="color:#f92672">=</span> fetchGit {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;learn-nix-demo-source&#34;</span>;
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/rectcircle/learn-nix-demo.git&#34;</span>;
    rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326&#34;</span>;
    ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
    shallow <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  };
  demo_03_fetchTarball <span style="color:#f92672">=</span> fetchTarball {
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre462063.2ce9b9842b5/nixexprs.tar.xz&#34;</span>;
  };
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/10-builtins-fetch.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_fetchurl&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/9bw6xyn3dnrlxp5vvis6qpmdyj4dq4xy-hello-2.1.1.tar.gz&#34;</span>,
  <span style="color:#f92672">&#34;demo_02_fetchGit&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source&#34;</span>,
  <span style="color:#f92672">&#34;demo_03_fetchTarball&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/10njnx13qh4x3z7j7q0jh7m64s0s95w1-source&#34;</span>
}</code></pre></div>
<h2 id="模块系统">模块系统</h2>

<p>nix 通过 <code>import path</code>， 执行其他文件的代码，并返回执行的结果。在 nix 中 import 是一个内置函数。这里的 path 可以是一个 <code>.nix</code> 文件，也可以是一个目录，如果是一个目录或压缩包的话，将执行该目录中的 <code>default.nix</code> 文件。示例如下：</p>

<p>通过 <code>import</code> 函数可以将 nix 代码拆分到文件和目录，以实现模块划分和代码复用。</p>

<p>前文介绍的 nixpkgs channel 本质上就是这样一个模块。下文有一些导入 nixpkgs 的一些惯用用法。</p>

<ul>
<li><p>示例 1：通过 github 提供的 archive 链接，导入一个历史上某个版本的 nixpkgs。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
    pkgs <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> (builtins<span style="color:#f92672">.</span>fetchTarball {
        url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/NixOS/nixpkgs/archive/d1c3fea7ecbed758168787fe4e4a3157e52bc808.tar.gz&#34;</span>;
    }) {};
<span style="color:#66d9ef">in</span></code></pre></div></li>

<li><p>示例 2：通过 git 命令，导入一个历史上某个版本的 nixpkgs。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#66d9ef">let</span>
    pkgs <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> (builtins<span style="color:#f92672">.</span>fetchGit {
        <span style="color:#75715e"># Descriptive name to make the store path easier to identify</span>
        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-old-revision&#34;</span>;
        url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/NixOS/nixpkgs/&#34;</span>;
        ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;refs/heads/nixpkgs-unstable&#34;</span>;
        rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;d1c3fea7ecbed758168787fe4e4a3157e52bc808&#34;</span>;
    }) {};
<span style="color:#66d9ef">in</span></code></pre></div></li>
</ul>

<p>完整示例</p>

<p><code>nix-lang-demo/demopath/default.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{
  c <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;demopath/default.nix var c&#34;</span>;
}</code></pre></div>
<p><code>nix-lang-demo/11-import.nix</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -iA nixpkgs.jq # 为了更好的展示结果，使用 jq 进行结果格式化展示。</span>
<span style="color:#75715e"># nix-instantiate --eval nix-lang-demo/11-import.nix --strict --json | jq</span>
<span style="color:#66d9ef">let</span>
  import_file <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">./01-hello.nix</span>;
  import_dir <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">./demopath</span>;
<span style="color:#66d9ef">in</span>
{
  demo_01_import_file <span style="color:#f92672">=</span> import_file;
  demo_02_import_dir <span style="color:#f92672">=</span> import_dir;
}</code></pre></div>
<p>执行代码 <code>nix-env -iA nixpkgs.jq &amp;&amp; nix-instantiate --eval nix-lang-demo/11-import.nix --strict --json | jq</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;demo_01_import_file&#34;</span>: <span style="color:#e6db74">&#34;hello world&#34;</span>,
  <span style="color:#f92672">&#34;demo_02_import_dir&#34;</span>: {
    <span style="color:#f92672">&#34;c&#34;</span>: <span style="color:#e6db74">&#34;demopath/default.nix var c&#34;</span>
  }
}</code></pre></div>
<h2 id="推导-derivation">推导 (derivation)</h2>

<blockquote>
<p>参考: <a href="https://nixos.org/guides/nix-pills/our-first-derivation.html">nix-pills/our-first-derivation</a> | <a href="https://nixos.org/guides/nix-pills/working-derivation.html">nix-pills/working-derivation.html</a></p>
</blockquote>

<h3 id="概述-1">概述</h3>

<p>前文，我们一直将 nix 定位为一个包管理工具。但从本质上来说，nix 的核心是一个包构建系统。</p>

<p>因此，nix 语言需要提供一套机制，可以让用户定义，软件包从源码到二进制产物的过程。</p>

<p>而推导（derivation）就是这样一个最重要的的一个内置函数。是 nix 作为一个构建系统的核心。</p>

<h3 id="参数说明">参数说明</h3>

<p>在 nix 中， derivation 内置函数，定义一个软件包重源码到二进制产物的过程，该函数传递一个属性集作为参数，包含如下属性：</p>

<ul>
<li><code>system</code> 必填，字符串，定义该构建过程要求的 CPU 架构（x86_64、arm）和操作系统名（linux、darwin）。可通过 <code>nix -vv --version</code> 命令获取（或者通过 <code>builtins.currentSystem</code> 变量获取，如果是支持所有平台，则可以直接使用这个参数），如果系统不匹配将失败（通过配置，nix 支持远端构建，参见： <a href="https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html">forward builds for other platforms</a>）。该字段会作为环境变量传递给 <code>builder</code> 进程。</li>
<li><code>name</code> 必填，字符串。被 nix-env 用作包的符号名称，并影响其最终存储路径 <code>/nix/store/$hash-$name</code>，如果同时支持多版本的场景吗，建议该字段为 <code>包名-版本号</code>。。该字段会作为环境变量传递给 <code>builder</code> 进程。</li>
<li><code>builder</code> 必填，字符串或路径，描述一个构建脚本，可以来是另一个 derivation、源码，如 <code>./builder.sh</code>。推荐使用 bash <code>&quot;${pkgs.bash}/bin/bash&quot;</code>。该字段指向的路径会拷贝到 <code>/nix/store</code> 中，并作为环境变量传递给 <code>builder</code> 进程。</li>
<li><code>args</code> 选填，字符串列表，传递给 <code>builder</code> 的命令行参数。推荐写法为 <code>[&quot;-c&quot; '' 编译脚本 '']</code>。</li>
<li><code>outputs</code> 选填，字符串列表，默认为 <code>[&quot;out&quot;]</code>。一般情况下，不需要更改（除非想精细化的管理依赖，如配置为 <code>[ &quot;lib&quot; &quot;headers&quot; &quot;doc&quot; ]</code>时，其他的推导只需要依赖 <code>lib</code> 目录，这种写法可以加速缓存下载）。nix 会在 <code>/nix/store</code> 中创建这个列表中声明的所有路径。然后，将该列表中的元素作为 key，对应的路径作为 value，作为环境变量传递给 <code>builder</code> 进程。</li>
<li><code>其他属性</code> 选填，支持字符串、数字、路径、列表、bool、null。这些字段会作为环境变量传递到 <code>builder</code> 进程中。需要说明的是：

<ul>
<li>路径类型，会拷贝到 <code>/nix/store</code> 中，然后将绝对路径传递 <code>builder</code> 给进程。</li>
<li>bool 类型 true，会转换为 1。bool 类型 false、null 会转换为空串。</li>
<li>列表类型，元素会转换为字符串，然后用空格分隔拼接成一个字符串。</li>
</ul></li>
</ul>

<h3 id="示例-3">示例</h3>

<h4 id="源码">源码</h4>

<p>假设我们有一个 Go 项目，该项目是一个命令行工具，希望通过 nix 编译和发行该包。本部分实现一下该项目：</p>

<p><code>nix-package-demo/go.mod</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">module</span> <span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#a6e22e">rectcircle</span><span style="color:#f92672">/</span><span style="color:#a6e22e">learn</span><span style="color:#f92672">-</span><span style="color:#a6e22e">nix</span><span style="color:#f92672">-</span><span style="color:#a6e22e">demo</span><span style="color:#f92672">/</span><span style="color:#a6e22e">nix</span><span style="color:#f92672">-</span><span style="color:#f92672">package</span><span style="color:#f92672">-</span><span style="color:#a6e22e">demo</span>

<span style="color:#66d9ef">go</span> <span style="color:#ae81ff">1.19</span></code></pre></div>
<p><code>nix-package-demo/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;hello world!&#34;</span>)
}</code></pre></div>
<h4 id="定义-derivation">定义 derivation</h4>

<p><code>nix-lang-demo/12-derivation.nix</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># drv_path=$(nix-instantiate nix-lang-demo/12-derivation.nix) &amp;&amp; echo &#34;drv_path: $drv_path&#34; &amp;&amp; echo &#34;drv: $(nix --extra-experimental-features nix-command show-derivation $drv_path)&#34; &amp;&amp; nix-store -r $drv_path &amp;&amp; nix-store --read-log $drv_path</span>
<span style="color:#75715e"># nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d  # 彻底卸载。</span>

<span style="color:#75715e"># 整体来开，该文件定义了一个函数，该函数，参数为 pkgs 默认会拿系统中的 nixpkgs，返回一个 derivation 的返回值。</span>
{pkgs <span style="color:#f92672">?</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">&lt;nixpkgs&gt;</span> { } }:
<span style="color:#66d9ef">let</span>
  derivation <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>derivation;
  <span style="color:#75715e"># pkgs = import &lt;nixpkgs&gt; {};</span>
  <span style="color:#75715e"># 从 github 中获取示例项目的源码，会存储到 /nix/store 中的一个子目录中。source 的值是一个指向这个子目录的路径。</span>
  source <span style="color:#f92672">=</span> fetchGit {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;learn-nix-demo-source&#34;</span>;
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/rectcircle/learn-nix-demo.git&#34;</span>;
    rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326&#34;</span>;
    ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
    shallow <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  };
<span style="color:#66d9ef">in</span> 
derivation {
  <span style="color:#75715e"># 由于 go 项目是跨平台的，所以，这里直接使用 builtins.currentSystem，表示支持任意平台。</span>
  system <span style="color:#f92672">=</span> builtins<span style="color:#f92672">.</span>currentSystem;
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-nix-package-demo-0.0.1&#34;</span>;
  <span style="color:#75715e"># 会启动 nixpkgs 的 bash 来构建项目。</span>
  builder <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>bash<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/bash&#34;</span>;
  <span style="color:#75715e"># 额外的环境变量，会传递到 builder 进程。</span>
  A <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>;
  <span style="color:#75715e"># bash 命令的参数。即 bash -c 脚本 。</span>
  args <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;-c&#34;</span> 
  <span style="color:#75715e"># 在这个脚本，观察下，nix 如何设置这个脚本的环境变量，以及文件系统，参见输出。</span>
  <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    set -e
</span><span style="color:#e6db74">    </span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>coreutils<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/echo &#34;&gt;&gt;&gt; export -p&#34; &amp;&amp; export -p &amp;&amp; echo
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; export PATH=</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>go_1_19<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin:</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>bash<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin:</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>coreutils<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin&#34; &amp;&amp; export PATH=&#34;</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>go_1_19<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin:</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>bash<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin:</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>coreutils<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin&#34; &amp;&amp; echo
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; pwd&#34; &amp;&amp; pwd &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; id&#34; &amp;&amp; id &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; ls -al /&#34; &amp;&amp; ls -al / &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; ls -al /bin&#34; &amp;&amp; ls -al /bin &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; ls -al /build&#34; &amp;&amp; ls -al /build &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; ls -al /nix/store&#34; &amp;&amp; ls -al /nix/store &amp;&amp; echo
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; mkdir -p $out/bin&#34; &amp;&amp; mkdir -p $out/bin &amp;&amp; echo
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; cd </span><span style="color:#e6db74">${</span>source<span style="color:#e6db74">}</span><span style="color:#e6db74">/nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./&#34; &amp;&amp; cd </span><span style="color:#e6db74">${</span>source<span style="color:#e6db74">}</span><span style="color:#e6db74">/nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./ &amp;&amp; echo
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">    echo &#34;&gt;&gt;&gt; ls -al $out/bin&#34; &amp;&amp; ls -al $out/bin &amp;&amp; echo
</span><span style="color:#e6db74">  &#39;&#39;</span>];
}</code></pre></div>
<h4 id="测试和输出分析">测试和输出分析</h4>

<p>执行 <code>drv_path=$(nix-instantiate nix-lang-demo/12-derivation.nix) &amp;&amp; echo &quot;drv_path: $drv_path&quot; &amp;&amp; echo &quot;drv: $()&quot; &amp;&amp; nix-store -r $drv_path &amp;&amp; nix-store --read-log $drv_path</code> 命令，输出可以分为三部分。</p>

<p>第一部分，<code>nix-instantiate nix-lang-demo/12-derivation.nix</code> 的执行，通过 <code>echo &quot;drv_path: $drv_path&quot;</code> 可以看出去，其将打印一个路径。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">drv_path: /nix/store/svf3hf64w6sadkc0gdpbss7ql0cr6s3d-my-nix-package-demo-0.0.1.drv</pre></div>
<p>这个路径命名为 <code>/nix/store/$hash-$name.drv</code>。可以看出，nix-instantiate 会执行 <code>nix-lang-demo/12-derivation.nix</code> 表达式（如果该文件返回的是一个函数类型，则会使用 <code>{}</code> 再调用该函数）。并将结果到该路径。</p>

<p><code>.drv</code> 文件是 nix 构建工具的输入，nix 会根据该文件的配置来执行构建（如有缓存，将直接拉取而跳过构建）。</p>

<p>第二部分， <code>nix --extra-experimental-features nix-command show-derivation $drv_path</code> 将使用 json 格式展示上一步产生的 <code>.drv</code> 文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;/nix/store/svf3hf64w6sadkc0gdpbss7ql0cr6s3d-my-nix-package-demo-0.0.1.drv&#34;</span>: {
    <span style="color:#f92672">&#34;args&#34;</span>: [
      <span style="color:#e6db74">&#34;-c&#34;</span>,
      <span style="color:#e6db74">&#34;set -e\n/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin/echo \&#34;&gt;&gt;&gt; export -p\&#34; &amp;&amp; export -p &amp;&amp; echo\n\necho \&#34;&gt;&gt;&gt; export PATH=/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin\&#34; &amp;&amp; export PATH=\&#34;/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin\&#34; &amp;&amp; echo\n\necho \&#34;&gt;&gt;&gt; pwd\&#34; &amp;&amp; pwd &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; id\&#34; &amp;&amp; id &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; ls -al /\&#34; &amp;&amp; ls -al / &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; ls -al /bin\&#34; &amp;&amp; ls -al /bin &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; ls -al /build\&#34; &amp;&amp; ls -al /build &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; ls -al /nix/store\&#34; &amp;&amp; ls -al /nix/store &amp;&amp; echo\n\necho \&#34;&gt;&gt;&gt; mkdir -p $out/bin\&#34; &amp;&amp; mkdir -p $out/bin &amp;&amp; echo\necho \&#34;&gt;&gt;&gt; cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./\&#34; &amp;&amp; cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./ &amp;&amp; echo\n\necho \&#34;&gt;&gt;&gt; ls -al $out/bin\&#34; &amp;&amp; ls -al $out/bin &amp;&amp; echo\n&#34;</span>
    ],
    <span style="color:#f92672">&#34;builder&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash&#34;</span>,
    <span style="color:#f92672">&#34;env&#34;</span>: {
      <span style="color:#f92672">&#34;A&#34;</span>: <span style="color:#e6db74">&#34;1&#34;</span>,
      <span style="color:#f92672">&#34;builder&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash&#34;</span>,
      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;my-nix-package-demo-0.0.1&#34;</span>,
      <span style="color:#f92672">&#34;out&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1&#34;</span>,
      <span style="color:#f92672">&#34;system&#34;</span>: <span style="color:#e6db74">&#34;x86_64-linux&#34;</span>
    },
    <span style="color:#f92672">&#34;inputDrvs&#34;</span>: {
      <span style="color:#f92672">&#34;/nix/store/c65s9ncxdkfcijaxn6c9gglcw1zyaapx-go-1.19.6.drv&#34;</span>: [
        <span style="color:#e6db74">&#34;out&#34;</span>
      ],
      <span style="color:#f92672">&#34;/nix/store/czc8ym3wasmrsnwvlxzavxlfpfi2zg65-bash-5.2-p15.drv&#34;</span>: [
        <span style="color:#e6db74">&#34;out&#34;</span>
      ],
      <span style="color:#f92672">&#34;/nix/store/psc5y2s3prwxf1ph760nd7n1978s4411-coreutils-9.1.drv&#34;</span>: [
        <span style="color:#e6db74">&#34;out&#34;</span>
      ]
    },
    <span style="color:#f92672">&#34;inputSrcs&#34;</span>: [
      <span style="color:#e6db74">&#34;/nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source&#34;</span>
    ],
    <span style="color:#f92672">&#34;outputs&#34;</span>: {
      <span style="color:#f92672">&#34;out&#34;</span>: {
        <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1&#34;</span>
      }
    },
    <span style="color:#f92672">&#34;system&#34;</span>: <span style="color:#e6db74">&#34;x86_64-linux&#34;</span>
  }
}</code></pre></div>
<p>重点关注，如下字段：</p>

<ul>
<li><code>inputDrvs</code> nix 会分析，我们的 nix 代码，分析我们是否引用了其他<strong>推导</strong>。本例中，我们在 builder 中使用了 <code>${pkgs.bash}</code>、在 args 中使用了 <code>${pkgs.go_1_19}</code>、<code>${pkgs.bash}</code>、<code>${pkgs.coreutils}</code>。因此， nix 识别出这些依赖，添加到了该字段。在 <code>nix-instantiate</code> 执行过程中，这些依赖就会被构建完成。</li>
<li><code>inputSrcs</code> nix 会分析，我们的 nix 代码，分析我们是否引用了其他<strong>路径</strong>。本例中，我们在 args 中引用 fetchGit 获取到的 <code>source</code> 路径。因此 nix 识别出了这些依赖，添加到了该字段。在 <code>nix-instantiate</code> 执行过程中，这些依赖就会被获取完成。</li>
<li><code>env</code> 字段中包含了 <code>A</code>，说明声明中的 <code>A</code> 属性被加到了环境变量中。此外 <code>outputs.out</code> 也被加到了环境变量中。</li>
<li><code>outputs</code> 可以看出 outputs 目录，已经被创建出来。</li>
</ul>

<p>此外，需要强调的是：</p>

<ul>
<li>所有的路径都在 <code>/nix/store</code> 目录中。nix 不会依赖除了 /nix/store 之外的其他目录，这保证了 nix 函数式不可变的特性。</li>
<li>可以看出 <code>outputs.out</code> 目录的 hash 值在编译执行之前就确定，从该特性可以看出，nix 的 hash 是由 nix 代码的执行情况决定的，而不是文件内容的 hash。这保证了，同样的 nix 代码生成的各种目录都是一致的。基于这一点 nix 才能实现二进制缓存。</li>
</ul>

<p>第三部分：<code>nix-store -r $drv_path &amp;&amp; nix-store --read-log $drv_path</code> 根据 <code>.drv</code> 进行编译（对应目录不存在的话），然后打印出输出。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1
&gt;&gt;&gt; export -p
declare -x A=&#34;1&#34;
declare -x HOME=&#34;/homeless-shelter&#34;
declare -x NIX_BUILD_CORES=&#34;4&#34;
declare -x NIX_BUILD_TOP=&#34;/build&#34;
declare -x NIX_LOG_FD=&#34;2&#34;
declare -x NIX_STORE=&#34;/nix/store&#34;
declare -x OLDPWD
declare -x PATH=&#34;/path-not-set&#34;
declare -x PWD=&#34;/build&#34;
declare -x SHLVL=&#34;1&#34;
declare -x TEMP=&#34;/build&#34;
declare -x TEMPDIR=&#34;/build&#34;
declare -x TERM=&#34;xterm-256color&#34;
declare -x TMP=&#34;/build&#34;
declare -x TMPDIR=&#34;/build&#34;
declare -x builder=&#34;/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash&#34;
declare -x name=&#34;my-nix-package-demo-0.0.1&#34;
declare -x out=&#34;/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1&#34;
declare -x system=&#34;x86_64-linux&#34;

&gt;&gt;&gt; export PATH=/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin

&gt;&gt;&gt; pwd
/build

&gt;&gt;&gt; id
uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld),65534(nogroup)

&gt;&gt;&gt; ls -al /
total 32
drwxr-x---   9 nixbld nixbld  4096 Mar 12 07:27 .
drwxr-x---   9 nixbld nixbld  4096 Mar 12 07:27 ..
drwxr-xr-x   2 nixbld nixbld  4096 Mar 12 07:27 bin
drwx------   2 nixbld nixbld  4096 Mar 12 07:27 build
drwxr-xr-x   4 nixbld nixbld  4096 Mar 12 07:27 dev
dr-xr-xr-x   2 nixbld nixbld  4096 Mar 12 07:27 etc
drwxr-xr-x   3 nixbld nixbld  4096 Mar 12 07:27 nix
dr-xr-xr-x 194 nobody nogroup    0 Mar 12 07:27 proc
drwxrwxrwt   2 nixbld nixbld  4096 Mar 12 07:27 tmp

&gt;&gt;&gt; ls -al /bin
total 224
drwxr-xr-x 2 nixbld nixbld   4096 Mar 12 07:27 .
drwxr-x--- 9 nixbld nixbld   4096 Mar 12 07:27 ..
-r-xr-xr-x 1 nixbld nixbld 217776 Jan  1  1970 sh

&gt;&gt;&gt; ls -al /build
total 8
drwx------ 2 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-x--- 9 nixbld nixbld 4096 Mar 12 07:27 ..

&gt;&gt;&gt; ls -al /nix/store
total 68
drwxrwxr-t 17 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-xr-x  3 nixbld nixbld 4096 Mar 12 07:27 ..
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 2w4k8nvdyiggz717ygbbxchpnxrqc6y9-gcc-12.2.0-lib
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15
dr-xr-xr-x  3 nixbld nixbld 4096 Jan  1  1970 633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6
dr-xr-xr-x  6 nixbld nixbld 4096 Jan  1  1970 76l4v99sk83ylfwkz8wmwrm4s8h73rhd-glibc-2.35-224
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 9zbi407givkvv1m0bd0icwcic3b3q24y-mailcap-2.1.53
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 bw9s084fzmb5h40x98mfry25blj4cr9r-acl-2.3.1
dr-xr-xr-x  3 nixbld nixbld 4096 Jan  1  1970 bx5ikpp0p8nx88xdldkx16w3k3jzd2qc-busybox-static-x86_64-unknown-linux-musl-1.36.0
dr-xr-xr-x  3 nixbld nixbld 4096 Jan  1  1970 dg8213bqr29hg180gf4ypcj2vvzw4fl3-tzdata-2022g
dr-xr-xr-x  5 nixbld nixbld 4096 Jan  1  1970 jn9kg98dsaajx4mh95rb9r5rf2idglqh-attr-2.5.1
dr-xr-xr-x  3 nixbld nixbld 4096 Jan  1  1970 jvl8dr21nrwhqywwxcl8di4j55765gvy-gmp-with-cxx-stage4-6.2.1
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 lg2skbyyn1d7nkczqjz8mms38z4nhj2b-iana-etc-20221107
dr-xr-xr-x  3 nixbld nixbld 4096 Jan  1  1970 qmnr18aqd08zdkhka695ici96k6nzirv-libunistring-1.0
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 vv6rlzln7vhxk519rdsrzmhhlpyb5q2m-libidn2-2.3.2
dr-xr-xr-x  4 nixbld nixbld 4096 Jan  1  1970 zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source

&gt;&gt;&gt; mkdir -p /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin

&gt;&gt;&gt; cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin/my-nix-p&gt;

&gt;&gt;&gt; ls -al /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin
total 1796
drwxr-xr-x 2 nixbld nixbld    4096 Mar 12 07:27 .
drwxr-xr-x 3 nixbld nixbld    4096 Mar 12 07:27 ..
-rwxr-xr-x 1 nixbld nixbld 1827660 Mar 12 07:27 my-nix-package-demo</pre></div>
<ul>
<li><code>export -p</code> 可以看出，nix builder 中的执行环境是一个和操作系统完全隔离的干净的环境。其中：

<ul>
<li><code>HOME=&quot;/homeless-shelter&quot;</code>、 <code>PATH=&quot;/path-not-set&quot;</code> 只是一个占位符。</li>
<li>上文 <code>.drv</code> 中的环境变量都正确的注入了。</li>
<li><code>pwd</code>、<code>TMP</code>、<code>TEMPDIR</code> 都在 <code>/build</code> 目录。</li>
</ul></li>
<li><code>id</code> 可以看出，nix 创建了一个构建用的用户 <code>1000(nixbld)</code>。</li>
<li><code>ls -al /</code> 可以看出，nix 应该利用了 Linux 的 Mount 和 User namespace 实现的构建隔离。</li>
</ul>

<h4 id="恢复现场">恢复现场</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d</code></pre></div>
<h3 id="derivation-和-stdenv-mkderivation">derivation 和 stdenv.mkDerivation</h3>

<p>这里可以看出，写一个 derivation 启动一个 bash 还是比较麻烦的，最麻烦的是，需要手动设置 PATH 环境变量。</p>

<p>为此，nixpkgs 封装了一个便捷的函数 <code>stdenv.mkDerivation</code>，该函数就是对 <code>derivation</code> 的封装，提供了更友好的编程接口。</p>

<p>因此，在实践中，一般使用 <code>stdenv.mkDerivation</code> 来定义一个推导。</p>

<p>关于 <code>stdenv.mkDerivation</code> 参见下文 <a href="#nixpkgs-分析">nixpkgs 分析</a>。</p>

<h2 id="常见-shell-nix-分析">常见 shell.nix 分析</h2>

<p>上一篇文章，我们使用 shell.nix 定义了一个项目的开发依赖。代码 <code>nix-package-demo/shell.nix</code> 如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># { pkgs ? import &lt;nixpkgs&gt; { } }:</span>
<span style="color:#66d9ef">let</span> 
  pkgs <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> ( builtins<span style="color:#f92672">.</span>fetchTarball { 
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz&#34;</span>; 
  }) {};
<span style="color:#66d9ef">in</span>
pkgs<span style="color:#f92672">.</span>mkShell {
  buildInputs <span style="color:#f92672">=</span>
    [
      pkgs<span style="color:#f92672">.</span>curl
      pkgs<span style="color:#f92672">.</span>jq
      pkgs<span style="color:#f92672">.</span>go
      pkgs<span style="color:#f92672">.</span>which
    ];
  shellHook <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    export TEST_ENV_VAR=ABC
</span><span style="color:#e6db74">  &#39;&#39;</span>;
}</code></pre></div>
<p>执行命令 <code>drv_path=$(nix-instantiate nix-package-demo/shell.nix) &amp;&amp; echo &quot;drv_path: $drv_path&quot; &amp;&amp; echo &quot;drv: $(nix --extra-experimental-features nix-command show-derivation $drv_path)&quot;</code> 可以看到输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">drv_path: /nix/store/wwmkmm2wvfjh5jh5mhb0anxqpz4s26cx-nix-shell.drv
drv: {
  &#34;/nix/store/wwmkmm2wvfjh5jh5mhb0anxqpz4s26cx-nix-shell.drv&#34;: {
    &#34;args&#34;: [
      &#34;-e&#34;,
      &#34;/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh&#34;
    ],
    &#34;builder&#34;: &#34;/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash&#34;,
    &#34;env&#34;: {
      &#34;__structuredAttrs&#34;: &#34;&#34;,
      &#34;buildInputs&#34;: &#34;/nix/store/gd51gknpxqaxyd0gycmszm8ckrvwvs0l-curl-7.88.0-dev /nix/store/7paksrb0nbm7q9x7rzzabqlgjci9rx8k-jq-1.6-dev /nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6 /nix/store/v0g0r8khhdxn8gwcx3yg57wmndzfdgz5-which-2.21&#34;,
      &#34;buildPhase&#34;: &#34;{ echo \&#34;------------------------------------------------------------\&#34;;\n  echo \&#34; WARNING: the existence of this path is not guaranteed.\&#34;;\n  echo \&#34; It is an internal implementation detail for pkgs.mkShell.\&#34;;\n  echo \&#34;------------------------------------------------------------\&#34;;\n  echo;\n  # Record all build inputs as runtime dependencies\n  export;\n} &gt;&gt; \&#34;$out\&#34;\n&#34;,
      &#34;builder&#34;: &#34;/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash&#34;,
      &#34;cmakeFlags&#34;: &#34;&#34;,
      &#34;configureFlags&#34;: &#34;&#34;,
      &#34;depsBuildBuild&#34;: &#34;&#34;,
      &#34;depsBuildBuildPropagated&#34;: &#34;&#34;,
      &#34;depsBuildTarget&#34;: &#34;&#34;,
      &#34;depsBuildTargetPropagated&#34;: &#34;&#34;,
      &#34;depsHostHost&#34;: &#34;&#34;,
      &#34;depsHostHostPropagated&#34;: &#34;&#34;,
      &#34;depsTargetTarget&#34;: &#34;&#34;,
      &#34;depsTargetTargetPropagated&#34;: &#34;&#34;,
      &#34;doCheck&#34;: &#34;&#34;,
      &#34;doInstallCheck&#34;: &#34;&#34;,
      &#34;mesonFlags&#34;: &#34;&#34;,
      &#34;name&#34;: &#34;nix-shell&#34;,
      &#34;nativeBuildInputs&#34;: &#34;&#34;,
      &#34;out&#34;: &#34;/nix/store/2zx26yarglz5wqbkl6mqbaxqfyinrixn-nix-shell&#34;,
      &#34;outputs&#34;: &#34;out&#34;,
      &#34;patches&#34;: &#34;&#34;,
      &#34;phases&#34;: &#34;buildPhase&#34;,
      &#34;preferLocalBuild&#34;: &#34;1&#34;,
      &#34;propagatedBuildInputs&#34;: &#34;&#34;,
      &#34;propagatedNativeBuildInputs&#34;: &#34;&#34;,
      &#34;shellHook&#34;: &#34;export TEST_ENV_VAR=ABC\n&#34;,
      &#34;stdenv&#34;: &#34;/nix/store/c3f4jdwzn8fm9lp72m91ffw524bakp6v-stdenv-linux&#34;,
      &#34;strictDeps&#34;: &#34;&#34;,
      &#34;system&#34;: &#34;x86_64-linux&#34;
    },
    &#34;inputDrvs&#34;: {
      &#34;/nix/store/65wj1fwk5f3wncd1j3dmk29k3nzghl8d-which-2.21.drv&#34;: [
        &#34;out&#34;
      ],
      &#34;/nix/store/c65s9ncxdkfcijaxn6c9gglcw1zyaapx-go-1.19.6.drv&#34;: [
        &#34;out&#34;
      ],
      &#34;/nix/store/czc8ym3wasmrsnwvlxzavxlfpfi2zg65-bash-5.2-p15.drv&#34;: [
        &#34;out&#34;
      ],
      &#34;/nix/store/r7wldahsa6maa0m7nnjf82azcy4g8hdh-jq-1.6.drv&#34;: [
        &#34;dev&#34;
      ],
      &#34;/nix/store/saw3hgzcr6lsy051kclm3y7kif8b4i6h-curl-7.88.0.drv&#34;: [
        &#34;dev&#34;
      ],
      &#34;/nix/store/xjk0c9yw2i25xr08ngk60bc47q9fw2jd-stdenv-linux.drv&#34;: [
        &#34;out&#34;
      ]
    },
    &#34;inputSrcs&#34;: [
      &#34;/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh&#34;
    ],
    &#34;outputs&#34;: {
      &#34;out&#34;: {
        &#34;path&#34;: &#34;/nix/store/2zx26yarglz5wqbkl6mqbaxqfyinrixn-nix-shell&#34;
      }
    },
    &#34;system&#34;: &#34;x86_64-linux&#34;
  }
}</pre></div>
<p>因此 <code>mkShell</code> 本质上就是创建了一个包含了声明依赖的 derivation。</p>

<p>而 <code>nix-shell</code> 的流程就是，先调用 <code>nix-instantiate nix-package-demo/shell.nix</code>，生成一个 <code>.drv</code> 文件，然后根据该文件配置，启动一个 Shell。</p>

<h2 id="nixpkgs-分析">nixpkgs 分析</h2>

<blockquote>
<p>参考： <a href="https://github.com/NixOS/nixpkgs">nixpkgs github</a> | <a href="https://nixos.org/manual/nixpkgs/stable/">nixpkgs 手册</a> |</p>
</blockquote>

<p>基于上面的背景知识，nixpkgs 和 nix channel 的原理可以很容易的立即。</p>

<p>nixpkgs 本质上就是一个 nix 代码库，该库主要包含如下两类内容：</p>

<ul>
<li>一些对 nix 原生能力进行易用化封装的函数，如 <code>mkShell</code>、<code>stdenv.mkDerivation</code>。</li>
<li>包含了开源世界 80000+ 个软件包的 <code>derivation</code> 声明。</li>
</ul>

<p>可以通过 <code>nixpkgs.hello</code> 的源码（<a href="https://github.com/NixOS/nixpkgs/blob/f94a71f899b26311b439c9efc25f915745b50a8c/pkgs/applications/misc/hello/default.nix">pkgs/applications/misc/hello/default.nix</a>），以及生成的 <code>.drv</code> 来了解，如何通过 <code>stdenv.mkDerivation</code> 来定一个软件包的 <code>derivation</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.hello
ls -al /nix/store/*-hello-*.drv
nix --extra-experimental-features nix-command show-derivation /nix/store/7ky0zmis8b384k5sx852i0fq7x9ir2jl-hello-2.12.1.drv</code></pre></div>
<p>这里重点介绍一下，nixpkgs 的 <code>stdenv.mkDerivation</code> 的属性集参数的一些重要属性（详见：<a href="https://nixos.org/manual/nixpkgs/stable/#chap-stdenv">Chapter 6. The Standard Environment</a>）。</p>

<ul>
<li><code>pname</code> 包名。</li>
<li><code>version</code> 包版本。最终对应 derivation name 为 <code>&quot;${pname}-${version}&quot;</code>。</li>
<li><code>src</code> 源代码路径一般等于 fetch 相关函数调用。在脚本可以通过 <code>src</code> 环境变量获取到。</li>
<li><code>nativeBuildInputs</code> 声明在编译时依赖的其他包（derivation），如 go 编译器，git 等。</li>
<li><code>buildInputs</code> 声明在运行时依赖的其他包（derivation），如 glibc 等，为了支持交叉编译，还有大量 <code>depsXxx</code> 相关属性，不太理解。</li>
<li><code>passthru</code> 该属性目前主要用户测试，该字段的变更不会影响 <code>.drv</code> 文件的生成，不会影响 hash 的生成。</li>

<li><p><code>xxxPhase</code> 该函数会执行位于 <code>pkgs/stdenv/generic/setup.sh</code> 中的 <code>genericBuild</code> 函数，该函数将构建过程分成了很多各阶段。如果项目使用 autotools 来管理编译过程，则一般不用修改该类字段。如果项目中没有提供 Makefile 则需要手动提供 <code>buildPhase</code>、<code>installPhase</code> 脚本。支持的所有阶段如下（<code>$</code> 开头的表示默认没有实现）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">$prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases</pre></div></li>
</ul>

<p>针对各种不同的编程语言和框架， nixpkgs 也提供了对应的便捷函数，如 <code>buildGoModule</code>，本文不多赘述，详见：<a href="https://nixos.org/manual/nixpkgs/stable/#chap-language-support">Chapter 17. Languages and frameworks</a>。</p>

<p>注意：不管是 nativeBuildInputs 还是 buildInputs，即使其产物在 Cache 中存在，也都会自动下载下来。似乎并不存在 build-only 方式的声明，参见： <a href="https://github.com/NixOS/nix/issues/8107">issue</a>。</p>

<h2 id="自定义-channel">自定义 channel</h2>

<p>根据 nixpkgs 分析章节，做一个自定义 channel 会非常的简单。</p>

<p>上文，推导（derivation）章节的示例已经定义了一个包了，下面我们使用同样的示例代码，定义两个包。</p>

<p>第一个包，使用 <code>stdenv.mkDerivation</code> 函数定义，<code>nix-lang-demo/13-mkderivation.nix</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix">{pkgs <span style="color:#f92672">?</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">&lt;nixpkgs&gt;</span> { } }:
<span style="color:#66d9ef">let</span>
  stdenv <span style="color:#f92672">=</span> pkgs<span style="color:#f92672">.</span>stdenv;
<span style="color:#66d9ef">in</span> 
stdenv<span style="color:#f92672">.</span>mkDerivation {
  pname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-nix-package-demo-build-by-my-mk-derivation&#34;</span>;
  version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.1&#34;</span>;
  src <span style="color:#f92672">=</span> fetchGit {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;learn-nix-demo-source&#34;</span>;
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/rectcircle/learn-nix-demo.git&#34;</span>;
    rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326&#34;</span>;
    ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
    shallow <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  };
  nativeBuildInputs <span style="color:#f92672">=</span> [ pkgs<span style="color:#f92672">.</span>go_1_19 pkgs<span style="color:#f92672">.</span>git ];
  buildPhase <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    cd nix-package-demo &amp;&amp; CGO_ENABLED=0 go build -o $pname ./
</span><span style="color:#e6db74">  &#39;&#39;</span>;
  installPhase <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    mkdir -p $out/bin
</span><span style="color:#e6db74">    cp $pname $out/bin
</span><span style="color:#e6db74">  &#39;&#39;</span>;
}</code></pre></div>
<p>第二个包，使用 <code>buildGoModule</code> 函数定义，<code>nix-lang-demo/14-build-go-module.nix</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/go/module.nix</span>
{ pkgs <span style="color:#f92672">?</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">&lt;nixpkgs&gt;</span> { } }:
pkgs<span style="color:#f92672">.</span>buildGoModule {
  pname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-nix-package-demo-by-build-go-module&#34;</span>;
  version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.1&#34;</span>;
  src <span style="color:#f92672">=</span> fetchGit {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;learn-nix-demo-source&#34;</span>;
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/rectcircle/learn-nix-demo.git&#34;</span>;
    rev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326&#34;</span>;
    ref <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;
    shallow <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
  };

  vendorHash <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;  <span style="color:#75715e"># 自动生成。</span>

  modRoot <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./nix-package-demo&#34;</span>;
  CGO_ENABLED <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
  postInstall <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    mv $out/bin/nix-package-demo $out/bin/$pname
</span><span style="color:#e6db74">  &#39;&#39;</span>;
}</code></pre></div>
<p>现在定义这个 channel 的 <code>./default.nix</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># nix-env -qaP -f ./</span>
<span style="color:#75715e"># nix-env -iA my-nix-package-demo_0_0_1 -f ./</span>
<span style="color:#75715e"># nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d  # 彻底卸载。</span>
<span style="color:#75715e"># nix-env -iA my-nix-package-demo-build-by-my-mk-derivation_0_0_1 -f ./</span>
<span style="color:#75715e"># nix-env -e my-nix-package-demo-build-by-my-mk-derivation-0.0.1 ; nix-collect-garbage -d  # 彻底卸载。</span>
<span style="color:#75715e"># nix-env -iA my-nix-package-demo-by-build-go-module_0_0_1 -f ./</span>
<span style="color:#75715e"># nix-env -e my-nix-package-demo-by-build-go-module-0.0.1 ; nix-collect-garbage -d  # 彻底卸载。</span>

{ pkgs <span style="color:#f92672">?</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">&lt;nixpkgs&gt;</span> { } }:
{
  my-nix-package-demo_0_0_1 <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">./nix-lang-demo/12-derivation.nix</span> { <span style="color:#66d9ef">inherit</span> pkgs; };
  my-nix-package-demo-build-by-my-mk-derivation_0_0_1 <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">./nix-lang-demo/13-mkderivation.nix</span> { <span style="color:#66d9ef">inherit</span> pkgs; };
  my-nix-package-demo-by-build-go-module_0_0_1 <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">./nix-lang-demo/14-build-go-module.nix</span> { <span style="color:#66d9ef">inherit</span> pkgs; };
}</code></pre></div>
<p>此时，通过 <code>nix-env -qaP -f ./</code> 即可像 nixpkgs 一样列出这个 channel 的三个包。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">my-nix-package-demo_0_0_1                            my-nix-package-demo-0.0.1
my-nix-package-demo-build-by-my-mk-derivation_0_0_1  my-nix-package-demo-build-by-my-mk-derivation-0.0.1
my-nix-package-demo-by-build-go-module_0_0_1         my-nix-package-demo-by-build-go-module-0.0.1</pre></div>
<p>可以使用如下命令安装卸载。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA my-nix-package-demo_0_0_1 -f ./
nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d  <span style="color:#75715e"># 彻底卸载。</span>
nix-env -iA my-nix-package-demo-build-by-my-mk-derivation_0_0_1 -f ./
nix-env -e my-nix-package-demo-build-by-my-mk-derivation-0.0.1 ; nix-collect-garbage -d  <span style="color:#75715e"># 彻底卸载。</span>
nix-env -iA my-nix-package-demo-by-build-go-module_0_0_1 -f ./
nix-env -e my-nix-package-demo-by-build-go-module-0.0.1 ; nix-collect-garbage -d  <span style="color:#75715e"># 彻底卸载。</span></code></pre></div>
<h2 id="其他说明">其他说明</h2>

<h3 id="纯函数性">纯函数性</h3>

<p>最后，讨论一下 nix 语言如何保证 nix 工具是一个纯函数包管理工具。</p>

<p>首先，纯函数指的是没有副作用的函数，也就是说，对于同一个参数的多次调用，一个纯函数可以保证，其返回值永远不变，且不会对外部世界产生任何影响。</p>

<p>从语法上看，nix 所有的语法、操作符都是纯函数性的。但是由于 nix 语言定义的是编译的过程，必然要涉及文件系统和网络相关的操作，如从 github 下载代码、读取文件、将编译产物写入文件。显然包含这些操作就的函数就不是纯函数了。</p>

<p>针对这种情况，nix 的解决办法是，所有对于路径的操作，nix 会根据固定的规则生成一个位于 <code>/nix/store</code> 的路径。如果是输入类路径，会将文件拷贝到这个位置。</p>

<p>关键在于这个路径个规则。由于 nix 除了路径和网络下载之外的所有操作都是纯函数的，因此 nix 代码不管运行多少次，到了需要处理目录的地方，其运行状态一定是完全一致，因此 nix 就可以根据运行状态生成一个 hash，并结合路径名生成该路径。这样，在狭义上 nix 并非纯函数，但是在逻辑上，却达到了纯函数的效果。</p>

<p>由于 nix 有了纯函数的保证，那么这些路径的操作就是可以被缓存的。这样，在配合二进制缓存，nix 的安装速度可以做到非常快。</p>

<p>这种机制，对纯函数性的保证实际上比较脆弱，如下的场景可能破坏 nix 的纯函数性，带来不可重现的问题。</p>

<ul>
<li>对于 fetchGit 可以利用 git 的 <code>rev</code> 机制，可以保证纯函数性。对于 <code>fetchTarball</code> 可以使用 <code>sha256</code> 保证纯函数性。但是对于 <code>fetchurl</code> 则无法保证纯函数型（因此在<a href="https://nixos.org/manual/nix/stable/command-ref/conf-file.html">严格评估模式 restricted evaluation mode</a>下，该函数是不可用的）。</li>
<li>在使用 derivation 中，总是会调用 shell 来执行命令，而 shell 是无法保证纯函数性的，例如用户在 shell 脚本中使用 curl 来下载内容，且没有校验和处理异常，则会破坏 nix 的纯函数性。</li>
</ul>

<p>因此，在开发一个 nix 包时，如果要保证纯函数性，则要求：</p>

<ul>
<li>不要使用 <code>fetchurl</code>。</li>
<li>在编写 shell 脚本时，不要使用 curl 下载内容，时刻注意该 shell 脚本是否是可重现的。</li>
</ul>

<h3 id="各种-name">各种 Name</h3>

<p>至此，当我们要安装一个包时，我们会遇到好几种 Name，在这里总结下这些 Name 之间的关系。</p>

<ul>
<li><code>derivation_name</code> 即这个包的名字：

<ul>
<li>定义位置：在调用 <code>derivation</code> 函数时，传递的 <code>name</code> 属性。</li>
<li>使用位置：

<ul>
<li>如果 <code>derivation</code> 没有配置 outputs 时（采用默认值 <code>[&quot;out&quot;]</code>），则该 out 为 <code>/nix/sotre/$hash_$derivation_name</code>。</li>
<li>使用 <code>nix-env -e $derivation_name</code> 删除包时。</li>
<li>查询包 <code>nix-env -qaP</code> 输出的第二列。</li>
</ul></li>
</ul></li>
<li><code>pname</code> 在 nixpkgs 的包名：

<ul>
<li>定义位置：在调用 <code>stdenv.mkDerivation</code> 函数是，传递的 <code>pname</code> 属性。</li>
<li>使用位置：

<ul>
<li><code>stdenv.mkDerivation</code> 函数传递给 derivation 函数的 name 时，传递的是 <code>$pname-$version</code>。也就是说：<code>$derivation_name=$pname-$version</code>。</li>
</ul></li>
</ul></li>
<li><code>attr_name</code> 执行一个 <code>default.nix</code> 后产生属性集中的属性名。

<ul>
<li>定义位置：<code>default.nix</code> 中最终返回的属性机中。</li>
<li>使用位置：

<ul>
<li>查询包 <code>nix-env -qaP</code> 输出的第一列，格式为 <code>$channel_name.$attr_name</code>。</li>
<li>查询包 <code>nix-env -qaP -f path/to/channel</code> 输出的第一列，格式为 <code>$attr_name</code>。</li>
<li>安装包 <code>nix-env -iA $channel_name.$attr_name</code>。</li>
<li>安装包 <code>nix-env -iA $attr_name -f path/to/channel</code>。</li>
</ul></li>
</ul></li>
</ul>

<h3 id="包存储结构">包存储结构</h3>

<p>传统的 Unix 包存储结构规范是 <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS</a> 。这个规范有如下特点：</p>

<ul>
<li>这个规范并不是强制的，可以遵循可以不遵循。</li>
<li>没有版本的概念，同一个包的不同版本会相互覆盖。</li>
<li>一个包的各个组成部分，在不同个目录。比如 so 文件在 /usr/lib，可执行文件在 /usr/bin。</li>
</ul>

<p>而 Nix 的包并不遵循 <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS</a> 规范，Nix 的包有如下特点：</p>

<ul>
<li>Nix 包存储结构是强制，是有 Nix 工具生成和维护的。</li>
<li>有版本的概念，同一个包的不同版本存储在不同的目录。</li>
<li>一个包的所有文件都存储在和其他包隔离的自己的目录中。</li>
</ul>

<p>下面是来自 <a href="https://edolstra.github.io/pubs/nspfssd-lisa2004-final.pdf">Nix 论文</a> 的包存储结构和依赖关系示意图。</p>

<p><img src="/image/nix-paper-nix-store.png" alt="Dolstra, Eelco; de Jonge, Merijn; Visser, Eelco (November 2004). &quot;Nix: A Safe and Policy-Free System for Software Deployment&quot; (PDF). LISA '04: Proceedings of the 18th USENIX Conference on System Administration. pp. 79–92. Retrieved 11 July 2023. Figure 4: The Store." /></p>

<ul>
<li>Nix 的所有包都存储在 <code>/nix/store</code> 下的目录中，这个目录的格式为 <code>$hash_$derivation_name</code>

<ul>
<li><code>$hash</code>：nix 包都是通过 nix 语言定义的，由于 nix 语言的纯函数性，因此对于每个 nix 包的制品的存储目录（在编译过程中成为 out 目录），生成的唯一的 hash 值。这个 hash 值存在的目的是，当该包的依赖变了的情况下，这个包虽然包名和版本号没变，但是其内容已经变了，这个包已经不是之前的包了，为了不可变性，这个 hash 也会变化。</li>
<li><code>$derivation_name</code>： 由包名和版本号构成。</li>
</ul></li>
<li>上图还展示了 Nix 包的依赖关系，这个关系在编译时，根据 Nix 语言包声明的依赖关系就决定了。</li>
<li>在传统的 Linux 发行版中（符合 <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS</a> 规范），像 libc 这种最常见的动态链接库都是存储在固定的路径中的如 <code>/lib/x86_64-linux-gnu</code>。如果一个包是通过源码编译，自然没有问题，在编译时 libc 也自动的被配置到对应的 <code>/nix/store/xxx-glibc-xxx/lib</code> 目录中。但是，某些专有软件并没有提供源码，此时这类软件的编译过程变为：下载常规 Linux 版本可执行文件，然后通过 <a href="https://github.com/NixOS/patchelf">patchelf</a> 工具修改 <a href="https://linux.die.net/man/8/ld-linux.so">ld-linux.so</a> 到 <code>/nix/store/xxx-glibc-xxx/lib</code> 路径即可，详见：<a href="https://nixos.wiki/wiki/Packaging/Binaries">wiki</a>。</li>
</ul>
]]></description></item><item><title>Nix 详解（二） 项目外部依赖管理</title><link>https://www.rectcircle.cn/posts/nix-2-project-external-dependencies-manager/</link><pubDate>Mon, 06 Mar 2023 02:28:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-2-project-external-dependencies-manager/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1 | <a href="https://github.com/rectcircle/learn-nix-demo">示例代码库</a></p>
</blockquote>

<h2 id="场景概述">场景概述</h2>

<p>在开发一个项目过程中，总是需要搭建一个完整的开发环境。</p>

<p>比如开发一个 Go 项目（go1.19），这个项目会有些脚本，这些脚本依赖 jq、curl。</p>

<p>此时，该项目的每个开发人员，都需要在自己的工作电脑中根据文档或者口口相传安装这些外部依赖。操作可能如下：</p>

<ul>
<li>前往 Go 官网，下载指定版本的 Go，如果同时开发多个项目，还要解决不同项目 Go 版本不一致的问题。</li>
<li>使用自己操作系统的包管理工具安装 jq、curl。</li>
</ul>

<p>这种手动操作存在如下问题：</p>

<ul>
<li>存在较高沟通和时间成本，搭建流程繁琐枯燥，尤其是大型项目。</li>
<li>不同的开发的设备配置可能不一致，在搭建开发环境的时候，极大概率可能会遇到各种奇奇怪怪的问题。</li>
<li>同一设备多个项目可能存在冲突。</li>
</ul>

<p>为了解决开发环境搭建的问题，各个语言都有自己的解决方案，如 python venv。</p>

<p>而 nix (<a href="https://nixos.org/manual/nix/stable/command-ref/nix-shell.html">nix-shell</a>) 提供了一种通用的优雅的，自动化的，可重现的，搭建隔离的开发环境的能力。</p>

<h2 id="临时开发环境">临时开发环境</h2>

<blockquote>
<p><a href="https://nix.dev/tutorials/ad-hoc-developer-environments">nix.dev/临时开发环境</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-shell -p go_1_19 jq curl</code></pre></div>
<p>如上命令作用是，打开一个 shell，并安装 <code>go_1_19</code>、<code>jq</code>、<code>curl</code> 这三个包。执行 <code>echo $PATH</code> 可以看到其被正确的配置了。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/7xf4f4d9jip5rjkzwvxwxqgmyhzzvyqk-bash-interactive-5.2-p15/bin:...:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games</pre></div>
<p>执行 exit 后，将回到之前的 shell，一切将恢复如初，系统环境不会有任何影响。</p>

<p>需要特别说明的是，上述命令并非完全可重现，原因如下：</p>

<ul>
<li>该命令仍然继承了当前 shell 的一些环境变量。在不同的设备中得到的 shell 环境可能有所差异。</li>
<li>在写这篇文章的时刻没有任何问题，但是随着时间的推移。如，两年后，这个命令可能就不生效。原因在于，该命令要安装的包来自 nixpkgs channel，而该 channel 随着时间的推移，会发生变化，比如未来 go_1_19 已经不再维护时，相关文件可能会直接被删除。</li>
</ul>

<p>因此，如果想得到一个可重现的临时开发环境，命令如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-shell -p go_1_19 jq curl --pure -I nixpkgs<span style="color:#f92672">=</span>https://github.com/NixOS/nixpkgs/archive/794f34657e066a5e8cc4bb34491fee02240c6ac4.tar.gz
<span style="color:#75715e"># nix-shell -p go_1_19 jq curl --pure -I nixpkgs=https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz</span></code></pre></div>
<ul>
<li><code>--pure</code> 指，不继承当期 shell 环境变量。</li>
<li><code>-I</code> 从指定 channel 版本中安装包（commit id 前往 <a href="https://github.com/NixOS/nixpkgs/commits/master">https://github.com/NixOS/nixpkgs/commits/master</a> 查询）。

<ul>
<li>中国大陆前往，<a href="https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/?C=M&amp;O=D">清华源 Release 目录</a>， 搜索最新的<code>nixpkgs-unstable@nixpkgs</code> 开头的最新，复制 nixexprs.tar.xz 路径。</li>
</ul></li>
</ul>

<p>除了直接进入一个 shell 外，还可以通过 <code>--command</code>（交互式）、<code>--run</code>（非交互式） 仅在该环境中一个命令，然后立即退出。</p>

<p>这里使用到了官方 channel nixpkgs，要想了解 nixpkgs，需要先了解 nix 领域特定语言，因此关于 nixpkgs 的详细介绍，参见下一篇关于 nix 领域特定语言的介绍。</p>

<p>这种方式对于临时测试十分有用。</p>

<h2 id="可重现-shell-脚本">可重现 shell 脚本</h2>

<blockquote>
<p><a href="https://nix.dev/tutorials/reproducible-scripts">nix.dev/可重现脚本</a> | <a href="https://devpress.csdn.net/cicd/62ee0a19c6770329307f3202.html">如何使用 Nix 轻松获取依赖项</a></p>
</blockquote>

<p>从上文可以看出，nix-shell 和 bash 很像。自然的，我们可以通过 shell shebang 机制，使用 nix-shell 来为 bash 脚本配置执行环境，并执行脚本内容。</p>

<p>创建 <code>nix-package-demo/demo.sh</code> 文件，编写内容如下:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env nix-shell
</span><span style="color:#75715e"></span><span style="color:#75715e">#! nix-shell -i bash --pure</span>
<span style="color:#75715e">#! nix-shell -p bash go_1_19 jq curl which</span>
<span style="color:#75715e">#! nix-shell -I nixpkgs=https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz</span>

echo $PATH
which go
which jq
which curl</code></pre></div>
<p><code>chmod +x nix-package-demo/demo.sh</code>，执行 <code>./nix-package-demo/demo.sh</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">/nix/store/7xf4f4d9jip5rjkzwvxwxqgmyhzzvyqk-bash-interactive-5.2-p15/bin:...:/nix/store/s29xjzid62937vc17jx6zi785nhk0plk-file-5.44/bin
/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin/go
/nix/store/hagvhrwy8jzj97kc7nyy9vr18xkg7xvk-jq-1.6-bin/bin/jq
/nix/store/yl319c7cyd6jb3ssizbdaknwv0543986-curl-7.88.0-bin/bin/curl</code></pre></div>
<p>说明：</p>

<ul>
<li>第一行，表示使用 nix-shell 解释器执行该脚本文件。</li>
<li>第二到第四行，为 nix-shell 的配置，可以多行，也可以一行。</li>
<li>第二行表示，使用 bash 解释器，并不继承当前进程的环境变量。</li>
<li>第三行表示，安装 bash go_1_19 jq curl which 包。</li>
<li>第四行表示，从指定 channel 版本中安装包。</li>
</ul>

<h2 id="通过-shell-nix-配置环境">通过 shell.nix 配置环境</h2>

<blockquote>
<p><a href="https://nixos.wiki/wiki/Development_environment_with_nix-shell">nix wiki 使用 nix-shell 配置开发环境</a></p>
</blockquote>

<p>nix-shell 除了支持通过命令行参数以及 shell shebang 方式配置依赖外。还支持通过 <code>shell.nix</code> 配置文件的方式来配置项目的依赖。</p>

<p>创建一个 <code>nix-package-demo/shell.nix</code> 文件，编写如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nix" data-lang="nix"><span style="color:#75715e"># { pkgs ? import &lt;nixpkgs&gt; { } }:</span>
<span style="color:#66d9ef">let</span> 
  pkgs <span style="color:#f92672">=</span> <span style="color:#f92672">import</span> ( builtins<span style="color:#f92672">.</span>fetchTarball { 
    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz&#34;</span>; 
  }) {};
<span style="color:#66d9ef">in</span>
pkgs<span style="color:#f92672">.</span>mkShell {
  buildInputs <span style="color:#f92672">=</span>
    [
      pkgs<span style="color:#f92672">.</span>curl
      pkgs<span style="color:#f92672">.</span>jq
      pkgs<span style="color:#f92672">.</span>go
      pkgs<span style="color:#f92672">.</span>which
    ];
  shellHook <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span><span style="color:#e6db74">    export TEST_ENV_VAR=ABC
</span><span style="color:#e6db74">  &#39;&#39;</span>;
}</code></pre></div>
<p><code>cd nix-package-demo</code>，然后执行 <code>nix-shell --pure</code> 即可进入 <code>shell.nix</code> 配置的 shell 中。</p>

<p>此时可重现脚本可以改为（<code>nix-package-demo/demo2.sh</code>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env nix-shell
</span><span style="color:#75715e"></span><span style="color:#75715e">#! nix-shell -i bash --pure shell.nix</span>

echo $PATH
which go
which jq
which curl</code></pre></div>
<p>注意，这里的 <code>shell.nix</code> 文件的查找，实测是基于脚本所在目录而不是进程 work dir。</p>

<p><code>shell.nix</code> 语法是 nix 定义的一套领域特定语言，本文不会深入探讨，在本文场景中，只需将开发环境外部依赖添加到 buildInputs 对应部分即可。</p>

<p>如想了解 nix 语言，参见本系列下一篇文章。</p>

<h2 id="与-direnv-结合使用">与 direnv 结合使用</h2>

<blockquote>
<p><a href="https://grass.show/post/create-environment-with-nix-and-direnv">使用Nix+direnv快速构建不同软件版本的开发环境</a></p>
</blockquote>

<p>通过上述配置，可以通过手动执行一个 nix-shell 获取到一个隔离的具有完整的依赖的 shell 环境。</p>

<p>但是这个过程不够自动化，仍需手动执行一下 nix-shell。此时可以通过 direnv 工具，实现 <code>cd</code> 到某个目录后，自动进入该 nix-shell，实施步骤如下：</p>

<ol>
<li><p>安装 direnv</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA nixpkgs.direnv</code></pre></div></li>

<li><p>配置 shell profile 文件，以 bash 为例，在 <code>~/.bashrc</code> 中添加（其他 shell 参见：<a href="https://direnv.net/docs/hook.html">官方文档</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">eval <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>direnv hook bash<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span></code></pre></div></li>

<li><p>在指定目录（一般为项目根目录），添加 <code>.envrc</code> 文件（<code>nix-package-demo/.envrc</code>），内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">use_nix</pre></div></li>

<li><p>打开一个新的 shell，通过 cd 进入如上目录，先执行一次 <code>direnv allow .</code> 对当前目录进行授权（防止网上 clone 下来的代码存在恶意脚本攻击）（每次修改 <code>.envrc</code> 都需要重新 allow 一下），之后再 <code>cd</code> 到该目录可自动应用 nix-shell 的配置。</p></li>
</ol>

<h2 id="与-ide-vscode-集成">与 IDE（VSCode） 集成</h2>

<p>通过上文的说明，shell 已经可以很好和 nix-shell 创建的开发环境集成了。但是现实中，IDE 能够识别 nix-shell 创建的开发环境更为重要。</p>

<p>以 VSCode 为例，可以使用 <a href="https://marketplace.visualstudio.com/items?itemName=arrterian.nix-env-selector">Nix Environment Selector</a> 扩展来实现：当 VSCode 打开一个包含 <code>shell.nix</code> 的目录时，通过该扩展的 <code>&gt;Nix-Env: Select Environment</code> （cmd + shift + p） 命令，选择一个 <code>.nix</code> 配置文件，该扩展会调用 nix-shell 完成依赖下载，并提示 Reload，Reload VSCode 后，该扩展会将从 nix-shell 获取到的环境变量设置的 VSCode 的扩展主机进程，这样其他扩展将可以感知到这个环境。</p>

<p>但是需要注意的是，这个扩展存在如下问题：</p>

<ul>
<li>某些场景，<code>.nix</code> 声明的依赖还未下载时，该扩展可能会阻塞 VSCode 加载其他的扩展。</li>
<li>由于某些场景，如果其他依赖开发环境的扩展比该扩展先激活，可能读取到的是配置前的环境变量，从而导致这些扩展找不到相关依赖（参见：<a href="https://github.com/arrterian/nix-env-selector/issues/66">issue</a>），这个问题比较致命，受限于 VSCode 机制（参见：<a href="https://github.com/microsoft/vscode/issues/152806">issue</a>），该问题通过常规办法可能难以解决。</li>
<li>该扩展不会自动配置 VSCode Terminal 的 Shell，因此仍然需要上文的 direnv。</li>
</ul>
]]></description></item><item><title>Nix 详解（一） 像传统包管理器一样使用 Nix</title><link>https://www.rectcircle.cn/posts/nix-1-package-manager/</link><pubDate>Sat, 25 Feb 2023 20:48:49 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nix-1-package-manager/</guid><description type="html"><![CDATA[

<blockquote>
<p>version: nix-2.14.1</p>
</blockquote>

<h2 id="简述">简述</h2>

<p>Nix 是一个 <code>*nix</code> (Linux、MacOS 等) 操作系统的 DevOps 工具集，其核心是一套设计严密的包管理工具。</p>

<p>和 Debian 系的 apt、Redhat 系的 yum 不同。Nix 在设计上是跨平台的，可以在任何 <code>*nix</code> 平台使用（这可能就是 nix 命名的来源）。</p>

<p>Nix 自称其是一个纯函数式，Nix 的包（每个版本）被视为函数式编程领域的值。具体而言，每个包的每个版本都会计算 Hash，并将该软件的所有文件都存放到一个带有 Hash 值的目录中（因此其没有采用 <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard"><code>FHS</code></a> 目录结构标准），如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/b6gvzjyb2pg0kjfwrjmg1vfhh54ad73z-firefox-33.1/</pre></div>
<p>通过 Nix 可以实现系统软件环境的可重现的、声明式的和可靠性。</p>

<ul>
<li>可重现：Nix 包的依赖只能使用其他的 Nix 包，且不存在没有声明的依赖。因此如果一个包在一台机器上工作，它也将在另一台机器上工作。</li>
<li>声明式：如果你在开发一个项目，Nix 可以根据配置文件，定制开发或编译环境，且这些开发环境可以在任何设备上复现。</li>
<li>可靠性：Nix 包的每个版本都存放在自己独立的目录中，因此在安装、升级过程中不会有文件覆盖的问题，这就保证了新版的安装不会影响到旧版本的任何内容，可以实现一键回滚。</li>
</ul>

<p>从上面的介绍可以看出，nix 的能力和 Docker 的部分能力存在重叠，特别在环境可重现方面。但是两者存在本质的不同，nix 是一个包管理和配置工具，而 docker 是一个构建和部署容器的工具。因此两者应用场景存在不同：</p>

<ul>
<li>nix 一般用在项目开发、编译阶段，可以通过配置文件直接在当前系统中，给开发人员提供一个可重现的开发环境。这个开发环境本质上是通过 PATH 环境变量生成的，各个 IDE 可以零成本集成。</li>
<li>docker 一般用在项目的构建和部署阶段。如果将 docker 应用在开发阶段，会存在如下问题：

<ul>
<li>IDE 集成困难，只能通过各个 IDE 提供的 Remote 特性才能进入容器，学习成本并不低。</li>
<li>修改开发环境步骤过长(修改 Dockerfile、构建 Dockerfile、删除旧容器、运行新容器），不易测试。</li>
</ul></li>
</ul>

<p>包管理工具实际上是一个 Linux 发行版的核心。反过来讲，拥有了一个包管理工具，很容易的就可以创造一个 Linux 发行版。因此 Nix 项目组还提供了一个将 Nix 作为包管理工具的发行版 NixOS。</p>

<p>本系列主要介绍 Nix 工具集，而不会介绍 NixOS。阅读本系列文章可以了解：</p>

<ul>
<li>如何安装配置和使用 Nix 包管理工具（类比 apt 那样使用）。</li>
<li>如何通过 Nix 为自己的项目配置一个可重现的开发环境（类比 Dockerfile）。</li>
<li>如何将已有的软件包发布为一个 Nix 包（类比构建一个 deb 包）。</li>
<li>介绍 Nix 各种机制的原理。</li>
<li>如何为自己的组织，私有化部署一套 Nix 基础设施（类似于建设一个 apt mirror 和一个私有 apt 源）。</li>
</ul>

<p>本文，是本系列的第一篇，将主要从传统的包管理器视角，介绍 nix 作为操作系统包管理器的相关能力。</p>

<p>本节参考：</p>

<ul>
<li><a href="https://blog.replit.com/nix-vs-docker">Will Nix Overtake Docker?</a></li>
<li><a href="https://nixos.org/manual/nix/stable/introduction.html">Nix Manual - Introduction</a></li>
<li><a href="https://nixos.org/">NixOS 首页</a></li>
<li><a href="https://nix.dev/">Welcome to nix.dev</a></li>
</ul>

<h2 id="快速开始">快速开始</h2>

<p>本节参考：</p>

<ul>
<li><a href="https://nixos.org/manual/nix/stable/quick-start.html">Nix Manual - 快速开始</a></li>
<li><a href="https://mirrors.tuna.tsinghua.edu.cn/help/nix/">清华大学 Nix 源</a></li>
</ul>

<h3 id="安装-nix">安装 Nix</h3>

<p>Nix 提供了两种安装方式，单用户安装和多用户安装。一般情况下单用户安装就足够了，因此本文介绍的是单用户安装。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 非中国大陆地区或全局科学上网用户，可以直接使用如下官方命令一键安装。</span>
bash &lt;<span style="color:#f92672">(</span>curl -L https://nixos.org/nix/install<span style="color:#f92672">)</span></code></pre></div>
<p>大陆用户，建议使用清华源，步骤如下：</p>

<ul>
<li><p>单用户安装且不自动添加包 channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sh &lt;<span style="color:#f92672">(</span>curl https://mirrors.tuna.tsinghua.edu.cn/nix/latest/install<span style="color:#f92672">)</span> --no-daemon --no-channel-add
source ~/.nix-profile/etc/profile.d/nix.sh</code></pre></div></li>

<li><p>配置官方包 channel nixpkgs 的二进制缓存服务。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p ~/.config/nix <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;substituters = https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store https://cache.nixos.org/&#39;</span> &gt; ~/.config/nix/nix.conf</code></pre></div></li>

<li><p>配置官方包 channel nixpkgs 的 mirrors。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 如下命令本质上是，将包 channel 配置写入 ~/.nix-channels 文件</span>
nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/nixpkgs-unstable nixpkgs
nix-channel --update <span style="color:#75715e"># 下载 channel 并更新 ~/.local/state/nix</span></code></pre></div></li>
</ul>

<h3 id="常用命令">常用命令</h3>

<p>以 Go 安装为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 更新 channel 源 （类似于 apt update)</span>
nix-channel --update
<span style="color:#75715e"># 搜索</span>
nix-env -qaP go
<span style="color:#75715e"># nixpkgs.go_1_18  go-1.18.10</span>
<span style="color:#75715e"># nixpkgs.go       go-1.19.5</span>
<span style="color:#75715e"># nixpkgs.go_1_20  go-1.20.1</span>

<span style="color:#75715e"># 安装</span>
nix-env -iA nixpkgs.go

<span style="color:#75715e"># 查看安装</span>
which go

<span style="color:#75715e"># 卸载 go (软件包并未删除，而是从环境变量里面去除)</span>
nix-env -e go
<span style="color:#75715e"># 真正删除没有被使用的软件包</span>
nix-collect-garbage -d

<span style="color:#75715e"># 更新 nix 自身</span>
nix-env -iA nixpkgs.nix nixpkgs.cacert</code></pre></div>
<h2 id="nix-包管理">Nix 包管理</h2>

<h3 id="概述">概述</h3>

<p>本章节介绍的是， 站在需要安装软件包的用户视角，如何使用 nix 获取、安装、升级、删除包。这些能力主要通过 nix-env 命令提供。</p>

<p>首先，一个包管理工具，必然有一个软件源（类似于 /etc/apt/source.list），在 Nix 中，被叫做 channel。因此，如上文快速开始所示，要使用 nix-env 之前，需要使用 <code>nix-channel</code> 子命令添加一个 channel。</p>

<p>然后即可使用 <code>nix-env</code> 对软件包进行管理。</p>

<ul>
<li><code>nix-env -qaP 关键词</code> 查询软件包。</li>
<li><code>nix-env -iA 包属性名</code> 安装软件包（<code>-A</code> 表示，使用包属性名定位软件包，格式为 <code>channel名.包名</code>）。</li>
<li><code>nix-env -e 包名</code> 卸载包（注意这里是包名）。</li>
<li><code>nix-env -uA 包属性名</code> 升级软件包。</li>
<li><code>nix-env -u</code> 升级所有软件包。</li>
</ul>

<h3 id="用户-profiles">用户 Profiles</h3>

<p>nix 通过 profile 机制，将安装的软件包应用到用户 shell 环境中。其原理如下：</p>

<ul>
<li><p>nix 在安装时，会在用户的 shell profile 中注入类似如下语句。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -e ~/.nix-profile/etc/profile.d/nix.sh <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> . ~/.nix-profile/etc/profile.d/nix.sh; <span style="color:#66d9ef">fi</span> <span style="color:#75715e"># added by Nix installer</span></code></pre></div></li>

<li><p>用户启动 shell 时，会执行 <code>. ~/.nix-profile/etc/profile.d/nix.sh</code> 脚本。该脚本的核心是给 PATH 添加 <code>~/.nix-profile/bin</code> 路径（通过 <code>echo $PATH</code> 可以看到)。观察 <code>~/.nix-profile</code>，可以看出：</p>

<ul>
<li><code>~/.nix-profile</code> 是一个软链，指向了 <code>/nix/var/nix/profiles/per-user/$username/profile</code>。</li>
<li><code>/nix/var/nix/profiles/per-user/$username/profile</code> 同样是一个软链，指向了 <code>profile-20-link</code></li>
<li><code>/nix/var/nix/profiles/per-user/$username/profile-20-link</code> 同样是一个软链，指向了 <code>/nix/store/g92kgz15smykgwqlhcd6lbphphqsm0a2-user-environment</code>。</li>

<li><p>最终，观察 <code>/nix/store/g92kgz15smykgwqlhcd6lbphphqsm0a2-user-environment/bin</code> （即 <code>~/.nix-profile/bin</code>），可以看到安装的软件的可执行文件的软链，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">hello -&gt; /nix/store/260q5867crm1xjs4khgqpl6vr9kywql1-hello-2.12.1/bin/hello
nix -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix
nix-build -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-build
nix-channel -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-channel
nix-collect-garbage -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-collect-garbage
nix-copy-closure -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-copy-closure
nix-daemon -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-daemon
nix-env -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-env
nix-hash -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-hash
nix-instantiate -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-instantiate
nix-prefetch-url -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-prefetch-url
nix-shell -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-shell
nix-store -&gt; /nix/store/n6vimgasfqxz4xbmbzyvh61llhrapya7-nix-2.14.1/bin/nix-store</pre></div></li>
</ul></li>

<li><p>在使用 <code>nix-env</code> 管理软件包时，流程应该如下所示：</p>

<ul>
<li>安装时，从 binary server 下载依赖和软件包到 <code>/nix/store</code> 中（或者本地编译，存储到 <code>/nix/store</code>）。</li>
<li>根据当前的 profile 即 <code>/nix/var/nix/profiles/per-user/$username/profile</code> 和安装、卸载的软件包的情况，生成一个新的 profile 目录，存放到 <code>/nix/store/$hash-user-environment</code></li>
<li>创建一个软链 <code>/nix/var/nix/profiles/per-user/$username/profile-$序号-link</code> 指向上一步的 profile。</li>
<li>修改软链 <code>/nix/var/nix/profiles/per-user/$username/profile</code> 指向 <code>profile-$序号-link</code>，完成。</li>
</ul></li>

<li><p>切换到历史上的其他版本。</p>

<ul>
<li><code>nix-env --list-generations</code> 查看历史所有环境列表</li>
<li><code>nix-env --rollback</code> 回滚到上一个版本，即将 <code>/nix/var/nix/profiles/per-user/$username/profile</code> 指向上一版本的 <code>profile-$序号-link</code>。</li>
<li><code>nix-env --switch-generation 43</code> 回滚到指定版本，即将 <code>/nix/var/nix/profiles/per-user/$username/profile</code> 指向上一版本的 <code>profile-43-link</code>。</li>
</ul></li>

<li><p>上面介绍的是默认的基于用户的 profile，nix 提供了生成和应用自定义 profile，而非使用 <code>/nix/var/nix/profiles/per-user/$username</code> 的方式。</p>

<ul>
<li><code>nix-env -p /nix/var/nix/profiles/other-profile -iA nixpkgs.nix nixpkgs.cacert nixpkgs.go</code> -p 参数可以手动指定 profile 的生成位置（注意，nix 自身不会自动添加）。</li>
<li><code>nix-env --switch-profile /nix/var/nix/profiles/other-profile</code> 将当前用户的 profile 切换到指定目录，即，修改 <code>~/.nix-profile</code> 软链的指向。</li>
</ul></li>

<li><p>垃圾回收机制。<code>nix-env</code> 核心是生成 profile 以及修改软链，<code>nix-env</code> 不会删除 /nix/sotre 下的软件包。因此需要通过 <code>nix-collect-garbage -d</code> 删除所有历史上的 profile 以及当前 profile 没有引用的，存放在 /nix/sotre 下的软件包。其原理是保留 <code>/nix/var/nix/gcroots</code> 中的和当前系统运行的进程中的，存在指向 <code>/nix/sotre</code> 的软件包（<code>nix-store --gc --print-roots</code> 可以通过该命令看到），其他则删除。</p></li>
</ul>

<p>本文描述了无 deamon 的单用户安装模式，因此默认只能单个用户（主用户）使用。根据本文的 profile 特性，实际上也是可以给其他用户使用，做法是：</p>

<ul>
<li>所有管理操作（包括安装包、配置 channel 等）都有主用户配置，这个主用户最好为 root。</li>
<li>为其他用户创建一个 profile （当然也可以复用主用户的 profile），然后创建软链，如 <code>~/.nix-profile -&gt; /nix/var/nix/profiles/per-user/rectcircle/profile</code>。</li>
<li>修改用户 profile 文件，如 <code>~/.bashrc</code>，添加 <code>if [ -e /home/rectcircle/.nix-profile/etc/profile.d/nix.sh ]; then . /home/rectcircle/.nix-profile/etc/profile.d/nix.sh; fi # added by Nix installer</code>。</li>
</ul>

<h3 id="channel-管理">Channel 管理</h3>

<p>在 Nix 中 Channel 类似于 apt source 的概念。可以通过如下命令，添加一个 Channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-channel --add https://nixos.org/channels/nixpkgs-unstable
<span style="color:#75715e"># 清华 mirror</span>
nix-channel --add https://mirrors.tuna.tsinghua.edu.cn/nix-channels/nixpkgs-unstable nixpkgs</code></pre></div>
<p>该命令会将配写入 <code>~/.nix-channels</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">https://mirrors.tuna.tsinghua.edu.cn/nix-channels/nixpkgs-unstable nixpkgs</pre></div>
<p>执行 <code>nix-channel --update</code> 将会从 url 中下载 channel 的内容，<code>nix-env</code> 会根据 <code>~/.nix-defexpr/channels</code> 获取到包信息，存储路径如下：</p>

<ul>
<li><code>~/.nix-defexpr/channels</code> 指向 <code>~/.local/state/nix/profiles/channels</code></li>
<li><code>~/.local/state/nix/profiles/channels</code> 指向 <code>channels-1-link</code></li>
<li><code>~/.local/state/nix/profiles/channels-1-link</code> 指向 <code>/nix/store/$hash-user-environment</code></li>

<li><p>因此 <code>~/.nix-defexpr/channels</code> 指向 <code>/nix/store/$hash-user-environment</code>，包含</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/nix/store/$hash-env-manifest.nix
/nix/store/$hash-nixpkgs/nixpkgs</pre></div></li>
</ul>

<p>最后，可以通过 <code>nix-channel --remove nixpkgs</code> 删除 channel。</p>

<p>本部分，只介绍使用者如何配置 channel。关于 channel 的目录结构，如何自定义一个私有 Channel，参见后续文章分析。</p>

<h3 id="安装旧版包">安装旧版包</h3>

<p>在 nix 中，官方的 Channel 是 <a href="https://github.com/NixOS/nixpkgs">nixpkgs</a>，这个 Channel 是通过 git 管理的。</p>

<p>通过 <code>nix-env -qaP go</code> 可以看到，目前最新版本提交的 nixpkgs 的 Go 只有最新的三个版本 1.18、1.19 和 1.20。</p>

<p>上文对于 Go 的安装，使用的是最新 commit 的 nixpkgs （通过 nix-channel 配置）。</p>

<p>而 <code>nix-env</code> 还提供了基于某个特殊版本的 nixpkgs 的安装机制。如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nix-env -iA go -f https://github.com/NixOS/nixpkgs/archive/d1c3fea7ecbed758168787fe4e4a3157e52bc808.tar.gz</code></pre></div>
<p>很多时候，我们希望，安装更旧版本的依赖时，就需要获取到包含更旧 Go 的配置的 nixpkgs 那个 commit 的快照。</p>

<p>因此，现在的问题是，如何通过包名查询历史版本对应的 commit，然后通过上文的类似于 <code>https://github.com/NixOS/nixpkgs/archive/$commitID.tar.gz</code> 的方式即可安装旧版本的包。</p>

<p>nixpkgs 官方并未提供该能力，但是幸运的是 nix 社区有一个站点可以查询这些信息： <a href="https://lazamar.co.uk/nix-versions/">https://lazamar.co.uk/nix-versions/</a> 。</p>

<p>其原理可以参见：<a href="https://lazamar.github.io/download-specific-package-version-with-nix/">该站点作者博客</a>。</p>

<p>此外，该项目已开源，参见： <a href="https://github.com/lazamar/nix-package-versions">lazamar/nix-package-versions</a>。</p>

<p>nixpkgs 官方关于安装旧版包的讨论参见：<a href="https://github.com/NixOS/nixpkgs/issues/9682">No way to install/use a specific package version? #9682</a>。</p>

<p>注意：</p>

<ul>
<li>中国大陆地区，建议先通过科学上网，clone 下整个 <a href="https://github.com/NixOS/nixpkgs">https://github.com/NixOS/nixpkgs</a> 仓库（几个 G 大小），然后 checkout 到指定版本，然后在通过 <code>nix-env -f</code> 指定到 nixpkgs 根目录目录。</li>
<li>从多个历史 commit 的 nixpkgs 安装包会导致磁盘占用快速上升。</li>
</ul>

<h2 id="安装脚本分析">安装脚本分析</h2>

<p>上文使用了 Nix 提供的安装脚本来安装 Nix（<a href="https://nixos.org/nix/install">官方</a> | <a href="https://mirrors.tuna.tsinghua.edu.cn/nix/latest/install">清华源</a>），其主要负责下载解压 nix 包，流程如下：</p>

<ul>
<li>根据当前操作系统情况，通过 wget 或 curl 对应架构的 <code>tar.xz</code> 包（以清华源、Linux x64 为例，地址为： <a href="https://mirrors.tuna.tsinghua.edu.cn/nix//nix-2.13.2/nix-2.13.2-x86_64-linux.tar.xz）到临时目录，大小约为">https://mirrors.tuna.tsinghua.edu.cn/nix//nix-2.13.2/nix-2.13.2-x86_64-linux.tar.xz）到临时目录，大小约为</a> 21M。</li>
<li>使用 sha256 （shasum 命令）校验包完整性，并解压（<code>tar -xJf &quot;$tarball&quot; -C &quot;$unpack&quot;</code>）到临时目录。</li>
<li>执行解压后的软件包里面的安装脚本 <code>$unpack/*/install</code>，并把命令行参数传递给他。</li>
</ul>

<p>先观察 nix 包主要包含各个平台的安装脚本和软件包存储存储目录 <code>store</code>，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">create-darwin-volume.sh
install
install-darwin-multi-user.sh
install-multi-user
install-systemd-multi-user.sh
.reginfo
store/</pre></div>
<p>nix 包安装脚本 <code>install</code> 流程如下（单用户模式）：</p>

<ul>
<li>设置环境变量（如 <code>nix=/nix/store/lsr79q5xqd9dv97wn87x12kzax8s8i1s-nix-2.13.2</code>），检查 nix 包目录、系统环境变量是否满足条件。</li>
<li>检查 <code>/nix</code> 目录是否存在，不存在则调用创建该目录并将该目录 owner 设置为当前执行安装脚本的用户，此刻需要用户输入 sudo 密码。</li>
<li>创建 <code>/nix/store</code> 和 <code>/nix/var/nix</code>。</li>
<li>将 <code>store/</code> 的全部目录复制到 <code>/nix/store</code> 目录中。</li>
<li>使用 <code>$nix/bin/nix-store --load-db</code> 命令加载数据库文件 <code>.reginfo</code>。</li>
<li>执行 <code>&quot;$nix/bin/nix-env&quot; -i &quot;$nix&quot;</code>，为当前用户创建 profile 文件，此步骤会创建 <code>~/.nix-profile/</code> 目录。</li>
<li>默认情况下，会执行 <code>&quot;$nix/bin/nix-channel&quot; --add https://nixos.org/channels/nixpkgs-unstable</code> 添加 nix 官方 channel nixpkgs，该步骤会创建 <code>~/.nix-channels</code> 文件（如上文所示，大陆地区使用 <code>--no-channel-add</code> 不添加，否则 update 阶段会很慢）。并执行 <code>&quot;$nix/bin/nix-channel&quot; --update nixpkgs</code>，该步骤会添加 <code>~/.nix-defexpr</code> 目录。</li>
<li>最后，会根据当前用户安装的 shell 情况，将类似于 <code>if [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then . ~/.nix-profile/etc/profile.d/nix.sh; fi # added by Nix installer</code> 的启用 nix 的语句添加到各个 shell 的配置文件中（目前支持：sh、bash、zsh、fish）。</li>
</ul>

<p>总结一下，单用户模式安装，nix 对系统的影响如下：</p>

<ul>
<li>添加 <code>/nix</code> 目录，约 100M。</li>
<li>添加 <code>~/.nix-profile</code> 软链。</li>
<li>添加 <code>~/.nix-channels</code> 文件。</li>
<li>添加 <code>~/.nix-defexpr</code> 目录。</li>
<li>添加 <code>~/.local/state/nix</code> 目录。</li>
</ul>

<p>因此如果想完全卸载单用户安装的 nix，直接删除掉上述文件和目录即可。</p>
]]></description></item><item><title>Linux 有用的内核参数</title><link>https://www.rectcircle.cn/posts/linux-useful-kernal-param/</link><pubDate>Mon, 20 Feb 2023 22:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-useful-kernal-param/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>Linux 提供了一些可以配置的内核参数，来配置内核的行为。</p>

<p>这些内核参数的读取和配置，底层都是通过 <code>/proc/sys</code> 虚拟文件文件系统提供的。</p>

<p>一般情况下，不需要直接操作 <code>/proc/sys</code> 虚拟文件系统，而是通过如下方式进行配置：</p>

<ul>
<li>临时配置，通过 <code>sysctl</code> 命令读取和配置。

<ul>
<li>读取，如 <code>sudo sysctl net.ipv4.ip_forward</code>。</li>
<li>配置，如 <code>sudo sysctl -w net.ipv4.ip_forward = 1</code>。</li>
</ul></li>
<li>永久配置，通过编写 <code>/etc/sysctl.conf</code> 或 <code>/etc/sysctl.d/*</code> 进行配置。

<ul>
<li>手动执行 <code>sudo sysctl -p</code> 将立即生效。</li>
<li>重启后将永久生效。</li>
</ul></li>
</ul>

<p>本文将介绍一些常见的参数。</p>

<h2 id="文件系统-watch">文件系统 watch</h2>

<h3 id="fs-inotify-max-user-watches"><code>fs.inotify.max_user_watches</code></h3>

<p>该参数限制了一个用户最多可以监视的文件或目录的数量（debian 11 默认为 8192）。在使用 VSCode、启动热编译 Server （主要是前端场景）等，需要通过 inotify 机制，监听大量文件变更的场景中，建议调大该参数，如 <code>fs.inotify.max_user_watches=524288</code>。</p>

<h3 id="fs-inotify-max-user-instances"><code>fs.inotify.max_user_instances</code></h3>

<p>该参数限制了一个用户可以创建的 inotify 实例的数量（debian 11 默认为 128）。在使用 VSCode、启动热编译 Server （主要是前端场景）等场景，调大了 <code>fs.inotify.max_user_watches</code> 参数，仍然报错时，可以尝试同时调大该参数。</p>

<h2 id="网络相关">网络相关</h2>

<h3 id="net-ipv4-ip-forward"><code>net.ipv4.ip_forward</code></h3>

<p>是否开启 ipv4 的 ip 转发特性（debian 11 默认为 0）。当需要转发到达该主机的外部的数据包到其他网络接口时需要开启，具体场景如下：</p>

<ul>
<li>当前主机安装了虚拟机、 Docker、 Kubenates 等虚拟化容器平台时。</li>
<li>当前主机作为二层/三层交换机，处理局域网的流量时。</li>
</ul>

<h3 id="net-ipv6-conf-all-forwarding"><code>net.ipv6.conf.all.forwarding</code></h3>

<p>和 ipv4 的 <code>net.ipv4.ip_forward</code> 一致，该参数是对应的 ipv6 的版本。其他参见上文。</p>

<h3 id="net-ipv4-ip-local-port-range"><code>net.ipv4.ip_local_port_range</code></h3>

<p>在 TCP/IP 协议中，一次网络通讯是由 <code>&lt;local_ip, local_port, remote_ip, remote_port, proto&gt;</code> 五元组标识的。在使用 Socket API 创建一个 TCP/UDP Client 时，我们实际上只给出了 <code>remote_ip, remote_port, proto</code>。而 <code>local_ip, local_port</code> 是有内核决定的。</p>

<p>其中 <code>local_port</code> 是内核随机选择的一个，而可选的范围就是 <code>net.ipv4.ip_local_port_range</code> 参数决定的。</p>

<p><code>net.ipv4.ip_local_port_range</code> 在 debian 11 中，默认值为 <code>32768 61000</code> （这里的 32768 刚好是 <code>2^15</code>）。</p>

<p>当系统上的应用程序（作为客户端）需要打开大量的网络连接时，可能会出现本地端口号不够用的情况。可以通过调整 <code>net.ipv4.ip_local_port_range</code> 参数的值来增加可供分配的本地端口号的数量，从而缓解该问题。</p>

<p>特别注意的是：</p>

<ul>
<li>Kubenates Serivice 的 NodePort 的可选范围是 <code>30000-32767</code>，如果修改 <code>net.ipv4.ip_local_port_range</code>，那么 k8s 也要进行对应的修改，防止两者存在交叉。</li>
<li>在系统上运行的 TCP/IP Server 应用程序监听端口时，不要选择 <code>net.ipv4.ip_local_port_range</code> 范围内的端口。否则，可能存在小概率的端口冲突问题。</li>
</ul>

<p>综上，万不得已不建议修改该参数。</p>
]]></description></item><item><title>SSH 反向代理</title><link>https://www.rectcircle.cn/posts/ssh-reverse-proxy/</link><pubDate>Sat, 28 Jan 2023 00:35:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/ssh-reverse-proxy/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>对于 HTTP 协议，存在很多通用的反向代理的软件和库，如：</p>

<ul>
<li>Nginx 软件。</li>
<li>Go 标准库的 <a href="https://pkg.go.dev/net/http/httputil#ReverseProxy"><code>net/http/httputil.ReverseProxy</code></a>。</li>
</ul>

<p>但是，在 SSH 协议方面，并没有找到类似的反向代理的软件和库。</p>

<p>因此，本文将探索，如何使用 Go 实现一个通用 SSH 反向代理库。</p>

<h2 id="需求">需求</h2>

<p>SSH 通用反向代理库需满足如下需求：</p>

<ul>
<li>认证拦截：client 和 proxy 之间，proxy 和 server 之间的认证是解耦的，可以灵活配置的。</li>
<li>功能透明：client 通过 proxy 连接到 server 所具备的能力需和 client 直连 server 所具备的能力对等。</li>
<li>可审计：proxy 需要可以拿到 SSH 数据包的明文数据，以支持功能过滤，访问记录。</li>
</ul>

<h2 id="设计">设计</h2>

<p>根据 <a href="/posts/ssh-protocol-and-go-lib/#ssh-协议">SSH 协议 和 Go SSH 库源码浅析 - ssh-协议</a> 可以得知，SSH 协议从顶到低可以分为三层：</p>

<ul>
<li>连接协议。</li>
<li>认证协议。</li>
<li>传输层协议。</li>
</ul>

<p>根据需求和 SSH 协议特点，一个经过 proxy 的 SSH 连接流量如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"> client                               proxy                          server
                              server        client
+-----------+            +----------------------------+            +-----------+ 
|  连接协议  |  &lt;------&gt;  |   连接协议 &lt;--2--&gt; 连接协议   |  &lt;------&gt;  |  连接协议  |    
|  认证协议  |  &lt;--1---&gt;  |    认证协议        认证协议   |  &lt;---3--&gt;  |  认证协议  |    
| 传输层协议  |  &lt;-----&gt;  |   传输层协议       传输层协议  |  &lt;------&gt;  | 传输层协议  |    
+-----------+            +----------------------------+            +-----------+ </pre></div>
<p>说明：</p>

<ul>
<li>proxy 包含两个部分，分别是一个 ssh server 和 ssh client。proxy ssh server 对接用户 ssh client，proxy ssh client 对接目标 ssh server。</li>
<li>上图 1，用户 ssh client 认证数据包对接 proxy ssh server。上图 3，proxy ssh client 认证数据包对接目标 ssh server。这两个部分为了支持 SSH 认证的拦截。</li>
<li>上图 2，是连接协议的数据包，是明文数据。

<ul>
<li>对这些数据包，可以进行审计。</li>
<li>对于审计通过的数据包，简单的进行 io copy 即可实现功能透明。</li>
</ul></li>
</ul>

<h2 id="实现">实现</h2>

<p>从上面的设计可以得知，如果想实现一个通用的 SSH 反向代理，只需实现 SSH 的认证和传输层协议。而对于连接协议，Proxy 不需要按照协议流程进行处理，而是由用户 client 和目标 server 进行处理，Proxy 简单的 io copy 即可（也可能需要进行某些数据包进行审计）。</p>

<p>根据 <a href="/posts/ssh-protocol-and-go-lib/#ssh-dial-源码">SSH 协议 和 Go SSH 库源码浅析 - ssh.Dial 源码</a>，可以得知，<code>ssh.connection.clientHandshake</code> 就已经实现了传输层协议和认证协议的流程。因此，基于只需基于 <a href="https://cs.opensource.google/go/x/crypto">golang.org/x/crypto</a> 进行少量的二次开发即可实现。</p>

<p>但是，<a href="https://cs.opensource.google/go/x/crypto">golang.org/x/crypto</a> 并没有将 SSH 传输层协议和认证协议的封装对象 API 暴露出来，因此不能通过 go mod 导入。解决的办法只能是将 <code>ssh.connection</code> 相关源码直接复制到项目里面，进行调用。</p>

<p>完成依赖的源码准备后，我们添加一个源文件，来讲 <code>ssh.connection</code> 能力暴露出去，<a href="https://github.com/rectcircle/sshpass_proxy">源码</a>如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">ssh</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">TrickTransport</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">connection</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>) <span style="color:#a6e22e">WritePacket</span>(<span style="color:#a6e22e">p</span> []<span style="color:#66d9ef">byte</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">transport</span>.<span style="color:#a6e22e">writePacket</span>(<span style="color:#a6e22e">p</span>)
}
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>) <span style="color:#a6e22e">ReadPacket</span>() ([]<span style="color:#66d9ef">byte</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">transport</span>.<span style="color:#a6e22e">readPacket</span>()
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>) <span style="color:#a6e22e">User</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">User</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewServerTrickTransport</span>(<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">config</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ServerConfig</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">fullConf</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">config</span>
	<span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">SetDefaults</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">MaxAuthTries</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
		<span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">MaxAuthTries</span> = <span style="color:#ae81ff">6</span>
	}
	<span style="color:#75715e">// Check if the config contains any unsupported key exchanges
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">kex</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">KeyExchanges</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">serverForbiddenKexAlgos</span>[<span style="color:#a6e22e">kex</span>]; <span style="color:#a6e22e">ok</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;ssh: unsupported key exchange %s for server&#34;</span>, <span style="color:#a6e22e">kex</span>)
		}
	}

	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">connection</span>{
		<span style="color:#a6e22e">sshConn</span>: <span style="color:#a6e22e">sshConn</span>{<span style="color:#a6e22e">conn</span>: <span style="color:#a6e22e">c</span>},
	}
	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">serverHandshake</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fullConf</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Close</span>()
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">TrickTransport</span>{
		<span style="color:#a6e22e">c</span>: <span style="color:#a6e22e">s</span>,
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClientTrickTransport</span>(<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">addr</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">config</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ClientConfig</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">fullConf</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">config</span>
	<span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">SetDefaults</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">HostKeyCallback</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Close</span>()
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;ssh: must specify HostKeyCallback&#34;</span>)
	}

	<span style="color:#a6e22e">conn</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">connection</span>{
		<span style="color:#a6e22e">sshConn</span>: <span style="color:#a6e22e">sshConn</span>{<span style="color:#a6e22e">conn</span>: <span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">user</span>: <span style="color:#a6e22e">fullConf</span>.<span style="color:#a6e22e">User</span>},
	}

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conn</span>.<span style="color:#a6e22e">clientHandshake</span>(<span style="color:#a6e22e">addr</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fullConf</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Close</span>()
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;ssh: handshake failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">TrickTransport</span>{
		<span style="color:#a6e22e">c</span>: <span style="color:#a6e22e">conn</span>,
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TrickTransportPacketCopy</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">TrickTransport</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">for</span> {
		<span style="color:#a6e22e">bytes</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">ReadPacket</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
		<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">WritePacket</span>(<span style="color:#a6e22e">bytes</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ErrIsDisconnectedByUser</span>(<span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) <span style="color:#66d9ef">bool</span> {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">e</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">err</span>.(<span style="color:#f92672">*</span><span style="color:#a6e22e">disconnectMsg</span>); <span style="color:#a6e22e">ok</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">Reason</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">11</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
}</code></pre></div>
<p>至此，proxy ssh server 和 proxy ssh client 实现完成。</p>

<h2 id="应用">应用</h2>

<p>这里利用上述库，实现一个 sshpass_proxy 命令，该命令类似于 sshpass，详见：<a href="https://github.com/rectcircle/sshpass_proxy/blob/master/README.md">sshpass_proxy README</a>。</p>

<p>大概逻辑如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          ssh                             sshpass_proxy                          server
                                       server        client
         +-----------+            +----------------------------+            +-----------+ 
         |  连接协议  |  &lt;------&gt;  |   连接协议 &lt;--2--&gt; 连接协议   |  &lt;------&gt;  |  连接协议  |    
         |  认证协议  |  &lt;--1---&gt;  |    认证协议        认证协议   |  &lt;---3--&gt;  |  认证协议  |    
         | 传输层协议  |  &lt;-----&gt;  |   传输层协议       传输层协议  |  &lt;------&gt;  | 传输层协议  |    
         +-----------+            +----------------------------+            +-----------+ 

底层连接             &lt;---- 4: stdio ----&gt;                     &lt;---- 5: tcp ----&gt;</pre></div>
<ul>
<li>上图 1 采用 none 身份认证，即不要认证直接连接。</li>
<li>上图 2 不做任何审计，只进行简单的 io copy。</li>
<li>上图 3 采用密码认证。</li>
<li>上图 4 ssh 的 <code>ProxyCommand</code> 参数，配置的是 ssh 和 sshpass_proxy 之间的底层连接是 stdio。</li>
<li>上图 5 sshpass_proxy 和目标 server 之间的底层连接是 tcp。</li>
</ul>

<p>核心<a href="https://github.com/rectcircle/sshpass_proxy/blob/master/sshpass_proxy.go">源码</a>如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">sshpass_proxy</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>

	<span style="color:#e6db74">&#34;github.com/rectcircle/sshpass_proxy/crypto/ssh&#34;</span>
)

<span style="color:#75715e">// SSHPassProxy 功能类似于 sshpass，但是原理完全不同。
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// SSHPassProxy 通过一个 ssh 传输层协议 proxy，实现 openssh 的客户端，可以对使用密码鉴权的 ssh server 实现免密登录。
</span><span style="color:#75715e">//
</span><span style="color:#75715e">//                                                           SSHPassProxy
</span><span style="color:#75715e">//  +--------+           +------------------------------------------------------------------------------+          +--------+
</span><span style="color:#75715e">//  | client | ---&gt; (clientConn) ssh transport server &lt;--- Packet Copy ---&gt; ssh transport client (serverConn) ---&gt; | server |
</span><span style="color:#75715e">//  +--------+           +------------------------------------------------------------------------------+          +--------+
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">SSHPassProxy</span>(
	<span style="color:#a6e22e">clientConn</span>, <span style="color:#a6e22e">serverConn</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>,
	<span style="color:#a6e22e">proxyServerHostPrivateKey</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Signer</span>,
	<span style="color:#a6e22e">serverAddr</span>, <span style="color:#a6e22e">serverPassword</span> <span style="color:#66d9ef">string</span>,
) <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 1. 使用 ssh server 对接来 client 连接，完成握手和免密鉴权，并获取到 ssh 传输层协议对象。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">proxyServerConfig</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ServerConfig</span>{
		<span style="color:#a6e22e">NoClientAuth</span>: <span style="color:#66d9ef">true</span>,
	}
	<span style="color:#a6e22e">proxyServerConfig</span>.<span style="color:#a6e22e">AddHostKey</span>(<span style="color:#a6e22e">proxyServerHostPrivateKey</span>)
	<span style="color:#a6e22e">proxyServerTransport</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">NewServerTrickTransport</span>(<span style="color:#a6e22e">clientConn</span>, <span style="color:#a6e22e">proxyServerConfig</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;failed handshake and authenticate with sshpass proxy server: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">serverUser</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">proxyServerTransport</span>.<span style="color:#a6e22e">User</span>()
	<span style="color:#75715e">// 2. 使用 ssh client 对接 server 连接，完成握手和密码鉴权，并获取到 ssh 传输层协议对象。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">proxyClientConfig</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ClientConfig</span>{
		<span style="color:#a6e22e">User</span>: <span style="color:#a6e22e">serverUser</span>,
		<span style="color:#a6e22e">Auth</span>: []<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">AuthMethod</span>{
			<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Password</span>(<span style="color:#a6e22e">serverPassword</span>),
		},
		<span style="color:#a6e22e">HostKeyCallback</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">hostname</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">remote</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Addr</span>, <span style="color:#a6e22e">key</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">PublicKey</span>) <span style="color:#66d9ef">error</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span> },
	}
	<span style="color:#a6e22e">proxyClientTransport</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">NewClientTrickTransport</span>(<span style="color:#a6e22e">serverConn</span>, <span style="color:#a6e22e">serverAddr</span>, <span style="color:#a6e22e">proxyClientConfig</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;failed handshake and authenticate with target server: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 转发
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errc</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">errc</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">TrickTransportPacketCopy</span>(<span style="color:#a6e22e">proxyServerTransport</span>, <span style="color:#a6e22e">proxyClientTransport</span>)
	}()
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">errc</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">TrickTransportPacketCopy</span>(<span style="color:#a6e22e">proxyClientTransport</span>, <span style="color:#a6e22e">proxyServerTransport</span>)
	}()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">errc</span>; <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ErrIsDisconnectedByUser</span>(<span style="color:#a6e22e">err</span>) {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}</code></pre></div>]]></description></item><item><title>SSH 协议 和 Go SSH 库源码浅析</title><link>https://www.rectcircle.cn/posts/ssh-protocol-and-go-lib/</link><pubDate>Fri, 27 Jan 2023 00:00:10 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/ssh-protocol-and-go-lib/</guid><description type="html"><![CDATA[

<h2 id="导读">导读</h2>

<p>SSH, The Secure Shell Protocol (安全 Shell 协议)，是一个使用广泛的网络协议。</p>

<p>在中文互联网世界，关于 SSH 协议的介绍，往往都把重点放到了安全（Secure）方面的细节。这样的文章对于开发者来说，意义并不大，原因在于：</p>

<ul>
<li>此类文章是以密码学为基础的。而密码学专业程度较高，对于开发者来说理解成本高。</li>
<li>其次，SSH 安全算法部分是 SSH 协议中最不可变的部分。即使完全理解了这部分，对于对 SSH 协议的二次开发，也没有什么帮助。</li>
</ul>

<p>因此，本文不会仔细介绍 SSH 中 Secure 的细节。而是从整体和分层的角度尝试理解协议作者的设计考量。</p>

<p>和 HTTP 协议一样，SSH 协议是一个标准化的协议，由 <a href="https://www.ietf.org/">IETF</a> 制定，主要的 RFC 有：</p>

<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc4251">RFC 4251: The Secure Shell (SSH) Protocol Architecture</a></li>
<li><a href="https://www.rfc-editor.org/rfc/rfc4252">RFC 4252: The Secure Shell (SSH) Authentication Protocol</a></li>
<li><a href="https://www.rfc-editor.org/rfc/rfc4253">RFC 4253: The Secure Shell (SSH) Transport Layer Protocol</a></li>
<li><a href="https://www.rfc-editor.org/rfc/rfc4254">RFC 4254: The Secure Shell (SSH) Connection Protocol</a></li>
</ul>

<p>还有一些<a href="https://www.omnisecu.com/tcpip/important-rfc-related-with-ssh.php">其他 RFC</a> 在实际场景中应用较窄，在此就不列举了。</p>

<p>RFC 文档是网络协议的完整定义，追求的是无歧义和准确性，这导致 RFC 文档对于初学者不够友好，比较晦涩。因此，本文对 SSH 协议的介绍不会按照 RFC 的顺序和结构来进行，而是按照更符合人类认知的方式来进行。对于一些重要的部分，本文会给出对应的 RFC 章节的引用，以方便定位。</p>

<p>本文假设读者使用过 SSH 客户端进行过远程登录。行文上，本文会以：从整体到局部，从低层到顶层，介绍 SSH 协议的包结构。然后以 SSH 登录一台主机执行一条命令的场景为例，通过追踪 Google 维护的 Go SSH 库 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh"><code>x/crypto/ssh</code></a> 的源码，来实际感受 SSH 协议的整个流程。本文希望读者可以：真正理解 SSH 的整体流程，理解 SSH 协议的设计考量，初步具备对 SSH 协议进行二次开发的能力。</p>

<h2 id="ssh-协议">SSH 协议</h2>

<h3 id="ssh-协议架构">SSH 协议架构</h3>

<blockquote>
<p>本部分主要来自于： <a href="https://www.rfc-editor.org/rfc/rfc4252">rfc4251</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">high level
             +-------------------------+---------------------+
             | Authentication Protocol | Connection Protocol |
             +-------------------------+---------------------+
             |           Transport Layer Protocol            |
             +-----------------------------------------------+
             |              Underlying Connection            |
             +-----------------------------------------------+
low  level</pre></div>
<p>SSH 协议由 3 个子协议构成。从底层到顶层分别是：</p>

<ul>
<li>传输层协议（<a href="https://www.rfc-editor.org/rfc/rfc4253">rfc4253</a>），定义了 SSH 协议数据包的格式以及 Key 交换算法。</li>
<li>认证协议（<a href="https://www.rfc-editor.org/rfc/rfc4252">rfc4252</a>），定义了 SSH 协议支持的用户身份认证算法。</li>
<li>连接协议（<a href="https://www.rfc-editor.org/rfc/rfc4254">rfc4254</a>），定义了 SSH 支持功能特性：交互式登录会话、TCP/IP 端口转发、X11 Forwarding。</li>
</ul>

<p>需要特别说明的是：</p>

<ul>
<li>传输层协议底层连接默认是 TCP 协议。但是，这并不是强制的，在现实中，SSH 可以运行在任意提供可靠性保证的底层连接之上。</li>
<li>从层次再看认证协议和连接协议可以认为处于同一层。从时序上来看，认证协议是连接协议的前置条件。</li>
</ul>

<h3 id="ssh-传输层协议">SSH 传输层协议</h3>

<h4 id="数据包-packet-结构">数据包 (Packet) 结构</h4>

<ul>
<li>字节序：大端（网络字节序）</li>
<li>SSH 最小传输单元为数据包 (Packet)，两个方向的数据包格式是一致的。</li>
<li>数据包格式如下：

<ul>
<li><code>uint32</code>    packet_length = len(payload) + len(padding) + 1。</li>
<li><code>byte</code>      padding_length = len(padding)。</li>
<li><code>[]byte</code>    payload。有效负载，消息 Message。</li>
<li><code>[]byte</code>    padding，随机字节数组。</li>
<li><code>[]byte</code>    mac (Message Authentication Code - MAC)</li>
</ul></li>
<li>数据包字段加密方式如下所示：

<ul>
<li>packet_length 和 packet_length 作为整体加密：<code>crypto/cipher.Stream.XORKeyStream(byte[0:5], byte[0:5])</code></li>
<li>payload 加密：<code>crypto/cipher.Stream.XORKeyStream(payload, payload)</code></li>
<li>padding 加密：<code>crypto/cipher.Stream.XORKeyStream(padding, padding)</code></li>
<li>mac 不需要加密</li>
</ul></li>
</ul>

<p>（更多参见：<a href="https://www.rfc-editor.org/rfc/rfc4253#section-6">rfc4253#section-6</a>）</p>

<h4 id="消息结构">消息结构</h4>

<p>Packet 定义的是 SSH 协议的最小传输单元，SSH 协议真正的业务数据是放在 payload 部分中的。在 SSH 协议中，payload 部分被称为消息 Message。</p>

<p>消息的格式各不相同，总的来说是由消息的类型来决定的，因此从整体看消息的结构为：</p>

<ul>
<li><code>byte</code>      消息类型编号。</li>
<li><code>[]byte</code>    消息数据，具体定义由消息类型决定。</li>
</ul>

<p>消息数据部分，可能包含多个字段，不同的字段的序列化方式参见：<a href="https://www.rfc-editor.org/rfc/rfc4251#section-5">rfc4251#section-5</a>。</p>

<p>SSH 协议对消息编号按照子协议类型进行了划分（<a href="https://www.rfc-editor.org/rfc/rfc4251#section-7">rfc4251#section-7</a>）：</p>

<ul>
<li>传输层协议：

<ul>
<li>1~19 传输层通用消息，如 disconnect, ignore, debug 等等。</li>
<li>20~29 Key 交换算法协商（参见下文：传输层协议流程）。</li>
<li>30~49 Key 交换（同一个编号，在不同的 Key 交换算法中定义是不同的）。</li>
</ul></li>
<li>认证协议：

<ul>
<li>50~59 用户认证通用消息。</li>
<li>60~79 给特定的用户认证方法使用（同一个编号，在不同的认证方法中定义是不同的）。</li>
</ul></li>
<li>连接协议：

<ul>
<li>80~89 连接协议通用消息。</li>
<li>90~127 Channel 相关消息。</li>
</ul></li>
<li>为客户端协议保留：128~191。</li>
<li>本地扩展：192~255。</li>
</ul>

<h4 id="传输层协议流程">传输层协议流程</h4>

<ol>
<li>建立底层连接（以 TCP 协议为例）：

<ul>
<li>Client 请求建立 TCP连接。</li>
<li>Server Accept 完成 TCP 连接建立。</li>
</ul></li>
<li>协议版本交换（<a href="https://www.rfc-editor.org/rfc/rfc4253#section-4.2">rfc4253#section-4.2</a>）：。

<ul>
<li>Client 发送字符串，必须以 <code>SSH-2.0-</code> 开头，以 <code>\r\n</code> 结尾。这部分可以是任意 ASCII 码 <code>&gt; 32</code> 的字符。如  <code>SSH-2.0-Go\r\n</code>，。</li>
<li>Server 发送字符串，格式要求和 Client 一致。如 <code>SSH-2.0-dropbear_2022.83\r\n</code>。</li>
</ul></li>
<li>Key 交换算法协商，参见：<a href="https://www.rfc-editor.org/rfc/rfc4253#section-7.1">rfc4253#section-7.1</a>，也可以参考下文具体编码示例。</li>
<li>Key 交换算法执行，比如 Diffie-Hellman Key Exchange 参见：<a href="https://www.rfc-editor.org/rfc/rfc4253#section-8">rfc4253#section-8</a>，也可以参考下文具体编码示例。</li>
</ol>

<p>解释：</p>

<ul>
<li>上述第 2、3、4 步，是 SSH 协议中的仅有的明文传输的部分。</li>
<li>上述第 2 步，是 SSH 协议中唯一一个消息格式不符合上文包格式定义的流程。本文介绍的 SSH 协议实际上是 SSH 协议的第 2 版。和其他网络协议类似，SSH 协议也是先有了实现，再进行标准化。因此在这一步，使用了文本格式，以实现对历史上旧版本的识别和兼容。</li>
<li>上述第 2 步，Client 和 Server 发送的字符串，没有前后依赖关系，一般情况下，在建立底层连接后，Client、Server 会立即向对方发送版本信息。</li>
<li>上述第 3、4 步，是 SSH 协议号称安全的关键步骤。SSH 的核心目标就是在不安全的底层连接（如 TCP）之上，建立一个安全的连接，以实现远程登录，端口转发等特性。因此，自然而然的想法就是对传输的数据进行加密。但是，加密必然需要 Client 和 Server 拥有配对的特定秘钥（key），这就是秘钥分发问题。<strong>非对称加密算法</strong>天然不存在秘钥分发问题，一种办法是所有数据均使用<strong>非对称加密算法</strong>加密，但是<strong>非对称加密算法</strong>性能太差，加解密成本难以接受。因此实际上 SSH 协议采用了如下思路：真正的数据加密仍然使用<strong>对称加密算法</strong>，而对称加密算法的秘钥，由<strong>非对称的加密算法</strong>进行保护，此类算法在 SSH 协议中有很多种，被称为 Key 交换算法。因为 Key 交换算法是 SSH 安全性的基石。没人可以 100% 保证某个 Key 交换算法一定是安全的。因此 SSH 协议在执行 Key 交换算法之前，需先进行 Key 交换算法协商，来确定要使用哪种 Key 交换算法。</li>
<li>上述第 3、4 步，不仅仅只在第一次连接执行一次，在整个 SSH 连接期间，会根据一些策略，重新执行以生成新的 Key，以保证安全性。</li>
</ul>

<h3 id="ssh-认证协议">SSH 认证协议</h3>

<p>SSH 支持如下几种身份认证协议：</p>

<ul>
<li><code>none</code>，服务端关闭身份认证，也就是说，任意用户都可以连接到该服务端（<a href="https://www.rfc-editor.org/rfc/rfc4252#section-5.2">rfc4252#section-5.2</a>）。</li>
<li><code>publickey</code>，基于公钥的身份认证（<a href="https://www.rfc-editor.org/rfc/rfc4252#section-7">rfc4252#section-7</a>）。</li>
<li><code>password</code>，基于密码的身份认证。（<a href="https://www.rfc-editor.org/rfc/rfc4252#section-8">rfc4252#section-8</a>）</li>
<li><code>hostbased</code>，比较少见，略（<a href="https://www.rfc-editor.org/rfc/rfc4252#section-9">rfc4252#section-9</a>）。</li>
<li><code>GSS-API</code>，校验 （<a href="https://www.rfc-editor.org/rfc/rfc4252">rfc4462</a>）。</li>
</ul>

<p>具体细节本部分就不多赘述了，想了解更多，可以参考上文 RFC 文档，也可以参见下文示例代码。</p>

<h3 id="ssh-连接协议">SSH 连接协议</h3>

<h4 id="channel">Channel</h4>

<p>SSH 连接协议定义的交互式登录终端会话、TCP/IP 端口转发、X11 Forwarding 的这些功能，都工作在自己的通道 (Channel) 之上的。</p>

<p>在 SSH 协议中，Channel 实现了对底层连接的多路复用，就是一个虚拟连接，这就是该子协议叫做连接协议的原因。具体而言 Channel：</p>

<ul>
<li>通过一个数字来进行标识和区分这些 Channel。</li>
<li>实现流控 （窗口）。</li>
</ul>

<p>因此，SSH 连接协议实现的这些功能，都需先建立 Channel，流程如下：</p>

<ul>
<li><p>服务端和客户端任意一方，发送类型为 <code>SSH_MSG_CHANNEL_OPEN</code> (90) 的消息，通知对方需要建立 Channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN (90)
string    channel type, 可选值为: &#39;session&#39;, &#39;x11&#39;, &#39;forwarded-tcpip&#39;, &#39;direct-tcpip&#39; 参见 https://www.rfc-editor.org/rfc/rfc4250#section-4.9.1
uint32    sender channel 编号
uint32    初始化窗口大小
uint32    最大包大小
....      下面是 channel type 特定数据</pre></div></li>

<li><p>另一方接收到消息后，回复类型为 <code>SSH_MSG_CHANNEL_OPEN_CONFIRMATION</code> (91) 或 <code>SSH_MSG_CHANNEL_OPEN_FAILURE</code> (92) 的消息来告知打开成功或者失败。
成功定义如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN_CONFIRMATION (91)
uint32    recipient channel 编号，这个是 SSH_MSG_CHANNEL_OPEN 中 sender channel 的值
uint32    sender channel 编号
uint32    初始化窗口大小
uint32    最大包大小
....      下面是 channel type 特定数据</pre></div>
<p>失败定义如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN_FAILURE (92)
uint32    recipient channel
uint32    错误码 reason code
string    描述，格式为 ISO-10646 UTF-8 encoding [RFC3629]
string    language tag [RFC3066]</pre></div>
<p>预定义的错误码定义如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    Symbolic name                           reason code
    -------------                           -----------
SSH_OPEN_ADMINISTRATIVELY_PROHIBITED          1
SSH_OPEN_CONNECT_FAILED                       2
SSH_OPEN_UNKNOWN_CHANNEL_TYPE                 3
SSH_OPEN_RESOURCE_SHORTAGE                    4</pre></div></li>
</ul>

<p>上文介绍了 Channel 建立的过程，细节参见 <a href="https://www.rfc-editor.org/rfc/rfc4254#section-5.1">rfc4254#section-5.1</a>。</p>

<p>Channel 建立完成后，在 Channel 中进行数据传输，主要有：</p>

<ul>
<li><p>流量控制类消息，调节窗口大小。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_WINDOW_ADJUST
uint32    recipient channel
uint32    bytes to add</pre></div></li>

<li><p>数据消息，消息的长度为 <code>min(数据长度, 窗口大小, 传输层协议的限制)</code>。</p>

<ul>
<li><p>普通数据，如交互式会话的标准输入、标准输出。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_DATA
uint32    recipient channel
string    data</pre></div></li>

<li><p>扩展数据，如交互式会话的标准出错，标准出错对应 data_type_code 为 1，是 <code>data_type_code</code> 唯一的预定义的值。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_EXTENDED_DATA
uint32    recipient channel
uint32    data_type_code
string    data</pre></div></li>
</ul></li>
</ul>

<p>Channel 关闭（<a href="https://www.rfc-editor.org/rfc/rfc4254#section-5.3">rfc4254#section-5.3</a>），在此不多赘述了。</p>

<p>最后，在打开一个特定类型的 Channel 后，需要对这个 Channel 进行 Channel 粒度的配置。如，建立了一个 session 类型的 Channel 后，请求对方创建一个伪终端 (pty、pseudo terminal)。这类的请求叫做 Channel 特定请求（<code>Channel-Specific Requests</code>），这类场景使用相同的数据格式：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST (98)
uint32    recipient channel，对方的 sender channel 编号
string    request type in US-ASCII characters only 请求类型，参见：https://www.rfc-editor.org/rfc/rfc4250#section-4.9.3
boolean   want reply 是否需要对方回复
....      下面是 request type 特定数据</pre></div>
<p>类似的，对于 <code>SSH_MSG_CHANNEL_REQUEST</code> 消息，如果  want reply 为 true，对方应使用 <code>SSH_MSG_CHANNEL_SUCCESS</code> (98)、<code>SSH_MSG_CHANNEL_FAILURE</code> (100) 进行回复。</p>

<h4 id="交互式会话">交互式会话</h4>

<p>在 SSH 语境下，会话（Session）代表远程执行一个程序。这个程序可能是 Shell、应用。同时，它可能有也可能没有一个 tty、可能涉及也可能不涉及 x11 forward。</p>

<ul>
<li><p>客户端打开一个类型为 <code>session</code> 的 Channel（为了安全 ssh 客户端应该拒绝创建 session 的请求）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN (90)
string    &#34;session&#34;
uint32    sender channel
uint32    initial window size
uint32    maximum packet size</pre></div></li>

<li><p>服务端回复一个类型为 <code>SSH_MSG_CHANNEL_OPEN_CONFIRMATION</code> 的消息。至此 Session 类型的 Channel 创建完成。</p></li>

<li><p>客户端可以请求创建一个伪终端（pty、Pseudo-Terminal）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;pty-req&#34;
boolean   want_reply
string    TERM environment variable value (e.g., vt100)
uint32    terminal width, characters (e.g., 80)
uint32    terminal height, rows (e.g., 24)
uint32    terminal width, pixels (e.g., 640)
uint32    terminal height, pixels (e.g., 480)
string    encoded terminal modes</pre></div></li>

<li><p>关于 x11 forward 参见 <a href="https://www.rfc-editor.org/rfc/rfc4254#section-6.3">rfc4254#section-6.3</a>。</p></li>

<li><p>客户端可以请求设置环境变量。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;env&#34;
boolean   want reply
string    variable name
string    variable value</pre></div></li>

<li><p>客户端启动一个 Shell、执行一个命令、调用一个子系统，如下三种情况同一个 Channel 三选一。</p>

<ul>
<li><p>启动一个 Shell</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;shell&#34;
boolean   want reply</pre></div></li>

<li><p>执行一个命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;exec&#34;
boolean   want reply
string    command</pre></div></li>

<li><p>调用其他子系统（如 sftp）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;subsystem&#34;
boolean   want reply
string    subsystem name</pre></div></li>
</ul></li>

<li><p>上述的启动的程序的输入输出通过如下类型的消息传输：</p>

<ul>
<li>标准输入、标准输出： <code>SSH_MSG_CHANNEL_DATA</code>，具体参见上文。</li>
<li>标准出错：<code>SSH_MSG_CHANNEL_EXTENDED_DATA</code>，扩展类型为 <code>SSH_EXTENDED_DATA_STDERR</code>，具体参见上文。</li>

<li><p>伪终端设置终端窗口大小指令(详见：<a href="https://www.rfc-editor.org/rfc/rfc4254#section-6.7">rfc4254#section-6.7</a>)：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;window-change&#34;
boolean   FALSE
uint32    terminal width, columns
uint32    terminal height, rows
uint32    terminal width, pixels
uint32    terminal height, pixels</pre></div></li>

<li><p>信号（详见：<a href="https://www.rfc-editor.org/rfc/rfc4254#section-6.9">rfc4254#section-6.9</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;signal&#34;
boolean   FALSE
string    signal name (without the &#34;SIG&#34; prefix)</pre></div></li>

<li><p>退出码（详见：<a href="https://www.rfc-editor.org/rfc/rfc4254#section-6.10">rfc4254#section-6.10</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &#34;exit-status&#34;
boolean   FALSE
uint32    exit_status</pre></div></li>

<li><p>退出信号（详见：<a href="https://www.rfc-editor.org/rfc/rfc4254#section-6.10">rfc4254#section-6.10</a>）：</p>

<p>`<code>
byte      SSH_MSG_CHANNEL_REQUEST
uint32    recipient channel
string    &quot;exit-signal&quot;
boolean   FALSE
string    signal name (without the &quot;SIG&quot; prefix)
boolean   core dumped
string    error message in ISO-10646 UTF-8 encoding
string    language tag [RFC3066]
</code></p></li>
</ul></li>
</ul>

<h4 id="tcp-ip-端口转发">TCP/IP 端口转发</h4>

<p>SSH 协议本质上，是建立了在 client 到 server 端这两个设备之间建立了一条加密通讯链路。SSH 基于此实现了两个方向的端口转发：</p>

<ul>
<li>本地转发（direct-tcpip）： 将 client 监听的 tcp 端口连接转发到 server 上。</li>
<li>远端转发（forwarded-tcpip）：将 server 监听的 tcp 端口连接转发到 client 上。</li>
</ul>

<p>如上两者，在协议层面上，最大的区别在于（<a href="https://groups.google.com/g/comp.security.ssh/c/qEss3K48wQY">forwarded-tcpip vs. direct-tcpip</a>）：</p>

<ul>
<li>对于远端转发：流量入口端口位于 server 端，因此 SSH 协议需要提供一种机制，可以让 client 告知 server 监听的 tcp 端口。</li>
<li>而对于本地转发：流量入口位于 client，因此 client 程序自身就可以自助的监听 tcp 端口，而不涉及 client 和 server 端的通讯，因此 client 监听端口不是 SSH 协议需要关心的内容。</li>
</ul>

<p><strong>direct-tcpip</strong> 流程</p>

<ul>
<li>client 监听一个 tcp 端口，并 accept 连接（该步骤不属于 ssh 协议，属于 ssh 的实现部分）。</li>

<li><p>client accept 返回后， client 发起建立一个类型为 <code>direct-tcpip</code> 的 Channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN
string    &#34;direct-tcpip&#34;
uint32    sender channel
uint32    initial window size
uint32    maximum packet size
string    host to connect
uint32    port to connect
string    originator IP address
uint32    originator port</pre></div></li>

<li><p>server 接收到消息后，和 <code>host to connect:port to connect</code> TCP 端口建立 TCP 连接。</p></li>

<li><p>至此，转发 Channel 建立完成，后续通过 <code>SSH_MSG_CHANNEL_DATA</code> 进行双向数据的转发。</p></li>

<li><p>该流程对应的 openssh client 命令为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh -L <span style="color:#f92672">[</span>LOCAL_IP:<span style="color:#f92672">]</span>LOCAL_PORT:DESTINATION:DESTINATION_PORT <span style="color:#f92672">[</span>USER@<span style="color:#f92672">]</span>SSH_SERVER</code></pre></div></li>
</ul>

<p><strong>forwarded-tcpip</strong> 流程</p>

<ul>
<li><p>准备阶段（具体参见： <a href="https://www.rfc-editor.org/rfc/rfc4254#section-7.1">rfc4254#section-7.1</a>）：</p>

<ul>
<li><p>client 请求 server 监听 tcp 端口，作为流量入口。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_GLOBAL_REQUEST
string    &#34;tcpip-forward&#34;
boolean   want reply
string    address to bind (e.g., &#34;0.0.0.0&#34;)
uint32    port number to bind</pre></div></li>

<li><p>server 根据请求信息，监听对应端口，并回复：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte     SSH_MSG_REQUEST_SUCCESS
uint32   port that was bound on the server</pre></div></li>
</ul></li>

<li><p>server accept 返回后， server 发起建立一个类型为 <code>direct-tcpip</code> 的 Channel。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">byte      SSH_MSG_CHANNEL_OPEN
string    &#34;forwarded-tcpip&#34;
uint32    sender channel
uint32    initial window size
uint32    maximum packet size
string    address that was connected
uint32    port that was connected
string    originator IP address
uint32    originator port</pre></div></li>

<li><p>client 接收到消息后，和 <code>address that was connected:port that was connected</code> TCP 端口建立 TCP 连接。</p></li>

<li><p>至此，转发 Channel 建立完成，后续通过 <code>SSH_MSG_CHANNEL_DATA</code> 进行双向数据的转发。</p></li>

<li><p>该流程对应的 openssh client 命令为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh -R <span style="color:#f92672">[</span>REMOTE:<span style="color:#f92672">]</span>REMOTE_PORT:DESTINATION:DESTINATION_PORT <span style="color:#f92672">[</span>USER@<span style="color:#f92672">]</span>SSH_SERVER</code></pre></div></li>
</ul>

<p>特别说明：</p>

<ul>
<li>每个 TCP 连接，都会创建一个 Channel。</li>
<li>关于端口转发部分，参见：<a href="https://www.rfc-editor.org/rfc/rfc4254#section-7">rfc4254#section-7</a>。</li>
</ul>

<h2 id="go-ssh-库">Go SSH 库</h2>

<p>主要介绍的是 <a href="https://github.com/golang/crypto">golang/x/crypto</a> 模块中的 SSH 库。</p>

<h3 id="准备">准备</h3>

<p>fork <a href="https://github.com/golang/crypto">golang/x/crypto</a>，并 clone 下来。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/rectcircle/crypto.git</code></pre></div>
<p>使用 IDE (VSCode) 打开。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">code crypto</code></pre></div>
<h3 id="核心-api">核心 API</h3>

<p>该库实现了 SSH 协议，全部 API 参见：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh">godoc</a>。</p>

<p>API 可以分为两个部分，分别是 Client 和 Server。下面将分别介绍。</p>

<h4 id="client">Client</h4>

<p>该库客户端能力通过 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client"><code>ssh.Client</code></a> 结构体提供。</p>

<p>该结构体的构造函数为： <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Dial"><code>func ssh.Dial(network, addr string, config *ClientConfig) (*Client, error)</code></a> 该函数流程如下：</p>

<ul>
<li>使用 <code>func net.Dail</code> 建立底层链接，获得 <code>net.Conn</code>。</li>
<li>调用 <code>func ssh.NewClientConn</code> 完成 SSH 传输层协议和认证协议（具体参见上文）部分。</li>
<li>调用 <code>func ssh.NewClient</code> 返回 <code>ssh.Client</code>。</li>
</ul>

<p>注意：如果想使用自定义的底层连接，可以自己构造一个实现了 <code>net.Conn</code> 的对象，然后参考上述的 <code>ssh.Dial</code> 的实现构造一个 <code>ssh.Client</code>。</p>

<p>上述构造函数第三个参数 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#ClientConfig"><code>ssh.ClientConfig</code></a> 结构体，用来配置 SSH Client。部分字段说明如下：</p>

<ul>
<li><code>User string</code> 用户名，对应 ssh 命令的 <code>ssh 用户名@xxx</code> 用户名部分。</li>
<li><code>Auth []AuthMethod</code> 鉴权方法，支持：

<ul>
<li>密码认证：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Password"><code>ssh.Password()</code></a> 和 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#PasswordCallback"><code>ssh.PasswordCallback()</code></a>。</li>
<li>gss api 认证，即 kerberos 认证：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#GSSAPIWithMICAuthMethod"><code>ssh.GSSAPIWithMICAuthMethod()</code></a> 。</li>
<li>公钥认证：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#PublicKeys"><code>ssh.PublicKeys()</code></a> 和 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#PublicKeysCallback"><code>ssh.PublicKeysCallback()</code></a>。</li>
<li>Keyboard 认证：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#KeyboardInteractiveChallenge"><code>ssh.KeyboardInteractiveChallenge()</code></a>。</li>
<li>多种认证依次尝试（模拟 ssh 命令的行为）：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#RetryableAuthMethod"><code>ssh.RetryableAuthMethod()</code></a>。</li>
</ul></li>
<li><code>HostKeyCallback HostKeyCallback</code> SSH Server Host Key 的校验，预防 SSH 中间人攻击，关于中间人攻击本文不多介绍，可以自行搜索。</li>
<li><code>BannerCallback BannerCallback</code>，在 SSH 认证协议部分，定义一种 Banner 消息类型，以允许服务端发送一些文本消息给客户端，具体参见 <a href="https://www.rfc-editor.org/rfc/rfc4252#section-5.4">rfc4252#section-5.4</a>。</li>
</ul>

<p>获取到 <code>*ssh.Client</code> 对象后，即可通过如下 API 使用 SSH 连接协议（具体参见上文）提供的能力。</p>

<ul>
<li>交互式会话：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.NewSession"><code>func (c *Client) NewSession() (*Session, error)</code></a>，将返回 <code>ssh.Session</code> 对象（交互式会话介绍，参见上文）。

<ul>
<li>请求创建一个伪终端 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.RequestPty"><code>func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error</code></a></li>
<li>请求设置环境变量 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Setenv"><code>func (s *Session) Setenv(name, value string) error</code></a></li>
<li>启动一个 Shell、执行一个命令、调用一个子系统：

<ul>
<li>启动 Shell：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Shell"><code>func (s *Session) Shell() error</code></a>。</li>
<li>执行命令（以下选择一个）：

<ul>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Start"><code>func (s *Session) Start(cmd string) error</code></a>，在远端启动一个命令。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Wait"><code>func (s *Session) Wait() error</code></a>，等待远端执行完成。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Run"><code>func (s *Session) Run(cmd string) error</code></a>，等价于先 Start 再 Wait。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.Output"><code>func (s *Session) Output(cmd string) ([]byte, error)</code></a>，在远端执行一个命令，并等待完成，并将标准输出作为字节数组返回。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.CombinedOutput"><code>func (s *Session) CombinedOutput(cmd string) ([]byte, error)</code></a>，执行一个命令，并等待完成，并将标准输出和标准出错合并作为字节数组返回。</li>
</ul></li>
<li>调用子系统：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.RequestSubsystem"><code>func (s *Session) RequestSubsystem(subsystem string) error</code></a>。</li>
</ul></li>
<li>通过如下 API 获取到标准输入、标准输出、标准出错和远端进行交互。

<ul>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.StdinPipe"><code>func (s *Session) StdinPipe() (io.WriteCloser, error)</code></a></li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.StdoutPipe"><code>func (s *Session) StdoutPipe() (io.Reader, error)</code></a></li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Session.StderrPipe"><code>func (s *Session) StderrPipe() (io.Reader, error)</code></a></li>
</ul></li>
</ul></li>
<li>端口转发（本地转发和远端转发介绍，参见上文）：

<ul>
<li>本地转发：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.Dial"><code>func (c *Client) Dial(n, addr string) (net.Conn, error)</code></a> 除了支持 TCP/IP 外，还支持 openssh 扩展的转发 <code>unix domain socket</code> 中，即 n 参数支持：&rdquo;tcp&rdquo;, &ldquo;tcp4&rdquo;, &ldquo;tcp6&rdquo;, &ldquo;unix&rdquo;。</li>
<li>本地转发：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.DialTCP"><code>func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error)</code></a>，只支持 TCP/IP。</li>
<li>远端转发：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.Listen"><code>func (c *Client) Listen(n, addr string) (net.Listener, error)</code></a> 除了支持 TCP/IP 外，还支持 openssh 扩展的转发 <code>unix domain socket</code> 中，即 n 参数支持：&rdquo;tcp&rdquo;, &ldquo;tcp4&rdquo;, &ldquo;tcp6&rdquo;, &ldquo;unix&rdquo;。</li>
<li>远端转发：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.ListenTCP"><code>func (c *Client) ListenTCP(n, addr string) (net.Listener, error)</code></a> 只支持 TCP/IP。</li>
<li>远端转发：<a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Client.ListenUnix"><code>func (c *Client) ListenUnix(socketPath string) (net.Listener, error)</code></a> 支持 <code>unix domain socket</code>。</li>
</ul></li>
</ul>

<h4 id="server">Server</h4>

<p>该库 server 能力通过 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#NewServerConn"><code>func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, &lt;-chan NewChannel, &lt;-chan *Request, error)</code></a> 函数提供。，该函数，完成了 SSH 传输层协议和认证协议（具体参见上文）部分。该函数的返回值说明如下：</p>

<ul>
<li><code>*ssh.ServerConn</code> 对 <code>net.Conn</code> 的封装，主要用于远端转发，具体参见下文。</li>
<li><code>&lt;-chan NewChannel</code> 获取由 ssh client 创建的 Channel，用来实现交互式会话、本地转发。</li>
<li><code>&lt;-chan *Request</code> 主要用于远端转发，对应 <code>SSH_MSG_GLOBAL_REQUEST</code> 消息，具体参见下文。</li>
<li><code>error</code> 处理 SSH 传输层协议和认证协议出现错误，如认证失败。</li>
</ul>

<p><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#NewChannel"><code>ssh.NewChannel</code></a> 接口对应一个 client 创建的 Channel 的消息（<code>SSH_MSG_CHANNEL_OPEN</code> 具体参见上文：<a href="#channel">Channel 部分</a>），方法有如下几个：</p>

<ul>
<li><code>ChannelType() string</code> channel 的类型，可选值为：&rsquo;session&rsquo;, &lsquo;x11&rsquo;, &lsquo;forwarded-tcpip&rsquo;, &lsquo;direct-tcpip&rsquo; 详见： <a href="https://www.rfc-editor.org/rfc/rfc4250#section-4.9.1">rfc4250#section-4.9.1</a></li>
<li><code>Accept() (Channel, &lt;-chan *Request, error)</code> 同意建立该 Channel。

<ul>
<li><code>ssh.Channel</code> 接口对应一个已经建立 Channel，在 server 端，该接口方法解释如下：

<ul>
<li><code>Read(data []byte) (int, error)</code> 从 Channel 中读取数据，对应 client -&gt; server 的 <code>SSH_MSG_CHANNEL_DATA</code> 消息（参见上文 <a href="#channel">channel</a>），在 session 场景对应 stdin。</li>
<li><code>Write(data []byte) (int, error)</code> 向 Channel 中写入数据，对应 server -&gt; client 的 <code>SSH_MSG_CHANNEL_DATA</code> 消息（参见上文 <a href="#channel">channel</a>），在 session 场景对应 stdout。</li>
<li><code>Close() error</code> 关闭该 channel，对应 <code>SSH_MSG_CHANNEL_CLOSE</code> 消息 （参见上文 <a href="#channel">channel</a>）。</li>
<li><code>CloseWrite() error</code></li>
<li><code>SendRequest(name string, wantReply bool, payload []byte) (bool, error)</code> 对应 server -&gt; client 在该 Channel 上的 <code>SSH_MSG_CHANNEL_REQUEST</code>（参见上文 <a href="#ssh-连接协议">SSH 连接协议</a>），主要在 session channel 场景有如下几个类型：

<ul>
<li><code>&quot;window-change&quot;</code>，发送 pty 的 window-change 信息。</li>
<li><code>&quot;exit-status&quot;</code>，发送 cmd 的退出码消息。</li>
</ul></li>
</ul></li>
<li><code>ssh.Request</code> 结构体对应 client -&gt; server 在该 Channel 上的 <code>SSH_MSG_CHANNEL_REQUEST</code>（参见上文 <a href="#ssh-连接协议">SSH 连接协议</a>）该结构体有如下几个字段和方法：

<ul>
<li><code>Type string</code> 字段，在 session channel 场景有用，有如下几个类型：

<ul>
<li><code>&quot;pty-req&quot;</code></li>
<li><code>&quot;shell&quot;</code></li>
<li><code>&quot;subsystem&quot;</code></li>
<li><code>&quot;env&quot;</code></li>
<li><code>&quot;exec&quot;</code></li>
</ul></li>
<li><code>WantReply bool</code> 字段，是否需要回复。</li>
<li><code>Payload []byte</code> 字段，type 特定数据，可以使用 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Unmarshal"><code>ssh.Unmarshal()</code></a> 方法进行反序列化。</li>
<li><code>func (r *Request) Reply(ok bool, payload []byte) error</code> 方法，对 <code>WantReply = true</code> 的方法，必须调用该函数进行回复。</li>
</ul></li>
</ul></li>
<li><code>Stderr() io.ReadWriter</code> server -&gt; client，对应 <code>SSH_MSG_CHANNEL_EXTENDED_DATA</code>，参见上文 <a href="#交互式会话">交互式会话</a>，在 session 场景对应 stderr。</li>
<li><code>Reject(reason RejectionReason, message string) error</code> 拒绝建立该 Channel。</li>
<li><code>ExtraData() []byte</code> 类型特定数据。</li>
</ul>

<p>从 API 上来看，Go SSH 库 Server API 比 Client API 更加的底层，需要开发者理解 <a href="#ssh-连接协议">SSH 连接协议</a> 消息相关细节才能很好的进行开发。而 Go SSH 库的 Client API 在比较高的层次，使用起来比较容易。</p>

<p>因此，如果想使用 Go 语言开发 SSH Server 相关需求，建议直接使用或者参考：<a href="https://github.com/gliderlabs/ssh">github.com/gliderlabs/ssh</a> 库，该库提供了类似于 http.Server 的，更高层次的 API。如：</p>

<ul>
<li>远端转发的<a href="https://github.com/gliderlabs/ssh/blob/master/_examples/ssh-remoteforward/portforward.go#L30">示例</a>和<a href="https://github.com/gliderlabs/ssh/blob/master/tcpip.go#L97">实现</a>，主要逻辑是对上文 <code>NewServerConn</code> 返回的：

<ul>
<li><code>&lt;-chan *Request</code>，接收到 <code>&quot;tcpip-forward&quot;</code> 监听端口，接收到 <code>&quot;cancel-tcpip-forward&quot;</code> 取消监听。</li>
<li><code>*ssh.ServerConn</code>，用户请求上面监听的端口时，调用 <code>OpenChannel</code> 建立一个 server -&gt; client 的 channel，并进行数据拷贝。</li>
</ul></li>
</ul>

<p>本文主要是探索 SSH 协议的相关结构，因此下文的示例的 server 仍然使用 Go SSH 库来实现。</p>

<h4 id="通用-api">通用 API</h4>

<ul>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Unmarshal"><code>func Unmarshal(data []byte, out interface{}) error</code></a> ssh 协议消息字段反序列函数。主要用于 <a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Request"><code>ssh.Request.Payload</code></a> 字段。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Marshal"><code>func Marshal(msg interface{}) []byte</code></a> ssh 协议消息字段序列函数。主要用于：

<ul>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Channel.SendRequest"><code>ssh.Channel.SendRequest</code></a> 函数。</li>
<li><a href="https://pkg.go.dev/golang.org/x/crypto/ssh#Request.Reply"><code>ssh.Request.Reply</code></a> 函数。</li>
</ul></li>
</ul>

<h3 id="编写示例代码">编写示例代码</h3>

<p>使用 Go SSH 库实现一个 Client 和 Server，实现在远程调用 env 命令。</p>

<p><code>ssh/demo/client/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>

	<span style="color:#e6db74">&#34;golang.org/x/crypto/ssh&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">u</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 连接到 server： 用户为当前用户，使用密码鉴权方式，密码为测试的 123456，不校验 HostKey 是否合法。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Dial</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>, <span style="color:#e6db74">&#34;127.0.0.1:2222&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ClientConfig</span>{
		<span style="color:#a6e22e">User</span>: <span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">Username</span>,
		<span style="color:#a6e22e">Auth</span>: []<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">AuthMethod</span>{
			<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Password</span>(<span style="color:#e6db74">&#34;123456&#34;</span>),
		},
		<span style="color:#a6e22e">HostKeyCallback</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">hostname</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">remote</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Addr</span>, <span style="color:#a6e22e">key</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">PublicKey</span>) <span style="color:#66d9ef">error</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span> },
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#75715e">// 启动一个交互式会话
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">session</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">NewSession</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#75715e">// 设置会话的环境变量
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">Setenv</span>(<span style="color:#e6db74">&#34;A&#34;</span>, <span style="color:#e6db74">&#34;1&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 执行命令 env，并获得输出
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">CombinedOutput</span>(<span style="color:#e6db74">&#34;env&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;&gt; env\n%s&#34;</span>, string(<span style="color:#a6e22e">output</span>))
}</code></pre></div>
<p><code>ssh/demo/server/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;crypto/rand&#34;</span>
	<span style="color:#e6db74">&#34;crypto/rsa&#34;</span>
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>

	<span style="color:#e6db74">&#34;golang.org/x/crypto/ssh&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleSession</span>(<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Channel</span>, <span style="color:#a6e22e">rc</span> <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Request</span>) {
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#a6e22e">envs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">rc</span> {
		<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Type</span> {
		<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;exec&#34;</span>:
			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">payload</span> = <span style="color:#66d9ef">struct</span>{ <span style="color:#a6e22e">Value</span> <span style="color:#66d9ef">string</span> }{}
			<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Unmarshal</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Payload</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">payload</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ssh session exec request unmarshal error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Reply</span>(<span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">nil</span>)
				<span style="color:#66d9ef">return</span>
			}
			<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Reply</span>(<span style="color:#66d9ef">true</span>, <span style="color:#66d9ef">nil</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ssh session exec reply error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Reply</span>(<span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">nil</span>)
				<span style="color:#66d9ef">return</span>
			}
			<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
				<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">Value</span>)
				<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Env</span> = <span style="color:#a6e22e">envs</span>
				<span style="color:#a6e22e">stdout</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">StdoutPipe</span>()
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					panic(<span style="color:#a6e22e">err</span>)
				}
				<span style="color:#a6e22e">stderr</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">StderrPipe</span>()
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					panic(<span style="color:#a6e22e">err</span>)
				}
				<span style="color:#a6e22e">stdin</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">StdinPipe</span>()
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					panic(<span style="color:#a6e22e">err</span>)
				}
				<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>()
				<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">stdin</span>, <span style="color:#a6e22e">c</span>)
				<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Stderr</span>(), <span style="color:#a6e22e">stderr</span>)
				<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">stdout</span>)
				<span style="color:#a6e22e">status</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">struct</span>{ <span style="color:#a6e22e">Status</span> <span style="color:#66d9ef">uint32</span> }{uint32(<span style="color:#ae81ff">0</span>)}
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Wait</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">exiterr</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">err</span>.(<span style="color:#f92672">*</span><span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">ExitError</span>); <span style="color:#a6e22e">ok</span> {
						<span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span> = uint32(<span style="color:#a6e22e">exiterr</span>.<span style="color:#a6e22e">ExitCode</span>())
					} <span style="color:#66d9ef">else</span> {
						<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ssh session cmd wait error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
						<span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span> = <span style="color:#ae81ff">1</span>
					}
				}
				<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendRequest</span>(<span style="color:#e6db74">&#34;exit-status&#34;</span>, <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">status</span>))
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
					<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ssh session send exit status error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
				}
				<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Close</span>()
			}()
		<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;env&#34;</span>:
			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">kv</span> = <span style="color:#66d9ef">struct</span>{ <span style="color:#a6e22e">Key</span>, <span style="color:#a6e22e">Value</span> <span style="color:#66d9ef">string</span> }{}
			<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Unmarshal</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Payload</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">kv</span>)
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ssh session env request unmarshal error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
				<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Reply</span>(<span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">nil</span>)
			}
			<span style="color:#a6e22e">envs</span> = append(<span style="color:#a6e22e">envs</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%s=%s&#34;</span>, <span style="color:#a6e22e">kv</span>.<span style="color:#a6e22e">Key</span>, <span style="color:#a6e22e">kv</span>.<span style="color:#a6e22e">Value</span>))
			<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Reply</span>(<span style="color:#66d9ef">true</span>, <span style="color:#66d9ef">nil</span>)
		<span style="color:#75715e">// case &#34;shell&#34;:     // 本 demo 暂不涉及。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// case &#34;pty-req&#34;:   // 本 demo 暂不涉及。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// case &#34;subsystem&#34;: // 本 demo 暂不涉及。
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">default</span>:
		}
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">HandleConn</span>(<span style="color:#a6e22e">conn</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">serverConfig</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ServerConfig</span>) {
	<span style="color:#75715e">// 完成 SSH 传输层协议和认证协议。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">serverConn</span>, <span style="color:#a6e22e">chans</span>, <span style="color:#a6e22e">reqs</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">NewServerConn</span>(<span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">serverConfig</span>)
	<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">serverConn</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 拒绝所有 SSH_MSG_GLOBAL_REQUEST 消息，即，不支持远端转发（forwarded-tcpip）。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如需支持远端转发，实现参见：https://github.com/gliderlabs/ssh/blob/30ec06db4e743ac9f827a69c8b8cfb84064a6dc7/tcpip.go#L97
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    1. 将消息进行反序列化。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    2. 调用 net.Listen 监听对应端口
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    3. listener 返回 conn 时，调用 serverConn.OpenChannel，消息类型为 forwarded-tcpip 向客户端请求创建一个 channel。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    4. 然后在 conn 和 channel 之间进行 io copy。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 本示例就不实现了。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">DiscardRequests</span>(<span style="color:#a6e22e">reqs</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">chans</span> {
		<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ChannelType</span>() {
		<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;session&#34;</span>:
			<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">r</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Accept</span>()
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;accept session channel error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">handleSession</span>(<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">r</span>)
		<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;x11&#34;</span>:
		<span style="color:#75715e">// case &#34;direct-tcpip&#34;: // 本示例暂不处理本地转发。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// case &#34;forwarded-tcpip&#34;: // 按照 rfc 说明 server 端不应该支持该类型。
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">default</span>:
			<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Reject</span>(<span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">UnknownChannelType</span>, <span style="color:#e6db74">&#34;unsupported channel type&#34;</span>)
		}
	}
}

<span style="color:#75715e">// 创建一个用于 ssh server 握手用的配置
</span><span style="color:#75715e">//   1. 采用密码校验
</span><span style="color:#75715e">//   2. hostKey 随机生成一个
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newSSHSererConfig</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ServerConfig</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">config</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ServerConfig</span>{
		<span style="color:#a6e22e">PasswordCallback</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">conn</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">ConnMetadata</span>, <span style="color:#a6e22e">password</span> []<span style="color:#66d9ef">byte</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">Permissions</span>, <span style="color:#66d9ef">error</span>) {
			<span style="color:#66d9ef">if</span> string(<span style="color:#a6e22e">password</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;123456&#34;</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
			}
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;password incorrect&#34;</span>)
		},
	}
	<span style="color:#a6e22e">priKey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rsa</span>.<span style="color:#a6e22e">GenerateKey</span>(<span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Reader</span>, <span style="color:#ae81ff">2048</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">sshSigner</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ssh</span>.<span style="color:#a6e22e">NewSignerFromKey</span>(<span style="color:#a6e22e">priKey</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">AddHostKey</span>(<span style="color:#a6e22e">sshSigner</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">config</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 构造 ssh server config
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">serverConfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newSSHSererConfig</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 监听 TCP 端口，并 accept 连接。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">listener</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Listen</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>, <span style="color:#e6db74">&#34;:2222&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">for</span> {
		<span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">listener</span>.<span style="color:#a6e22e">Accept</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">HandleConn</span>(<span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">serverConfig</span>)
	}
}</code></pre></div>
<h3 id="运行实例代码">运行实例代码</h3>

<ul>
<li>先运行 server 端 <code>go run ./ssh/demo/server</code>。</li>

<li><p>再运行 client 端 <code>go run ./ssh/demo/client</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&gt; env
A=1
PWD=/Users/xxx/Workspace/personal/go-x-crypto
SHLVL=1
_=/usr/bin/env</pre></div></li>
</ul>

<h3 id="开启-debug-日志">开启 debug 日志</h3>

<p>Go SSH 库中有几个常量，可以打开 Debug 日志，以追踪源码流程，分别是：</p>

<ul>
<li><code>ssh/handshake.go:20</code> 打印传输层协议的 Key 交换流程相关 Debug 日志。</li>
<li><code>ssh/mux.go:18</code> 打印连接协议相关的 Debug 日志。</li>
<li><code>ssh/transport.go:17</code> 打印传输层协议发送和接收到的 Packet 的类型。</li>
</ul>

<p>本文重点观察传输层协议的部分。因此，只打开 <code>ssh/transport.go:17</code> 日志。</p>

<h3 id="客户端流程追踪">客户端流程追踪</h3>

<h4 id="客户端传输层协议输出分析">客户端传输层协议输出分析</h4>

<p>重新按照上文 <a href="#运行实例代码">运行实例代码</a> 方式运行，并观察 client 代码的输出。</p>

<p><code>#</code> 号开头为说明。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># Go 源码：ssh/messages.go
# 消息编号：https://www.rfc-editor.org/rfc/rfc4250#section-4.1

# 连接层协议部分：https://www.rfc-editor.org/rfc/rfc4253
2023/01/26 22:43:43 write client 20   # SSH_MSG_KEXINIT, client -&gt; server, key 交换初始化消息，算法协商。
2023/01/26 22:43:43 read client 20    # SSH_MSG_KEXINIT, server -&gt; client, key 交换初始化消息，算法协商。
2023/01/26 22:43:43 write client 30   # client -&gt; server, key 交换算法执行。
2023/01/26 22:43:43 read client 31    # server -&gt; client, key 交换算法执行。
2023/01/26 22:43:43 write client 21   # SSH_MSG_NEWKEYS, client -&gt; server, key 交换算法完成。
2023/01/26 22:43:43 read client 21    # SSH_MSG_NEWKEYS, server -&gt; client, key 交换算法完成。
2023/01/26 22:43:43 read client 7     # server -&gt; client, 与 SSH 协议扩展有关，参见：https://www.rfc-editor.org/rfc/rfc8308
2023/01/26 22:43:43 write client 5    # SSH_MSG_SERVICE_REQUEST, client -&gt; server, 请求 ssh-userauth 服务。
2023/01/26 22:43:43 read client 6     # SSH_MSG_SERVICE_ACCEPT, server -&gt; client, 接收鉴权服务请求。

# 认证协议部分：https://www.rfc-editor.org/rfc/rfc4252
2023/01/26 22:43:43 write client 50   # SSH_MSG_USERAUTH_REQUEST, client -&gt; server, 请求鉴权
2023/01/26 22:43:43 read client 51    # SSH_MSG_USERAUTH_FAILURE, server -&gt; client, 鉴权失败
2023/01/26 22:43:43 write client 50   # SSH_MSG_USERAUTH_REQUEST, client -&gt; server, 请求鉴权
2023/01/26 22:43:43 read client 52    # SSH_MSG_USERAUTH_SUCCESS, server -&gt; client, 鉴权成功

# 连接协议部分： https://www.rfc-editor.org/rfc/rfc4254
2023/01/26 22:43:43 write client 90   # SSH_MSG_CHANNEL_OPEN, client -&gt; server, 打开 Channel
2023/01/26 22:43:43 read client 91    # SSH_MSG_CHANNEL_OPEN_CONFIRMATION, server -&gt; client, 打开 Channel 成功
2023/01/26 22:43:43 write client 98   # SSH_MSG_CHANNEL_REQUEST, client -&gt; server, Channel 请求，应该是设置环境变量
2023/01/26 22:43:43 read client 99    # SSH_MSG_CHANNEL_SUCCESS, server -&gt; client, Channel 请求成功
2023/01/26 22:43:43 write client 98   # SSH_MSG_CHANNEL_REQUEST, client -&gt; server, Channel 请求，应该是运行 env 命令
2023/01/26 22:43:43 read client 99    # SSH_MSG_CHANNEL_SUCCESS, server -&gt; client, Channel 请求成功
2023/01/26 22:43:43 write client 96   # SSH_MSG_CHANNEL_EOF, client -&gt; server, 关闭写通道。
2023/01/26 22:43:43 read client 94    # SSH_MSG_CHANNEL_DATA, server -&gt; client, 服务端写回标准输出。
2023/01/26 22:43:43 write client 93   # SSH_MSG_CHANNEL_WINDOW_ADJUST, client -&gt; server, 滑动窗口调整。
2023/01/26 22:43:43 read client 98    # SSH_MSG_CHANNEL_REQUEST, server -&gt; client, 服务端告知命令退出码。
2023/01/26 22:43:43 read client 97    # SSH_MSG_CHANNEL_CLOSE, server -&gt; client, 服务端关闭 Channel。
2023/01/26 22:43:43 write client 97   # SSH_MSG_CHANNEL_CLOSE, client -&gt; server, 客户端关闭 Channel。</pre></div>
<h4 id="ssh-dial-源码">ssh.Dial 源码</h4>

<ul>
<li>进入 <code>ssh/demo/client/main.go:18</code> 函数 <code>ssh.Dial</code> 定义。

<ul>
<li><code>ssh/client.go:177</code> 调用 <code>net.DialTimeout</code> 和服务端建立 TCP 连接。</li>
<li><code>ssh/client.go:181</code> 调用 <code>NewClientConn</code>，该函数，完成了 SSH 传输层协议和认证协议的流程，并构造一个实现了连接层协议的 mux 对象。

<ul>
<li><code>ssh/client.go:83</code> 调用 <code>conn.clientHandshake</code>，该函数，完成了 SSH 传输层协议和认证协议的流程。

<ul>
<li><code>ssh/client.go:100</code> 函数 <code>exchangeVersions</code>，完成客户端和服务端的协议版本协商。</li>
<li><code>ssh/client.go:105</code> 函数 <code>newClientTransport</code>。

<ul>
<li><code>ssh/client.go:126</code> 调用 <code>newHandshakeTransport</code> 函数构造 <code>*handshakeTransport</code>。 特别注意的是：

<ul>
<li>在 <code>ssh/handshake.go:117</code> 将 <code>t.readBytesLeft</code> 初始化为一个较大值。</li>
<li>在 <code>ssh/handshake.go:118</code> 将 <code>t.writeBytesLeft</code> 初始化为一个较大值。</li>
<li>在 <code>ssh/handshake.go:121</code> 语句 <code>t.requestKex &lt;- struct{}{}</code>，发起首次 key 交换流程。</li>
</ul></li>
<li>并启动两个协程，分别是：<code>go t.readLoop()</code> 和 <code>go t.kexLoop()</code>，具体流程参见下文。</li>
<li>返回并赋值给 <code>c.transport</code>。</li>
</ul></li>
<li><code>ssh/client.go:108</code> 调用 <code>c.transport.waitSession()</code>，该函数会在上述两个协程，完成 SSH 传输层协议（即 Key 交换算法协商、Key 交换算法执行）后返回。</li>
<li><code>ssh/client.go:113</code> 调用 <code>c.clientAuthenticate(config)</code>，执行 SSH 认证协议流程。</li>
</ul></li>
<li><code>ssh/client.go:87</code> 调用 <code>newMux</code> 构造一个实现了连接层协议的 mux 对象。</li>
</ul></li>
<li><code>ssh/client.go:185</code> 调用 <code>NewClient</code> 构造一个客户端结构体 <code>Client</code>，来提供高层次的 SSH 连接层协议 API。</li>
</ul></li>
</ul>

<h4 id="客户端传输层协议源码">客户端传输层协议源码</h4>

<p><code>handshakeTransport</code> （<code>ssh/handshake.go</code>）结构体是传输层协议封装，该结构体说明如下：</p>

<ul>
<li>两个协程 <code>go t.readLoop()</code>（<code>ssh/handshake.go:196</code>） 和 <code>go t.kexLoop()</code> （<code>ssh/handshake.go:261</code>）协作，在首次和 Key 老化后，在后台完成 Key 交换。下面按照时序介绍首次 Key 交换的流程：

<ul>
<li>时序 1 <code>kexLoop</code> 函数：

<ul>
<li><code>ssh/handshake.go:275</code> 进入 <code>case &lt;-t.requestKex</code> 分支（前面的代码有写入，参见 <a href="#sshdial-源码">ssh.Dial 源码</a> 部分的说明）。</li>
<li><code>ssh/handshake.go:280</code> 调用 <code>t.sendKexInit()</code>，给服务端发送 <code>SSH_MSG_KEXINIT</code> 消息（20，<a href="https://www.rfc-editor.org/rfc/rfc4253#section-7.1">rfc4253#section-7.1</a>）本次循环结束。</li>
<li><code>ssh/handshake.go:270</code> 等待 <code>select</code> 返回。</li>
</ul></li>
<li>时序 2 <code>readLoop</code> 函数：

<ul>
<li><code>ssh/handshake.go:376</code> 读取服务端 <code>KexInit</code> 消息。</li>
<li><code>ssh/handshake.go:412</code> 将 <code>KexInit</code> 消息通过 <code>startKex</code> channel 告知 <code>kexLoop</code>。</li>
<li><code>ssh/handshake.go:413</code> 等待 key 交换完成。</li>
</ul></li>
<li>时序 3 <code>kexLoop</code> 函数：

<ul>
<li><code>ssh/handshake.go:271</code> 进入 <code>case request, ok = &lt;-t.startKex</code> 分支，<code>request != nil</code>，跳出 268 行 for 循环。</li>
<li><code>ssh/handshake.go:303</code> 进入 <code>enterKeyExchange</code> 函数，执行 Key 交换流程。</li>
<li><code>ssh/handshake.go:328</code> 告知 <code>readLoop</code>。</li>
</ul></li>
<li>时序 4 <code>readLoop</code> 函数：

<ul>
<li><code>ssh/handshake.go:413</code> 返回 <code>SSH_MSG_NEWKEYS</code> （21）。</li>
<li><code>ssh/handshake.go:209</code> 将消息发送给 <code>t.incoming</code>。</li>
</ul></li>
<li>时序 5 <code>ssh/client.go:83</code> 的 <code>conn.clientHandshake</code> 函数：

<ul>
<li><code>ssh/handshake.go:155</code> 函数 <code>waitSession</code> 返回，后续进入认证流程。</li>
</ul></li>
</ul></li>
<li>协程 <code>go t.readLoop()</code> 和 函数 <code>t.readPacket()</code>，按照传输层协议的包格式，解密出消息字节数组（上文 <a href="#数据包-packet-结构">数据包 (Packet) 结构</a> 的 payload），等待上层认证协议和连接协议处理。</li>
<li>函数 <code>t.writePacket()</code> 将消息字节数组（上文 <a href="#数据包-packet-结构">数据包 (Packet) 结构</a> 的 payload）加密并封装到数据包中，并发送到服务端。该函数由上层认证协议和连接协议调用。</li>
<li><code>go t.readLoop()</code>、 <code>t.readPacket()</code> 和 <code>t.writePacket()</code> 底层调用的是 <code>keyingTransport</code> 接口，其唯一的实现是 <code>transport</code> （<code>ssh/transport.go:42</code>）结构体，该结构体底层调用的是 <code>connectionState</code> (<code>ssh/transport.go:69</code>) 结构体的方法。

<ul>
<li><code>connectionState</code> 的类似于 <code>io.ReadWriter</code>，有如下两个方法：

<ul>
<li><code>writePacket(packet []byte) error</code>，这里的 packet 命名有问题，实际上是消息字节数组（上文 <a href="#数据包-packet-结构">数据包 (Packet) 结构</a> 的 payload）。</li>
<li><code>readPacket() ([]byte, error)</code>，返回消息字节数组（上文 <a href="#数据包-packet-结构">数据包 (Packet) 结构</a> 的 payload）。</li>
</ul></li>
<li><code>connectionState</code> 依赖 <code>packetCipher</code> （<code>ssh/transport.go:55</code>）接口来 packet 的加解密。从上文可以知道：

<ul>
<li>传输层协议的 key 交换流程的包是不需要加密的，对应的实现是 <code>noneCipher</code>。</li>
<li>除了 key 交换流程的包都需要使用 key 交换获取到的 key 进行加解密。对应的实现有许多个，如：<code>streamPacketCipher</code> 等。</li>
</ul></li>
</ul></li>
</ul>

<h4 id="客户端认证和连接协议源码">客户端认证和连接协议源码</h4>

<p>客户端认证和连接协议源码本文不再逐行分析源码了。下面记录一下相关源码的位置：</p>

<ul>
<li>客户端认证，调用链为：

<ul>
<li><code>ssh.Dial</code> 函数 <code>ssh/client.go:181</code> 对 <code>ssh.NewClientConn</code> 的调用。</li>
<li><code>ssh/client.go:83</code> 对 <code>ssh.connection.clientHandshake</code> 的调用。</li>
<li><code>ssh/client.go:13</code> 对 <code>ssh.connection.clientAuthenticate</code> 的调用。</li>
</ul></li>
<li>客户端认证协议，源文件：<code>ssh/client_auth.go</code>。</li>
<li>客户端连接协议，源文件：<code>ssh/mux.go</code></li>
</ul>

<h3 id="服务端流程追踪">服务端流程追踪</h3>

<p>服务端流程分析可以参考客户端的分析，在此不多赘述了。</p>

<h3 id="和-openssh-关系">和 OpenSSH 关系</h3>

<p>业界对于 SSH 协议的标准实现是 OpenSSH。该实现不仅实现了标准 rfc 中的 ssh，还对 ssh 进行了扩展。具体参见 <a href="https://www.openssh.com/specs.html">openssh specs</a>。</p>

<p>由于 OpenSSH 协议是事实上的标准，因此 Go 的 SSH 库也对 OpenSSH 的扩展进行了支持。从源码中搜索 <code>@openssh.com</code> 可以看到这部分的内容。</p>

<p>关于 SSH 协议的厂商扩展标准，参见： <a href="https://www.rfc-editor.org/rfc/rfc8308">rfc8308</a>。</p>

<h3 id="scp-和-sftp">scp 和 sftp</h3>

<p>基于 SSH 的文件传输有 scp 和 sftp 两种方式：</p>

<ul>
<li>scp 是基于命令的标准 IO 实现的。本地 scp 命令会使用 SSH 连接协议，打开一个 session，通过 <code>SSH_MSG_CHANNEL_REQUEST</code> 的 exec 在远端执行 scp 命令。远端 scp 会读取文件，按照 scp 协议将文件写入标准输出，这个标准输出通过 SSH Channel 传递到本地的 scp 这个进程中，本地 scp 按照协议协议标准输出，并写入本地文件，即可完成文件书传输。更多参见：

<ul>
<li>go scp client 库：<a href="https://github.com/bramvdbogaerde/go-scp">bramvdbogaerde/go-scp</a>。</li>
<li>scp 协议分析文章：<a href="https://blog.singee.me/2021/01/02/d9e5fe31d708454fb99869a4c9d78f24/">scp 原理</a></li>
</ul></li>
<li>sftp 是基于 SSH 连接协议的子系统实现的，对应的是 <code>SSH_MSG_CHANNEL_REQUEST</code> 的 subsystem。更多参见：

<ul>
<li>go sftp server 和 client 库：<a href="https://github.com/pkg/sftp">pkg/sftp</a>。</li>
</ul></li>
</ul>
]]></description></item><item><title>轻量级 SSH 开源项目 Dropbear</title><link>https://www.rectcircle.cn/posts/lightweight-ssh-dropbear/</link><pubDate>Wed, 28 Dec 2022 14:12:50 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/lightweight-ssh-dropbear/</guid><description type="html"><![CDATA[

<h2 id="简介">简介</h2>

<p><a href="https://matt.ucc.asn.au/dropbear/dropbear.html">Dropbear</a> 是一个轻量级以 <a href="https://github.com/mkj/dropbear">MIT 许可证</a>开源的 SSH2 (<a href="https://www.rfc-editor.org/rfc/rfc4253">rfc4253</a>) 项目。具有如下特点：</p>

<ul>
<li>小的内存和磁盘占用，在 x86 平台，使用 uClibc 的静态编译最小产物约 110kb。</li>
<li>Dropbear ssh server 支持 openssh client 的 X11 forwarding， authentication-agent forwarding。</li>
<li>Dropbear ssh server 兼容 OpenSSH 的 <code>~/.ssh/authorized_keys</code> 公钥认证。</li>
<li>服务器、客户端、密钥生成器和密钥转换器可以编译成单个二进制文件（如 busybox）。</li>
<li>编译时可以根据需求开启和关闭某些功能（<a href="https://github.com/mkj/dropbear/blob/master/default_options.h">默认宏</a>，通过添加 <code>localoptions.h</code> 或者 <code>CFLAGS=-DXXX=xxx</code> 修改配置），以裁剪产物大小。</li>
<li>ssh client 支持使用 SSH TCP forward，用单条命令，通过多个 SSH 主机建立隧道，实现多跳模式。如 <code>dbclient user1@hop1,user2@hop2,destination</code>。</li>
</ul>

<p>支持静态编译。可以编译成如下可执行文件（当然类似 busybox，可以编译成 all-in-one 单独的可执行文件）：</p>

<ul>
<li>dropbear (ssh server)，参见：<a href="https://linux.die.net/man/8/dropbear">手册</a></li>
<li>dbclient (ssh client)，参见：<a href="https://linux.die.net/man/1/dbclient">手册</a>。</li>
<li>dropbearkey 给 dropbear 创建 dropbear 格式的私钥，参见：<a href="https://linux.die.net/man/8/dropbearkey">手册</a></li>
<li>dropbearconvert 将私钥格式在 dropbear 与 openssh 之间互相转换，参见：<a href="https://manpages.debian.org/testing/dropbear-bin/dropbearconvert.1.en.html">手册</a></li>
<li>scp 基于 ssh 协议的文件拷贝。</li>
</ul>

<p>相关文档如下：</p>

<ul>
<li><a href="https://matt.ucc.asn.au/dropbear/dropbear.html">项目主页</a></li>
<li><a href="https://github.com/mkj/dropbear">项目代码和 README</a></li>
<li><a href="https://github.com/mkj/dropbear/blob/master/INSTALL">编译安装文档</a></li>
<li><a href="https://github.com/mkj/dropbear/blob/master/MULTI">all-in-one 编译文档</a></li>
<li><a href="https://github.com/mkj/dropbear/blob/master/SMALL">裁剪和尺寸优化文档</a></li>
<li><a href="https://github.com/mkj/dropbear/blob/master/default_options.h">特性宏默认配置源代码</a></li>
</ul>

<p>注意：</p>

<ul>
<li>本文将重点介绍 dropbear ssh server，编译安装，容器运行，以及如何对 dropbear 进行定制开发，以实现自定义鉴权的支持。</li>
<li>本文使用标准的 OpenSSH SSH Client 进行测试，以验证 dropbear ssh server 的兼容性。</li>
<li>本文测试的 dropbear 版本为 <a href="https://github.com/mkj/dropbear/blob/DROPBEAR_2022.83/INSTALL"><code>DROPBEAR_2022.83</code></a>。</li>
</ul>

<h2 id="编译安装">编译安装</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 版本</span>
export DROPBEAR_VERSION<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;DROPBEAR_2022.83&#34;</span>
export ZLIB_VERSION<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1.2.13&#34;</span>

<span style="color:#75715e"># 使用 musl-libc 编译</span>
sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt install -y musl-tools

<span style="color:#75715e"># 下载编译 zlib 为静态链接库</span>
wget https://zlib.net/zlib-$ZLIB_VERSION.tar.gz
tar -xzvf zlib-$ZLIB_VERSION.tar.gz
rm -rf zlib-$ZLIB_VERSION.tar.gz
cd zlib-$ZLIB_VERSION
./configure --help
CC<span style="color:#f92672">=</span>musl-gcc ./configure --static --prefix<span style="color:#f92672">=</span><span style="color:#e6db74">`</span>pwd<span style="color:#e6db74">`</span>-built
make <span style="color:#f92672">&amp;&amp;</span> make install 
<span style="color:#75715e"># DESTDIR=`pwd`-built</span>
cd ..

<span style="color:#75715e"># 下载并编译 dropbear</span>
git clone https://github.com/mkj/dropbear.git
cd dropbear
git checkout $DROPBEAR_VERSION
./configure <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --enable-static <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --with-zlib<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;../zlib-</span>$ZLIB_VERSION<span style="color:#e6db74">-built&#34;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    CC<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;musl-gcc&#34;</span>
    <span style="color:#75715e"># CFLAGS=&#34;-Wl,-static&#34; \</span>
    <span style="color:#75715e"># LDFLAGS=&#34;-static&#34;</span>
make PROGRAMS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;dropbear&#34;</span> <span style="color:#f92672">&amp;&amp;</span> make PROGRAMS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;dropbear&#34;</span> DESTDIR<span style="color:#f92672">=</span><span style="color:#e6db74">`</span>pwd<span style="color:#e6db74">`</span>-built install
cd ../</code></pre></div>
<p>执行 <code>tree dropbear-built</code> 检查编译产物，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">dropbear-built
└── usr
    └── local
        ├── sbin
        │   └── dropbear
        └── share
            └── man
                └── man8
                    └── dropbear.8

6 directories, 2 files</pre></div>
<p>注意：</p>

<ul>
<li><code>ls -alh dropbear-built/usr/local/sbin/dropbear</code> 可以发现 dropbear 空间占用极小，只有 491K 左右。</li>
<li><code>ldd dropbear-built/usr/local/sbin/dropbear</code> 可以看出如上方式是静态编译，在任意相同 CPU 架构的 Linux 平台均可以运行。</li>
</ul>

<h2 id="运行">运行</h2>

<p>在虚拟机运行 ssh server。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo mkdir -p /etc/dropbear
sudo ./dropbear-built/usr/local/sbin/dropbear -E -F -R -p <span style="color:#ae81ff">2222</span> </code></pre></div>
<p>在其他机器使用 openssh client 进行连接。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh rectcircle@192.168.57.3 -p <span style="color:#ae81ff">2222</span></code></pre></div>
<p>可以发现可以正常进入。</p>

<p>下面，通过 docker 使用 busybox 镜像启动，并登录。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker run -it -v <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/dropbear-built/usr/local/sbin/dropbear:/sbin/dropbear -p <span style="color:#ae81ff">2222</span>:2222 busybox:latest sh
mkdir /etc/dropbear
/sbin/dropbear -E -F -R -p <span style="color:#ae81ff">2222</span></code></pre></div>
<p>在宿主机，通过 openssh client 进行连接。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh -p <span style="color:#ae81ff">2222</span> root@localhost</code></pre></div>
<p>发现可以进入容器中，验证了上述静态编译的效果。</p>

<h2 id="问题">问题</h2>

<p>在测试过程中发现如下问题：</p>

<ul>
<li>目前 debian 11 已经将<a href="https://fedoraproject.org/wiki/Changes/yescrypt_as_default_hashing_method_for_shadow">默认密码散列算法</a>修改成 <a href="https://manpages.debian.org/unstable/libcrypt-dev/crypt.5.en.html#yescrypt"><code>yescrypt</code></a>。而 <a href="https://git.musl-libc.org/cgit/musl/tree/src/crypt/crypt_r.c">musl 加密算法</a>不支持该算法，因此上面使用 musl 编译的 dropbear，密码登录在 debian 中失败。解决办法就是使用 <code>chpasswd</code> 来配置密码，例如 <code>echo 'root:123456' | chpasswd -c SHA256</code></li>
</ul>

<h2 id="实现自定义鉴权">实现自定义鉴权</h2>

<p>从上述可以看出 dropbear 异常轻量级，且可以在同一 CPU 架构的任意 Linux 镜像环境中运行。因此 dropbear 可以作为通用能力注入到任意的 Linux 环境中，而不需要考虑不同 Linux 发行版兼容性问题。</p>

<p>在如上的场景，当多个用户对多个 Linux 环境拥有不同的访问权限时，如果使用密码或者公私钥的方式来进行鉴权存在很大不便和安全性问题。因此，需要一种中心化的方案，可以灵活的配置用户对 Linux 环境的权限。</p>

<p>可选的方案有如下几种：</p>

<ul>
<li>Kerberos (GSSAPI)，业界成熟的解决方案。缺点是历史悠久，机制比较复杂，缺乏中文资料，学习成本比较高，定制成本高。（dropbear 似乎不支持）</li>
<li>基于 <code>PasswordAuthentication</code> 验证方式进行定制。有两种实现方式：

<ul>
<li>基于 LinuxPAM 实现，灵活性高。（dropbear 支持，但是如果启用该特性，则很难进程静态编译，没有走通）。</li>
<li>直接修改源码中 Password 的部分，灵活性高。（dropbear 对应的源代码文件为 <a href="https://github.com/mkj/dropbear/blob/master/svr-authpasswd.c"><code>svr-authpasswd.c</code></a>，下文探索的方式）。</li>
</ul></li>
</ul>

<p>因此，如果想在 SSH Server 实现高度可定制的鉴权，且不破坏 SSH 协议标准，推荐的方案是使用 dropbear 作为 ssh server，通过修改 <a href="https://github.com/mkj/dropbear/blob/master/svr-authpasswd.c"><code>svr-authpasswd.c</code></a> 来进行自定义鉴权。流程大概如下：</p>

<ol>
<li>用户在鉴权中心页面，创建一个 Token。</li>
<li>用户调用 ssh 命令连接到 dropbear ssh server，在提示输入密码时，填写 Token。</li>
<li>dropbear ssh server 在进行密码校验时，会调用修改过的 <a href="https://github.com/mkj/dropbear/blob/master/svr-authpasswd.c"><code>svr-authpasswd.c</code></a> 代码，该代码会调用一个外部命令，并将用户登录的操作系统用户，密码（Token）作为参数，传递给这个外部命令，如果该命令退出码为 0，则认为认证成功，否则认为认证失败。</li>
<li>这个外部命令调用鉴权中心 API，参数为：Linux 环境标识（如主机名），登录的操作系统用户，密码（Token），返回：是否有权限。如果有权限，则 <code>exit 0</code>，否则 <code>exit 1</code>。</li>
</ol>

<p>注意：</p>

<ul>
<li>上述第 1 步的 Token 的生成只是个简单示例。可以通过一个 CLI 唤起一个网页与通过网页进行鉴权和 Token 生成，实现更好用户体验。</li>
<li>上述第 2 步是个手动操作示例。有两种方式可以实现 Token 输入自动化：

<ul>
<li>通过一个类似 sshpass 的 CLI 包装 ssh 命令实现注入 Token。</li>
<li>通过一个 CLI，这个 CLI 会配置到用户配置 <code>~/.ssh/config</code> 的 <code>ProxyCommand</code> 中，这 CLI 会作为中间人拦截认证过程。</li>
</ul></li>
</ul>

<p>下面，通过修改 <a href="https://github.com/mkj/dropbear/blob/master/svr-authpasswd.c"><code>svr-authpasswd.c</code></a> 源码，添加一些日志，来探索如何实现上述第 3 步。</p>

<p>修改 <code>svr-authpasswd.c</code> （<a href="https://github.com/rectcircle/dropbear/commit/3533a7974765611c3212c64f1a6f84f73f774b25">github commit</a>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">// 33 行
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 73 行
</span><span style="color:#75715e"></span>printf(<span style="color:#e6db74">&#34;user=%s uid=%u passwd=%s passwdcrypt=%s testcrypt=%s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ses.authstate.pw_name, ses.authstate.pw_uid, password, passwdcrypt, testcrypt);</code></pre></div>
<p>重新编译运行后，使用 <code>ssh root@localhost -p 2222</code> 并输入密码 <code>123456</code>，可以看到 dropbear 打印如下日志。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">user=root uid=0 passwd=123456 passwdcrypt=$5$KDM.1/A3kjHyuqm$.1la1CUeFr/aLLF2FOJTADfsE11PcegBbQl9v9DU6GA testcrypt=$5$KDM.1/A3kjHyuqm$.1la1CUeFr/aLLF2FOJTADfsE11PcegBbQl9v9DU6GA</pre></div>
<p>因此，如果想实现自定义密码校验，只需在 <a href="https://github.com/mkj/dropbear/blob/DROPBEAR_2022.83/svr-authpasswd.c#L126"><code>svr-authpasswd.c</code> 的 126 行</a>，fork 一个子进程将上述测试的变量通过环境变量传递过去，根据子进程的退出码决定是否发送成功消息 <code>send_msg_userauth_success()</code>。</p>
]]></description></item><item><title>Go 静态编译 和 CGO</title><link>https://www.rectcircle.cn/posts/go-static-compile-and-cgo/</link><pubDate>Thu, 08 Dec 2022 22:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-static-compile-and-cgo/</guid><description type="html"><![CDATA[

<h2 id="linux-c-静态编译">Linux C 静态编译</h2>

<p>本部分，将复习一下学校里可能学过的 Linux C 编译相关的知识，以帮助更好的理解 Go 语言的静态编译和 CGO。</p>

<p>具体参见博客：<a href="/posts/linux-c-static-compile/">《Linux C 静态编译》</a>。</p>

<h2 id="cgo">CGO</h2>

<h3 id="目的">目的</h3>

<p>Go 作为近些年来新晋的现代编程语言，而 C 语言是历史悠久，拥有很多优秀的函数库，和操作系统 API 集成更好。</p>

<p>因此，为了更好了使用 C 语言生态，Go 提供了调用 C 语言函数的能力，这个能力被称为 CGO。</p>

<h3 id="写法">写法</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#75715e">/*
</span><span style="color:#75715e">#include &lt;stdio.h&gt;
</span><span style="color:#75715e">#include &lt;stdlib.h&gt;
</span><span style="color:#75715e">
</span><span style="color:#75715e">int add(int a, int b) {
</span><span style="color:#75715e">    return a + b;
</span><span style="color:#75715e">}
</span><span style="color:#75715e">
</span><span style="color:#75715e">void println(char *s) {
</span><span style="color:#75715e">    printf(&#34;%s\n&#34;, s);
</span><span style="color:#75715e">}
</span><span style="color:#75715e">*/</span>
<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;C&#34;</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;unsafe&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CAdd</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> int(<span style="color:#a6e22e">C</span>.<span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">C</span>.int(<span style="color:#ae81ff">1</span>), <span style="color:#a6e22e">C</span>.int(<span style="color:#ae81ff">2</span>)))
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CPrintln</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) {
	<span style="color:#a6e22e">cs</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">C</span>.<span style="color:#a6e22e">CString</span>(<span style="color:#a6e22e">s</span>)
    <span style="color:#a6e22e">C</span>.<span style="color:#a6e22e">Go</span>
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">C</span>.<span style="color:#a6e22e">free</span>(<span style="color:#a6e22e">unsafe</span>.<span style="color:#a6e22e">Pointer</span>(<span style="color:#a6e22e">cs</span>))
	<span style="color:#a6e22e">C</span>.println(<span style="color:#a6e22e">cs</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;CAdd(1, 2) = %d\n&#34;</span>, <span style="color:#a6e22e">CAdd</span>(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>))
	<span style="color:#a6e22e">CPrintln</span>(<span style="color:#e6db74">&#34;abc&#34;</span>)
}</code></pre></div>
<ul>
<li>通过 <code>import &quot;C&quot;</code> 语句表示使用 CGO，C 的源码，通过该语句紧挨着上方的注释提供。</li>
<li>通过 <code>C.c函数名</code> 的方式，即可调用 C 语言的函数。函数的参数和返回值的类型只能使用 <code>C.xxx</code> 类型。如上所示：

<ul>
<li><code>C.int</code> C 语言的 int，其他非指针基础类型类似。</li>
<li><code>C.CString</code> 将一个 Go 语言的 string 转换为 C 语言的 <code>char*</code>，注意，使用完成后，需要调用 free。</li>
<li><code>C.GoString</code> 将一个 C 语言的 <code>char*</code> 转化为 Go 语言的 string。</li>
<li>更多参见：<a href="https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-03-cgo-types.html">Go语言高级编程 - CGO - 类型转换</a></li>
</ul></li>
<li>本文重点是静态编译，对 CGO 有个概念即可，更多关于 CGO，参见：<a href="https://chai2010.cn/advanced-go-programming-book/ch2-cgo/index.html">Go语言高级编程 - CGO</a>。</li>
</ul>

<h3 id="编译过程-默认">编译过程（默认）</h3>

<p>通过 <code>go clean --cache &amp;&amp; rm -rf main</code> 清理缓存后，通过 <code>go build -work -x main.go</code> 命令即可观察到，上述包含 CGO 的代码的编译过程，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">WORK<span style="color:#f92672">=</span>/tmp/go-build3000103738
mkdir -p $WORK/b001/
cd /home/rectcircle/learn-cgo
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> CGO_LDFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;&#34;-g&#34; &#34;-O2&#34;&#39;</span> /usr/local/lib/go/pkg/tool/linux_amd64/cgo -objdir $WORK/b001/ -importpath command-line-arguments -- -I $WORK/b001/ -g -O2 ./main.go
cd $WORK
gcc -fno-caret-diagnostics -c -x c - -o /dev/null <span style="color:#f92672">||</span> true
gcc -Qunused-arguments -c -x c - -o /dev/null <span style="color:#f92672">||</span> true
gcc -fdebug-prefix-map<span style="color:#f92672">=</span>a<span style="color:#f92672">=</span>b -c -x c - -o /dev/null <span style="color:#f92672">||</span> true
gcc -gno-record-gcc-switches -c -x c - -o /dev/null <span style="color:#f92672">||</span> true
cd $WORK/b001
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I /home/rectcircle/learn-cgo -fPIC -m64 -pthread -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -fdebug-prefix-map<span style="color:#f92672">=</span>$WORK/b001<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -o ./_x001.o -c _cgo_export.c
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I /home/rectcircle/learn-cgo -fPIC -m64 -pthread -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -fdebug-prefix-map<span style="color:#f92672">=</span>$WORK/b001<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -o ./_x002.o -c main.cgo2.c
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I /home/rectcircle/learn-cgo -fPIC -m64 -pthread -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -fdebug-prefix-map<span style="color:#f92672">=</span>$WORK/b001<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -o ./_cgo_main.o -c _cgo_main.c
cd /home/rectcircle/learn-cgo
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> gcc -I . -fPIC -m64 -pthread -fmessage-length<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -fdebug-prefix-map<span style="color:#f92672">=</span>$WORK/b001<span style="color:#f92672">=</span>/tmp/go-build -gno-record-gcc-switches -o $WORK/b001/_cgo_.o $WORK/b001/_cgo_main.o $WORK/b001/_x001.o $WORK/b001/_x002.o -g -O2
TERM<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;dumb&#39;</span> /usr/local/lib/go/pkg/tool/linux_amd64/cgo -dynpackage main -dynimport $WORK/b001/_cgo_.o -dynout $WORK/b001/_cgo_import.go
cat &gt;$WORK/b001/_gomod_.go <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39; # internal
</span><span style="color:#e6db74">package main
</span><span style="color:#e6db74">import _ &#34;unsafe&#34;
</span><span style="color:#e6db74">//go:linkname __debug_modinfo__ runtime.modinfo
</span><span style="color:#e6db74">var __debug_modinfo__ = &#34;0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nmod\tgithuh.com/rectcircle/learn-go-supervisord\t(devel)\t\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2&#34;
</span><span style="color:#e6db74">EOF</span>
cat &gt;$WORK/b001/importcfg <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39; # internal
</span><span style="color:#e6db74"># import config
</span><span style="color:#e6db74">packagefile fmt=/usr/local/lib/go/pkg/linux_amd64/fmt.a
</span><span style="color:#e6db74">packagefile runtime/cgo=/usr/local/lib/go/pkg/linux_amd64/runtime/cgo.a
</span><span style="color:#e6db74">packagefile syscall=/usr/local/lib/go/pkg/linux_amd64/syscall.a
</span><span style="color:#e6db74">packagefile runtime=/usr/local/lib/go/pkg/linux_amd64/runtime.a
</span><span style="color:#e6db74">EOF</span>
/usr/local/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath <span style="color:#e6db74">&#34;</span>$WORK<span style="color:#e6db74">/b001=&gt;&#34;</span> -p main -lang<span style="color:#f92672">=</span>go1.17 -buildid vq722mILkfSlVJkLngsl/vq722mILkfSlVJkLngsl -goversion go1.17.3 -D _/home/rectcircle/learn-cgo -importcfg $WORK/b001/importcfg -pack $WORK/b001/_cgo_gotypes.go $WORK/b001/main.cgo1.go $WORK/b001/_cgo_import.go $WORK/b001/_gomod_.go
/usr/local/lib/go/pkg/tool/linux_amd64/pack r $WORK/b001/_pkg_.a $WORK/b001/_x001.o $WORK/b001/_x002.o <span style="color:#75715e"># internal</span>
/usr/local/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a <span style="color:#75715e"># internal</span>
cp $WORK/b001/_pkg_.a /home/rectcircle/.cache/go-build/7e/7e99e06a115dae258bf0ea0a07606e643c1cfd0c87a62ae41b9963d5438c0264-d <span style="color:#75715e"># internal</span>
cat &gt;$WORK/b001/importcfg.link <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39; # internal
</span><span style="color:#e6db74">packagefile command-line-arguments=$WORK/b001/_pkg_.a
</span><span style="color:#e6db74">packagefile fmt=/usr/local/lib/go/pkg/linux_amd64/fmt.a
</span><span style="color:#e6db74">packagefile runtime/cgo=/usr/local/lib/go/pkg/linux_amd64/runtime/cgo.a
</span><span style="color:#e6db74">packagefile syscall=/usr/local/lib/go/pkg/linux_amd64/syscall.a
</span><span style="color:#e6db74">packagefile runtime=/usr/local/lib/go/pkg/linux_amd64/runtime.a
</span><span style="color:#e6db74">packagefile errors=/usr/local/lib/go/pkg/linux_amd64/errors.a
</span><span style="color:#e6db74">packagefile internal/fmtsort=/usr/local/lib/go/pkg/linux_amd64/internal/fmtsort.a
</span><span style="color:#e6db74">packagefile io=/usr/local/lib/go/pkg/linux_amd64/io.a
</span><span style="color:#e6db74">packagefile math=/usr/local/lib/go/pkg/linux_amd64/math.a
</span><span style="color:#e6db74">packagefile os=/usr/local/lib/go/pkg/linux_amd64/os.a
</span><span style="color:#e6db74">packagefile reflect=/usr/local/lib/go/pkg/linux_amd64/reflect.a
</span><span style="color:#e6db74">packagefile strconv=/usr/local/lib/go/pkg/linux_amd64/strconv.a
</span><span style="color:#e6db74">packagefile sync=/usr/local/lib/go/pkg/linux_amd64/sync.a
</span><span style="color:#e6db74">packagefile unicode/utf8=/usr/local/lib/go/pkg/linux_amd64/unicode/utf8.a
</span><span style="color:#e6db74">packagefile sync/atomic=/usr/local/lib/go/pkg/linux_amd64/sync/atomic.a
</span><span style="color:#e6db74">packagefile internal/bytealg=/usr/local/lib/go/pkg/linux_amd64/internal/bytealg.a
</span><span style="color:#e6db74">packagefile internal/itoa=/usr/local/lib/go/pkg/linux_amd64/internal/itoa.a
</span><span style="color:#e6db74">packagefile internal/oserror=/usr/local/lib/go/pkg/linux_amd64/internal/oserror.a
</span><span style="color:#e6db74">packagefile internal/race=/usr/local/lib/go/pkg/linux_amd64/internal/race.a
</span><span style="color:#e6db74">packagefile internal/unsafeheader=/usr/local/lib/go/pkg/linux_amd64/internal/unsafeheader.a
</span><span style="color:#e6db74">packagefile internal/abi=/usr/local/lib/go/pkg/linux_amd64/internal/abi.a
</span><span style="color:#e6db74">packagefile internal/cpu=/usr/local/lib/go/pkg/linux_amd64/internal/cpu.a
</span><span style="color:#e6db74">packagefile internal/goexperiment=/usr/local/lib/go/pkg/linux_amd64/internal/goexperiment.a
</span><span style="color:#e6db74">packagefile runtime/internal/atomic=/usr/local/lib/go/pkg/linux_amd64/runtime/internal/atomic.a
</span><span style="color:#e6db74">packagefile runtime/internal/math=/usr/local/lib/go/pkg/linux_amd64/runtime/internal/math.a
</span><span style="color:#e6db74">packagefile runtime/internal/sys=/usr/local/lib/go/pkg/linux_amd64/runtime/internal/sys.a
</span><span style="color:#e6db74">packagefile internal/reflectlite=/usr/local/lib/go/pkg/linux_amd64/internal/reflectlite.a
</span><span style="color:#e6db74">packagefile sort=/usr/local/lib/go/pkg/linux_amd64/sort.a
</span><span style="color:#e6db74">packagefile math/bits=/usr/local/lib/go/pkg/linux_amd64/math/bits.a
</span><span style="color:#e6db74">packagefile internal/poll=/usr/local/lib/go/pkg/linux_amd64/internal/poll.a
</span><span style="color:#e6db74">packagefile internal/syscall/execenv=/usr/local/lib/go/pkg/linux_amd64/internal/syscall/execenv.a
</span><span style="color:#e6db74">packagefile internal/syscall/unix=/usr/local/lib/go/pkg/linux_amd64/internal/syscall/unix.a
</span><span style="color:#e6db74">packagefile internal/testlog=/usr/local/lib/go/pkg/linux_amd64/internal/testlog.a
</span><span style="color:#e6db74">packagefile io/fs=/usr/local/lib/go/pkg/linux_amd64/io/fs.a
</span><span style="color:#e6db74">packagefile time=/usr/local/lib/go/pkg/linux_amd64/time.a
</span><span style="color:#e6db74">packagefile unicode=/usr/local/lib/go/pkg/linux_amd64/unicode.a
</span><span style="color:#e6db74">packagefile path=/usr/local/lib/go/pkg/linux_amd64/path.a
</span><span style="color:#e6db74">EOF</span>
mkdir -p $WORK/b001/exe/
cd .
/usr/local/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode<span style="color:#f92672">=</span>exe -buildid<span style="color:#f92672">=</span>hIqSnweqCct6EAyj-ie9/vq722mILkfSlVJkLngsl/sc8_MwUXPVexy5egmON-/hIqSnweqCct6EAyj-ie9 -extld<span style="color:#f92672">=</span>gcc $WORK/b001/_pkg_.a
/usr/local/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out <span style="color:#75715e"># internal</span>
mv $WORK/b001/exe/a.out main</code></pre></div>
<p>当项目中包含，<code>import &quot;C&quot;</code> 语句时：</p>

<ul>
<li>预处理：首先调用 <code>pkg/tool/linux_amd64/cgo</code> 对 go 源码进行预处理，生成中间源码文件，在上例中，位于 <code>/tmp/go-build3000103738/b001</code>，其中 <code>main.cgo2.c</code> 是 C 语言的源码部分。</li>
<li>编译 C 源码：调用系统默认的 C 语言编译器（可通过 <code>CC</code> 环境变量指定），在本例中为 gcc。

<ul>
<li>将 c 源码文件编译成 <code>.o</code> 文件，如 <code>main.cgo2.c</code> 被编译成了 <code>_x002.o</code>。</li>
<li>将所有 <code>.o</code> 生成 <code>_cgo_.o</code>。</li>
</ul></li>
<li>生成动态链接库信息：调用 <code>pkg/tool/linux_amd64/cgo</code> 根据 <code>_cgo_.o</code> 生成包含动态链接信息的代码文件，位于 <code>_cgo_import.go</code>。</li>
<li>编译 Go 源码：<code>pkg/tool/linux_amd64/compile</code> 编译 go 源码生成 <code>_pkg_.a</code>。</li>
<li>打包 C 源码的 <code>.o</code>：<code>pkg/tool/linux_amd64/pack</code> 将 C 源码生成的 <code>.o</code> 打包到 <code>_pkg_.a</code> 中。</li>
<li>写入 buildid：<code>pkg/tool/linux_amd64/buildid</code> 将 buildid 写入 <code>_pkg_.a</code>。</li>
<li>生成可执行文件（链接阶段）：<code>pkg/tool/linux_amd64/link</code> 为 <code>_pkg_.a</code> 链接上 Go 源码引入的其他 go package，并生成可执行文件 <code>a.out</code></li>
<li>复制产物：将 <code>a.out</code> 复制到目标位置。</li>
</ul>

<p>通过 <code>ldd main</code> 查看，动态链接库情况可以看出，CGO 引入如下动态链接库：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">        linux-vdso.so.1 (0x00007ffe875d2000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f058bbf5000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f058ba20000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f058bc1f000)</pre></div>
<h3 id="编译过程-静态">编译过程（静态）</h3>

<p>通过 <code>go clean --cache &amp;&amp; rm -rf main</code> 清理缓存后，通过 <code>go build -work -x -ldflags &quot;-linkmode external -extldflags -static&quot; main.go</code> 命令即可观察到，上述包含 CGO 的代码的编译过程，和上面的默认编译相比，输出唯一的变化如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/local/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=O0JDvIMHA7Jvz8sJWN7S/vq722mILkfSlVJkLngsl/sc8_MwUXPVexy5egmON-/O0JDvIMHA7Jvz8sJWN7S -linkmode external -extldflags -static -extld=gcc $WORK/b001/_pkg_.a</pre></div>
<p>通过 <code>ldd main</code> 查看，动态链接库情况可以看出，静态链接后，没有了动态链接库：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">        不是动态可执行文件</pre></div>
<h3 id="cgo-enabled-环境变量">CGO_ENABLED 环境变量</h3>

<p>通过 <code>go clean --cache &amp;&amp; rm -rf main</code> 清理缓存后，通过 <code>CGO_ENABLED=0 go build -work -x main.go</code> 命令即可观察到，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">WORK=/tmp/go-build3880112141
go: no Go source files</pre></div>
<p>可以看出，当 <code>CGO_ENABLED=0</code>，go 编译器会忽略掉包含了 <code>import &quot;C&quot;</code> 的源代码文件，直接报错。</p>

<p>也就是说，默认情况下 <code>CGO_ENABLED=1</code>。另外，当项目使用了 CGO 时，必须保证 <code>CGO_ENABLED=1</code>，否则将无法进行编译。</p>

<h3 id="cgo-的跨平台问题">CGO 的跨平台问题</h3>

<p>Go 语言的一大特色就是，一次编写，多次编译，到处运行。而 C 语言的跨平台特性非常差，因此使用 CGO，会让 Go 语言的跨平台特性劣化到 C 语言的水准。</p>

<p>其次，通过 <code>ldd main</code> （默认编译）的输出。可以看到，由于我们使用 CGO，而引入对外部动态链接库的依赖，而纯 Go 代码不会有此问题。</p>

<p>最后，Go 语言本身已经比较强大了，多数常规能力通过纯 Go 基本都可以实现（某些与操作系统特性紧密相连的场景除外，如 runc）。</p>

<p>因此，这里的建议是：能用 Go 实现的就用 Go 来实现。</p>

<h2 id="go-标准库-和-cgo">Go 标准库 和 CGO</h2>

<p>Go 的标准库基本上所有的包都是通过 Go 语言来实现。</p>

<p>但是，对于如下的标注库中的包（可能出于性能考虑），除了提供 Go 语言的实现外，还提供了 CGO 的实现：</p>

<ul>
<li><code>os/user</code></li>
<li><code>net</code></li>
</ul>

<p>在默认情况下（Linux 平台，CGO_ENABLED=1），项目使用了如上包中的函数，在编译时，将使用 CGO 的实现。此时 ldd 查看可执行文件，将看到 <code>libc.so</code> 等动态链接库的依赖。如 <code>main.go</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>
)
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Lookup</span>(<span style="color:#e6db74">&#34;root&#34;</span>))
}</code></pre></div>
<p><code>ldd main</code> 将输出：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">        linux-vdso.so.1 (0x00007fff43bf0000)
        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7e3b1b1000)
        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7e3afdc000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7e3b1db000)</pre></div>
<p>此时，通过两种方式可以实现使用标准库中纯 Go 的实现：</p>

<ul>
<li>方式 1：设置 <code>CGO_ENABLED=0</code> 环境变量，禁用 CGO，命令为 <code>CGO_ENABLED=0 go build main.go</code>。</li>
<li>方式 2：使用标准库这两个包提供的条件编译标签，强制指定使用纯 Go 的实现。命令为 <code>go build -tags osusergo,netgo main.go</code>。</li>
</ul>

<h2 id="go-静态编译场景">Go 静态编译场景</h2>

<p>这里的静态编译指的是，Go 项目编译出的可执行文件是没有任何动态链接库的依赖。</p>

<table>
<thead>
<tr>
<th></th>
<th>项目有 CGO 代码 且 外部库<strong>有</strong>用到 dlopen (如 glibc)</th>
<th>项目有 CGO 代码 且 外部库<strong>未</strong>用到 dlopen (如  musl-libc)</th>
<th>项目无 CGO 代码</th>
</tr>
</thead>

<tbody>
<tr>
<td>使用标准库 cgo 实现</td>
<td>①</td>
<td>②</td>
<td>③</td>
</tr>

<tr>
<td>使用标准库 go  实现</td>
<td>④</td>
<td>⑤</td>
<td>⑥</td>
</tr>
</tbody>
</table>

<p>下面的场景，在 Linux 平台，编译当前项目 <code>./cmd</code> 目录下的 <code>main</code> 包为例。</p>

<h3 id="场景-①">场景 ①</h3>

<p>针对该场景，无法实现静态编译，如果强制使用如下命令，将警告，并在很多场景直接 panic。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 使用 -ldflags 指定：</span>
<span style="color:#75715e">#   -linkmode external : 使用外部链接器</span>
<span style="color:#75715e">#   --extldflags -static : 告知 C 语言编译器使用静态编译。</span>
go build -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> ./cmd</code></pre></div>
<p>编译时，将出现，类似如下警告：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Using &#39;xxx&#39; in statically linked applications requires at runtime the shared libraries from the glibc version used for linking</pre></div>
<h3 id="场景-②">场景 ②</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 musl-libc 编译器</span>
sudo apt-get install musl-tools
<span style="color:#75715e"># 使用 CC 指定 C 语言编译器为 musl-gcc</span>
<span style="color:#75715e"># 使用 -ldflags 指定：</span>
<span style="color:#75715e">#   -linkmode external : 使用外部链接器</span>
<span style="color:#75715e">#   --extldflags -static : 告知 C 语言编译器使用静态编译。</span>
CC<span style="color:#f92672">=</span>musl-gcc go build -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> ./cmd</code></pre></div>
<p>注意，如果当前项目不包含 CGO 的依赖如上传递了 <code>-ldflags</code> 参数，编译将报错。</p>

<h3 id="场景-③">场景 ③</h3>

<p>由于 Go 标准库的 CGO 实现，依赖 libc，因此，无法使用 glibc。所以解法和 <strong>场景 ②</strong> 一致。</p>

<h3 id="场景-④">场景 ④</h3>

<p>无法实现，原因参见： <strong>场景 ①</strong>。</p>

<h3 id="场景-⑤">场景 ⑤</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 musl-libc 编译器</span>
sudo apt-get install musl-tools
<span style="color:#75715e"># 使用 CC 指定 C 语言编译器为 musl-gcc</span>
<span style="color:#75715e"># 使用 -tags osusergo,netgo 指定：标准库使用纯 Go 实现。</span>
<span style="color:#75715e"># 使用 -ldflags 指定：</span>
<span style="color:#75715e">#   -linkmode external : 使用外部链接器</span>
<span style="color:#75715e">#   --extldflags -static : 告知 C 语言编译器使用静态编译。</span>
CC<span style="color:#f92672">=</span>musl-gcc go build -tags osusergo,netgo -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> ./cmd</code></pre></div>
<p>注意，如果当前项目不包含 CGO 的依赖如上传递了 <code>-ldflags</code> 参数，编译将报错。</p>

<h3 id="场景-⑥">场景 ⑥</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># CGO_ENABLED=0 直接禁用 CGO 即可</span>
CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> go build ./cmd</code></pre></div>]]></description></item><item><title>Linux C 静态编译</title><link>https://www.rectcircle.cn/posts/linux-c-static-compile/</link><pubDate>Mon, 05 Dec 2022 12:13:10 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-c-static-compile/</guid><description type="html"><![CDATA[

<h2 id="linux-c-编译过程">Linux C 编译过程</h2>

<p>Linux 又称 <a href="https://www.gnu.org/gnu/linux-and-gnu.html">GNU/Linux</a> 。自然而然 Linux 提供的系统调用（API）通过 glibc 提供，通过 C 语言描述，因此 C 语言是离 Linux 内核最近的编程语言。</p>

<p>GCC 的编译一个可执行文件的过程，可以按照顺序分为如下 4 个阶段，这 4 个阶段前一个阶段是后一个阶段输入进行连接。</p>

<ul>
<li>预处理。输入 <code>.c</code> (C 语言代码文件) 和 <code>.h</code> （头文件） 源代码文件。输出预编译文件 <code>.i</code>。该过程是对 C 语言宏的处理。</li>
<li>编译，输出汇编文件 <code>.s</code>。该过程将 C 代码转换为等价的汇编代码。</li>
<li>汇编，生成目标文件 <code>.o</code>。该过程将汇编代码转换为机器语言代码。</li>
<li>链接，生成可执行文件。该过程将 <code>.o</code> 依赖的外部全局变量和函数的定义链接到 <code>.o</code> 文件中，并生成可执行文件。</li>
</ul>

<p>这里需要特别说明的是：</p>

<ul>
<li>上文介绍的时 C 语言的可执行文件的编译过程。在 Linux 中，最终编译产物还是两种其他类：静态链接库 (<code>.so</code>) 和动态链接库 (<code>.a</code>)。要编译生成这两种类型的产物的前 3 个阶段和编译一个可执行文件的过程一样，都需要预处理、编译、汇编的过程。</li>
<li>Go 语言编译成可执行文件的过程，在链接阶段和 GCC 的过程是类似的，因此我们需要重点看链接的过程。其他过程本文不多做介绍。</li>
</ul>

<h2 id="动态链接原理和优点">动态链接原理和优点</h2>

<p>我们编写的代码，最终会编译成可执行文件，这个可执行文件会占用磁盘空间。可执行文件执行时，可执行文件本身会加载到内存中，占用内存资源。</p>

<p>编写一个 C 语言的程序，需要调用一些通用的函数（如 <a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#ISO-C">ISO C</a>）以及一些操作系统提供的系统调用的封装函数。这些依赖，基本上是所有 Linux C 的应用程序所必备的。</p>

<p>在计算机发展早期，磁盘和内存的资源是及其昂贵的。如果每个程序，都需要将这些函数的实现编译到可执行文件中，这样就造成了磁盘和内存资源的浪费。</p>

<p>为此，操作系统，通过动态链接的能力，以节约，可执行文件自身占在用的磁盘空间，以及其在加载后占用内存资源。</p>

<p>Linux 动态链接的流程为：</p>

<ul>
<li>将函数库，编译成动态链接库 (<code>.so</code>)。</li>
<li>可执行文件如果依赖该函数库的函数，在链接阶段将该函数库的名字 (标识) 声明在可执行文件中。此时，生成的可执行文件就不会包含动态链接库的内容，而只存在一个引用，从而节省了磁盘空间。</li>
<li>可执行文件在执行时，当调用位于动态链接库中的函数时，会首先查找该动态链接库是否加载过了，如果已经加载过了，则执行执行已加载的函数代码，否则去磁盘中查找对应的动态链接库，并加载。这就保证同一个动态链接库，在内存中只存在一份，从而节省了内存空间。</li>
</ul>

<p>以上，是动态链接要解决的主要问题。除了上述流程外，Linux 还提供了另一种使用动态链接库的方法：在代码中动态的调用 <code>ldopen</code> 等系统调用来加载甚至替换一个外部函数库。利用这个特性，可以实现代码程序的热更新(参考： <a href="https://howardlau.me/programming/c-cpp-hot-reload.html">Linux C/C++ 实现热更新</a>)，不需要重启进程。</p>

<h2 id="动态链接的缺点">动态链接的缺点</h2>

<p>没有什么好处是没有代价的，动态链接的本质是一种复用，复用意味着一种耦合。因此会带来如下问题：</p>

<ul>
<li>应用程序存在外部依赖，这给程序的部署带来困难。</li>
<li>多个程序依赖不同版本的同一动态链接库时，存在的冲突的问题。</li>
</ul>

<p>目前，随着磁盘和存储的成本不断地降低，动态链接的缺点带来的问题已经远大于其优点带来的收益了。为了解决这些问题，又提出了很多技术，如：</p>

<ul>
<li>在移动端（如 Android），将应用程序的所有依赖打包到一个压缩包 (<code>.apk</code>)，包括 <code>.so</code>。</li>
<li>在服务端，将所有的程序和依赖 (<code>so</code>) 容打包成一个镜像，并以容器化方式运行 (mount namespace)。</li>
<li>各种编程语言通过如下方式解决这些问题：

<ul>
<li>通过虚拟机封装操作系统的差异，如 Java。</li>
<li>支持通过静态编译的方式生成无外部依赖的可执行文件，如 Go。</li>
</ul></li>
</ul>

<h2 id="c-语言的库-和-libc">C 语言的库 和 libc</h2>

<p>C 语言虽然是一种高级语言，但是和其他的编程语言相比，有一个特殊的身份，即系统编程语言，具体而言就是：</p>

<ul>
<li>多数操作系统是由 C 语言编写的，这要求其标准库是可选的，且需要能操作非常底层的硬件资源，需要具有巨大灵活性。</li>
<li>多数操作系统的系统调用 (API) 是通过 C 语言函数调用方式提供。</li>
</ul>

<p>因此，C 语言函数库的可以分为标准的跨平台的部分、操作系统专有两个部分：</p>

<ul>
<li>跨平台的部分主要的标准有：<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#ISO-C">ISO C</a>、<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#POSIX">POSIX</a>。</li>
<li>操作系统专有的库有：<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#Berkeley-Unix">BSD</a>、<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#SVID">SVID</a>、<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#XPG">XPG</a>、Linux。这些特殊特定函数，会通过宏来开启，如 Linux 的 <a href="http://musl.libc.org/doc/1.1.24/manual.html"><code>_GNU_SOURCE</code></a>。</li>
</ul>

<p>这样 在编写 C 程序时，对于的选择就有了三种：</p>

<ul>
<li>不使用标准库，在无操作系统的嵌入式领域或者操作系统领域。</li>
<li>仅使用跨平台的标准库，这样编写的 C 程序：

<ul>
<li>只是用 ISO C 几乎可以在所有的操作系统中使用，包括 windows。</li>
<li>使用了 POSIX，及基本上可以在类 Unix 跨平台编译。</li>
</ul></li>
<li>使用了操作系统专有的库，则大概率只能在该操作系统进行运行。对于 Linux ，一些主流的开源 Unix 应该都是可以运行的。</li>
</ul>

<p>libc 指 C 语言标准库。不同的 libc 对如上标准或库的情况也是不一样的。一般情况下，会实现标准的 <a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#ISO-C">ISO C</a>、<a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#POSIX">POSIX</a>，在加上该 libc 面向的操作系统的专有库。</p>

<p>下面介绍，在 Linux 中，<a href="http://www.etalabs.net/compare_libcs.html">主流的 libc 库</a>：</p>

<ul>
<li><a href="https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html">glibc</a>，大而全的历史包袱很重的 libc 库，据说代码质量很差。为了实现 Linux 宣扬的其他类 Unix 操作系统可以在 Linux 中编译，因此支持上述的所有库。是 GNU 基金会下的产物，服务端领域主流的 Linux 版采用的 libc 实现，是 Linux 系统事实上的 libc 标准实现。采用 LGPL 协议（<a href="https://www.zyxtech.org/2016/04/28/55/">静态编译不友好</a>），下文会专门介绍。</li>
<li><a href="http://www.musl-libc.org/intro.html">musl-libc</a>，定位为下一代 Linux 设备，采用 MIT 协议，专为静态编译设计，支持主流的指令集。主要应用云原生和嵌入式领域。</li>
<li><a href="https://uclibc-ng.org/">uClibc-ng</a>，面向嵌入式的 libc，采用 LGPL 协议（<a href="https://www.zyxtech.org/2016/04/28/55/">静态编译不友好</a>）。</li>
</ul>

<h2 id="glibc-的动态链接问题">glibc 的动态链接问题</h2>

<blockquote>
<p>参考：<a href="https://sourceware.org/glibc/wiki/FAQ#Even_statically_linked_programs_need_some_shared_libraries_which_is_not_acceptable_for_me.__What_can_I_do.3F">glibc FAQ</a> | <a href="https://stackoverflow.com/questions/57476533/why-is-statically-linking-glibc-discouraged">stackoverflow</a></p>
</blockquote>

<p>glibc 有一个比较大的问题，即默认情况下 glibc 不支持静态链接。主要原因是：</p>

<ul>
<li>glibc 的一些实现是依赖其他动态链接库实现的，比如 NSS, gconv, IDN 以及 thread cancellation（通过 dlopen 方式，所以 ldd 命令看不到，但是源码可以看出来，如：<a href="https://github.com/bminor/glibc/blob/master/nss/nss_module.c">libnss 相关</a>）。</li>
<li>这些动态链接库又声明了对 glibc 的依赖，这样就造成了循环依赖。比如，静态编译了 glibc，由于 glibc 依赖了 <code>libnss3.so</code>（<code>sudo ldconfig -p | grep nss</code>），而 <code>ldd /usr/lib/x86_64-linux-gnu/libnss3.so</code>，此时我们的程序还是会加载一个 glibc 的动态链接库。</li>
<li>这就造成了两个问题：

<ul>
<li>我们的程序间接依赖 <code>libnss3.so</code> 的动态链接库，且 ldd 也看不到，这与我们想要的静态链接，无 <code>.so</code> 依赖背道而驰。</li>
<li>在运行时，同一个 glibc 函数/全局变量在两个地方都有是实现，一个是静态编译的，一个是通过类似 <code>libnss3.so</code>  间接引入的 <code>libc.so.6</code>（即 glibc），这可能带来并发问题。</li>
</ul></li>
<li>因此，在发布于 2018 的 glibc 2.27 中，在编译阶段如果指定了静态链接，就会出现警告（只要使用到了 ldopen 之类的函数都会报该问题）。</li>
</ul>

<p>比如，下面一份来自 Go 标准库中 <code>os/user</code> 的一份 cgo 代码的部分。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#define _GNU_SOURCE
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;pwd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">static</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">mygetpwuid_r</span>(<span style="color:#66d9ef">int</span> uid, <span style="color:#66d9ef">struct</span> passwd <span style="color:#f92672">*</span>pwd,
	<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>buf, size_t buflen, <span style="color:#66d9ef">struct</span> passwd <span style="color:#f92672">**</span>result) {
	<span style="color:#66d9ef">return</span> getpwuid_r(uid, pwd, buf, buflen, result);
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">main</span>()
{}</code></pre></div>
<p>在 debian 11 中，通过 <code>gcc main.c  -static</code> 静态编译。将出现如下警告：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/bin/ld: /tmp/ccB7Eh74.o: in function `mygetpwuid_r&#39;:
main.c:(.text+0x34): 警告：Using &#39;getpwuid_r&#39; in statically linked applications requires at runtime the shared libraries from the glibc version used for linking</pre></div>
<p>我们观察 a.out 的动态链接库情况 <code>ldd a.out</code> 可以发现输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">        不是动态可执行文件</pre></div>
<p>但是实际上，执行 <code>a.out</code> 会隐式的依赖 <code>libnss3.so</code> 和 <code>libc.so.6</code>（即 glibc）。实际上，这比动态链接编译的程序还要糟糕。因为该程序隐藏了其依赖。所以该警告必须要消除。</p>

<p>注意：glibc 的 FAQ 给出了一种解决方案是，在编译 glibc 时，通过 <code>--enable-static-nss</code> 将其依赖的 NSS 也静态编译，但是官方并不推荐。因此，本文并不介绍此做法。</p>

<h2 id="musl-libc-实现静态链接">musl-libc 实现静态链接</h2>

<p>解决上述 glibc 问题，最好的办法就是使用 musl-libc，因为上文提到了，musl-libc 就是专门为静态链接而设计的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt update
sudo apt -y install musl-tools
musl-gcc main.c -static</code></pre></div>]]></description></item><item><title>可观测性（二）Metrics &amp; Prometheus</title><link>https://www.rectcircle.cn/posts/observability-2-metrics-prometheus/</link><pubDate>Sun, 20 Nov 2022 20:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/observability-2-metrics-prometheus/</guid><description type="html"><![CDATA[

<blockquote>
<p>Prometheus v2.37.2 （文档：<a href="https://hulining.gitbook.io/prometheus/">中文</a>|<a href="https://prometheus.io/docs/">英文</a>）</p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Metrics (指标) 是反应系统状态的具有明确定义的数值。比如一个 Web 服务的响应时间，请求量，数据库连接数等等。Metrics 是由一系列带有属性的随着时间变化的数值分组/聚合得来的，一般可以绘制成关于时间的图表。</p>

<p>因此 Metrics 是系统具有良好的可观测性的关键，基于 Metrics 可以制作：</p>

<ul>
<li>定义系统异常报警规则。</li>
<li>系统监控仪表盘 (数据看板)。</li>
</ul>

<p>基于以上手段，可以系统开发和运维人员提供系统内部情况，以：</p>

<ul>
<li>保证系统稳定运行。</li>
<li>更好的发现系统可优化项。</li>
<li>量化系统优化效果。</li>
</ul>

<p>提供 Metrics 的数据收集、数据存储、数据查询的系统一般称为系统监控系统平台。</p>

<p>Prometheus 就是目前最主流的开源的监控平台。是 CNCF 继 Kubenates 之后的第二个托管项目。</p>

<h2 id="架构">架构</h2>

<blockquote>
<p>图片来源：<a href="https://prometheus.io/docs/introduction/overview/">Prometheus 官网</a>）</p>
</blockquote>

<p><img src="/image/prometheus-architecture.png" alt="iamge" /></p>

<p>Prometheus 由如下组件组成：</p>

<ul>
<li><a href="https://github.com/prometheus/prometheus">Prometheus server</a> 系统核心，提供数据收集、数据存储、数据查询能力，主要由如下部分组成：

<ul>
<li>Retrieval 定时从被监控的应用程序拉取 (pull) 指标数据。</li>
<li>TSDB 时序数据库，存储指标数据。</li>
<li>HTTP Server 提供配置、数据查询能力。</li>
</ul></li>
<li><a href="/docs/instrumenting/clientlibs/">client libraries</a> 嵌入到被监控的应用程序，会启动一个符合 Prometheus 规范的 http 端点来暴露指标， Retrieval 会定时从该端点拉取指标数据。</li>
<li><a href="https://github.com/prometheus/pushgateway">push gateway</a>  某些场景，被监控应用程序很短的，或者无法暴露端口，此时可以 push gateway 通过推送 (push) 的方式主动发送指标数据。该 push gateway 会暴露一个服符合 Prometheus 规范的 http 端点，让 Retrieval 来拉取 (pull) 应用程序的指标数据。</li>
<li><a href="https://prometheus.io/docs/instrumenting/exporters/">exporters</a> 用来监控某个类应用程序的服务，这些服务，会通过 client libraries 暴露这些应用程序的内部指标。</li>
<li><a href="https://github.com/prometheus/alertmanager">alertmanager</a> 报警管理器，用来对接各种报警系统，Prometheus server 会推送报警数据 (push) 到 alertmanager，alertmanager 在调用各种报警系统。</li>
</ul>

<h2 id="快速开始">快速开始</h2>

<h3 id="下载运行-prometheus">下载运行 Prometheus</h3>

<p>Prometheus 是由 Go 语言编写，因此只需下载一个静态编译的可执行文件即一键启动。</p>

<p>前往<a href="https://prometheus.io/download/">下载页</a>，下载 prometheus 软件包。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">tar xvfz prometheus-*.tar.gz
cd prometheus-*/
./prometheus --help  <span style="color:#75715e"># Mac 需要打开系统偏好设置 -&gt; 安全性和隐私，允许改程序运行。</span></code></pre></div>
<h3 id="配置-prometheus">配置 Prometheus</h3>

<p>在软件包中，包含一个简单配置文件 <code>./prometheus.yml</code>，重要内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># 全局配置</span>
global:
  scrape_interval: 15s <span style="color:#75715e"># 抓取数据的间隔时间，默认为 1 分钟。</span>
  evaluation_interval: 15s <span style="color:#75715e"># 每 15 秒重新加载并检查一次规则，参见 rule_files。 默认值为 1 分钟。</span>
  <span style="color:#75715e"># scrape_timeout: # 抓取数据的超时时间，默认为 10 秒.</span>

<span style="color:#75715e"># 报警配置</span>
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          <span style="color:#75715e"># - alertmanager:9093</span>

<span style="color:#75715e"># 规则配置文件，检查更新时间通过 global.evaluation_interval 配置</span>
rule_files:
  <span style="color:#75715e"># - &#34;first_rules.yml&#34;</span>
  <span style="color:#75715e"># - &#34;second_rules.yml&#34;</span>

<span style="color:#75715e"># 数据抓取配置，该配置包含一个去抓取的端点：</span>
<span style="color:#75715e"># 这里是抓取 Prometheus 程序自身。</span>
scrape_configs:
  <span style="color:#75715e"># job_name 字段会作为 `job=&lt;job_name&gt;` 附加到抓取到数据的 labels 中。</span>
  - job_name: <span style="color:#e6db74">&#34;prometheus&#34;</span>

    <span style="color:#75715e"># metrics_path: # 默认是 &#39;/metrics&#39;</span>
    <span style="color:#75715e"># scheme: # 默认是 &#39;http&#39;.</span>

    static_configs:
      - targets: [<span style="color:#e6db74">&#34;localhost:9090&#34;</span>]</code></pre></div>
<p>配置文件的说明，参见：<a href="https://prometheus.io/docs/operating/configuration">官方文档</a>。</p>

<h3 id="启动-prometheus">启动 Prometheus</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">./prometheus --config.file<span style="color:#f92672">=</span>prometheus.yml</code></pre></div>
<ul>
<li>通过 <code>http://localhost:9090</code> 可以访问 Prometheus 的 WebUI。</li>
<li>通过 <code>http://localhost:9090/metrics</code> 可以获取到 Prometheus 自己暴露的指标。</li>
</ul>

<h3 id="使用-prometheus-表达式-promql-查询">使用 Prometheus 表达式 (PromQL) 查询</h3>

<ul>
<li>打开 <code>http://localhost:9090/graph</code>。</li>
<li>输入 <code>promhttp_metric_handler_requests_total</code> （这个指标数据类型为 <code>counter</code> 计数器，数据类型参见下文），可以获取到当前时刻，该指标在每种 labels 下的计数。</li>
<li>输入 <code>promhttp_metric_handler_requests_total{code=&quot;200&quot;}</code>，可以获取到当前时刻，该指标满足 label <code>code=&quot;200&quot;</code> 情况的计数。</li>
<li>输入 <code>count(promhttp_metric_handler_requests_total)</code>，可以获取到当前指标在所有 labels 组合的情况下的指标数。</li>
<li>输入 <code>rate(promhttp_metric_handler_requests_total{code=&quot;200&quot;}[1m])</code> （在选中时间范围内，t 时刻的 counter 减去 1 分钟之前 counter 的差除以 60s 值） 可以查看计数器的变化率。</li>
<li>关于 Prometheus 表达式语法，参见下文或<a href="https://prometheus.io/docs/querying/basics/">官方文档</a>。</li>
</ul>

<h2 id="数据模型">数据模型</h2>

<blockquote>
<p><a href="https://prometheus.io/docs/concepts/data_model/">DATA MODEL</a> | <a href="https://prometheus.io/docs/concepts/metric_types/">METRIC TYPES</a></p>
</blockquote>

<p>Prometheus 的 metrics 在存储上，本质上是时间序列（时序数据）。每个时间序列都有一个唯一的 name 作为唯一标识符以及可选的被称为 labels 的键值对，以及样本值和样本时间采样时间戳。</p>

<ul>
<li>metric name 格式必须满足 <code>[a-zA-Z_:][a-zA-Z0-9_:]*</code> 正则表达式（冒号最好不要使用），如 <code>http_requests_total</code>。</li>
<li>metric label 用来标识改时序数据的维度。PromQL 可以基于这些维度进行过滤和聚合，更改任何 label 的值，包括添加或删除 label，都会创建一个新的时间序列。

<ul>
<li>label name 必须满足 <code>[a-zA-Z_][a-zA-Z0-9_]*</code> 正则表达式。以 <code>__</code> 开头的标签名称保留供内部使用。</li>
<li>label value 是个 Unicode 字符串。</li>
<li>跪安与 label 的最佳实践，参见：<a href="https://prometheus.io/docs/practices/naming/">best practices for naming metrics and labels</a>。</li>
</ul></li>
<li>metric sample (样本) 即上到的采样数值，每个样本包含：

<ul>
<li>value 类型为 float64（v2.40 起 value 可以是直方图数据，参见下文）。</li>
<li>timestamp 毫秒精度的时间戳。</li>
</ul></li>
<li>一个给定 name 和 labels 的时间序列经常使用如下符号表示：<code>&lt;metric name&gt;{&lt;label name&gt;=&lt;label value&gt;, ...}</code>，比如 <code>api_http_requests_total{method=&quot;POST&quot;, handler=&quot;/messages&quot;}</code>。</li>
</ul>

<p>Prometheus 目前支持如下四种数据类型。</p>

<h3 id="counter-计数器">Counter (计数器)</h3>

<p>Counter 代表一个单调递增的计数器，其值只能递增，或者在重新启动时重置为零。一般用于如下计算某个值的变化率。例如，您可以使用计数器来表示服务的请求数、完成的任务数或错误数。</p>

<p>也就是说，应用程序想要上报一个 Counter 类型的指标时，只有一个 <code>Add</code> 函数。</p>

<p>注意，一般不用 Counter 表示某总量的值，因为 Counter 在服务重启后会清零。</p>

<p>举个例子，想监控某后端系统的 QPS 时，上报一个类型为 Counter 的 Metric。</p>

<ul>
<li>Name 为 <code>http_requests_total</code>。</li>
<li>Labels 为：

<ul>
<li><code>code</code>：返回的状态码。</li>
<li><code>method</code>：http 请求的方法。</li>
<li><code>handler</code>：请求的路径。</li>
</ul></li>
</ul>

<p>假设采样周期为 1 分钟，且有两个接口分别：</p>

<ul>
<li><code>GET /handler1</code> 可能返回 <code>200</code>、<code>500</code>。</li>
<li><code>POST /handler2</code> 可能返回 <code>200</code>、<code>400</code>。</li>
</ul>

<p>在程序启动后：</p>

<ul>
<li>第一分钟

<ul>
<li><code>GET /handler1</code> 返回 200，的数量为 100</li>
<li><code>GET /handler1</code> 返回 500，的数量为 10</li>
<li><code>POST /handler2</code> 返回 200，的数量为 50</li>
<li><code>POST /handler2</code> 返回 400，的数量为 5</li>
</ul></li>
<li>第二分钟

<ul>
<li><code>GET /handler1</code> 返回 200，的数量为 200</li>
<li><code>GET /handler1</code> 返回 500，的数量为 5</li>
<li><code>POST /handler2</code> 返回 200，的数量为 100</li>
<li><code>POST /handler2</code> 返回 400，的数量为 30</li>
</ul></li>
</ul>

<p>此时，</p>

<ul>
<li><p>第一分钟末，Prometheus 获取到的数据为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">http_requests_total{method=GET, handler=/handler1, code=200} 100
http_requests_total{method=GET, handler=/handler1, code=500} 10
http_requests_total{method=POST, handler=/handler2, code=200} 50
http_requests_total{method=POST, handler=/handler2, code=400} 5</pre></div></li>

<li><p>第二分钟末，Prometheus 获取到的数据为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">http_requests_total{method=GET, handler=/handler1, code=200} 300
http_requests_total{method=GET, handler=/handler1, code=500} 15
http_requests_total{method=POST, handler=/handler2, code=200} 150
http_requests_total{method=POST, handler=/handler2, code=400} 35</pre></div></li>

<li><p>此时在计算 QPS 时，可以通过两个时间的 counter 的差值除以总时间，而得到 QPS（对应函数为 <code>rate</code>）。</p></li>
</ul>

<h3 id="gauge-仪表盘">Gauge (仪表盘)</h3>

<p>Gauge 代表一个可任意变化的值，这个值可增可减。该类型可以用来表示某时刻的总数。比如数据库连接数量、内存使用量等。</p>

<p>也就是说，应用程序想要上报一个 Gauge 类型的指标时，可以通过 <code>Set</code> 函数设置成任意值。</p>

<h3 id="histogram-直方图">Histogram (直方图)</h3>

<blockquote>
<p>v2.40 新增</p>
</blockquote>

<p>Histogram 统计的时某个值（通常是请求持续时间或响应大小等）所在的区间（被称为存储桶 Bucket）的数量。</p>

<p>举个例子，比如一个名为 <code>http_requests_time</code> 的 Histogram，用来统计请求耗时的分布情况。</p>

<p>配置了 6 个 Bucket：</p>

<ul>
<li>桶 <code>&lt; 0.2</code> 秒</li>
<li>桶 <code>&lt; 0.4</code> 秒</li>
<li>桶 <code>&lt; 0.6</code> 秒</li>
<li>桶 <code>&lt; 0.8</code> 秒，</li>
<li>桶 <code>&lt; 1</code> 秒，</li>
<li>桶 <code>&lt; 无穷大</code> 秒。</li>
</ul>

<p>在程序启动的一分钟内：</p>

<ul>
<li>请求耗时 <code>&lt; 0.2</code> 秒的，有 7 次请求耗时分别为： 0.02, 0.1, 0.15, 0.15, 0.16, 0.17, 0.18 。</li>
<li>请求耗时 <code>&lt; 0.4</code> 且 <code>&gt;= 0.2</code> 秒的，有 2 次请求耗时分别为： 0.3, 0.35 。</li>
<li>请求耗时 <code>&lt; 0.6</code> 且 <code>&gt;= 0.4</code> 秒的，有 1 次请求耗时分别为： 0.5 。</li>
<li>请求耗时 <code>&lt; 0.8</code> 且 <code>&gt;= 0.6</code> 秒的，没有符合要求的请求。</li>
<li>请求耗时 <code>&lt; 1</code> 且 <code>&gt;= 0.8</code> 秒的，没有符合要求的请求。</li>
<li>请求耗时 <code>&lt; 无穷大</code> 且 <code>&gt;= 1</code> 秒的，没有符合要求的请求。</li>
</ul>

<p>通过 <code>Observe()</code> 函数，此时 Histogram 会产生如下几个时序数据。</p>

<ul>
<li><code>&lt;basename&gt;_bucket{le=&quot;&lt;upper inclusive bound&gt;&quot;}</code> 每个 Bucket 的<strong>计数</strong>。

<ul>
<li><code>http_requests_time_bucket{le=&quot;0.2&quot;} 7</code></li>
<li><code>http_requests_time_bucket{le=&quot;0.4&quot;} 9</code> 可以看出，统计的时 <code>&lt;0.4</code> 的所以包含 <code>&lt;0.2</code> 的数目，下面同理。</li>
<li><code>http_requests_time_bucket{le=&quot;0.6&quot;} 10</code></li>
<li><code>http_requests_time_bucket{le=&quot;0.8&quot;} 10</code></li>
<li><code>http_requests_time_bucket{le=&quot;1.0&quot;} 10</code></li>
<li><code>http_requests_time_bucket{le=&quot;+Inf&quot;} 10</code></li>
</ul></li>
<li><code>&lt;basename&gt;_sum</code> <strong>值的总和</strong>。

<ul>
<li><code>http_requests_time_sum 2.08</code> （计算方法为 <code>0.02 + 0.1 + 0.15 + 0.15 + 0.16 + 0.17 + 0.18 + 0.3 + 0.35 + 0.5</code>）</li>
</ul></li>
<li><code>&lt;basename&gt;_count</code> 等价于 <code>&lt;basename&gt;_bucket{le=&quot;+Inf&quot;}</code>

<ul>
<li><code>http_requests_time_count 10</code></li>
</ul></li>
</ul>

<p>在程序运行的第二分钟后，这里的所有指标，都是基于之前第一分钟之后的数据进行累加的（和 Counter 有点类似）。</p>

<p>基于如上，可以计算出：</p>

<ul>
<li>请求 QPS：<code>rate(&lt;basename&gt;_count)</code>，即在第一分钟为：<code>10/60</code> 次每秒。</li>
<li>平均耗时： <code>rate(&lt;basename&gt;_sum[1m]) / rate(&lt;basename&gt;_count[1m])</code>，即在第一分钟为：<code>2.08 / 10 = 0.208</code></li>
<li>分位数：假设我们想计算 90% 的分位数，算法如下（参考：<a href="https://juejin.cn/post/6844903907265642509">一文搞懂 Prometheus 的直方图</a>）：

<ul>
<li>按照 le 标签排序，查找第一个满足 <code>&gt;= 90% * &lt;basename&gt;_count</code> 的 le（本例中为 <code>&gt;= 0.9 * 10 = 9</code> 即 le 为 0.4），并记：

<ul>
<li>bucketStart = 这个 le 标签的值，即 0.4</li>
<li>bucketEnd = 下一个 le 标签的值，即 0.6</li>
<li>bucketStartCount = 上一个 le 的数量，即 7</li>
<li>bucketEndCount = 上一个 le 的数量，即 9</li>
<li>count = bucketEndCount - bucketStartCount ，即 <code>9-7 = 2</code>，</li>
<li>rank = 90% 在 count 中的序号，即 <code>90% * &lt;basename&gt;_count + 1 - bucketStartCount</code>，即 <code>9-7=2</code></li>
</ul></li>
<li>最终，公式为：<code>bucketStart + (bucketEnd-bucketStart)*float64(rank/count)</code>，即 <code>0.4 + 0.2*(2/2) = 0.6</code> 和真实值 <code>0.5</code> 相差不大，在样本量更大的情况下，将更加精确。</li>
<li>Prometheus 提供了相关的函数 <code>histogram_quantile</code>。</li>
</ul></li>
</ul>

<h3 id="summary">Summary</h3>

<p>用来在客户端直接计算出某个值（通常是请求持续时间或响应大小等）的分位数，然后直接上报到 Prometheus。</p>

<p>通过 <code>Observe()</code> 函数，Summary 会上报如下三个时序指标：</p>

<ul>
<li><code>&lt;basename&gt;{quantile=&quot;&lt;φ&gt;&quot;}</code> 某个分位数的值。</li>
<li><code>&lt;basename&gt;_sum</code> 和 Histogram 的一致，为 <strong>值的总和</strong>。</li>
<li><code>&lt;basename&gt;_count</code> 为 <code>Observe()</code> 函数调用的次数。</li>
</ul>

<p>因此，通过 Summary 可以计算出：</p>

<ul>
<li>QPS：<code>rate(&lt;basename&gt;_count)</code></li>
<li>平均值： <code>rate(&lt;basename&gt;_sum[1m]) / rate(&lt;basename&gt;_count[1m])</code>。</li>
<li>分位数：<code>&lt;basename&gt;{quantile=&quot;&lt;φ&gt;&quot;}</code></li>
</ul>

<h3 id="histogram-vs-summary">Histogram vs Summary</h3>

<blockquote>
<p>参考：Histogram and Summary （<a href="https://hulining.gitbook.io/prometheus/practices/histograms">中文</a>|<a href="https://prometheus.io/docs/practices/histograms/">英文</a>）</p>
</blockquote>

<ul>
<li>Histogram

<ul>
<li>客户端性能消耗小，服务端查询分位数时消耗大。</li>
<li>可以在查询期间自由计算各种不同的分位数。</li>
<li>分位数的精度无法保证，其精确度受桶的配置、数据分布、数据量大小情况影响。</li>
<li>可聚合，可以计算全局分位数。</li>
<li>客户端兼容性好。</li>
</ul></li>
<li>Summary

<ul>
<li>客户端性能消耗大（因为分位数计算发生在客户端），服务端查询分位数时消耗小。</li>
<li>只能查询客户端上报的哪些分位数。</li>
<li>分位数的精度可以得到保证，精度会影响客户端的消耗。</li>
<li>不可聚合，无法计算全局分位数（因此不支持多实例，平行扩展的 http 服务）。</li>
<li>客户端兼容性不好。</li>
</ul></li>
</ul>

<p>综上所述，大多数场景使用 Histogram 更为灵活。</p>

<h2 id="客户端库的使用">客户端库的使用</h2>

<p>以 Go 为例，展示上面所有数据类型的上报。</p>

<h3 id="go-client-概述">Go Client 概述</h3>

<p>模块 github.com/prometheus/client_golang 包含如下内容（详见：<a href="https://pkg.go.dev/github.com/prometheus/client_golang/prometheus">go doc</a>）：</p>

<ul>
<li><a href="https://pkg.go.dev/github.com/prometheus/client_golang/prometheus">prometheus</a> 包，实现了 Metrics 的核心接口：

<ul>
<li>四种 Metrics 类型：Counter、Gauge、Histogram、Summary，以及对应的 Vec。</li>
<li>Registry、Registerer、Gatherer 接口，用来管理注册的指标。</li>
</ul></li>
<li><a href="https://pkg.go.dev/github.com/prometheus/client_golang@v1.14.0/prometheus/collectors">prometheus/collectors</a> 包，实现了 Go 进程和 Go Runtime 相关的指标定义和收集。</li>
<li><a href="https://pkg.go.dev/github.com/prometheus/client_golang@v1.14.0/prometheus/promauto">prometheus/graphite</a> 包，提供了将 Prometheus 指标推送到 Graphite 服务器的能力。</li>
<li><a href="https://pkg.go.dev/github.com/prometheus/client_golang@v1.14.0/prometheus/promauto">prometheus/promauto</a> 包，提供了一种创建四种 Metrics 类型的可读性更好的 API 风格。</li>
<li><a href="https://pkg.go.dev/github.com/prometheus/client_golang@v1.14.0/prometheus/promhttp">prometheus/promhttp</a> 包，提供围绕 HTTP 服务器和客户端的工具。

<ul>
<li>定义和实现了一系列针对 http server / client 的指标，并通过包装函数（Middleware）方式提供。</li>
<li>实现了暴露 metrics 的断点，以供 Prometheus Server Pull 采集。</li>
</ul></li>
<li>prometheus/push 包，提供了将指标推送到 push gateway 的能力。</li>
</ul>

<h3 id="示例">示例</h3>

<h4 id="监控-http-server">监控 HTTP Server</h4>

<p>实现一个 HTTP Server 的 Middleware 可以上报每个请求的监控数据。</p>

<p><code>02-prometheus/metrics.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#75715e">// 本例仅用来展示 Prometheus Go SDK 的用法，不可用于生产。
</span><span style="color:#75715e">// 1. http middleware 可以直接使用 github.com/prometheus/client_golang/prometheus/promhttp 包。
</span><span style="color:#75715e">// 2. go runtime 可以直接使用 github.com/prometheus/client_golang/prometheus/collectors 包。
</span><span style="color:#75715e"></span>
<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;runtime&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/prometheus/client_golang/prometheus&#34;</span>
	<span style="color:#e6db74">&#34;github.com/prometheus/client_golang/prometheus/promhttp&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">HTTPMetricsMiddleware</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">reg</span>                    <span style="color:#f92672">*</span><span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">Registry</span>
	<span style="color:#a6e22e">httpRequestsTotal</span>      <span style="color:#f92672">*</span><span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">CounterVec</span>
	<span style="color:#a6e22e">goMemstatsAllocBytes</span>   <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">Gauge</span>
	<span style="color:#a6e22e">httpDurations</span>          <span style="color:#f92672">*</span><span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">SummaryVec</span>
	<span style="color:#a6e22e">httpDurationsHistogram</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">HistogramVec</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewHTTPMetrics</span>(<span style="color:#a6e22e">reg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">Registry</span>, <span style="color:#a6e22e">normMean</span>, <span style="color:#a6e22e">normDomain</span> <span style="color:#66d9ef">float64</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span> {
	<span style="color:#75715e">// 一些进程粒度的标签，比如 pod name 之类的，这里使用 pid 模拟。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ConstLabels</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{
		<span style="color:#e6db74">&#34;pid&#34;</span>: <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>()),
	}
	<span style="color:#a6e22e">httpLabelNames</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;handler&#34;</span>, <span style="color:#e6db74">&#34;method&#34;</span>, <span style="color:#e6db74">&#34;status_code&#34;</span>}
	<span style="color:#a6e22e">m</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span>{
		<span style="color:#a6e22e">reg</span>: <span style="color:#a6e22e">reg</span>,
		<span style="color:#75715e">// 创建一个 Counter 类型的指标：每个请求会增加 1。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 下文， SummaryVec 或者 httpDurationsHistogram 会自动上报该指标，这里仅做演示。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">httpRequestsTotal</span>: <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">NewCounterVec</span>(
			<span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">CounterOpts</span>{
				<span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;http_requests_total&#34;</span>,
				<span style="color:#a6e22e">Help</span>:        <span style="color:#e6db74">&#34;HTTP request total.&#34;</span>,
				<span style="color:#a6e22e">ConstLabels</span>: <span style="color:#a6e22e">ConstLabels</span>,
			},
			<span style="color:#a6e22e">httpLabelNames</span>,
		),
		<span style="color:#75715e">// 创建一个 Gauge 类型的指标：统计当前时刻的 go runtime memstats alloc。
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 下文， SummaryVec 或者 httpDurationsHistogram 会自动上报该指标，这里仅做演示。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">goMemstatsAllocBytes</span>: <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">NewGauge</span>(<span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">GaugeOpts</span>{
			<span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;go_memstats_alloc_bytes&#34;</span>,
			<span style="color:#a6e22e">Help</span>:        <span style="color:#e6db74">&#34;HTTP request total.&#34;</span>,
			<span style="color:#a6e22e">ConstLabels</span>: <span style="color:#a6e22e">ConstLabels</span>,
		}),
		<span style="color:#75715e">// 创建一个 SummaryVec 类型的指标：按照 handler 标签，计算请求耗时的 50% 90% 99% 分位数。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">httpDurations</span>: <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">NewSummaryVec</span>(
			<span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">SummaryOpts</span>{
				<span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;http_durations_seconds&#34;</span>,
				<span style="color:#a6e22e">Help</span>:        <span style="color:#e6db74">&#34;HTTP latency distributions.&#34;</span>,
				<span style="color:#a6e22e">Objectives</span>:  <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">float64</span>]<span style="color:#66d9ef">float64</span>{<span style="color:#ae81ff">0.5</span>: <span style="color:#ae81ff">0.05</span>, <span style="color:#ae81ff">0.9</span>: <span style="color:#ae81ff">0.01</span>, <span style="color:#ae81ff">0.99</span>: <span style="color:#ae81ff">0.001</span>},
				<span style="color:#a6e22e">ConstLabels</span>: <span style="color:#a6e22e">ConstLabels</span>,
			},
			<span style="color:#a6e22e">httpLabelNames</span>,
		),
		<span style="color:#75715e">// 和上面的 httpDurations 类似，但是类型为 Histogram
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// Histogram 分为 20 个桶，桶的划分为：
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//   * 区间 [normMean-5*normDomain, normMean+0.5*normDomain]
</span><span style="color:#75715e"></span>		<span style="color:#75715e">//   * 步长为 0.5*normDomain
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 举个例子，当 normMean = 1, normDomain = 0.2 时，桶划分为： {0, 0.1, 0.2, ..., 1, ..., 1.8, 1.9}
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">httpDurationsHistogram</span>: <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">NewHistogramVec</span>(
			<span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">HistogramOpts</span>{
				<span style="color:#a6e22e">Name</span>:                        <span style="color:#e6db74">&#34;http_durations_histogram_seconds&#34;</span>,
				<span style="color:#a6e22e">Help</span>:                        <span style="color:#e6db74">&#34;HTTP latency distributions.&#34;</span>,
				<span style="color:#a6e22e">Buckets</span>:                     <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">LinearBuckets</span>(<span style="color:#a6e22e">normMean</span><span style="color:#f92672">-</span><span style="color:#ae81ff">5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">normDomain</span>, <span style="color:#ae81ff">.5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">normDomain</span>, <span style="color:#ae81ff">20</span>),
				<span style="color:#a6e22e">NativeHistogramBucketFactor</span>: <span style="color:#ae81ff">1.1</span>,
				<span style="color:#a6e22e">ConstLabels</span>:                 <span style="color:#a6e22e">ConstLabels</span>,
			},
			<span style="color:#a6e22e">httpLabelNames</span>,
		),
	}
	<span style="color:#a6e22e">reg</span>.<span style="color:#a6e22e">MustRegister</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpRequestsTotal</span>)
	<span style="color:#a6e22e">reg</span>.<span style="color:#a6e22e">MustRegister</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">goMemstatsAllocBytes</span>)
	<span style="color:#a6e22e">reg</span>.<span style="color:#a6e22e">MustRegister</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpDurations</span>)
	<span style="color:#a6e22e">reg</span>.<span style="color:#a6e22e">MustRegister</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpDurationsHistogram</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">m</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span>) <span style="color:#a6e22e">MetricsHandler</span>() <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">promhttp</span>.<span style="color:#a6e22e">HandlerFor</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">reg</span>, <span style="color:#a6e22e">promhttp</span>.<span style="color:#a6e22e">HandlerOpts</span>{
		<span style="color:#75715e">// Opt into OpenMetrics to support exemplars.
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">EnableOpenMetrics</span>: <span style="color:#66d9ef">true</span>,
		<span style="color:#75715e">// Pass custom registry
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">Registry</span>: <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">reg</span>,
	})
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span>) <span style="color:#a6e22e">WrapHandler</span>(<span style="color:#a6e22e">handlerName</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">handler</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
		<span style="color:#a6e22e">startTime</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>()
		<span style="color:#a6e22e">ww</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metricsHTTPResponseWrapper</span>{
			<span style="color:#a6e22e">ResponseWriter</span>: <span style="color:#a6e22e">w</span>,
			<span style="color:#a6e22e">statusCode</span>:     <span style="color:#ae81ff">0</span>,
		}
		<span style="color:#a6e22e">handler</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">ww</span>, <span style="color:#a6e22e">r</span>)
		<span style="color:#a6e22e">duration</span> <span style="color:#f92672">:=</span> float64(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Since</span>(<span style="color:#a6e22e">startTime</span>)) <span style="color:#f92672">/</span> float64(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
		<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprint</span>(<span style="color:#a6e22e">ww</span>.<span style="color:#a6e22e">statusCode</span>)
		<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpRequestsTotal</span>.<span style="color:#a6e22e">WithLabelValues</span>(<span style="color:#a6e22e">handlerName</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Method</span>, <span style="color:#a6e22e">statusCode</span>).<span style="color:#a6e22e">Add</span>(<span style="color:#ae81ff">1</span>)
		<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpDurations</span>.<span style="color:#a6e22e">WithLabelValues</span>(<span style="color:#a6e22e">handlerName</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Method</span>, <span style="color:#a6e22e">statusCode</span>).<span style="color:#a6e22e">Observe</span>(<span style="color:#a6e22e">duration</span>)
		<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">httpDurationsHistogram</span>.<span style="color:#a6e22e">WithLabelValues</span>(<span style="color:#a6e22e">handlerName</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Method</span>, <span style="color:#a6e22e">statusCode</span>).<span style="color:#a6e22e">Observe</span>(<span style="color:#a6e22e">duration</span>)
	})
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span>) <span style="color:#a6e22e">StartBackgroundReportGoCollector</span>(<span style="color:#a6e22e">interval</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>) {
	<span style="color:#75715e">// 这只是例子，想要统计 go runtime 相关的指标，可以直接使用 go collector，参见：https://github.com/prometheus/client_golang/blob/main/examples/gocollector/main.go
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// https://gist.github.com/j33ty/79e8b736141be19687f565ea4c6f4226
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">for</span> {
			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">stat</span> <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">MemStats</span>
			<span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">ReadMemStats</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">stat</span>)
			<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">goMemstatsAllocBytes</span>.<span style="color:#a6e22e">Set</span>(float64(<span style="color:#a6e22e">stat</span>.<span style="color:#a6e22e">Alloc</span>))
			<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">interval</span>)
		}
	}()
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">metricsHTTPResponseWrapper</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>
	<span style="color:#a6e22e">statusCode</span> <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">w</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">metricsHTTPResponseWrapper</span>) <span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">statusCode</span> <span style="color:#66d9ef">int</span>) {
	<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCode</span>
	<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">ResponseWriter</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">statusCode</span>)
}</code></pre></div>
<p><code>02-prometheus/server.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;math/rand&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/prometheus/client_golang/prometheus&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handler1</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;handler1 handling&#34;</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Float64</span>() <span style="color:#f92672">*</span> float64(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)))
	<span style="color:#a6e22e">statusCodes</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">400</span>, <span style="color:#ae81ff">500</span>}
	<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
	<span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Intn</span>(<span style="color:#ae81ff">100</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> &lt; <span style="color:#ae81ff">91</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">0</span>]
	} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> &lt; <span style="color:#ae81ff">97</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">1</span>]
	} <span style="color:#66d9ef">else</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">2</span>]
	}
	<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">statusCode</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handler2</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;handler2 handling&#34;</span>)
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Float64</span>() <span style="color:#f92672">*</span> float64(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)))
	<span style="color:#a6e22e">statusCodes</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">400</span>, <span style="color:#ae81ff">500</span>}
	<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
	<span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Intn</span>(<span style="color:#ae81ff">100</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> &lt; <span style="color:#ae81ff">93</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">0</span>]
	} <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> &lt; <span style="color:#ae81ff">95</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">1</span>]
	} <span style="color:#66d9ef">else</span> {
		<span style="color:#a6e22e">statusCode</span> = <span style="color:#a6e22e">statusCodes</span>[<span style="color:#ae81ff">2</span>]
	}
	<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">statusCode</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Run</span>() {
	<span style="color:#a6e22e">reg</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">prometheus</span>.<span style="color:#a6e22e">NewRegistry</span>()
	<span style="color:#a6e22e">metrics</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewHTTPMetrics</span>(<span style="color:#a6e22e">reg</span>, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0.2</span>)
	<span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">StartBackgroundReportGoCollector</span>(<span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/handler1&#34;</span>, <span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">WrapHandler</span>(<span style="color:#e6db74">&#34;/handler1&#34;</span>, <span style="color:#a6e22e">handler1</span>))
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/handler2&#34;</span>, <span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">WrapHandler</span>(<span style="color:#e6db74">&#34;/handler2&#34;</span>, <span style="color:#a6e22e">handler2</span>))
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/metrics&#34;</span>, <span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">MetricsHandler</span>().<span style="color:#a6e22e">ServeHTTP</span>)

	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ListenAndServe</span>(<span style="color:#e6db74">&#34;:8083&#34;</span>, <span style="color:#66d9ef">nil</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">Run</span>()
}</code></pre></div>
<h4 id="编写模拟请求的客户端">编写模拟请求的客户端</h4>

<p><code>02-prometheus/server_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;math/rand&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RequestHandler</span>(<span style="color:#a6e22e">handlerName</span> <span style="color:#66d9ef">string</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;http://localhost:8083&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">handlerName</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestRun</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">Run</span>()
	<span style="color:#a6e22e">handlerNameChan</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">string</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">for</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Float64</span>() &lt; <span style="color:#ae81ff">0.6</span> {
				<span style="color:#a6e22e">handlerNameChan</span> <span style="color:#f92672">&lt;-</span> <span style="color:#e6db74">&#34;/handler1&#34;</span>
			} <span style="color:#66d9ef">else</span> {
				<span style="color:#a6e22e">handlerNameChan</span> <span style="color:#f92672">&lt;-</span> <span style="color:#e6db74">&#34;/handler2&#34;</span>
			}
		}
	}()
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#ae81ff">100</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> { <span style="color:#75715e">// 并发度
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
			<span style="color:#66d9ef">for</span> {
				<span style="color:#a6e22e">RequestHandler</span>(<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">handlerNameChan</span>)
			}
		}()
	}
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">130</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
}</code></pre></div>
<h4 id="配置-prometheus-server">配置 Prometheus Server</h4>

<p><code>02-prometheus/prometheus.yml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># ~/Downloads/prometheus-2.37.2.darwin-amd64/prometheus --config.file=02-prometheus/prometheus.yml --storage.tsdb.path=&#34;prometheus-tsdb-data/&#34;</span>
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: <span style="color:#e6db74">&#34;demo&#34;</span>
    <span style="color:#75715e"># metrics_path: # 默认是 &#39;/metrics&#39;</span>
    static_configs:
      - targets: [<span style="color:#e6db74">&#34;localhost:8083&#34;</span>]</code></pre></div>
<h4 id="启动测试">启动测试</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个终端</span>
~/Downloads/prometheus-2.37.2.darwin-amd64/prometheus --config.file<span style="color:#f92672">=</span><span style="color:#ae81ff">02</span>-prometheus/prometheus.yml --storage.tsdb.path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;prometheus-tsdb-data/&#34;</span>

<span style="color:#75715e"># 第二个终端</span>
cd <span style="color:#ae81ff">02</span>-prometheus <span style="color:#f92672">&amp;&amp;</span> go test -timeout 600s -run ^TestRun$ ./ -v --count<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span></code></pre></div>
<p>等待第二个终端运行结束，打开 <code>http://localhost:9090/graph</code>，切换到 <code>Graph</code> 标签，输入如下表达式查看绘图：</p>

<ol>
<li><code>rate(http_requests_total[30s])</code> 请求 qps。</li>
<li><code>go_memstats_alloc_bytes</code> 当前进程分配的内存。</li>
<li><code>http_durations_seconds{quantile='0.9'}</code> 请求耗时 90% 分位数，应该在 0.9 附近。</li>
<li><code>rate(http_durations_seconds_count[30s])</code> 请求 qps，和第 1 个结果完全一致。</li>
<li><code>rate(http_durations_seconds_sum[30s]) / rate(http_durations_seconds_count[30s])</code> 请求平均耗时，应在在 0.5 附近。</li>
<li><code>histogram_quantile(0.9, rate(http_durations_histogram_seconds_bucket[30s]))</code> 请求耗时 90% 分位数，应该在 0.9 附近。</li>
<li><code>rate(http_durations_histogram_seconds_count[30s])</code> 请求 qps，和第 1 个结果完全一致。</li>
<li><code>rate(http_durations_histogram_seconds_sum[30s]) / rate(http_durations_histogram_seconds_count[30s])</code> 请求平均耗时，应在在 0.5 附近。</li>
</ol>

<h3 id="通过-push-gateway-上报">通过 Push Gateway 上报</h3>

<h4 id="安装运行-push-gateway">安装运行 Push Gateway</h4>

<ul>
<li><p>下载并运行 Push Gateway（<a href="https://prometheus.io/download/#pushgateway">下载页面</a>|<a href="https://github.com/prometheus/pushgateway">源码页面</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">tar xvfz pushgateway-*.tar.gz
cd pushgateway-*/
./pushgateway --help  <span style="color:#75715e"># Mac 需要打开系统偏好设置 -&gt; 安全性和隐私，允许改程序运行。</span>
./pushgateway</code></pre></div></li>

<li><p>打开 <code>http://localhost:9091</code> 可以查看 pushgateway 的工作情况。</p></li>
</ul>

<h4 id="示例代码改造">示例代码改造</h4>

<p><code>02-prometheus/metrics.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
<span style="color:#f92672">import</span> (
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#e6db74">&#34;github.com/prometheus/client_golang/prometheus/push&#34;</span>
)

<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">HTTPMetricsMiddleware</span>) <span style="color:#a6e22e">StartMetricsPush</span>(<span style="color:#a6e22e">interval</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>) {
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">for</span> {
			<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">push</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;http://localhost:9091/metrics&#34;</span>, <span style="color:#e6db74">&#34;demo_by_pushgateway&#34;</span>).<span style="color:#a6e22e">Gatherer</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">reg</span>).<span style="color:#a6e22e">Push</span>()
			<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">interval</span>)
		}
	}()
}
<span style="color:#f92672">//</span> <span style="color:#f92672">...</span></code></pre></div>
<p><code>02-prometheus/server.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Run</span>() {
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">StartBackgroundReportGoCollector</span>(<span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">StartMetricsPush</span>(<span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/handler1&#34;</span>, <span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">WrapHandler</span>(<span style="color:#e6db74">&#34;/handler1&#34;</span>, <span style="color:#a6e22e">handler1</span>))
	<span style="color:#75715e">// ... 
</span><span style="color:#75715e"></span>}
<span style="color:#f92672">//</span> <span style="color:#f92672">...</span></code></pre></div>
<h4 id="配置-prometheus-server-1">配置 Prometheus Server</h4>

<p><code>02-prometheus/prometheus-pushgateway.yml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># ~/Downloads/prometheus-2.37.2.darwin-amd64/prometheus --config.file=02-prometheus/prometheus-pushgateway.yml --storage.tsdb.path=&#34;prometheus-tsdb-data/&#34;</span>
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: <span style="color:#e6db74">&#34;pushgateway&#34;</span>
    <span style="color:#75715e"># metrics_path: # 默认是 &#39;/metrics&#39;</span>
    honor_labels: <span style="color:#66d9ef">true</span>  <span style="color:#75715e"># 不覆盖 metrics 自身的 job 和 instance 标签，参见： https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config</span>
    static_configs:
      - targets: [<span style="color:#e6db74">&#34;localhost:9091&#34;</span>]</code></pre></div>
<h4 id="启动测试-push">启动测试 (Push)</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个终端</span>
rm -rf prometheus-tsdb-data/ <span style="color:#f92672">&amp;&amp;</span> ~/Downloads/prometheus-2.37.2.darwin-amd64/prometheus --config.file<span style="color:#f92672">=</span><span style="color:#ae81ff">02</span>-prometheus/prometheus-pushgateway.yml --storage.tsdb.path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;prometheus-tsdb-data/&#34;</span>

<span style="color:#75715e"># 第二个终端</span>
cd <span style="color:#ae81ff">02</span>-prometheus <span style="color:#f92672">&amp;&amp;</span> go test -timeout 600s -run ^TestRun$ ./ -v --count<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span></code></pre></div>
<p>打开 <code>http://localhost:9091</code> 观察 pushgateway 是否收到消息</p>

<p>等待第二个终端运行结束，打开 <code>http://localhost:9090/graph</code>，切换到 <code>Graph</code> 标签，输入类似上文 <a href="#启动测试">示例-启动测试</a>，如 <code>rate(http_requests_total[30s])</code>，即可看到从 Pushgateway 采集到的来自示例代码 push 上来的指标。</p>

<h2 id="数据查询-promql">数据查询 PromQL</h2>

<blockquote>
<p>参考： <a href="https://prometheus.io/docs/prometheus/2.37/querying/basics/">QUERYING PROMETHEUS</a></p>
</blockquote>

<p>Prometheus 自定义了一种表达式查询语言，被称为 PromQL。真正的查询实例还需要提供如下参数：</p>

<ul>
<li>开始时间</li>
<li>结束时间</li>
<li>查询解析度 (step / query resolution)</li>
</ul>

<p>Prometheus 会根据这些 PromQL、开始时间、结束时间、查询解析度，生成评估时间序列。然后到时序数据库中查询、过滤、聚合数据。</p>

<h3 id="表达式数据类型">表达式数据类型</h3>

<p>PromQL 表达式的结果有如下四种类型：</p>

<ul>
<li>Instant vector，一组时间序列，每个时间序列包含一个样本，所有样本共享相同的时间戳，如。</li>
<li>Range vector， 一组时间序列，其中包含每个时间序列随时间变化的一系列数据点</li>
<li>Scalar， 一个简单的数字浮点值。</li>
<li>String，一个简单的字符串值；目前未使用。</li>
</ul>

<p>举个例子假设一个 Counter 类型的指标 <code>http_requests_total</code> 的采样间隔是 15s。此时：</p>

<ul>
<li><p><code>http_requests_total</code> 这个表达式就是一个 Instant vector ，一个示例为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">时间                 值
2022-11-18 18:00:15   1
2022-11-18 18:00:30   3
2022-11-18 18:00:45   9
2022-11-18 18:01:00   20</pre></div></li>

<li><p><code>http_requests_total[30s]</code> 这个表达式就是一个 Range vector，一个示例为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">时间范围                                     值
2022-11-18 18:00:15 ~ 2022-11-18 18:00:30   [1, 3]
2022-11-18 18:00:45 ~ 2022-11-18 18:01:00   [9, 20]</pre></div></li>
</ul>

<h3 id="字面量和注释">字面量和注释</h3>

<ul>
<li><p>字符串，使用单引号双引号以及反引号包裹，采用 <a href="https://go.dev/ref/spec#String_literals">Go 语言转义规则</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&#34;this is a string&#34;
&#39;these are unescaped: \n \\ \t&#39;
`these are not unescaped: \n &#39; &#34; \t`</pre></div></li>

<li><p>浮点数字面量。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">23
-2.43
3.4e-9
0x8f
-Inf
NaN</pre></div></li>

<li><p>注释，以 <code>#</code> 号开头。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    # This is a comment</pre></div></li>
</ul>

<h3 id="时间序列选择">时间序列选择</h3>

<p>在编写 PromQL 时，首先需要的是选择一个待查询的时间序列（Instant vector 或 Range vector），然后才能使用运算符和函数对这些数据进行处理，以得到最终想要的图表或者报警数据源。</p>

<h4 id="instant-vector">Instant vector</h4>

<ul>
<li><code>http_requests_total</code> 选择所有名为 <code>http_requests_total</code> 的时间序列</li>

<li><p><code>http_requests_total{job=&quot;prometheus&quot;,group=&quot;canary&quot;}</code> 选择所有名为 <code>http_requests_total</code> 且 label 的 <code>job=&quot;prometheus&quot;,group=&quot;canary&quot;</code> 的时间序列。通过 <code>{k1=&quot;v1&quot;[...,kn=&quot;vn&quot;]}</code>  的方式可以过滤符合指定条件标签的时间序列。除了 <code>=</code>，还支持如下匹配符：</p>

<ul>
<li><code>=</code> 严格等于。</li>
<li><code>!=</code> 不等于。</li>
<li><code>=~</code> 匹配正则表达式（<code>env=~&quot;foo&quot;</code> 等价于 <code>env=~&quot;^foo$&quot;</code>）。</li>
<li><code>!~</code> 不匹配正则表达式。</li>
</ul>

<p>一些例子如下：</p>

<ul>
<li><code>http_requests_total{environment=~&quot;staging|testing|development&quot;,method!=&quot;GET&quot;}</code>。</li>
<li><code>http_requests_total</code> 等价于 <code>{__name__=&quot;http_requests_total&quot;}</code>。</li>
</ul></li>
</ul>

<h4 id="range-vector">Range Vector</h4>

<p>在 Instant vector 表达式的后方，添加 <code>[$TimeDuration]</code> 即可创建 Range Vector，如： <code>http_requests_total{job=&quot;prometheus&quot;}[5m]</code>。time duration 为 go time duration 格式：</p>

<ul>
<li><code>ms</code> - 毫秒</li>
<li><code>s</code> - 秒</li>
<li><code>m</code> - 分钟</li>
<li><code>h</code> - 小时hours</li>
<li><code>d</code> - 天 = 24h</li>
<li><code>w</code> - 周 = 7d</li>
<li><code>y</code> - 年 = 365d</li>
</ul>

<p>也支持组合，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">5h
1h30m
5m
10s</pre></div>
<h4 id="进行时间相对偏移">进行时间相对偏移</h4>

<p><code>offset $TimeDuration</code> 对评估时间进行偏移，假设对表达式 <code>http_requests_total offset 5m</code> 的查询开始时间为 11:00， 结束时间为 11:05，采样周期为 1m，此时：</p>

<ul>
<li>评估时间 11:01，查询到的为 11:01 - 5m 即 10:56 的数据。</li>
<li>评估时间 11:02，查询到的为 11:02 - 5m 即 10:57 的数据。</li>
<li>评估时间 11:03，查询到的为 11:03 - 5m 即 10:58 的数据。</li>
<li>评估时间 11:04，查询到的为 11:04 - 5m 即 10:59 的数据。</li>
<li>评估时间 11:05，查询到的为 11:05 - 5m 即 11:00 的数据。</li>
</ul>

<p>在图表上来看相当于修改时间序列的时间戳，即坐标系不变，图形向右移动。</p>

<p>一些例子如下：</p>

<ul>
<li><code>http_requests_total offset 5m</code> 评估时间 5 分钟前的时间序列。</li>
<li><code>rate(http_requests_total[5m] offset 1w)</code> 评估时间 1 周前的 http_requests_total 的 5 分钟 rate。</li>
<li><code>rate(http_requests_total[5m] offset -1w)</code> 评估时间 1 周<strong>后</strong>的 http_requests_total 的 5 分钟 rate。</li>
</ul>

<h4 id="修改为绝对时间">修改为绝对时间</h4>

<p><code>@ $UnixTimestamp</code> （UnixTimestamp 为秒时间戳） 修改 Instant vector 和 Range Vector 单个查询的评估时间为固定值，假设对表达式 <code>http_requests_total @ 1668909600</code> （2022-11-20T10:00:00+08）的查询开始时间为 11:00， 结束时间为 11:05，采样周期为 1m，此时：</p>

<ul>
<li>评估时间 11:01，查询到的为固定值 1668909600 即 10:00 的数据。</li>
<li>评估时间 11:02，查询到的为固定值 1668909600 即 10:00 的数据。</li>
<li>评估时间 11:03，查询到的为固定值 1668909600 即 10:00 的数据。</li>
<li>评估时间 11:04，查询到的为固定值 1668909600 即 10:00 的数据。</li>
<li>评估时间 11:05，查询到的为固定值 1668909600 即 10:00 的数据。</li>
</ul>

<p>在图表上来看，指标变为一条水平的直线。</p>

<p>一些例子如下：</p>

<ul>
<li><code>http_requests_total @ 1609746000</code> 始终查询时间戳为 1609746000 的数据。</li>
<li><code>sum(http_requests_total{method=&quot;GET&quot;} @ 1609746000)</code></li>
<li><code>http_requests_total offset 5m @ 1609746000</code> 支持和 offset 一起使用。</li>
<li><code>http_requests_total @ 1609746000 offset 5m</code> 支持和 offset 一起使用， <code>@</code> 和 <code>offest</code> 的顺序不重要。</li>

<li><p><code>start()</code> 和 <code>end()</code> 可以作为 @ 的特殊值。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">http_requests_total @ start()
rate(http_requests_total[5m] @ end())</pre></div></li>
</ul>

<h3 id="运算符和函数">运算符和函数</h3>

<h4 id="算数运算符">算数运算符</h4>

<ul>
<li><code>+</code> 加</li>
<li><code>-</code> 减</li>
<li><code>*</code> 乘</li>
<li><code>/</code> 除</li>
<li><code>%</code> 求余 (modulo)</li>
<li><code>^</code> 次幂</li>
<li><code>atan2</code> 参见 <a href="https://pkg.go.dev/math#Atan2">https://pkg.go.dev/math#Atan2</a> 。</li>
</ul>

<p>算数运算符的数据类型。</p>

<ul>
<li>两个 scalar 之间：类型直接运算。</li>
<li>scalar 和 instant vector 之间：用 scalar 和 instant vector 的每个值进行运算。</li>
<li>两个 instant vectors：参见下文的 <a href="#向量匹配">向量匹配</a>。</li>
</ul>

<h4 id="比较运算符">比较运算符</h4>

<ul>
<li><code>==</code> 相等</li>
<li><code>!=</code> 不等于</li>
<li><code>&gt;</code> 大于</li>
<li><code>&lt;</code> 小于</li>
<li><code>&gt;=</code> 大于等于</li>
<li><code>&lt;=</code> 小于等于。</li>
</ul>

<p>算数运算符的数据类型。</p>

<ul>
<li>两个 scalar 之间：如 <code>1 &gt; bool 0</code>（<code>1 &gt; 0</code> 将报错），返回 1 (true)。</li>
<li>scalar 和 instant vector 之间：根据是否添加 <code>bool</code> 标识符分为如下两种情况：

<ul>
<li><code>http_requests_total &gt; 100</code> 保留向量值大于 100 的值，丢弃其他值。</li>
<li><code>http_requests_total &gt; bool 100</code> 将向量值大于 100 的值变为 1 (true)，将小于 100 的值变为 0 (false)，且指标名将被删除。</li>
</ul></li>
<li>两个 instant vector 之间：按照下文的 <a href="#向量匹配">向量匹配</a> 进行匹配。

<ul>
<li>如果不添加 <code>bool</code> 标识符，则结果为过滤后，表达式成立的左侧的值。不满足条件（表达式非法、没有匹配上、表达式结果为 false）的将被丢弃。</li>
<li>如果添加 <code>bool</code> 标识符，向量的值将变为 1 或 0。1 表示表达式结果为 true， 0 表示表达式非法、没有匹配上、表达式结果为 false。</li>
</ul></li>
</ul>

<h4 id="集合运算符">集合运算符</h4>

<p>集合运算符只能在两个 instant vector （vector1、vector2）进行运算。vector1 的元素在 vector2 是否存在的判断是根据向量的标签进行查找判断的。</p>

<p>可以这么理解，假设 vector1 和 vector2 做集合运算，按照标签可以将两者的所有元素划分为如下四种类型：</p>

<ul>
<li><code>left(vector1)</code></li>
<li><code>middle(vector1)</code></li>
<li><code>middle(vector2)</code></li>
<li><code>right(vector2)</code></li>
</ul>

<p>此时，如下三种集合运算：</p>

<ul>
<li><code>and</code> (交集) ，<code>vector1 and vector2</code> 结果为： vector1 对应的标签在 vector2 也存在的 vector1 值的集合，即 <code>middle(vector1)</code>。</li>
<li><code>or</code> (union)，<code>vector1 or vector2</code> 结果为： vector1 全部的元素 以及 vector2 中标签在 vector1 中不存在的元素，即 <code>left(vector1)</code>、<code>middle(vector1)</code> 和 <code>right(vector2)</code>。</li>
<li><code>unless</code> (补集)，<code>vector1 and vector2</code> 结果为： vector1 对应的标签在 vector2 不存在存在的 vector1 值的集合，即 <code>left(vector1)</code>。</li>
</ul>

<h4 id="向量匹配">向量匹配</h4>

<p>以如下数据为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">method_code:http_errors:rate5m{method=&#34;get&#34;, code=&#34;500&#34;}  24
method_code:http_errors:rate5m{method=&#34;get&#34;, code=&#34;404&#34;}  30
method_code:http_errors:rate5m{method=&#34;put&#34;, code=&#34;501&#34;}  3
method_code:http_errors:rate5m{method=&#34;post&#34;, code=&#34;500&#34;} 6
method_code:http_errors:rate5m{method=&#34;post&#34;, code=&#34;404&#34;} 21

method:http_requests:rate5m{method=&#34;get&#34;}  600
method:http_requests:rate5m{method=&#34;del&#34;}  34
method:http_requests:rate5m{method=&#34;post&#34;} 120</pre></div>
<p>向量匹配的原则是，按照标签进行匹配，如果用于匹配的标签只有一方存在，那么这个标签对应的数据将被丢弃。</p>

<p>如上所示，两个指标进行向量匹配时，其对应的标签可能并不一样。为了让标签可以对应的上，在进行运算符操作时，可以使用 ignore 或者 on 来选择用于匹配的标签。</p>

<ul>
<li>ignore 表示忽略掉指定的标签，而保留其他标签。</li>

<li><p>on 表示只使用指定的标签，忽略掉未声明的标签。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;vector expr&gt; &lt;bin-op&gt; ignoring(&lt;label list&gt;) &lt;vector expr&gt;
&lt;vector expr&gt; &lt;bin-op&gt; on(&lt;label list&gt;) &lt;vector expr&gt;</pre></div></li>
</ul>

<p>使用 ignore 或者 on 可能会带来一个问题，即同一个待匹配的标签存在多个值的情况，可以分如下情况讨论。</p>

<ul>
<li><p>一对一。以 <code>method_code:http_errors:rate5m{code=&quot;500&quot;} / ignoring(code) method:http_requests:rate5m</code> 为例，因为 <code>{code=&quot;500&quot;}</code> 和 <code>ignoring(code)</code> 成对出现，因此左侧的向量仍然是一个标签对应一个值。所以左右按照标签进行对应即可。因此结果如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">// left ({code=&#34;500&#34;} / ignoring(code))
method_code:http_errors:rate5m{method=&#34;get&#34;/*, code=&#34;500&#34; */}  24
method_code:http_errors:rate5m{method=&#34;post&#34;/*, code=&#34;500&#34;*/} 6
// right
method:http_requests:rate5m{method=&#34;get&#34;}  600
method:http_requests:rate5m{method=&#34;post&#34;} 120

{method=&#34;get&#34;}  0.04            //  24 / 600
{method=&#34;post&#34;} 0.05            //   6 / 120</pre></div></li>

<li><p>多对一以及一对多。需使用 <code>group_left</code> 或 <code>group_right</code> 来约束，左右哪一方是 <strong>多</strong> 的一方。以 <code>method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m</code> 为例，结果为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">// left （/ ignore code group_left，匹配时，只是用 method 进行匹配）
method_code:http_errors:rate5m{method=&#34;get&#34;/*, code=&#34;500&#34;/*}  24
method_code:http_errors:rate5m{method=&#34;get&#34;/*, code=&#34;404&#34;/*}  30
method_code:http_errors:rate5m{method=&#34;post&#34;/*, code=&#34;500&#34;/*} 6
method_code:http_errors:rate5m{method=&#34;post&#34;/*, code=&#34;404&#34;*/} 21
// right
method:http_requests:rate5m{method=&#34;get&#34;}  600
method:http_requests:rate5m{method=&#34;post&#34;} 120

{method=&#34;get&#34;, code=&#34;500&#34;}  0.04            //  24 / 600
{method=&#34;get&#34;, code=&#34;404&#34;}  0.05            //  30 / 600
{method=&#34;post&#34;, code=&#34;500&#34;} 0.05            //   6 / 120
{method=&#34;post&#34;, code=&#34;404&#34;} 0.175           //  21 / 120</pre></div></li>

<li><p>多对多，不支持。</p></li>
</ul>

<h4 id="聚合操作">聚合操作</h4>

<p>对某个时刻的值，进行聚合。换句话来说，是减少标签的数目。支持的函数如下：</p>

<ul>
<li><code>sum</code> (在指定维度上求和)</li>
<li><code>max</code> (在指定维度上求最大值)</li>
<li><code>min</code> (在指定维度上求最小值)</li>
<li><code>avg</code> (在指定维度上求平均值)</li>
<li><code>group</code> (all values in the resulting vector are 1)</li>
<li><code>stddev</code> (在指定维度上求标准差)</li>
<li><code>stdvar</code> (在指定维度上求方差)</li>
<li><code>count</code> (统计向量元素的个数)</li>
<li><code>count_values</code> (统计具有相同数值的元素数量)</li>
<li><code>bottomk</code> (样本值中最小的 k个值)</li>
<li><code>topk</code> (样本值中最大的 k个值)</li>
<li><code>quantile</code> (在指定维度上统计 φ-quantile 分位数(0 ≤ φ ≤ 1))</li>
</ul>

<p>语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;aggr-op&gt; [without|by (&lt;label list&gt;)] ([parameter,] &lt;vector expression&gt;)
// or
&lt;aggr-op&gt;([parameter,] &lt;vector expression&gt;) [without|by (&lt;label list&gt;)]</pre></div>
<p>假设 <code>http_requests_total</code> 只有三个标签：<code>instance</code>, <code>application</code>, <code>group</code> 。一些例子如下：</p>

<ul>
<li><code>sum without (instance) (http_requests_total)</code> 等价于 <code>sum by (application, group) (http_requests_total)</code> 去除 instance 标签维度，计算请求量。</li>
<li><code>sum(http_requests_total)</code> 不需要区分标签，计算全局请求量。</li>
<li><code>topk(5, sum by (instance) (http_requests_total))</code> 计算获取到请求量前 5 的实例。</li>
</ul>

<h4 id="二元运算符优先级">二元运算符优先级</h4>

<p>从高到底分别是：</p>

<ul>
<li><code>^</code></li>
<li><code>*</code>, <code>/</code>, <code>%</code>, <code>atan2</code></li>
<li><code>+</code>, <code>-</code></li>
<li><code>==</code>, <code>!=</code>, <code>&lt;=</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&gt;</code></li>
<li><code>and</code>, <code>unless</code></li>
<li><code>or</code></li>
</ul>

<p>相同优先级的运算符是左结合的。例如，2 <em>3 % 2 等价于 (2</em> 3) % 2。但是 ^ 是右结合的，所以 2 ^ 3 ^ 2 等价于 2 ^ (3 ^ 2)。</p>

<h4 id="函数">函数</h4>

<blockquote>
<p>参见：<a href="https://prometheus.io/docs/prometheus/2.37/querying/functions/">FUNCTIONS</a>。</p>
</blockquote>

<p>这里重点提一下：</p>

<ul>
<li><code>rate(v range-vector)</code> 上文多次提到该函数，其计算的时指标的每秒变化率。如 <code>rate(http_requests_total[5m])</code>，计算结果为 <code>(value[t+5m] - value[t])/ 300</code>。</li>
<li>针对 Range vector PromQL 提供 <code>&lt;aggregation&gt;_over_time()</code> 一系列聚合函数，可以将 <code>Range vector</code> 转换为 <code>Instant vector</code>。</li>
</ul>

<h3 id="与-sql-对比">与 SQL 对比</h3>

<p>PromQL 处理的是时序数据，而 SQL 处理的通用的关系数据。本质上可以说 PromQL 是 SQL 的一个特例，因此 SQL 应该是可以完整的表达 PromQL 的语义的。</p>

<table>
<thead>
<tr>
<th>PromQL</th>
<th>SQL</th>
</tr>
</thead>

<tbody>
<tr>
<td>metric name</td>
<td>表名</td>
</tr>

<tr>
<td>label</td>
<td>列 (维度列)</td>
</tr>

<tr>
<td>时间戳</td>
<td>列 (创建时间)</td>
</tr>

<tr>
<td>value</td>
<td>列 (事实列)</td>
</tr>

<tr>
<td>一次查询条件 $StartTime、$EndTime</td>
<td>select 创建时间 as 评估时间, value where 创建时间 between $StartTime and $EndTime</td>
</tr>

<tr>
<td><code>http_requests_total</code></td>
<td>from 表名</td>
</tr>

<tr>
<td><code>{}</code></td>
<td>where 维度列条件</td>
</tr>

<tr>
<td><code>offset</code></td>
<td>select 创建时间 + offset as 评估时间 where 创建时间 + offset between $StartTime and $EndTime</td>
</tr>

<tr>
<td><code>@</code></td>
<td>可能需要用到开窗函数 <code>first_value</code>，略</td>
</tr>

<tr>
<td>算数运算符 (scalar 和 scalar)</td>
<td><code>select 1 + 1</code> 或 <code>select 1 + (select ...)</code></td>
</tr>

<tr>
<td>算数运算符 (scalar 和 instant vector)</td>
<td><code>select 2 * value from ...</code></td>
</tr>

<tr>
<td>算数运算符 (instant 和 instant vector)</td>
<td><code>select value1 * value2 from ... &lt;inner/left/right&gt; join ... on 事实列 where value1 is not null and value2 is not null</code></td>
</tr>

<tr>
<td>比较运算符 (不带 bool)</td>
<td><code>select value from ... where value &lt; 1</code> 或 <code>select value1 from ... &lt;inner/left/right&gt; join ... on 维度列 where value1 is not null and value2 is not null and value1 &lt; value2</code></td>
</tr>

<tr>
<td>比较运算符 (带 bool)</td>
<td><code>select if(value &lt; 1, 1, 0) from ...</code> 或 <code>select if(value1 &lt; value2, 1, 0) from ... &lt;inner/left/right&gt; join ... on 维度列 where value1 is not null and value2 is not null</code></td>
</tr>

<tr>
<td>集合运算符 (and)</td>
<td><code>select value1 from ... inner join ... on 维度列</code></td>
</tr>

<tr>
<td>集合运算符 (or)</td>
<td><code>select if(value1 is not nul, value1, value2) from ... full join ... on 维度列</code></td>
</tr>

<tr>
<td>集合运算符 (unless)</td>
<td><code>select if(value1 is not nul, value1, value2) from ... left join ... on 维度列 where value2 is null</code></td>
</tr>

<tr>
<td>聚合操作</td>
<td><code>select 指定维度列, 聚合函数(value) from ... group by 指定维度列)</code></td>
</tr>

<tr>
<td>接收 Range vector 的函数操作</td>
<td><code>select 所有维度列, 创建时间戳 - 创建时间戳%5m, 聚合函数(value) from ... group by 所有维度列, 创建时间戳 - 创建时间戳%5m</code></td>
</tr>
</tbody>
</table>

<h2 id="数据大盘-grafana">数据大盘 Grafana</h2>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/visualization/grafana/">官方文档</a></p>
</blockquote>

<h2 id="报警接入">报警接入</h2>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/alerting/latest/overview/">官方文档</a></p>
</blockquote>

<h2 id="exporter">Exporter</h2>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/instrumenting/exporters/">官方文档</a>。</p>
</blockquote>

<h3 id="node-exporter">Node Exporter</h3>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/guides/node-exporter/">官方文档</a></p>
</blockquote>

<h3 id="mysql-exporter">MySQL Exporter</h3>

<p>参见：<a href="https://github.com/prometheus/mysqld_exporter">github</a>。</p>

<h3 id="redis-exporter">Redis exporter</h3>

<p>参见：<a href="https://github.com/oliver006/redis_exporter">github</a>。</p>

<h3 id="rocketmq-exporter">RocketMQ exporter</h3>

<p>参见：<a href="https://github.com/apache/rocketmq-exporter">github</a>。</p>

<h3 id="实现一个-exporter">实现一个 Exporter</h3>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/instrumenting/writing_exporters/">官方文档</a></p>
</blockquote>

<h2 id="最佳实践">最佳实践</h2>

<blockquote>
<p>参考：<a href="https://prometheus.io/docs/practices/">官方文档</a></p>
</blockquote>
]]></description></item><item><title>进程管理器（四） Go supervisord</title><link>https://www.rectcircle.cn/posts/process-manager-04-go-supervisord/</link><pubDate>Sun, 13 Nov 2022 18:07:54 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/process-manager-04-go-supervisord/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>在 《进程管理器》 的三个章节，介绍了：</p>

<ul>
<li><a href="/posts/process-manager-01-linux-background-knowledge/">（一）</a>：Linux 进程管理器的背景知识。</li>
<li><a href="/posts/process-manager-02-single-process-tini-source/">（二）</a>：单进程管理器 tini 的源码分析。</li>
<li><a href="/posts/process-manager-03-single-process-tini-go-impl/">（三）</a>：如何使用 Go 实现一个 tini 程序。</li>
</ul>

<p>tini 是一个最小化的进程管理器，但是其只能管理一个进程。本节，将介绍另一个轻量级进程管理器 supervisord，和 tini 相比，该进程管理器可以管理多个进程。</p>

<p>supervisord 是由 Python 实现。但是，Python 需要一个外部的 Python 解释器依赖，在容器化场景，这要求镜像中需要安装 Python 环境，这对于容器来讲有点重。因此，本文要介绍的是由 go 语言实现的 <a href="https://github.com/ochinchina/supervisord">ochinchina/supervisord</a>。</p>

<h2 id="编译">编译</h2>

<p>由于 Go 语言支持静态编译的特性，因此该版本的 supervisord，可以编译成没有任何外部依赖的可执行文件。</p>

<p>源码和版本选择：目前最新的标签为 <a href="https://github.com/ochinchina/supervisord/tree/v0.7.3">v0.7.3</a> 发布于 2021 年 5 月 3 日，距今已一年半。master 有大量的到代码，从 commit 历史来看，这段时间存在大量的 bugfix，因此本文采用写作时最新的 master 分支（commit 为： <a href="https://github.com/ochinchina/supervisord/tree/b1093f8906480aac2a7c82c8fa94e1e518fd6a62"><code>b1093f8906480aac2a7c82c8fa94e1e518fd6a62</code></a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/ochinchina/supervisord.git
cd supervisord
go generate
GOOS<span style="color:#f92672">=</span>linux go build -tags release -a -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> -o supervisord
./supervisord --version
<span style="color:#75715e"># v0.7.3</span></code></pre></div>
<h2 id="基本使用">基本使用</h2>

<p>go 实现的 supervisord，通过如上命令，编译产物只有唯一的可执行文件 <code>supervisord</code>，其静态编译大小在 Linux x86 平台约 19MB。</p>

<p>执行 <code>./supervisord --version &amp;&amp; ./supervisord --help</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Usage:
  supervisord [OPTIONS] [command]

Application Options:
  -c, --configuration= the configuration file
  -d, --daemon         run as daemon
      --env-file=      the environment file

Help Options:
  -h, --help           Show this help message

Available commands:
  ctl      Control a running daemon
  init     initialize a template
  service  install/uninstall/start/stop service
  version  show the version of supervisor</pre></div>
<p>supervisord 可执行文件主要包含一个主命令和四个工具类的子命令。</p>

<ul>
<li><code>supervisord [OPTIONS]</code>，启动 supervisord 进程。</li>
<li><code>supervisord ctl ...</code>，即 supervisorctl，操作 supervisord 进程。</li>
<li><code>supervisord init -o filename</code>，生成一个配置文件模板。</li>
<li><code>supervisord service ...</code>，为 supervisord 进程生成当前操作系统进程管理器对应的配置文文件，以 systemd 为例，将生成一个 <code>.service</code> 文件。</li>
<li><code>supervisord version</code>，打印版本。</li>
</ul>

<h3 id="启动-supervisord">启动 supervisord</h3>

<p>通过 <code>supervisord [OPTIONS]</code> 可以直接启动 supervisord。options，说明如下：</p>

<ul>
<li><p><code>-c</code> 或 <code>--configuration=</code> 指定配置文件路径。关于配置文件，参见下文：<a href="#配置文件">配置文件</a>。不填写时，会再按照如下顺序搜索配置文件（注意，和 <a href="http://supervisord.org/configuration.html#configuration-file">Python 版</a>不同）：</p>

<ol>
<li><code>$CWD/supervisord.conf</code></li>
<li><code>$CWD/etc/supervisord.conf</code></li>
<li><code>/etc/supervisord.conf</code></li>
<li><code>/etc/supervisor/supervisord.conf</code> (since Supervisor 3.3.0)</li>
<li><code>../etc/supervisord.conf</code> (Relative to the executable)</li>
<li><code>../supervisord.conf</code> (Relative to the executable)</li>
</ol></li>

<li><p><code>-d</code> 或 <code>--daemon</code> 后台模式运行（fork 两次）。</p></li>

<li><p><code>--env-file=</code> 环境变量文件（即 <code>.env</code> 文件）。</p></li>
</ul>

<h3 id="操作-supervisord">操作 supervisord</h3>

<p>supervisord 提供了 ctl 子命令来操作进程。支持：</p>

<ul>
<li>对进程/组的 status, start, stop, signal, pid (获取进程 id), fg（前台化） 操作。</li>
<li>对 supervisord 的 shutdown, reload 操作。</li>
</ul>

<p>supervisord ctl 是通过 XMLRPC 方式来操作一个已经启动的 supervisord。因此，需要先获取到 supervisord 的地址，supervisord ctl 会连接按照如下顺序获取到的第一个地址，并进行连接和 rpc 调用：</p>

<ul>
<li>指定了 <code>-s</code> 或者 <code>--serverurl=</code> 选项。</li>
<li>按照上文 <a href="#启动-supervisord">启动 supervisord</a> 加载配置的方式获取到配置文件，读取 <code>[supervisorctl]</code> 中的 <code>serverurl</code> 配置。</li>
<li>最后，兜底使用 <code>http://localhost:9001</code>。</li>
</ul>

<p>supervisord ctl 的示例操作如下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">supervisord ctl status
supervisord ctl status program-1 program-2...
supervisord ctl status group:*
supervisord ctl stop program-1 program-2...
supervisord ctl stop group:*
supervisord ctl stop all
supervisord ctl start program-1 program-2...
supervisord ctl start group:*
supervisord ctl start all
supervisord ctl shutdown
supervisord ctl reload
supervisord ctl signal &lt;signal_name&gt; &lt;process_name&gt; &lt;process_name&gt; ...
supervisord ctl signal all
supervisord ctl pid &lt;process_name&gt;
supervisord ctl fg &lt;process_name&gt;</code></pre></div>
<h3 id="webui">WebUI</h3>

<p>supervisord 启动的 server 除了暴露基于 http 的 xmlrpc 端口外，还会提供一个 WebUI。通过该 webui，可以可视化的启动进程。</p>

<h2 id="配置文件">配置文件</h2>

<p>supervisord 配置文件格式为 ini （Windows-INI-style），文件后缀名推荐为 <code>.conf</code>，其可配置的内容包括：</p>

<ul>
<li>supervisord 进程自身的配置，如日志等（<code>[supervisord]</code>）。</li>
<li>supervisord server 配置（<code>[unix_http_server]</code> 和 <code>[inet_http_server]</code>）。

<ul>
<li>提供 http 接口（xmlrpc）以支持 supervisorctl 操作 supervisord。</li>
<li>提供 一个 WebUI 可以操作 supervisord。</li>
</ul></li>
<li>supervisorctl 执行时读取的配置，如配置 supervisord server 的 url （<code>[supervisorctl]</code>）。</li>
<li>supervisord 管理的进程配置（<code>[program:x]</code>、<code>[program-default]</code>、<code>[group:x]</code>）。</li>
<li>supervisord 内部事件监听器，启动一个用来接收 supervisord 内部事件的程序 （<code>[eventlistener:x]</code>）。</li>
<li>加载其他文件 (<code>[include]</code>)</li>
</ul>

<p>配置文件的一些配置值支持通过 <code>%(ENV_X)s</code> 语法进行引用环境变量，参考下文说明。</p>

<p>本部分只列出 go 版本 supervisord 支持的配置项。</p>

<p>supervisord 的配置文件支持 <code>[include]</code> 配置段来加载其他配置文件，语法如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[include]</span>
<span style="color:#a6e22e">files</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/an/absolute/filename.conf /an/absolute/*.conf foo.conf config??.conf</span></code></pre></div>
<h3 id="supervisord-自身配置">supervisord 自身配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[supervisord]</span>
<span style="color:#a6e22e">logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/file/to/path    ; 默认为 $CWD/supervisord.log。支持 %(here)s 环境变量语法。supervisord 自身日志输出文件。设置为 /dev/stdout 之类的特殊文件时，需要将 logfile_maxbytes 设置为 0。</span>
<span style="color:#a6e22e">logfile_maxbytes</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">50MB    ; 默认为 50MB。 日志文件轮换的阈值（当日志文件大于该值时，将创建一个新的文件）。支持  KB, MB, GB 单位的字符串， 0 表示不轮换。</span>
<span style="color:#a6e22e">logfile_backups</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">10       ; 默认为 10。日志文件轮换后保留的最大日志文件个数。</span>
<span style="color:#a6e22e">loglevel</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">info            ; 默认为 info。日志级别，可选为 trace, debug, info, warning, error, fatal and panic。注意，可选值和 Python 版不同。</span>
<span style="color:#a6e22e">pidfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/file/to/path    ; 默认为 $CWD/supervisord.pid。支持 %(here)s 环境变量语法。写入当前 supervisord 进程 PID 的文件路径。</span>
<span style="color:#a6e22e">minfds</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1024              ; 默认为 1024。在 supervisord 启动时至少保留这个数量的文件描述符资源 (Rlimit nofiles)。</span>
<span style="color:#a6e22e">minprocs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">20              ; 默认为 20。在 supervisord 启动时至少保留这个数量的进程资源 (Rlimit noproc)。 </span>
<span style="color:#a6e22e">identifier</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">supervisord   ; 默认为 supervisor。此 supervisord 进程的标识符。如果在同一命名空间中的一台机器上运行多个 supervisord，则需要。主要用于 rpc 接口。</span></code></pre></div>
<h3 id="supervisord-server-配置">supervisord server 配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[inet_http_server]</span>
<span style="color:#a6e22e">port</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">127.0.0.1:9001         ; 默认为 None，不监听。监听的 tcp 端口。如：127.0.0.1:9001，:9001。</span>
<span style="color:#a6e22e">username</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">test1              ; 默认为 None。无鉴权。</span>
<span style="color:#a6e22e">password</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">thepassword        ; 默认为 None。无鉴权。密码明文以及 SHA-1 摘要的形式，参见 [unix_http_server] 的 password。</span>

<span style="color:#66d9ef">[unix_http_server]</span>
<span style="color:#a6e22e">file</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/path/to/socket_file ; [unix_http_server] 存在时，默认为 /tmp/supervisord.sock，否则为 None。不监听。支持 %(here)s 环境变量语法。rpc/http 服务所在监听的 uds。文件权限默认为 755。当存在 [unix_http_server] 时必填。</span>
<span style="color:#75715e">; chmod = 0777              ; go 版本不支持。</span>
<span style="color:#75715e">; chown = nobody:nogroup    ; go 版本不支持</span>
<span style="color:#a6e22e">username</span><span style="color:#f92672">=</span><span style="color:#e6db74">test1              ; 默认为 None。无鉴权。</span>
<span style="color:#a6e22e">password</span><span style="color:#f92672">=</span><span style="color:#e6db74">{SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d  ; 默认为 None。无鉴权。密码明文以及 SHA-1 摘要的形式。</span></code></pre></div>
<h3 id="supervisorctl-配置">supervisorctl 配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[supervisorctl]</span>
<span style="color:#a6e22e">serverurl</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">unix:///tmp/supervisor.sock  ; 默认值为 http://localhost:9001。 supervisord ctl 会读取该参数，通过该 url 提供的 xmlrpc 接口操作 supervisord。</span>
<span style="color:#a6e22e">username</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">chris                         ; 用户名</span>
<span style="color:#a6e22e">password</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">123                           ; 密码</span>
<span style="color:#75715e">; prompt = mysupervisor                    ; go 版本不支持</span></code></pre></div>
<h3 id="进程配置">进程配置</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/bin/cat                   ; 无默认值。进程启动命令。支持绝对路径以及相对于 $PATH 的相对路径，命令行参数也应该写在这里。需要特别注意的是，因为分号 (;) 是 ini 文件的注释，因此如果包含分号，需要使用反斜杠进行转义 \;。</span>
<span style="color:#a6e22e">process_name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%(program_name)s        ; 默认为 %(program_name)s。进程名。除非配置了 numprocs，否则无需关心该参数。</span>
<span style="color:#a6e22e">numprocs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1                           ; 默认为 1。启动的进程数量，如果大于 1，则 process_name 必须配置，且配置的值必须包含 %(process_num)s 。</span>
<span style="color:#75715e">; numprocs_start = 0                   ; go 不支持。%(process_num)s 的其实值。</span>
<span style="color:#a6e22e">autostart</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true                       ; 默认为 true。是否在 supervisord 启动时自动启动该进程。</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1                          ; 默认为 1。程序在启动后持续多少秒，才将进程状态从 starting 转换到 running，如果运行时长没有达到该限制，则会按照 startretries 进行重试。0 表示不约束最小运行时长。其值必须是整数。</span>
<span style="color:#a6e22e">startretries</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">3                       ; 默认为 3。表示程序启动失败多少次后，才将该进程状态设置为 FATAL 状态。实测该参数只有 startsecs 不为 0 才生效。</span>
<span style="color:#a6e22e">autorestart</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true                     ; 默认为 unexpected。配置程序重启策略。可选值为：false - 永不自动重启，true - 总是自动重启，unexpected - 只有程序启动失败才自动重启（取决于 exitcodes 参数）。实测该参数只有 startsecs 不为 0 才生效。</span>
<span style="color:#a6e22e">exitcodes</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0,2                          ; 默认为 0。影响 autorestart = unexpected：如果进程退出码不为该参数指定的值，会重新启动。</span>
<span style="color:#a6e22e">stopsignal</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">TERM                        ; 默认为 TERM。 supervisord ctl stop 时的信号。</span>
<span style="color:#a6e22e">stopwaitsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">10                        ; 默认为 10。优雅退出的时间。</span>
<span style="color:#a6e22e">stopasgroup</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true                       ; 默认为 false。supervisord ctl stop 信号是否发送给整个进程组。</span>
<span style="color:#a6e22e">killasgroup</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true                       ; 默认为 false。supervisord ctl stop 强制退出时，是否发送给整个进程组。</span>
<span style="color:#a6e22e">redirect_stderr</span><span style="color:#f92672">=</span><span style="color:#e6db74">false                    ; 默认为 false。是否将 stderr 重定向到 stdout（相当于 /the/program 2&gt;&amp;1）。</span>
<span style="color:#a6e22e">stdout_logfile</span><span style="color:#f92672">=</span><span style="color:#e6db74">AUTO                      ; go 版本默认为 /dev/null。日志输出位置，支持 /dev/null, /dev/stdout, syslog, syslog @[protocol:]host[:port]., /path/to/file。支持多个输出目标，以逗号分割，如：test.log, /dev/stdout。</span>
<span style="color:#a6e22e">stdout_logfile_maxbytes</span><span style="color:#f92672">=</span><span style="color:#e6db74">50MB             ; 默认 50MB。日志文件轮换的阈值（当日志文件大于该值时，将创建一个新的文件）。支持  KB, MB, GB 单位的字符串， 0 表示不轮换。</span>
<span style="color:#a6e22e">stdout_logfile_backups</span><span style="color:#f92672">=</span><span style="color:#e6db74">10                ; 默认为 10。日志文件轮换后保留的最大日志文件个数。</span>
<span style="color:#a6e22e">stderr_logfile</span><span style="color:#f92672">=</span><span style="color:#e6db74">AUTO                      ; 参见 stdout_logfile。</span>
<span style="color:#a6e22e">stderr_logfile_maxbytes</span><span style="color:#f92672">=</span><span style="color:#e6db74">50MB             ; 参见 stdout_logfile_maxbytes。</span>
<span style="color:#a6e22e">stderr_logfile_backups</span><span style="color:#f92672">=</span><span style="color:#e6db74">10                ; 参见 stdout_logfile_backups。</span>
<span style="color:#a6e22e">environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">KEY=&#34;val&#34;,KEY2=&#34;val2&#34;        ; 环境变量。</span>
<span style="color:#a6e22e">priority</span><span style="color:#f92672">=</span><span style="color:#e6db74">999                             ; 默认 999。只影响进程启动关闭的顺序。数字越小，越先启动后停止。</span>
<span style="color:#a6e22e">user</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">user1                             ; 默认为 supervisord 进程用户。指定进程启动所在的用户， supervisord 进程必须为 root 才行。</span>
<span style="color:#a6e22e">directory</span><span style="color:#f92672">=</span><span style="color:#e6db74">/tmp                           ; 默认继承 supervisord。进程工作目录。</span>
<span style="color:#75715e">; serverurl=AUTO                           ; 默认为 AUTO。向该进程通过环境变量 SUPERVISOR_SERVER_URL 传递 supervisord 的 url。实测 go 版本不支持。</span>
<span style="color:#75715e">; 下面参数只有 Go 版本存在。</span>
<span style="color:#a6e22e">restartpause</span><span style="color:#f92672">=</span><span style="color:#e6db74">0                           ; 默认为 0。重启时，停止后等待的秒数。</span>
<span style="color:#a6e22e">restart_when_binary_changed</span><span style="color:#f92672">=</span><span style="color:#e6db74">false        ; 默认为 false。是否在程序二进制文件发生更改后重启。</span>
<span style="color:#a6e22e">restart_cmd_when_binary_changed</span><span style="color:#f92672">=</span>         <span style="color:#e6db74">; 默认为 &#34;&#34;。程序文件发生更改后，使用重启命令字符串。</span>
<span style="color:#a6e22e">restart_signal_when_binary_changed</span><span style="color:#f92672">=</span>      <span style="color:#e6db74">; 默认为 &#34;&#34;。程序文件发生更改后，则发送信号以重新启动的信号。</span>
<span style="color:#a6e22e">restart_directory_monitor</span><span style="color:#f92672">=</span>               <span style="color:#e6db74">; 默认为 &#34;&#34;。为重新启动目的而被监视的路径。</span>
<span style="color:#a6e22e">restart_file_pattern</span><span style="color:#f92672">=</span>                    <span style="color:#e6db74">; 默认为 &#34;&#34;。如果文件在 restart_directory_monitor 下发生更改并且文件名与此模式匹配，则进行重新启动。</span>
<span style="color:#a6e22e">restart_cmd_when_file_changed</span><span style="color:#f92672">=</span>           <span style="color:#e6db74">; 默认为 &#34;&#34;。如果 restart_directory_monitor 下具有模式 restart_file_pattern 的任何受监视文件发生更改时的重启命令。</span>
<span style="color:#a6e22e">restart_signal_when_file_changed</span><span style="color:#f92672">=</span>        <span style="color:#e6db74">; 默认为 &#34;&#34;。如果 restart_directory_monitor 下任何模式为 restart_file_pattern 的监控文件发生变化，该信号将被发送到程序。</span>
<span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span>                             <span style="color:#e6db74">; 默认为空。该程序的依赖。影响程序的启动顺序。</span></code></pre></div>
<p>一些重试场景的说明：</p>

<ul>
<li><p>某个<strong>非</strong>常驻进程，运行时间很短（小于 1s）。且可能失败（exit != 0）。此时：</p>

<ul>
<li><p>无论成功失败与否，只执行一次（似乎有 bug，参见下文：<a href="#startsecs-参数为-0-进程状态异常">startsecs 参数为 0 进程状态异常</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span>  <span style="color:#e6db74">sh -c &#39;sleep 0.5 &amp;&amp; echo &#34;模拟非常驻进程异常退出了&#34; &amp;&amp; exit 1&#39; ; 测试</span>
<span style="color:#a6e22e">stdout_logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/dev/stdout</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0</span>
<span style="color:#75715e">; startretries = 3          ; startsecs = 0 该参数无意义。</span>
<span style="color:#75715e">; autorestart = unexpected  ; startsecs = 0 该参数无意义。</span></code></pre></div></li>

<li><p>重试 3 次都失败后不再重试： supervisord 无法实现该特性。</p></li>

<li><p>无限重试直到成功： supervisord 无法优雅实现该特性，只能通过 sleep 来实现。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span>  <span style="color:#e6db74">sh -c &#39;sleep 2 &amp;&amp; echo &#34;模拟真实的命令失败了&#34; &amp;&amp; exit 1&#39; ; 测试</span>
<span style="color:#a6e22e">stdout_logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/dev/stdout</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1   ; 设置为 0 的话，autorestart 不会生效。</span>
<span style="color:#75715e">; startretries = 3  ; command 参数 sleep 2 了秒钟，因此这个参数没有意义。</span>
<span style="color:#a6e22e">autorestart</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">unexpected</span></code></pre></div></li>
</ul></li>

<li><p>某个常驻进程：</p>

<ul>
<li><p>退出后（不管正常还是异常退出）无限重试：supervisord 无法优雅实现。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span>  <span style="color:#e6db74">sh -c &#39;sleep 0.1 &amp;&amp; echo &#34;模拟真实的常驻进程正常退出了&#34; &amp;&amp; exit 0&#39; ; 测试</span>
<span style="color:#a6e22e">stdout_logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/dev/stdout</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
<span style="color:#a6e22e">startretries</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">2147483647  ; 需要设置为非常大的值（这里写的是 2^32-1），否则可能在第 1 就退出了，不会无限重试了。</span>
<span style="color:#a6e22e">autorestart</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true</span></code></pre></div></li>

<li><p>退出后（不管正常还是异常退出）重试 3 次：supervisord 无法优雅实现。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span>  <span style="color:#e6db74">sh -c &#39;sleep 2 &amp;&amp; echo &#34;模拟真实的常驻进程正常退出了&#34; &amp;&amp; exit 0&#39; ; 测试</span>
<span style="color:#a6e22e">stdout_logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/dev/stdout</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">2147483647     ; 永远处于 starting 状态（这里写的是 2^32-1 秒），让 startretries 生效。会造成进程状态永远达不到 running 的问题。</span>
<span style="color:#a6e22e">startretries</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">3          ; 重试 3 次。</span>
<span style="color:#75715e">; autorestart = true    ; 该参数无意义了。</span></code></pre></div></li>
</ul></li>
</ul>

<h3 id="进程分组配置">进程分组配置</h3>

<p>对上文 <code>[program:x]</code> 进行配置分组，有如下两个作用：</p>

<ul>
<li>通过 priority 控制一组进程的启动顺序。</li>

<li><p>通过 supervisord ctl 批量操作进程。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[group:x]</span>
<span style="color:#a6e22e">programs</span><span style="color:#f92672">=</span><span style="color:#e6db74">bar,baz</span>
<span style="color:#a6e22e">priority</span><span style="color:#f92672">=</span><span style="color:#e6db74">999</span></code></pre></div></li>
</ul>

<h3 id="eventlistener-配置">eventlistener 配置</h3>

<p>启动一个事件监听进程，参见：<a href="#监听事件">编程交互-监听事件</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[eventlistener:x]</span>
<span style="color:#75715e">; 略 和 [program:x] 基本一致。</span></code></pre></div>
<h2 id="编程交互">编程交互</h2>

<h3 id="配置进程">配置进程</h3>

<p>supervisord 进程的配置是通过配置文件方式来实现的。因此，如果想通过编程的方式来配置一个进程，则需要规划好配置文件的格式。推荐的规划如下：</p>

<ul>
<li>一个 supervisord 主配置文件。如位于 <code>/etc/supervisord.conf</code>。</li>
<li>多个 supervisord 的辅助配置文件。如位于 <code>/etc/supervisord.d/*.conf</code></li>
</ul>

<p>此时主配置文件 <code>/etc/supervisord.conf</code> 关于配置文件相关的配置如下所示。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[include]</span>
<span style="color:#a6e22e">files</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/etc/supervisord.d/*.conf</span>
<span style="color:#75715e">; ...</span></code></pre></div>
<p>需要对进程进行管理的程序，只需要在 <code>/etc/supervisord.d</code> 目录下添加相关配置文件，然后 reload 即可。</p>

<h3 id="操作进程">操作进程</h3>

<p>supervisord 配置里启动一个 rpc server，然后就可以通过 rpc 协议来操作这些进程了，因此在主配置文件添加 rpc server 的相关配置，如果是本机管理，建议使用 socket 文件的方式。如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[unix_http_server]</span>
<span style="color:#a6e22e">file</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/var/run/supervisord.sock</span>

<span style="color:#66d9ef">[supervisorctl]</span>
<span style="color:#a6e22e">serverurl</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">unix:///var/run/supervisord.sock</span></code></pre></div>
<p>此时，可以使用 ochinchina/supervisord 提供的 <a href="https://github.com/ochinchina/supervisord/tree/master/xmlrpcclient">xmlrpcclient</a> 模块（<code>go get github.com/ochinchina/supervisord/xmlrpcclient</code>），来实现对进程的管理。</p>

<h3 id="监听事件">监听事件</h3>

<blockquote>
<p>协议参见：<a href="http://supervisord.org/events.html">Python 版文档</a>。</p>
</blockquote>

<p>略</p>

<h2 id="其他说明">其他说明</h2>

<h3 id="生产环境建议">生产环境建议</h3>

<p><a href="https://github.com/ochinchina/supervisord">ochinchina/supervisord</a> 项目从其文档，项目管理，代码风格，测试覆盖度等方面来看，质量并不高。因此如果在生产环境使用该项目，需要对所有依赖的功能，做好充分的测试。此外开发人员需要阅读、修改源码，来解决的问题的能力。</p>

<h3 id="进程状态机">进程状态机</h3>

<p><img src="/image/supervisord-subprocess-transitions.png" alt="image" /></p>

<ul>
<li>STOPPED 进程从未启动过。</li>
<li>STARTING 进程正在启动中（进程启动，且持续时间 &lt; startsecs。如果 startsecs = 0，则跳过该状态）。</li>
<li>RUNNING 程序正在运行中（进程启动，且持续时间 &gt;= startsecs。或 startsecs = 0 直接进入该状态）。</li>
<li>BACKOFF 进程退出太快 （进程启动，在 &lt; startsecs 之前就退出了，会进入该状态。然后如果还有 startretries 机会，会立即转换到 STARING 状态，尝试再次启动）。</li>
<li>STOPPING 进程停止或者从未启动过。</li>
<li>EXITED 进程从 RUNNING 状态退出（根据 autorestart、exitcodes 决定是否要重启）。</li>
<li>FATAL 进程在经历了 startretries 次启动后，仍然未成功，则切换到该状态。</li>
<li>UNKNOWN 未知，supervisord 发生问题。</li>
</ul>

<h3 id="按顺序启动进程">按顺序启动进程</h3>

<ul>
<li>方式 1：（Go 版本独有）通过 program 配置段的 <code>depends_on</code> 参数可以按照顺序启动进程。</li>
<li>方式 2：通过 program 配置段的 <code>priority</code> 参数指定顺序。</li>
</ul>

<h3 id="进程更新自动重启">进程更新自动重启</h3>

<p>通过 program 配置段的 <code>restart_xxx</code> 相关配置可以实现。</p>

<h3 id="编译问题">编译问题</h3>

<p>官方给的编译命令是：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go generate
GOOS<span style="color:#f92672">=</span>linux go build -tags release -a -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> -o supervisord</code></pre></div>
<p>这条命令需要再 alpine (musl-libc) 的操作系统中，编译的结果接口才能正常。如果在常规的 glibc 的 Linux 发行版中（如 debian）编译，将出现如下警告：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/tmp/go-link-2389416050/000002.o: In function `mygetgrouplist&#39;:
/xxx/src/os/user/getgrouplist_unix.go:18: warning: Using &#39;getgrouplist&#39; in statically linked applications requires at runtime the shared libraries from the glibc version used for linking</pre></div>
<p>这将导致编译出的二进制依赖 glibc，在 glibc 版本不正确，或者没有 glibc 的镜像中，使用 <code>program.user</code> 配置将 panic。</p>

<p>如果仍在 debian 系统进行编译，有如下几种解决办法：</p>

<ol>
<li><p>强制指定 musl-libc 编译。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt-get update <span style="color:#f92672">&amp;&amp;</span> sudo apt-get install -y musl-tools
GOOS<span style="color:#f92672">=</span>linux CC<span style="color:#f92672">=</span>musl-gcc go build -tags release -a -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> -o output/supervisord</code></pre></div></li>

<li><p>关闭 cgo。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">GOOS<span style="color:#f92672">=</span>linux CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> go build -tags release -a -ldflags <span style="color:#e6db74">&#34;-extldflags -static&#34;</span> -o output/supervisord</code></pre></div></li>

<li><p>单独设置 <code>os/user</code> 使用纯 go 实现。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">GOOS<span style="color:#f92672">=</span>linux go build -tags osusergo,release -a -ldflags <span style="color:#e6db74">&#34;-linkmode external -extldflags -static&#34;</span> -o supervisord</code></pre></div></li>
</ol>

<h3 id="问题">问题</h3>

<h4 id="缺失健康和就绪检查">缺失健康和就绪检查</h4>

<ul>
<li>supervisord 对于进程的监控做的很不到位，没有健康检查机制（比如监控某个端口是否存在，http 请求状态等）。</li>
<li>supervisord 对于没有对进程的就绪检查，只能通过 startsecs 参数来给程序设定初始化时间。某些场景，两个程序相互依赖，如 B -&gt; A，且 B 需要等待 A 就绪 B 才能启动，且 A 的初始化时间不定，此时 supervisord 就没法很好的支持，只能在 B 程序中实现等待逻辑。</li>
</ul>

<h4 id="缺失-pid-文件方式的进程管理">缺失 pid 文件方式的进程管理</h4>

<p>某些程序启动后立即退出，但是会产生一个 pid 文件，后续对该程序的管理以改 pid 文件为准。</p>

<p>supervisord 原生不支持该模式，在 Python 版中，需要通过一个 <code>pidproxy</code> 的程序来进行代理，参见：<a href="http://supervisord.org/subprocess.html#pidproxy-program">pidproxy</a>。</p>

<h4 id="startsecs-参数为-0-进程状态异常">startsecs 参数为 0 进程状态异常</h4>

<p>配置文件如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[program:x]</span>
<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span>  <span style="color:#e6db74">sh -c &#39;sleep 0.5 &amp;&amp; echo &#34;模拟非常驻进程异常退出了&#34; &amp;&amp; exit 1&#39; ; 测试</span>
<span style="color:#a6e22e">stdout_logfile</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/dev/stdout</span>
<span style="color:#a6e22e">startsecs</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0</span>
<span style="color:#75715e">; startretries = 3          ; startsecs = 0 该参数无意义。</span>
<span style="color:#75715e">; autorestart = unexpected  ; startsecs = 0 该参数无意义。</span></code></pre></div>
<p>等待退出后，执行 <code>./supervisord ctl status x</code> 获取到 <code>x</code> 的状态仍然是 running，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">x                                Running   pid 12994, uptime 0:00:10</pre></div>]]></description></item><item><title>Kitex 自定义底层连接</title><link>https://www.rectcircle.cn/posts/kitex-customize-underlying-connection/</link><pubDate>Fri, 04 Nov 2022 15:53:55 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/kitex-customize-underlying-connection/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="https://www.cloudwego.io/zh/docs/kitex/">Kitex</a>: <a href="https://github.com/cloudwego/kitex/tree/v0.4.3">v0.4.3</a></p>
</blockquote>

<h2 id="背景">背景</h2>

<p>Kitex 是字节跳动开源的一款 Golang 微服务 RPC 框架。其默认使用的底层连接 （<code>net.Conn</code>） 是字节跳动自研的 <a href="https://github.com/cloudwego/netpoll"><code>netpoll</code></a> 网络库，而不是 Go 标准网络库。</p>

<p>但是，某些场景，我们可能有需求让 RPC 跑在其他的底层连接中，比如：</p>

<ul>
<li>Kitex Over Go 标准库实现的 TCP 连接。</li>
<li>Kitex Over Websocket。</li>
<li>Kitex Over Yamux （多路复用）实现反向请求。</li>
</ul>

<p>此时，就没法使用 Kitex 默认的 netpoll 了，需要自定义底层连接。幸运的是， Kitex 提供了这个能力。</p>

<h2 id="项目初始化">项目初始化</h2>

<blockquote>
<p>本文示例代码：<a href="https://github.com/rectcircle/kitex-customize-underlying-connection">rectcircle/kitex-customize-underlying-connection</a></p>
</blockquote>

<ul>
<li><p>安装代码生成器并初始化项目。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装</span>
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest
<span style="color:#75715e"># 验证</span>
kitex --version
thriftgo --version
<span style="color:#75715e"># 创建 Go 项目</span>
go mod init github.com/rectcircle/kitex-customize-underlying-connection
<span style="color:#75715e"># 删除默认的 server 代码以及无用的脚本等内容</span></code></pre></div></li>

<li><p>编写 <code>idl/echo.thrift</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-thrift" data-lang="thrift"><span style="color:#f92672">namespace</span> go api

<span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Request</span> {
<span style="color:#ae81ff">1</span>: <span style="color:#66d9ef">string</span> message
}

<span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Response</span> {
<span style="color:#ae81ff">1</span>: <span style="color:#66d9ef">string</span> message
}

<span style="color:#66d9ef">service</span> <span style="color:#a6e22e">Echo</span> {
Response <span style="color:#a6e22e">echo</span><span style="color:#f92672">(</span><span style="color:#ae81ff">1</span>: Request req)
}</code></pre></div></li>

<li><p>代码生成并配置依赖。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kitex -module github.com/rectcircle/kitex-customize-underlying-connection -service example ./idl/echo.thrift
go get github.com/cloudwego/kitex@latest
go mod tidy</code></pre></div></li>

<li><p>编写 server 逻辑 <code>server/server.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">server</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>

	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EchoImpl</span> <span style="color:#66d9ef">struct</span>{}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">EchoImpl</span>) <span style="color:#a6e22e">Echo</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">resp</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">Response</span>, <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span> = <span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">NewResponse</span>()
	<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Message</span> = <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Message</span>
	<span style="color:#66d9ef">return</span>
}</code></pre></div></li>
</ul>

<h2 id="实现">实现</h2>

<h3 id="使用默认的-netpoll-网络库">使用默认的 Netpoll 网络库</h3>

<ul>
<li><p>编写 server <code>cmd/01-netpoll/server/main.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#a6e22e">api</span> <span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/server&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">svr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">NewServer</span>(new(<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">EchoImpl</span>))

	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">svr</span>.<span style="color:#a6e22e">Run</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
	}
}</code></pre></div></li>

<li><p>编写 client <code>cmd/01-netpoll/client/main.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client/callopt&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">echo</span>.<span style="color:#a6e22e">NewClient</span>(<span style="color:#e6db74">&#34;example&#34;</span>, <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithHostPorts</span>(<span style="color:#e6db74">&#34;127.0.0.1:8888&#34;</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">req</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">Request</span>{<span style="color:#a6e22e">Message</span>: <span style="color:#e6db74">&#34;Say hello by netpoll&#34;</span>}
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Echo</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">callopt</span>.<span style="color:#a6e22e">WithRPCTimeout</span>(<span style="color:#ae81ff">3</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Message</span>)
}</code></pre></div></li>

<li><p>测试运行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个终端</span>
go run ./cmd/01-netpoll/server 
<span style="color:#75715e"># 第二个终端</span>
go run ./cmd/01-netpoll/client
<span style="color:#75715e"># 输出为： 2022/11/04 16:34:00 Say hello by netpoll</span></code></pre></div></li>
</ul>

<h3 id="使用标准库网络库">使用标准库网络库</h3>

<blockquote>
<p>本示例存在死循环导致的 CPU 占用过高问题，需要官方解决，参见 Issue : <a href="https://github.com/cloudwego/kitex/issues/701">gonet.gonetTransServerFactory has dead loop #701</a>。</p>

<p>20230213 更新: 官方已修复，预计在 <code>v0.4.5</code> 发布。</p>
</blockquote>

<p>根据如下信息：</p>

<ul>
<li><a href="https://www.cloudwego.io/zh/docs/kitex/tutorials/framework-exten/transport/">传输模块扩展文档</a>，关于指定自定义的传输模块的说明。</li>
<li><a href="https://www.cloudwego.io/zh/blog/2022/08/26/kitex-v0.4.0-%E7%89%88%E6%9C%AC%E5%8F%91%E5%B8%83/">Kitex v0.4.0 版本发布博客</a>，关于 gonet 的支持。</li>
<li><a href="https://github.com/cloudwego/kitex/tree/develop/pkg/remote/trans/gonet">Kitex trans/gonet 相关源码</a>。</li>
</ul>

<p>可以得知， Kitex 提供了标准的灵活替换底层网络库的能力（官方称为传输层），并在 v0.4.0 添加了对 gonet 的支持。</p>

<p>具体实现要点，参见下文源码中的 <strong>改造点</strong>。</p>

<ul>
<li><p>编写 server <code>cmd/02-stdnet/server/main.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>

	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/pkg/remote/trans/gonet&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/server&#34;</span>
	<span style="color:#a6e22e">api</span> <span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
	<span style="color:#a6e22e">serverImpl</span> <span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/server&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">addr</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">ResolveTCPAddr</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>, <span style="color:#e6db74">&#34;:8889&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">svr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">NewServer</span>(new(<span style="color:#a6e22e">serverImpl</span>.<span style="color:#a6e22e">EchoImpl</span>),
		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithServiceAddr</span>(<span style="color:#a6e22e">addr</span>),
		<span style="color:#75715e">// 改造点：server 传输层使用 go 标准网络库
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithTransServerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewTransServerFactory</span>()),
		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithTransHandlerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewSvrTransHandlerFactory</span>()),
	)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">svr</span>.<span style="color:#a6e22e">Run</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
	}
}</code></pre></div></li>

<li><p>编写 client <code>cmd/02-stdnet/client/main.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client/callopt&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/pkg/remote/trans/gonet&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">echo</span>.<span style="color:#a6e22e">NewClient</span>(<span style="color:#e6db74">&#34;example&#34;</span>,
		<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithHostPorts</span>(<span style="color:#e6db74">&#34;127.0.0.1:8889&#34;</span>),
		<span style="color:#75715e">// 改造点：client 传输层使用 go 标准网络库
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithTransHandlerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewCliTransHandlerFactory</span>()),
	)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">req</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">Request</span>{<span style="color:#a6e22e">Message</span>: <span style="color:#e6db74">&#34;Say hello by go std net&#34;</span>}
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Echo</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">callopt</span>.<span style="color:#a6e22e">WithRPCTimeout</span>(<span style="color:#ae81ff">3</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Message</span>)
}</code></pre></div></li>

<li><p>测试运行</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个终端</span>
go run ./cmd/02-stdnet/server
<span style="color:#75715e"># 第二个终端</span>
go run ./cmd/02-stdnet/client
<span style="color:#75715e"># 输出为： 2022/11/04 17:41:07 Say hello by go std net</span></code></pre></div></li>
</ul>

<h3 id="使用-websocket">使用 Websocket</h3>

<blockquote>
<p>本示例存在死循环导致的 CPU 占用过高问题，需要官方解决，参见 Issue : <a href="https://github.com/cloudwego/kitex/issues/701">gonet.gonetTransServerFactory has dead loop #701</a>。</p>

<p>20230213 更新: a) 上述问题官方已修复，预计在 <code>v0.4.5</code> 发布。2) x/net/websocket 已不推荐使用，替换为 nhooyr.io/websocket。</p>
</blockquote>

<p>某些场景，TCP 可能没法直接使用，但是 Websocket 可以使用，此时想实现 Kitex Over Websocket。</p>

<p>上面可以看出，Kitex 官方提供了 gonet 是基于 TCP 的 <code>net.Conn</code>，那么将 Websocket 封装成一个 <code>net.Conn</code> 是否就可以实现了呢？实测是可以的，基本思路是：</p>

<ul>
<li><p>Client</p>

<ul>
<li>使用 <code>client.WithDialer</code> 选项自定义一个 Websocket 的 Dialer，用来建立 Websocket 连接，并返回 <code>net.Conn</code></li>
<li>通过 <code>client.WithTransHandlerFactory(gonet.NewCliTransHandlerFactory())</code>，使用 gonet 传输层。</li>

<li><p>代码 <code>cmd/03-websocket/client/main.go</code> 如下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/client/callopt&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/pkg/remote&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/pkg/remote/trans/gonet&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
	<span style="color:#e6db74">&#34;nhooyr.io/websocket&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WebsocketKitexDialer</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">ServerURL</span> <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewWebsocketKitexDialer</span>(<span style="color:#a6e22e">serverURL</span> <span style="color:#66d9ef">string</span>) <span style="color:#a6e22e">remote</span>.<span style="color:#a6e22e">Dialer</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">WebsocketKitexDialer</span>{
		<span style="color:#a6e22e">ServerURL</span>: <span style="color:#a6e22e">serverURL</span>,
	}
}

<span style="color:#75715e">// DialTimeout implements remote.Dialer
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">d</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexDialer</span>) <span style="color:#a6e22e">DialTimeout</span>(<span style="color:#a6e22e">network</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">address</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">timeout</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>) (<span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">wsConn</span>, <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Dial</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">d</span>.<span style="color:#a6e22e">ServerURL</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">DialOptions</span>{
		<span style="color:#a6e22e">CompressionMode</span>: <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">CompressionDisabled</span>, <span style="color:#75715e">// 默认压缩模式有概率触发 panic，禁用之。
</span><span style="color:#75715e"></span>	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">NetConn</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">wsConn</span>, <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">MessageBinary</span>), <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">echo</span>.<span style="color:#a6e22e">NewClient</span>(<span style="color:#e6db74">&#34;example&#34;</span>,
		<span style="color:#75715e">// 这只是一个 mock
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithHostPorts</span>(<span style="color:#e6db74">&#34;127.0.0.1:8890&#34;</span>),
		<span style="color:#75715e">// 改造点：使用自定义 dialer 获取 net.Conn
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithDialer</span>(<span style="color:#a6e22e">NewWebsocketKitexDialer</span>(<span style="color:#e6db74">&#34;ws://127.0.0.1:8890/kitex-ws&#34;</span>)),
		<span style="color:#75715e">// 改造点：client 传输层使用 go 标准网络库
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WithTransHandlerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewCliTransHandlerFactory</span>()),
	)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">req</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">Request</span>{<span style="color:#a6e22e">Message</span>: <span style="color:#e6db74">&#34;Say hello by websocket&#34;</span>}
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Echo</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">callopt</span>.<span style="color:#a6e22e">WithRPCTimeout</span>(<span style="color:#ae81ff">3</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Message</span>)
}</code></pre></div></li>
</ul></li>

<li><p>Server</p>

<ul>
<li>使用将 <code>http.Server</code> 封装成一个 <code>net.Listener</code>，通过 <code>server.WithListener(l)</code> 传递给 Server，这个 <code>net.Listener</code> 的逻辑是：接收到 Websocket 请求连接封装成 <code>net.Conn</code>，并通过 Accept 函数返回给 Kitex 框架。</li>
<li>通过 <code>server.WithTransServerFactory(gonet.NewTransServerFactory())</code>、<code>server.WithTransHandlerFactory(gonet.NewSvrTransHandlerFactory())</code> 配置使用 gonet 传输层。</li>

<li><p>代码 <code>cmd/03-websocket/server/main.go</code> 如下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;net/url&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
	<span style="color:#e6db74">&#34;sync&#34;</span>

	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/pkg/remote/trans/gonet&#34;</span>
	<span style="color:#e6db74">&#34;github.com/cloudwego/kitex/server&#34;</span>
	<span style="color:#a6e22e">api</span> <span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/kitex_gen/api/echo&#34;</span>
	<span style="color:#a6e22e">serverImpl</span> <span style="color:#e6db74">&#34;github.com/rectcircle/kitex-customize-underlying-connection/server&#34;</span>
	<span style="color:#e6db74">&#34;nhooyr.io/websocket&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WebsocketAddr</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">URL</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">URL</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ResolveWebsocketAddr</span>(<span style="color:#a6e22e">rawURL</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketAddr</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">u</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">rawURL</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">WebsocketAddr</span>{
		<span style="color:#a6e22e">URL</span>: <span style="color:#a6e22e">u</span>,
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Network implements net.Addr
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketAddr</span>) <span style="color:#a6e22e">Network</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Scheme</span>
}

<span style="color:#75715e">// String implements net.Addr
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketAddr</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimPrefix</span>(<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">String</span>(), <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Scheme</span><span style="color:#f92672">+</span><span style="color:#e6db74">&#34;://&#34;</span>)
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ClosedConnWrapper</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>
	<span style="color:#a6e22e">closed</span>        <span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{}
	<span style="color:#a6e22e">closeChanOnce</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Once</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClosedConnWrapper</span>(<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">ClosedConnWrapper</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ClosedConnWrapper</span>{
		<span style="color:#a6e22e">Conn</span>:   <span style="color:#a6e22e">c</span>,
		<span style="color:#a6e22e">closed</span>: make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{}),
	}
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ClosedConnWrapper</span>) <span style="color:#a6e22e">Close</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// fmt.Println(&#34;=====&#34;)
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">closeChanOnce</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#66d9ef">func</span>() { close(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">closed</span>) })
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ClosedConnWrapper</span>) <span style="color:#a6e22e">CloseChan</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{} {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">closed</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WebsocketKitexServer</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">addr</span>     <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketAddr</span>
	<span style="color:#a6e22e">server</span>   <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Server</span>
	<span style="color:#a6e22e">connChan</span> <span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewWebsocketKitexServer</span>(<span style="color:#a6e22e">websocketURL</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ResolveWebsocketAddr</span>(<span style="color:#a6e22e">websocketURL</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">WebsocketKitexServer</span>{
		<span style="color:#a6e22e">addr</span>:     <span style="color:#a6e22e">a</span>,
		<span style="color:#a6e22e">connChan</span>: make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>),
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Accept implements net.Listener
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>) <span style="color:#a6e22e">Accept</span>() (<span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">connChan</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#75715e">// Addr implements net.Listener
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>) <span style="color:#a6e22e">Addr</span>() <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Addr</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">addr</span>
}

<span style="color:#75715e">// Close implements net.Listener
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>) <span style="color:#a6e22e">Close</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">Close</span>()
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>) <span style="color:#a6e22e">websocketHandle</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
	<span style="color:#a6e22e">wsConn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Accept</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">AcceptOptions</span>{
		<span style="color:#a6e22e">CompressionMode</span>: <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">CompressionDisabled</span>, <span style="color:#75715e">// 默认压缩模式有概率触发 panic，禁用之。
</span><span style="color:#75715e"></span>	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;accept websocket conn error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewClosedConnWrapper</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">NetConn</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">wsConn</span>, <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">MessageBinary</span>))
	<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">connChan</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">c</span>
	<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">CloseChan</span>()
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WebsocketKitexServer</span>) <span style="color:#a6e22e">Start</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">mux</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewServeMux</span>()
	<span style="color:#a6e22e">mux</span>.<span style="color:#a6e22e">Handle</span>(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Path</span>, <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">websocketHandle</span>))

	<span style="color:#a6e22e">server</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Server</span>{<span style="color:#a6e22e">Addr</span>: <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Host</span>, <span style="color:#a6e22e">Handler</span>: <span style="color:#a6e22e">mux</span>}
	<span style="color:#66d9ef">go</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">ListenAndServe</span>() <span style="color:#75715e">// nolint
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">server</span> = <span style="color:#a6e22e">server</span>
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">l</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewWebsocketKitexServer</span>(<span style="color:#e6db74">&#34;ws://[::]:8890/kitex-ws&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">l</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#a6e22e">svr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">NewServer</span>(new(<span style="color:#a6e22e">serverImpl</span>.<span style="color:#a6e22e">EchoImpl</span>),
		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithListener</span>(<span style="color:#a6e22e">l</span>),
		<span style="color:#75715e">// 改造点：server 传输层使用 go 标准网络库
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithTransServerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewTransServerFactory</span>()),
		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithTransHandlerFactory</span>(<span style="color:#a6e22e">gonet</span>.<span style="color:#a6e22e">NewSvrTransHandlerFactory</span>()),
	)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">svr</span>.<span style="color:#a6e22e">Run</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
	}
}</code></pre></div></li>
</ul></li>

<li><p>测试运行</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个终端</span>
go run ./cmd/03-websocket/server
<span style="color:#75715e"># 第二个终端</span>
go run ./cmd/03-websocket/client 
<span style="color:#75715e"># 输出为： 2022/11/04 20:32:51 Say hello by websocket</span></code></pre></div></li>
</ul>

<h3 id="使用-yamux-实现反向请求">使用 Yamux 实现反向请求</h3>

<blockquote>
<p><a href="http://github.com/hashicorp/yamux">hashicorp/yamux</a></p>
</blockquote>

<p>从上文 Websocket 的实现可以看出 Kitex 的 gonet 传输层，通过对 Dialer、Listener 的自定义，可以支持任意实现了 <code>net.Conn</code> 接口的底层。而，Yamux 是满足该模型的，因此 Yamux 多路复用器也很容易实现，在此就不多赘述了。</p>

<p>实现参见： <a href="https://github.com/rectcircle/kitex-customize-underlying-connection/tree/master/cmd/04-yamux">github</a>。</p>
]]></description></item><item><title>可观测性（一）Opentracing &amp; Jaeger</title><link>https://www.rectcircle.cn/posts/observability-1-opentracing-jaeger/</link><pubDate>Sun, 30 Oct 2022 21:45:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/observability-1-opentracing-jaeger/</guid><description type="html"><![CDATA[

<blockquote>
<p>Opentracing Version: 1.1 | Jaeger version: 1.38</p>
</blockquote>

<p>实验代码库： <a href="https://github.com/rectcircle/learn-observability">rectcircle/learn-observability</a>。</p>

<h2 id="业界背景">业界背景</h2>

<p>OpenTracing 通过提供平台无关、厂商无关的 API，使得开发人员能够方便的添加（或更换）追踪系统的实现。在 2016 年 11 月, CNCF (云原生计算基金会) 技术委员会投票接受 OpenTracing 作为Hosted 项目，这是 CNCF 的第三个项目，第一个是 Kubernetes，第二个是 Prometheus。</p>

<p>2022 年 1 月 31 日，CNCF 正式宣布 OpenTracing 归档，OpenTracing 和 OpenCensus 一起合并到了 OpenTelemetry，并提供了 OpenTracing 的<a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opentracing.md">兼容层</a>，更多参见： <a href="https://www.cncf.io/blog/2022/01/31/cncf-archives-the-opentracing-project/">CNCF 博客</a> 和 <a href="https://github.com/opentracing/specification/issues/163">github issue</a>。</p>

<blockquote>
<p>💡 OpenTelemetry和 OpenCensus 相比不支持 metrics。</p>
</blockquote>

<p>由于 OpenTracing 归档不久，因此应该有很多历史项目仍然在使用 OpenTracing，因此了解 OpenTracing 仍十分有必要。</p>

<h2 id="解决的问题">解决的问题</h2>

<p>在单体应用中，用户请求只会由一个服务进行处理。在这种场景，只需要在上报日志和指标时，携带唯一的无状态的请求 ID，即方便的检索某个请求的日志和指标。</p>

<p>而随着微服务的到来，用户请求在后端会形成一个多个微服务之间的调用链。在这种场景，如何方便的检索某个请求涉及的所有微服务的请求日志和指标，是一个亟需解决的问题。</p>

<p>OpenTracing 就是一个解决该问题的业界标准，其定义了如下概念：</p>

<ul>
<li>Trace: 调用链，一般情况下，一个用户请求就会形成一个调用链，调用链是一个有向无环图。</li>
<li>Span: 一次调用，在微服务场景一般定义为一次 RPC 调用。Span 时 Trace 这个有向无环图的节点。</li>
</ul>

<p>有了这两个概念，就可以解决如上问题，流程如下所示：</p>

<ul>
<li>用户的请求到达第一个服务的处理函数时，会生成一个 TraceID_1、SpanID_1。

<ul>
<li>该服务上报日志、指标时，会携带 TraceID_1、SpanID_1 这两个参数。</li>
<li>该服务调用其他微服务时，会将 TraceID_1、SpanID_1 作为隐含参数传递下去。</li>
</ul></li>
<li>某服务的某个函数被调用时，会解析使用传递过来的 TraceID_1，并创建新的 SpanID_2。

<ul>
<li>该服务上报日志、指标时，会携带 TraceID_1、SpanID_2、上一级的 SpanID_1、两个 Span 的关系。</li>
<li>该服务调用其他微服务时，会将 TraceID_1、SpanID_2 作为隐含参数传递下去，依次类推。</li>
</ul></li>
<li>在查询时，通过 TraceID_1 或者 SpanID_1、SpanID_2， 即可方便的检索某个请求涉及的所有微服务的请求日志和指标。</li>
<li>其他说明

<ul>
<li>通过如上推演，日志和指标在上报和存储上，携带 <code>&lt;TraceID, SpanID, PreviousSpanID, Relation&gt;</code> 四元组即可。</li>
<li>需要记录每个 <code>SpanID</code> 的起止时间，这样可以很方便分析整个 Trace 中哪个 Span 的耗时情况。</li>
<li>OpenTracing 标准只定义的上报用的客户端的概念和 API 标准，参见下文。而以上所有 TraceID 并不在 OpenTracing 标准中，也就是说 Trace 是隐式的，因为通过 Span 的关系即可获取到整个有向无环图。</li>
</ul></li>
</ul>

<p>以上就是 OpenTracing 的核心部分的概念和实现流程推演，除此之外，OpenTracing 值得一提的：</p>

<ul>
<li>上面的 SpanID_1 和 SpanID_2 的关系是父子关系（<code>ChildOf</code>），父子关系一般是同步调用，父需要等待子的完成。OpenTracing 还定义了一种跟随关系 （<code>FollowsFrom</code>），跟随关系一般是异步调用，父不需要等待子的完成，在微服务场景，通过消息队列的异步处理可以使用 <code>FollowsFrom</code> 关系。</li>
<li>Span 可以关联如下内容：

<ul>
<li>Tags 类型为 <code>map[string]字符串,bool,数字</code>。</li>
<li>Log 包含如下字段：

<ul>
<li><code>map[string]any</code></li>
<li>可选的时间</li>
</ul></li>
</ul></li>
<li>从上文可以看出，如果想使用 OpenTracing，需要再 RPC 调用时，传递 TraceID 和 SpanID 这两个参数（被称为 SpanContext）。除了这两个参数外，OpenTracing 还可以在 RPC 调用过程中传递一个 <code>map[string]string</code> 类型的数据 baggage （注意该特性存在较大的开销）。</li>
</ul>

<h2 id="opentracing-标准实现-jaeger">OpenTracing 标准实现 Jaeger</h2>

<p><a href="https://www.jaegertracing.io/">Uber 开源的 Jaeger</a> 是 OpenTracing 的一个标准的后端实现。关于的<a href="https://www.jaegertracing.io/docs/1.38/architecture/">架构</a>和<a href="https://www.jaegertracing.io/docs/1.38/deployment/">部署</a>，本文不过多介绍。</p>

<p>本部分将主要介绍，通过 Docker 一键启动一个 Jaeger 服务，以及 Jaeger 客户端的使用。</p>

<h3 id="一键启动">一键启动</h3>

<blockquote>
<p><a href="https://www.jaegertracing.io/docs/1.38/getting-started/">Jaeger Getting Started</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker run --rm -it --name jaeger <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -e COLLECTOR_ZIPKIN_HOST_PORT<span style="color:#f92672">=</span>:9411 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -e COLLECTOR_OTLP_ENABLED<span style="color:#f92672">=</span>true <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">6831</span>:6831/udp <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">6832</span>:6832/udp <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">5778</span>:5778 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">16686</span>:16686 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">4317</span>:4317 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">4318</span>:4318 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">14250</span>:14250 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">14268</span>:14268 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">14269</span>:14269 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p <span style="color:#ae81ff">9411</span>:9411 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  jaegertracing/all-in-one:1.38</code></pre></div>
<h3 id="创建-tracer">创建 Tracer</h3>

<p><code>Jaeger</code> 提供了多种语言的客户端，这里仅介绍 Go 语言的客户端库 <a href="https://github.com/uber/jaeger-client-go">uber/jaeger-client-go</a>。通过该库可以创建一个实现了 <a href="https://github.com/opentracing/opentracing-go">opentracing/opentracing-go</a> 接口的客户端。</p>

<p><code>01-opentracing/tracing/tracing.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">tracing</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>

	<span style="color:#e6db74">&#34;github.com/uber/jaeger-client-go/rpcmetrics&#34;</span>

	<span style="color:#e6db74">&#34;github.com/opentracing/opentracing-go&#34;</span>
	<span style="color:#e6db74">&#34;github.com/uber/jaeger-client-go/config&#34;</span>
)

<span style="color:#75715e">// 创建一个 Jaeger opentracing.Tracer
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewTracer</span>(<span style="color:#a6e22e">serviceName</span> <span style="color:#66d9ef">string</span>) (<span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">cfg</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">Configuration</span>{
		<span style="color:#75715e">// 服务名
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">ServiceName</span>: <span style="color:#a6e22e">serviceName</span>,
		<span style="color:#75715e">// 采样配置
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 以创建 tracing 的节点的配置有效，如 A -&gt; B -&gt; C，则 A 采样策略生效，B、C 遵循 A 的决定。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">Sampler</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">SamplerConfig</span>{
			<span style="color:#75715e">// 更多参见：https://www.jaegertracing.io/docs/1.38/sampling/#client-sampling-configuration
</span><span style="color:#75715e"></span>			<span style="color:#75715e">// const: Param 为 1 表示全部采样（全部上报），为 0 关闭采样（永远不上报），
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">Type</span>:  <span style="color:#e6db74">&#34;const&#34;</span>,
			<span style="color:#a6e22e">Param</span>: <span style="color:#ae81ff">1</span>,
		},
		<span style="color:#a6e22e">Reporter</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">ReporterConfig</span>{
			<span style="color:#75715e">// 将 span 提交日志，上报到外部日志服务
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">LogSpans</span>: <span style="color:#66d9ef">true</span>,
		},
	}
	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cfg</span>.<span style="color:#a6e22e">FromEnv</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;cannot parse Jaeger env vars: %s&#34;</span>, <span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">metricsFactory</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewJaegerMetricsFactory</span>()
	<span style="color:#a6e22e">tracer</span>, <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cfg</span>.<span style="color:#a6e22e">NewTracer</span>(
		<span style="color:#75715e">// 用来记录 Jaeger 自身的一些错误以及 Span 提交（需启用 Reporter.LogSpans），到外部日志服务。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">Logger</span>(<span style="color:#a6e22e">NewJaegerLogger</span>()),
		<span style="color:#75715e">// 用来上报 Span 的一些统计指标到外部 Metrics 服务。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">Metrics</span>(<span style="color:#a6e22e">metricsFactory</span>),
		<span style="color:#75715e">// 用来观察 Span 创建的事件。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">Observer</span>(<span style="color:#a6e22e">rpcmetrics</span>.<span style="color:#a6e22e">NewObserver</span>(<span style="color:#a6e22e">metricsFactory</span>, <span style="color:#a6e22e">rpcmetrics</span>.<span style="color:#a6e22e">DefaultNameNormalizer</span>)),
	)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;cannot initialize Jaeger Tracer: %s&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tracer</span>, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p>Jaeger 创建 <code>opentracing.Tracer</code> 的配置可以分为如下两类：</p>

<ul>
<li><code>Tracer</code> 的配置

<ul>
<li>服务名</li>
<li>采样器</li>
</ul></li>
<li><code>Jaeger</code> 自身的监控

<ul>
<li>Span 提交日志</li>
<li>Span 相关的指标</li>
<li>Span 创建事件的回调函数</li>
</ul></li>
</ul>

<p>最终返回的 <code>opentracing.Tracer</code> 是 OpenTracing 标准定义的，参见下文。</p>

<h2 id="编程接口-api">编程接口 API</h2>

<blockquote>
<p><a href="https://opentracing-contrib.github.io/opentracing-specification-zh/specification.html">OpenTracing语义标准</a> | <a href="https://github.com/opentracing/opentracing-go">opentracing/opentracing-go</a></p>
</blockquote>

<p>示例参见：<code>01-opentracing/tracing/tracing_test.go</code></p>

<h3 id="创建和完成-span">创建和完成 Span</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Service2B</span>(<span style="color:#a6e22e">tracer2</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>, <span style="color:#a6e22e">httpHeader</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Header</span>) {
	<span style="color:#75715e">// 准备 Span 一个，一般在中间件中实现，反序列化 SpanContext
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">BSpan</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Span</span>
	<span style="color:#a6e22e">tags</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tags</span>{<span style="color:#e6db74">&#34;b&#34;</span>: <span style="color:#ae81ff">2</span>}
	<span style="color:#a6e22e">previousContext</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer2</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">HTTPHeaders</span>, <span style="color:#a6e22e">httpHeader</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">BSpan</span> = <span style="color:#a6e22e">tracer2</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;B&#34;</span>, <span style="color:#a6e22e">tags</span>, <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">ChildOf</span>(<span style="color:#a6e22e">previousContext</span>))
	} <span style="color:#66d9ef">else</span> {
		<span style="color:#a6e22e">BSpan</span> = <span style="color:#a6e22e">tracer2</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;B&#34;</span>, <span style="color:#a6e22e">tags</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">BSpan</span>.<span style="color:#a6e22e">Finish</span>()

	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Service1A</span>(<span style="color:#a6e22e">tracer1</span>, <span style="color:#a6e22e">tracer2</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>) {
	<span style="color:#75715e">// 准备 Span 一个，一般在中间件中实现
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ASpan</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer1</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;A&#34;</span>, <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tags</span>{<span style="color:#e6db74">&#34;a&#34;</span>: <span style="color:#ae81ff">1</span>})
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ASpan</span>.<span style="color:#a6e22e">Finish</span>()

	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<ul>
<li>函数声明：
<a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Tracer"><code>StartSpan(operationName string, opts ...StartSpanOption) Span</code></a></li>
<li>可用 StartSpanOption：

<ul>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Tags"><code>opentracing.Tags</code></a> 或 <a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Tag"><code>opentracing.Tag</code></a> 配置该 Span 的 Tag。</li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#ChildOf"><code>opentracing.ChildOf</code></a> 指定上一级的 SpanContext，且关系为 ChildOf。</li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#FollowsFrom"><code>opentracing.FollowsFrom</code></a> 指定上一级的 SpanContext，且关系为 FollowsFrom。</li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#StartTime"><code>opentracing.StartTime</code></a> 指定该 Span 的开始时间。</li>
</ul></li>
<li>通过 <code>Span.Finish</code> 函数可以结束一个 Span。</li>
</ul>

<h3 id="记录-span-日志">记录 Span 日志</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Service1A</span>(<span style="color:#a6e22e">tracer1</span>, <span style="color:#a6e22e">tracer2</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>) {
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ASpan</span>.<span style="color:#a6e22e">LogFields</span>(<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;message&#34;</span>, <span style="color:#e6db74">&#34;Service1A called&#34;</span>))
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<ul>
<li>函数声明参见：<a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.LogFields(fields ...log.Field)</code></a> 或 <a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.LogKV(alternatingKeyValues ...interface{})</code></a>。</li>
</ul>

<h3 id="设置和读取-span-的-baggage">设置和读取 Span 的 baggage</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Service1A</span>(<span style="color:#a6e22e">tracer1</span>, <span style="color:#a6e22e">tracer2</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>) {
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ASpan</span>.<span style="color:#a6e22e">SetBaggageItem</span>(<span style="color:#e6db74">&#34;BaggageA&#34;</span>, <span style="color:#e6db74">&#34;123&#34;</span>)
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Service2B</span>(<span style="color:#a6e22e">tracer2</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>, <span style="color:#a6e22e">httpHeader</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Header</span>) {
	<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 业务逻辑
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">BSpan</span>.<span style="color:#a6e22e">LogFields</span>(
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;message&#34;</span>, <span style="color:#e6db74">&#34;Service2B called&#34;</span>),
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;BaggageA&#34;</span>, <span style="color:#a6e22e">BSpan</span>.<span style="color:#a6e22e">BaggageItem</span>(<span style="color:#e6db74">&#34;BaggageA&#34;</span>)),
	)
}</code></pre></div>
<ul>
<li>通过 <a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.SetBaggageItem(restrictedKey, value string)</code></a> 可以设置 Baggage 一对 key、value，Baggage 会传递到与之关联的 Span 中。</li>
<li>通过 <a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.BaggageItem(restrictedKey string) string</code></a> 可以读取 Baggage 一个 key 的数据，如果不存在则返回空字符串。</li>
<li>注意：在 jaeger 实现中，如果序列化的格式是 <code>opentracing.TextMap</code>、<code>opentracing.HTTPHeaders</code> 时，反序列化后 Baggage 的 key 将转换为全部小写。</li>
</ul>

<h3 id="spancontext-序列化和反序列化">SpanContext 序列化和反序列化</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestExtractAndInject</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">tracer</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewTracer</span>(<span style="color:#e6db74">&#34;test&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">rootSpan</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;root&#34;</span>)
	<span style="color:#a6e22e">rootSpan</span>.<span style="color:#a6e22e">SetBaggageItem</span>(<span style="color:#e6db74">&#34;BaggageRoot&#34;</span>, <span style="color:#e6db74">&#34;456&#34;</span>)

	<span style="color:#a6e22e">textMapCarrier</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">TextMapCarrier</span>{}
	<span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Inject</span>(<span style="color:#a6e22e">rootSpan</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">TextMap</span>, <span style="color:#a6e22e">textMapCarrier</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== textMapCarrier: %v\n&#34;</span>, <span style="color:#a6e22e">textMapCarrier</span>)
	<span style="color:#a6e22e">httpHeaderCarrier</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">HTTPHeadersCarrier</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Header</span>{})
	<span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Inject</span>(<span style="color:#a6e22e">rootSpan</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">HTTPHeaders</span>, <span style="color:#a6e22e">httpHeaderCarrier</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== httpHeaderCarrier: %v\n&#34;</span>, <span style="color:#a6e22e">httpHeaderCarrier</span>)
	<span style="color:#a6e22e">binaryCarrier</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>{}
	<span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Inject</span>(<span style="color:#a6e22e">rootSpan</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Binary</span>, <span style="color:#a6e22e">binaryCarrier</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== binaryCarrier: %v\n&#34;</span>, <span style="color:#a6e22e">binaryCarrier</span>)

	<span style="color:#a6e22e">root1SpanContext</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">TextMap</span>, <span style="color:#a6e22e">textMapCarrier</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">root2SpanContext</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">HTTPHeaders</span>, <span style="color:#a6e22e">httpHeaderCarrier</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">root3SpanContext</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Binary</span>, <span style="color:#a6e22e">binaryCarrier</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">child1Span</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;child&#34;</span>, <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">ChildOf</span>(<span style="color:#a6e22e">root1SpanContext</span>))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== child1Span BaggageRoot: %v\n&#34;</span>, <span style="color:#a6e22e">child1Span</span>.<span style="color:#a6e22e">BaggageItem</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">ToLower</span>(<span style="color:#e6db74">&#34;BaggageRoot&#34;</span>))) <span style="color:#75715e">// 使用 opentracing.TextMap 像是个 bug，不区分大小写。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">child2Span</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;child&#34;</span>, <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">ChildOf</span>(<span style="color:#a6e22e">root2SpanContext</span>))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== child2Span BaggageRoot: %v\n&#34;</span>, <span style="color:#a6e22e">child2Span</span>.<span style="color:#a6e22e">BaggageItem</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">ToLower</span>(<span style="color:#e6db74">&#34;BaggageRoot&#34;</span>))) <span style="color:#75715e">// 使用 opentracing.HTTPHeaders 像是个 bug，不区分大小写。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">child3Span</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tracer</span>.<span style="color:#a6e22e">StartSpan</span>(<span style="color:#e6db74">&#34;child&#34;</span>, <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">ChildOf</span>(<span style="color:#a6e22e">root3SpanContext</span>))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== child3Span BaggageRoot: %v\n&#34;</span>, <span style="color:#a6e22e">child3Span</span>.<span style="color:#a6e22e">BaggageItem</span>(<span style="color:#e6db74">&#34;BaggageRoot&#34;</span>))
}</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== textMapCarrier: map[uber-trace-id:55d2ee3a9f8863f5:55d2ee3a9f8863f5:0000000000000000:1 uberctx-BaggageRoot:456]
=== httpHeaderCarrier: map[Uber-Trace-Id:[55d2ee3a9f8863f5:55d2ee3a9f8863f5:0000000000000000:1] Uberctx-Baggageroot:[456]]
=== child1Span BaggageRoot: 456
=== child2Span BaggageRoot: 456
=== child3Span BaggageRoot: 456</pre></div>
<p>说明：</p>

<ul>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Tracer"><code>Tracer.Inject(sm SpanContext, format interface{}, carrier interface{}) error</code></a> 将 SpanContext 安寨 format 格式序列化到 carrier 中。</li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Tracer"><code>Tracer.Extract(format interface{}, carrier interface{}) (SpanContext, error)</code></a> 将 carrier 按照 format 格式反序列为 SpanContext。</li>
<li>原生支持的 format 为：

<ul>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#BuiltinFormat"><code>opentracing.TextMap</code></a></li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#BuiltinFormat"><code>opentracing.HTTPHeaders</code></a></li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#BuiltinFormat"><code>opentracing.Binary</code></a></li>
</ul></li>
</ul>

<h3 id="设置-span-的属性">设置 Span 的属性</h3>

<p>Tags 和 OperationName 除了可以在 Span 的时候设置外，还可以随时添加：</p>

<ul>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.SetTag(key string, value interface{}) Span</code></a> 为了支持链式调用，将返回自身。</li>
<li><a href="https://pkg.go.dev/github.com/opentracing/opentracing-go@v1.2.0#Span"><code>Span.SetOperationName(operationName string) Span</code></a> 为了支持链式调用，将返回自身。</li>
</ul>

<h3 id="框架集成">框架集成</h3>

<h4 id="原理">原理</h4>

<p>从上文的编程接口可以看出，如果微服务想接入 OpenTracing，需要做如下事情：</p>

<ul>
<li>Server：从请求的参数获取序列化的 carrier，如果存在则通过 <code>Tracer.Extract</code> 反序列化为 SpanContext，并通过 <code>StartSpan</code> 函数创建 <code>Span</code>。并注入 <code>context.Context</code> 中，以给处理函数使用。</li>
<li>Client：从 <code>context.Context</code> 中获取 <code>Span</code>，并通过 <code>Tracer.Inject</code> 序列化到 carrier 中，并在调用 Server 的时候传递。</li>
</ul>

<h4 id="httpserver">HTTPServer</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Middleware</span>(<span style="color:#a6e22e">tr</span> <span style="color:#a6e22e">opentracing</span>.<span style="color:#a6e22e">Tracer</span>, <span style="color:#a6e22e">h</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>, <span style="color:#a6e22e">options</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">MWOption</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span></code></pre></div>
<ul>
<li>使用 Middleware 函数对 http.Handler 进行包装即可，更多参见：<a href="https://pkg.go.dev/github.com/opentracing-contrib/go-stdlib@v1.0.0/nethttp#Middleware">go docs</a>。</li>
</ul>

<h4 id="httpclient">HTTPClient</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">tracing</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>

	<span style="color:#e6db74">&#34;github.com/opentracing-contrib/go-stdlib/nethttp&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestHTTPClient</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">tracer</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewTracer</span>(<span style="color:#e6db74">&#34;test&#34;</span>)

	<span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{<span style="color:#a6e22e">Transport</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">nethttp</span>.<span style="color:#a6e22e">Transport</span>{}}
	<span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewRequest</span>(<span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#e6db74">&#34;http://qq.com&#34;</span>, <span style="color:#66d9ef">nil</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// req = req.WithContext(ctx) // extend existing trace, if any
</span><span style="color:#75715e"></span>
	<span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">ht</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">nethttp</span>.<span style="color:#a6e22e">TraceRequest</span>(<span style="color:#a6e22e">tracer</span>, <span style="color:#a6e22e">req</span>)
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ht</span>.<span style="color:#a6e22e">Finish</span>()

	<span style="color:#a6e22e">res</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#a6e22e">respBody</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(string(<span style="color:#a6e22e">respBody</span>))
}</code></pre></div>
<p>接入流程</p>

<ul>
<li>使用 <code>nethttp.Transport</code> 创建 <code>http.Client</code>。</li>
<li>使用 <code>req = req.WithContext(ctx)</code> 注入 ctx。</li>
<li>使用 <code>req, ht := nethttp.TraceRequest(tracer, req)</code> 包装 request。</li>
<li>使用 <code>defer ht.Finish()</code> 设置结束函数调用。</li>
</ul>

<h2 id="实例">实例</h2>

<h3 id="需求描述">需求描述</h3>

<p>假设我们在开发短信验证码登录需求，该需求包含如下接口：</p>

<ul>
<li>验证码发送需求</li>
</ul>

<p>假设我们的服务依赖关系如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                        ------&gt; Redis
                       |
API 服务 ---(RPC)---&gt; 认证服务 ---(MQ)---&gt; 短信服务</pre></div>
<p>其他说明：</p>

<ul>
<li>RPC 协议使用 HTTP。</li>
<li>示例代码使用 Go 和标准库实现。</li>
</ul>

<h3 id="实验代码">实验代码</h3>

<p>参见： <a href="https://github.com/rectcircle/learn-observability/tree/master/01-opentracing">rectcircle/learn-observability</a></p>

<h3 id="部署测试">部署测试</h3>

<p>首先，参考：<a href="#一键启动">Jaeger 一键启动</a>，启动 Jaeger 服务。</p>

<p>然后，启动测试服务。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 第一个 shell</span>
JAEGER_AGENT_HOST<span style="color:#f92672">=</span>localhost go run ./01-opentracing/api
<span style="color:#75715e"># 第二个 shell</span>
JAEGER_AGENT_HOST<span style="color:#f92672">=</span>localhost go run ./01-opentracing/auth
<span style="color:#75715e"># 第三个 shell</span>
JAEGER_AGENT_HOST<span style="color:#f92672">=</span>localhost go run ./01-opentracing/sms</code></pre></div>
<p>发起请求：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl -v <span style="color:#e6db74">&#39;localhost:8080/api/v1/SendSMSCode?PhoneNumber=123&#39;</span></code></pre></div>
<p>访问 <a href="http://localhost:16686">http://localhost:16686</a> 查看 tracing。</p>

<h2 id="总结">总结</h2>

<p>通过接入 OpenTracing 可以实现：</p>

<ul>
<li>从请求粒度追踪，某个请求对各个微服务调用链每个节点的起止时间和日志。</li>
</ul>

<p>OpenTracing 不足和问题：</p>

<ul>
<li>没有对自定义 metrics 的抽象、<a href="https://github.com/jaegertracing/jaeger/issues/649">暂不支持外部日志</a> （<a href="https://zhuanlan.zhihu.com/p/74930691">OpenTelemetry 日志好像也没有完全统合</a>）</li>
<li>官方已经归档，新项目建议迁移到 OpenTelemetry。</li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://opentracing.io/">官方站点</a></li>
<li><a href="https://github.com/opentracing">github</a></li>
<li>v1.1 标准文档：<a href="https://opentracing-contrib.github.io/opentracing-specification-zh/">中文</a> | <a href="https://opentracing.io/specification/">英文</a></li>
<li>OpenTracing Server：<a href="https://www.jaegertracing.io/">Uber 开源的 Jaeger</a></li>
<li>OpenTracing Go Client：<a href="https://github.com/opentracing/opentracing-go">opentracing/opentracing-go</a></li>
<li>OpenTracing Go HTTP 集成：<a href="https://github.com/opentracing-contrib/go-stdlib">opentracing-contrib/go-stdlib</a></li>
</ul>
]]></description></item><item><title>Nginx 反向代理 Upstream 失败重试和封禁机制</title><link>https://www.rectcircle.cn/posts/nginx-upstream-failed-retry-ban/</link><pubDate>Thu, 20 Oct 2022 19:43:04 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nginx-upstream-failed-retry-ban/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>在 Nginx 中，使用 proxy_pass 进行反向代理时，可以使用： <code>proxy_next_upstream</code>、<code>proxy_next_upstream_tries</code>、<code>server 的 max_fails 参数</code>、<code>server 的 fail_timeout 参数</code> 来配置失败重试以及 upstream server 的封禁机制。</p>

<p>这里通过实验来验证这些字段的含义和用途，防止错误配置出现线上 bug。</p>

<h2 id="重试机制">重试机制</h2>

<ul>
<li><a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream"><code>proxy_next_upstream</code></a>：默认值为 <code>error timeout</code>，用来定义哪些场景配置为：不成功的尝试（unsuccessful attempts）。可以配置为 <code>error timeout http_500</code> 等。</li>
<li><a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream_tries"><code>proxy_next_upstream_tries</code></a>：默认值为 <code>0</code> 即不限制，用来定义不成功的尝试（unsuccessful attempts）的尝试次数，注意这里是尝试（<code>tries</code>） 不是重试（<code>retries</code>），也就是说：

<ul>
<li><code>0</code> 表示不限制，尝试所有 upstream 的 server。</li>
<li><code>1</code> 表示只尝试 1 次，也就是说，第 1 次尝试，不管是否为不成功的尝试（unsuccessful attempts），就立即返回。</li>
<li><code>2</code> 表示尝试 2 次，也就是说，第 1 次尝试，如果为不成功的尝试（unsuccessful attempts），就额外在尝试 1 次。</li>
</ul></li>
</ul>

<h2 id="server-封禁">server 封禁</h2>

<ul>
<li><a href="http://nginx.org/en/docs/http/ngx_http_upstream_module.html"><code>server 的 max_fails 参数</code></a>：默认值为 <code>1</code>，用来配置 server 封禁，介绍参见下文。</li>
<li><a href="http://nginx.org/en/docs/http/ngx_http_upstream_module.html"><code>server 的 fail_timeout 参数</code></a>：<code>10s</code>，用来配置 server 封禁，介绍参见下文。</li>
</ul>

<p>Nginx 会为 upstream 的每个 server 维护一个 <code>counter</code>，这个 <code>counter</code> 被定义为，从当前时间往前 <code>fail_timeout</code> 的时间段内（实现上可能是简单的分时间段，需要看源码），该 server 作为第 1 个尝试 server 且这次尝试被定义为不成功的尝试（unsuccessful attempts）的尝试的请求的次数。举个例子：</p>

<ul>
<li>某 upstream 有 3 个 server，一个请求过来：

<ul>
<li>按照负载均衡策略，第 1 个尝试了 server1，但是响应命中了 <code>proxy_next_upstream</code> 参数，为不成功的尝试（unsuccessful attempts）。</li>
<li>按照 <code>proxy_next_upstream_tries = 2</code> 又尝试了 server2，但是响应命中了 <code>proxy_next_upstream</code> 参数，为不成功的尝试（unsuccessful attempts）。</li>
<li>Nginx 给客户端返回 server1 的响应。</li>
</ul></li>
<li>此时：

<ul>
<li>server1 的 <code>counter</code> 递增 1。</li>
<li>server2 的 <code>counter</code> 不变。</li>
</ul></li>
</ul>

<p>当某个 server 的 <code>counter</code> 等于 <code>max_fails</code> 后，这个 server 将被封禁 <code>fail_timeout</code>，封禁期间该节点将不会尝试。</p>

<p>当封禁到期后，该 server 重新接收请求，并将 <code>counter</code> 归零。</p>

<p>需要特别注意的是：实测，Nginx 以上的节点封禁规则生效的前提为 <code>proxy_next_upstream_tries != 1</code>。</p>

<h2 id="配置示例">配置示例</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#75715e"># ...
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">upstream</span> <span style="color:#e6db74">demo</span> {
        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8001</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8002</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8003</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>    }
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span>       <span style="color:#ae81ff">8000</span>;
        <span style="color:#f92672">server_name</span>  <span style="color:#e6db74">localhost</span>;

        <span style="color:#f92672">location</span> <span style="color:#e6db74">/500</span> {
            <span style="color:#f92672">proxy_next_upstream</span> <span style="color:#e6db74">error</span> <span style="color:#e6db74">timeout</span> <span style="color:#e6db74">http_500</span>; <span style="color:#75715e"># 哪些错误认为是不成功的尝试（unsuccessful attempts），默认为 error timeout，这里的只 http_500 是个例子，强烈不建议设置，否则可能导致异常的节点封禁。
</span><span style="color:#75715e"></span>            <span style="color:#f92672">proxy_next_upstream_tries</span> <span style="color:#ae81ff">2</span>;                <span style="color:#75715e"># 尝试下一个 server 的次数，默认为 0 不限制
</span><span style="color:#75715e"></span>            <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://demo</span>; 
        }
    }
    <span style="color:#75715e"># ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<h2 id="实验过程">实验过程</h2>

<h3 id="网络拓扑">网络拓扑</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">client ---&gt; nginx (:8000) ---&gt; server 1 (:8001)
                           |
                           +-&gt; server 2 (:8002)
                           |
                            -&gt; server 3 (:8003)</pre></div>
<h3 id="实验代码">实验代码</h3>

<h4 id="server">Server</h4>

<p>(<code>server/main.go</code>)</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;sync/atomic&#34;</span>
)

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">counter</span> <span style="color:#66d9ef">int32</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">resp502</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
	<span style="color:#a6e22e">atomic</span>.<span style="color:#a6e22e">AddInt32</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">counter</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">clientcounter</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Query</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;counter&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;(%s) server counter: %d, client counter:%s\n&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>], <span style="color:#a6e22e">counter</span>, <span style="color:#a6e22e">clientcounter</span>)
	<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#ae81ff">500</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;(%s) server counter: %d&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>], <span style="color:#a6e22e">counter</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/500&#34;</span>, <span style="color:#a6e22e">resp502</span>)
	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ListenAndServe</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;:%s&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>]), <span style="color:#66d9ef">nil</span>)
}</code></pre></div>
<h4 id="client">Client</h4>

<p>(<code>client/main.go</code>)</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;sync/atomic&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">counter</span> <span style="color:#66d9ef">int32</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">req500Path</span>() {
	<span style="color:#a6e22e">atomic</span>.<span style="color:#a6e22e">AddInt32</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">counter</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;http://localhost:%s/500?counter=%d&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>], <span style="color:#a6e22e">counter</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;client counter: %d, error: %s\n&#34;</span>, <span style="color:#a6e22e">counter</span>, <span style="color:#a6e22e">err</span>)
		<span style="color:#66d9ef">return</span>
	}
	<span style="color:#a6e22e">contentBytes</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;client counter: %d, status: %d, body: %s\n&#34;</span>, <span style="color:#a6e22e">counter</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StatusCode</span>, string(<span style="color:#a6e22e">contentBytes</span>))
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#ae81ff">20</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
		<span style="color:#a6e22e">req500Path</span>()
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	}
}</code></pre></div>
<h4 id="nginx">Nginx</h4>

<p><code>nginx.conf</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">user</span>  <span style="color:#e6db74">nginx</span>;
<span style="color:#66d9ef">worker_processes</span>  <span style="color:#e6db74">auto</span>;

<span style="color:#66d9ef">error_log</span>  <span style="color:#e6db74">/var/log/nginx/error.log</span> <span style="color:#e6db74">notice</span>;
<span style="color:#66d9ef">pid</span>        <span style="color:#e6db74">/var/run/nginx.pid</span>;


<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span>  <span style="color:#ae81ff">1024</span>;
}


<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">include</span>       <span style="color:#e6db74">/etc/nginx/mime.types</span>;
    <span style="color:#f92672">default_type</span>  <span style="color:#e6db74">application/octet-stream</span>;

    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;

    <span style="color:#f92672">access_log</span>  <span style="color:#e6db74">/var/log/nginx/access.log</span>  <span style="color:#e6db74">main</span>;

    <span style="color:#f92672">sendfile</span>        <span style="color:#66d9ef">on</span>;
    <span style="color:#75715e">#tcp_nopush     on;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">keepalive_timeout</span>  <span style="color:#ae81ff">65</span>;

    <span style="color:#75715e">#gzip  on;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">upstream</span> <span style="color:#e6db74">demo</span> {
        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8001</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8002</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>        <span style="color:#f92672">server</span>  127.0.0.1:<span style="color:#ae81ff">8003</span>;  <span style="color:#75715e"># 默认: max_fails=1 fail_timeout=10s
</span><span style="color:#75715e"></span>    }
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span>       <span style="color:#ae81ff">8000</span>;
        <span style="color:#f92672">server_name</span>  <span style="color:#e6db74">localhost</span>;

        <span style="color:#f92672">location</span> <span style="color:#e6db74">/500</span> {
            <span style="color:#f92672">proxy_next_upstream</span> <span style="color:#e6db74">error</span> <span style="color:#e6db74">timeout</span> <span style="color:#e6db74">http_500</span>; <span style="color:#75715e"># 哪些错误认为是不成功的尝试（unsuccessful attempts），默认为 error timeout，这里的 http_500 是个例子，强烈不建议设置，否则可能导致异常的节点封禁。
</span><span style="color:#75715e"></span>            <span style="color:#f92672">proxy_next_upstream_tries</span> <span style="color:#ae81ff">2</span>;                <span style="color:#75715e"># 尝试下一个 server 的次数，默认为 0 不限制
</span><span style="color:#75715e"></span>            <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://demo</span>; 
        }
    }
    <span style="color:#75715e"># include /etc/nginx/conf.d/*.conf;
</span><span style="color:#75715e"></span>}</code></pre></div>
<h3 id="运行代码">运行代码</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 修改 proxy_next_upstream_tries 重复实验</span>

<span style="color:#75715e"># 测试</span>
go run ./server <span style="color:#ae81ff">8001</span>  <span style="color:#75715e"># 终端 1</span>
go run ./server <span style="color:#ae81ff">8002</span>  <span style="color:#75715e"># 终端 2</span>
go run ./server <span style="color:#ae81ff">8003</span>  <span style="color:#75715e"># 终端 3</span>
docker run --network host --name demo-nginx -v <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.23.2  <span style="color:#75715e"># 终端 4</span>
go run ./client <span style="color:#ae81ff">8000</span>  <span style="color:#75715e"># 终端 5</span>

<span style="color:#75715e"># 恢复现场</span>
<span style="color:#75715e"># ctrl + c 终端 1~5</span>
docker rm -f demo-nginx</code></pre></div>
<h3 id="输出分析">输出分析</h3>

<ul>
<li><p>修改 <code>proxy_next_upstream_tries</code> 为 1 时，Nginx 只请求了一个 server，且没有任何节点被封禁，所有请求，按照<strong>轮询负载均衡策略</strong>，获得到了 upstream server 返回的 500。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># go run ./server 8001
(8001) server counter: 1, client counter:1
(8001) server counter: 2, client counter:4
(8001) server counter: 3, client counter:7
(8001) server counter: 4, client counter:10
(8001) server counter: 5, client counter:13
(8001) server counter: 6, client counter:16
(8001) server counter: 7, client counter:19

# go run ./server 8002
(8002) server counter: 1, client counter:2
(8002) server counter: 2, client counter:5
(8002) server counter: 3, client counter:8
(8002) server counter: 4, client counter:11
(8002) server counter: 5, client counter:14
(8002) server counter: 6, client counter:17
(8002) server counter: 7, client counter:20

# go run ./server 8003
(8003) server counter: 1, client counter:3
(8003) server counter: 2, client counter:6
(8003) server counter: 3, client counter:9
(8003) server counter: 4, client counter:12
(8003) server counter: 5, client counter:15
(8003) server counter: 6, client counter:18</pre></div></li>

<li><p><code>proxy_next_upstream_tries</code> 为 0、2、3、4、5 &hellip; 时：</p>

<ul>
<li>第 1 个请求，请求了 <code>8001</code>，<code>8002</code>，封禁了 <code>8001</code>，返回 <code>8002</code> 响应的 500。</li>
<li>第 2 个请求，请求了 <code>8003</code>，<code>8002</code>，封禁了 <code>8003</code>，返回 <code>8002</code> 响应的 500。</li>
<li>第 3 个请求，请求了 <code>8002</code>，此时没有了可用的 server，返回 Nginx 的 502。</li>
<li>第 12 个请求，请求了 <code>8001</code>，<code>8001</code> 刚被解封，又被封禁，此时没有了可用的 server，返回 Nginx 的 502。</li>
<li>第 13 个请求，请求了 <code>8003</code>，<code>8003</code> 刚被解封，又被封禁，此时没有了可用的 server，返回 Nginx 的 502。</li>

<li><p>第 14 个请求，请求了 <code>8002</code>，<code>8002</code> 刚被解封，又被封禁，此时没有了可用的 server，返回 Nginx 的 502。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># go run ./server 8001
(8001) server counter: 1, client counter:1
(8001) server counter: 2, client counter:12

# go run ./server 8002
(8002) server counter: 1, client counter:1
(8002) server counter: 2, client counter:2
(8002) server counter: 3, client counter:3
(8002) server counter: 4, client counter:14

# go run ./server 8003
(8003) server counter: 1, client counter:2
(8003) server counter: 2, client counter:13</pre></div></li>
</ul></li>
</ul>
]]></description></item><item><title>容器核心技术（八） User Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-8-namespace-user/</link><pubDate>Sat, 15 Oct 2022 00:15:42 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-8-namespace-user/</guid><description type="html"><![CDATA[

<h2 id="背景知识">背景知识</h2>

<p>Linux 所有 Namespace 中最复杂的一部分，在了解 User Namespace 之前，最好前置阅读：<a href="/posts/linux-process-permission/">Linux 进程权限</a>。</p>

<h2 id="描述">描述</h2>

<p>User Namespace 实现了对进程权限的隔离，其特点如下所示：</p>

<ul>
<li>关系： User Namespace 之间存在父子关系（换句话说，User Namespace 在宏观上可以看成一棵树，内核限制最多 32 层）。</li>
<li>和进程的关系：每一个进程都会关联一个 User Namespace。</li>
<li>初始： 在 Linux 系统启动时，内核会创建一个，初始 User Namespace（换句话说，在 Linux 中的普通进程和该初始 User Namespace 中关联）。</li>
<li>创建： 使用 <code>CLONE_NEWUSER</code> 标志调用 <a href="https://man7.org/linux/man-pages/man2/clone.2.html"><code>clone(2) 系统调用</code></a> 会创建一个新的 User Namespace （当然 <a href="https://man7.org/linux/man-pages/man2/unshare.2.html"><code>unshare(2) 系统调用</code></a> 也可以，在此不多赘述）。指的特别说明的是，和其他 Namespace 不同，创建 User Namespace 不需要任何特权（换句话说，任意的用户的进程都可以创建一个新的 User Namespace），该 User Namespace 和其创建时所在 User Namespace 构成父子关系。</li>
<li>和 Capabilities 关系：

<ul>
<li>Capabilities 是按 User Namespace 隔离的。</li>
<li>新创建 User Namespace 的进程拥有当前内核所定义的全部的 Capabilities （具体而言，<code>cat /proc/新创建User Namespace的进程ID/status | grep Cap</code> 得到的输出是和 <code>cat /proc/1/status | grep Cap</code> 一样，其 <code>CapEff</code>、<code>CapPrm</code>、<code>CapBnd</code> 都是 <code>000001ffffffffff</code>）（需要特别注意的是，必须在执行 <code>execve(2)</code> 系统调用之前。由于 <code>unshare 命令</code> 会在创建名字空间后，执行了 execve，因此 <code>unshare</code> 命令创建的 shell 中执行 <code>cat /proc/$$/status | grep Cap</code>，看到只有 <code>CapBnd</code> 是 <code>000001ffffffffff</code>，其他均为 0）。</li>
<li>只有拥有该 User Namespace 的 <code>CAP_SYS_ADMIN</code> 能力才能通过 <a href="https://man7.org/linux/man-pages/man2/setns.2.html"><code>setns(2) 系统调用</code></a> 加入该 User Namespace，加入后该进程将拥有当前内核所定义的全部的 Capabilities。</li>
<li>在一个 User Namespace 中，<a href="https://man7.org/linux/man-pages/man2/execve.2.html"><code>execve(2) 系统调用</code></a> 会重新计算 Capabilities，参见：<a href="/posts/linux-process-permission/">Linux 进程权限</a>。</li>
<li>另一个 User Namespace 进程是否拥有某 User Namespace 的 Capabilities：

<ul>
<li>如果一个进程在该 User Namespace 中拥有的 Capabilities，则同样拥有子孙 User Namespace 对应的 Capabilities （比如初始 User Namespace 的 root 进程同样拥有其他所有 User Namespace 的所有 Capabilities）。</li>
<li>父 User Namespace 中创建该子 User Namespace 的有效用户 ID，会被设置为该子 User Namespace 的所有者，因此父 User Namespace 中具有同样有效用户 ID 的进程将具有该子 User Namespace 的全部的 Capabilities。</li>
</ul></li>
</ul></li>
<li>和其他 Namespace 的关系：

<ul>
<li>其他 Namespace 会和其创建时的 User Namespace 关联（所有者），这意味着，拥有该 User Namespace 对应的 Capabilities 的进程就有权限操纵这些其他 Namespace 的资源。</li>
<li>在使用 <a href="https://man7.org/linux/man-pages/man2/clone.2.html"><code>clone(2) 系统调用</code></a> 或 <a href="https://man7.org/linux/man-pages/man2/unshare.2.html"><code>unshare(2) 系统调用</code></a>  创建其他 Namespace 时，如果有 <code>CLONE_NEWUSER</code> 标志，则内核会先创建出 User Namespace，然后再创建其他的 Namespace。然后，这些其他的 Namespace 和这个刚刚创建的 User Namespace 关联。</li>
</ul></li>
<li>非初始 User Namespace 进程的说明和限制：

<ul>
<li>有些系统调用操作的资源并没有对应的 Namespace 进行隔离，因此只能在初始 User Namespace 中可以调用如：

<ul>
<li>更改系统时间 （<code>CAP_SYS_TIME</code>）</li>
<li>加载内核模块 （<code>CAP_SYS_MODULE</code>）</li>
<li>创建块设备 （<code>CAP_MKNOD</code>）</li>
</ul></li>
<li>当一个非初始 User Namespace 关联了一个 Mount Namespace 时，该进程即使拥有 <code>CAP_SYS_ADMIN</code> 也只允许 mount 如下文件系统：

<ul>
<li><code>/proc</code> (since Linux 3.8)</li>
<li><code>/sys</code> (since Linux 3.8)</li>
<li><code>devpts</code> (since Linux 3.9)</li>
<li><code>tmpfs(5)</code> (since Linux 3.9)</li>
<li><code>ramfs</code> (since Linux 3.9)</li>
<li><code>mqueue</code> (since Linux 3.9)</li>
<li><code>bpf</code> (since Linux 4.4)</li>
<li><code>overlayfs</code> (since Linux 5.11)</li>
</ul></li>
<li>当一个非初始 User Namespace 关联了一个 Cgroup Namespace 时，该进程拥有 <code>CAP_SYS_ADMIN</code>，自 Linux 4.6 起，将允许 mount Cgroup v1 和 v2 的文件系统。</li>
<li>当一个非初始 User Namespace 关联了一个 PID Namespace 时，该进程拥有 <code>CAP_SYS_ADMIN</code>，自 Linux 3.8 起，将允许 mount /proc 文件系统。</li>
<li>注意，mount 基于块的文件系统时，只允许拥有 <code>CAP_SYS_ADMIN</code> 的初始 User Namespace 操作。</li>
</ul></li>
<li>User Namespace 之间的 ID 映射。

<ul>
<li>新创建的 User Namespace 需要通过向 <code>/proc/[pid]/uid_map</code> 和 <code>/proc/[pid]/gid_map</code> 文件写入配置才能使用 <a href="https://man7.org/linux/man-pages/man2/setuid.2.html"><code>setuid(2)</code></a>、 <a href="https://man7.org/linux/man-pages/man2/setgid.2.html"><code>setgid(2)</code></a> 等与 id 相关的系统调用。顾名思义 <code>gid_map</code> 和 <code>uid_map</code> 时类似的，因此只介绍 <code>uid_map</code>。</li>
<li><code>uid_map</code> 的格式为：

<ul>
<li>每行包含三个用空格分隔的 32 位无符号整数，分别为（<code>to-user-id-start from-user-id-start range</code>）：

<ul>
<li><code>to-user-id-start</code> 如果当前文件为 <code>/proc/[pid]/uid_map</code>，则该值为 <code>[pid]</code> 所在 User Namespace 的用户 ID</li>
<li><code>from-user-id-start</code> 取决于读取 <code>/proc/[pid]/uid_map</code> 进程所在的 User Namespace（不同 User Namespace 进程读 <code>uid_map</code> 看到的第二列的内容是不一样的。）。

<ul>
<li>如果和 <code>[pid]</code> 所在的 User Namespace 相同，则 <code>from-user-id-start</code> 表示映射到父 User Namespace 的用户 ID。</li>
<li>如果和 <code>[pid]</code> 所在的 User Namespace 不同，则 <code>from-user-id-start</code> 表示映射读写 <code>/proc/[pid]/uid_map</code> 进程所在的 User Namespace 的用户 ID。</li>
</ul></li>
<li>range 表示映射的范围，必须大于 0。</li>
</ul></li>
</ul></li>
<li><code>uid_map</code> 写入说明

<ul>
<li>只能写入一次，也就是说一旦确定则不能修改，刚创建时该文件是空的。</li>
<li>写入必须以换行符结尾。包含多行，Linux 4.14 之前最多 5 行，Linux 4.15 起，最多 340 行，多行中的映射范围不允许有重叠，最少写入 1 行。</li>
<li>写入的进程必须拥有该文件 User Namespace 的 <code>CAP_SETUID</code> (<code>CAP_SETGID</code>) 的 capability 且 写入的进程的 User Namespace 必须是当前 User Namespace 或者 父 User Namespace。</li>
<li>写入的映射的用户 ID（组 ID）必须依次在父用户命名空间。</li>
<li>如果想映射父进程的 0 (即 <code>xxx 0 xxx</code>)，除了满足上述要求外：还要求（Since Linux 5.12，解决一个安全漏洞）：

<ul>
<li>如果是该 User Namespace 的进程写入，要求创建该 User Namespace 时的父进程必须有的 <code>CAP_SETFCAP</code> capability。</li>
<li>如果是该 User Namespace 的父 User Namespace 的进程写入，要求该父进程必须有的 <code>CAP_SETFCAP</code> capability。</li>
</ul></li>
<li>以下两个 case 需要特别说明：

<ul>
<li>当写入进程有父 User Namespace 的 <code>CAP_SETUID</code> (<code>CAP_SETGID</code>) capability 时，则没有其他限制（按照如上规则。此情况，只有父进程写入常见场景才满足）。</li>
<li>否则，存在如下限制（子进程写入场景）：

<ul>
<li>写入进程和创建该 User Namespace 的父进程有相同 effective user ID （EUID），且写入的内容必须包含一个映射到父进程的 EUID 的行。</li>
<li>写入在 gid_map 之前，必须通过写入 <code>&quot;deny&quot;</code> 到 <code>/proc/[pid]/setgroups</code> 文件，来禁用 <a href="https://man7.org/linux/man-pages/man2/setgroups.2.html"><code>setgroups(2)</code></a> 系统调用。</li>
</ul></li>
</ul></li>
<li>综上所述，推荐的模式是，父进程创建完 User Namespace 后，在父进程中写入 id map，然后通过进程通讯技术（如 pipe）通知位于新的 User Namespace 中的子进程。</li>
</ul></li>
<li>初始 User Namespace 没有父 User Namespace，但为了一致 <code>cat /proc/1/uid_map</code> 返回 <code>0          0 4294967295</code> （<code>4294967295 = 2^32-1</code>，<code>2^32</code> 即 <code>-1</code> 不被映射，原因是在一些系统调用中表示无用户）</li>
<li><code>/proc/[pid]/setgroups</code>

<ul>
<li>通过写入 <code>&quot;deny&quot;</code> 到 <code>/proc/[pid]/setgroups</code> 来禁用 <a href="https://man7.org/linux/man-pages/man2/setgroups.2.html"><code>setgroups(2)</code></a> 系统调用 （加入自：Linux 3.19，解决安全问题）。</li>
<li><code>/proc/[pid]/setgroups</code> 的默认值：

<ul>
<li>初始 User Namespace 其默认值为 <code>&quot;allow&quot;</code>。</li>
<li>子 User Namespace 的默认值会继承父 User Namespace 的值。如果继承来的默认值为 <code>&quot;deny&quot;</code>，则无法再设置为 <code>&quot;allow&quot;</code>。</li>
</ul></li>
<li><code>/proc/[pid]/setgroups</code> 可以在写入 <code>/proc/[pid]/gid_map</code> 前写入多次。</li>
</ul></li>
<li><code>uid_map</code> 的作用

<ul>
<li>进程身份：获取进程身份（如 <a href="https://man7.org/linux/man-pages/man2/getuid.2.html"><code>getuid(2)</code></a>、 <a href="https://man7.org/linux/man-pages/man2/getgid.2.html"><code>getgid(2)</code></a>） 和 获取文件信息（如 <a href="https://man7.org/linux/man-pages/man2/stat.2.html"><code>stat(2)</code></a>） 的系统调用获取到的 ID 都是映射到当前进程所在 User Namespace 的 ID（根据 uid_map 配置的字段进行映射。）。</li>
<li>文件访问：当一个进程访问一个文件时，需要将该进程 id 映射到初始 User Namespace 中来确定是否有权限。当通过 <a href="https://man7.org/linux/man-pages/man2/stat.2.html"><code>stat(2)</code></a> 查看该文件的所有者 ID 时，则映射到当前 User Namespace。</li>
<li>文件特权操作：除了 User Namespace 的进程需要拥有 <code>CAP_CHOWN</code>, <code>CAP_DAC_OVERRIDE</code>, <code>CAP_DAC_READ_SEARCH</code>, <code>CAP_FOWNER</code>, <code>CAP_FSETID</code> 这些权限外，还需要操作的文件的所属用户和所属组都必须已经映射到当前 User Namespace 中了（<code>CAP_FOWNER</code> 只要求所属用户映射即可，所属组可以不映射）。</li>
<li>执行 Set-user-ID 或 set-group-ID 程序文件：如果该文件已经被映射，则以映射后的 User/Group ID 为准，如果没映射，则忽略 Set-user-ID 或 set-group-ID 位（即不改变 euid/egid，类似于 <a href="https://man7.org/linux/man-pages/man2/mount.2.html">mount(2)</a> 使用了 <code>MS_NOSUID</code> 标志）。</li>
<li>Unix 套接字也会进行映射，参见 <a href="https://man7.org/linux/man-pages/man7/unix.7.html">unix(7)</a> 的 SCM_CREDENTIALS。</li>
<li>一个例子，父进程用户 id 是 1000，创建的当前进程绑定了一个新的 User Namespace，且配置的 <code>/proc/self/uid_map</code> 的内容为 <code>0 1000 500</code>，则：

<ul>
<li>当前进程调用 <a href="https://man7.org/linux/man-pages/man2/getuid.2.html"><code>getuid(2)</code></a> 返回 <code>0</code></li>
<li>当前进程 对父进程的家目录调用 <a href="https://man7.org/linux/man-pages/man2/stat.2.html"><code>stat(2)</code></a> 看到的文件 owner 也为 <code>0</code> 即 root。</li>
<li>当前进程可以通过 <code>chown</code> 修改父级成家目录文件的所有者（TODO 待确认）。</li>
</ul></li>
</ul></li>
<li>未映射的 ID

<ul>
<li>在各种情况（如 <code>stat(2)</code>、<code>getuid(2)</code>），均返回为溢出用户/组，定义在 <code>/proc/sys/kernel/overflowuid</code>、<code>/proc/sys/kernel/overflowgid</code> 一般为 <code>65534</code>。</li>
<li>在某些情况，进程没有映射的其他 User Namespace 的进程，读 <code>uid_map</code>、<code>gid_map</code> 文件，第二个字段将返回 <code>4294967295</code> （<code>-1</code>）。</li>
</ul></li>
</ul></li>
</ul>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<ul>
<li>测试程序逻辑如下：

<ul>
<li>进程 A：测试程序所在的进程为进程 A。

<ul>
<li>观察自己的 Capabilities。</li>
<li>创建一个测试文件 testFile。</li>
<li>使用 <code>SIGCHLD | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS</code> 标志，通过 <code>clone(2)</code> 系统调用创建一个进程 B。</li>
<li>写入 <code>0 0 4294967295</code> 到进程 B 的 <code>uid_map</code> 和 <code>gid_map</code>，并通过 pipe 通知进程 B。</li>
<li>等待进程 B 退出。</li>
<li>删除测试文件 testFile。</li>
</ul></li>
<li>进程 B 引导阶段：即在执行 exec 之前。

<ul>
<li>等待进程 B 写入 <code>uid_map</code> 和 <code>gid_map</code> 的完成通知。</li>
<li>观察自己的 Capabilities。</li>
<li>尝试通过 chown(2) 系统调用，修改 testFile 文件的 Owner。</li>
<li>重新挂载 <code>/proc</code>。</li>
<li>通过 <code>execve(2) 系统调用</code> 在进程 B 执行一段 shell 测试程序：

<ul>
<li>观察自己的 Capabilities。</li>
<li>观察自己身份。</li>
<li>执行 <code>ps -ef</code></li>
<li>观察 <code>~</code> 和 <code>/</code> 目录。</li>
<li>修改并查看测试文件 testFile 文件。</li>
<li>通过 <code>sudo chown</code> 修改 testFile 文件的 owner</li>
</ul></li>
</ul></li>
</ul></li>
<li>编译后，通过 <code>sudo setcap CAP_SETUID,CAP_SETGID,CAP_SETFCAP,CAP_DAC_OVERRIDE+ep a.out</code> 给程序添加相关 Caps。</li>
<li>使用普通用户（拥有免密 sudo 权限）执行如上测试程序。</li>
</ul>

<h3 id="c-源码">C 源码</h3>

<p>由于 <code>execve(2) 系统调用</code> 会改变进程的 Capabilities，因此测试程序只能用 C 语言编写。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">// sudo apt install -y libcap2-bin
</span><span style="color:#75715e">// gcc src/c/01-namespace/06-user/main.c &amp;&amp; sudo setcap CAP_SETUID,CAP_SETGID,CAP_SETFCAP,CAP_DAC_OVERRIDE+ep a.out  &amp;&amp; ./a.out
</span><span style="color:#75715e">// sudo getcap a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE	     </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;    // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt;   // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;    // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;	   // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;	   // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For execv(3), sleep(3), read(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;	   // For exit(3), system(3), free(3), realloc(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;errno.h&gt;	   // For errno(3), strerror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;	   // For strtok(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;fcntl.h&gt;     // For open(2)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">							   } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>testFileName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;testFile&#34;</span>;

<span style="color:#75715e">// https://stackoverflow.com/a/44894946
</span><span style="color:#75715e"></span><span style="color:#75715e">/* Size of each input chunk to be
</span><span style="color:#75715e">   read and allocate for. */</span>

<span style="color:#75715e">#define  READALL_CHUNK  4096
</span><span style="color:#75715e">#define  READALL_OK          0  </span><span style="color:#75715e">/* Success */</span><span style="color:#75715e">
</span><span style="color:#75715e">#define  READALL_INVALID    -1  </span><span style="color:#75715e">/* Invalid parameters */</span><span style="color:#75715e">
</span><span style="color:#75715e">#define  READALL_ERROR      -2  </span><span style="color:#75715e">/* Stream error */</span><span style="color:#75715e">
</span><span style="color:#75715e">#define  READALL_TOOMUCH    -3  </span><span style="color:#75715e">/* Too much input */</span><span style="color:#75715e">
</span><span style="color:#75715e">#define  READALL_NOMEM      -4  </span><span style="color:#75715e">/* Out of memory */</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">/* This function returns one of the READALL_ constants above.
</span><span style="color:#75715e">   If the return value is zero == READALL_OK, then:
</span><span style="color:#75715e">	 (*dataptr) points to a dynamically allocated buffer, with
</span><span style="color:#75715e">	 (*sizeptr) chars read from the file.
</span><span style="color:#75715e">	 The buffer is allocated for one extra char, which is NUL,
</span><span style="color:#75715e">	 and automatically appended after the data.
</span><span style="color:#75715e">   Initial values of (*dataptr) and (*sizeptr) are ignored.
</span><span style="color:#75715e">*/</span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">readall</span>(FILE <span style="color:#f92672">*</span>in, <span style="color:#66d9ef">char</span> <span style="color:#f92672">**</span>dataptr, size_t <span style="color:#f92672">*</span>sizeptr)
{
	<span style="color:#66d9ef">char</span>  <span style="color:#f92672">*</span>data <span style="color:#f92672">=</span> NULL, <span style="color:#f92672">*</span>temp;
	size_t size <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
	size_t used <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
	size_t n;

	<span style="color:#75715e">/* None of the parameters can be NULL. */</span>
	<span style="color:#66d9ef">if</span> (in <span style="color:#f92672">==</span> NULL <span style="color:#f92672">||</span> dataptr <span style="color:#f92672">==</span> NULL <span style="color:#f92672">||</span> sizeptr <span style="color:#f92672">==</span> NULL)
		<span style="color:#66d9ef">return</span> READALL_INVALID;

	<span style="color:#75715e">/* A read error already occurred? */</span>
	<span style="color:#66d9ef">if</span> (ferror(in))
		<span style="color:#66d9ef">return</span> READALL_ERROR;

	<span style="color:#66d9ef">while</span> (<span style="color:#ae81ff">1</span>) {

		<span style="color:#66d9ef">if</span> (used <span style="color:#f92672">+</span> READALL_CHUNK <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">&gt;</span> size) {
			size <span style="color:#f92672">=</span> used <span style="color:#f92672">+</span> READALL_CHUNK <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>;

			<span style="color:#75715e">/* Overflow check. Some ANSI C compilers
</span><span style="color:#75715e">			   may optimize this away, though. */</span>
			<span style="color:#66d9ef">if</span> (size <span style="color:#f92672">&lt;=</span> used) {
				free(data);
				<span style="color:#66d9ef">return</span> READALL_TOOMUCH;
			}

			temp <span style="color:#f92672">=</span> realloc(data, size);
			<span style="color:#66d9ef">if</span> (temp <span style="color:#f92672">==</span> NULL) {
				free(data);
				<span style="color:#66d9ef">return</span> READALL_NOMEM;
			}
			data <span style="color:#f92672">=</span> temp;
		}

		n <span style="color:#f92672">=</span> fread(data <span style="color:#f92672">+</span> used, <span style="color:#ae81ff">1</span>, READALL_CHUNK, in);
		<span style="color:#66d9ef">if</span> (n <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
			<span style="color:#66d9ef">break</span>;

		used <span style="color:#f92672">+=</span> n;
	}

	<span style="color:#66d9ef">if</span> (ferror(in)) {
		free(data);
		<span style="color:#66d9ef">return</span> READALL_ERROR;
	}

	temp <span style="color:#f92672">=</span> realloc(data, used <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>);
	<span style="color:#66d9ef">if</span> (temp <span style="color:#f92672">==</span> NULL) {
		free(data);
		<span style="color:#66d9ef">return</span> READALL_NOMEM;
	}
	data <span style="color:#f92672">=</span> temp;
	data[used] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;\0&#39;</span>;

	<span style="color:#f92672">*</span>dataptr <span style="color:#f92672">=</span> data;
	<span style="color:#f92672">*</span>sizeptr <span style="color:#f92672">=</span> used;

	<span style="color:#66d9ef">return</span> READALL_OK;
}



<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_caps</span>() {
	FILE <span style="color:#f92672">*</span>f <span style="color:#f92672">=</span> fopen(<span style="color:#e6db74">&#34;/proc/self/status&#34;</span>, <span style="color:#e6db74">&#34;r&#34;</span>);
	<span style="color:#66d9ef">if</span> (f <span style="color:#f92672">==</span> NULL)
		errExit(<span style="color:#e6db74">&#34;fopen&#34;</span>);
	<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>buf;
	size_t len;
	<span style="color:#66d9ef">if</span> (readall(f, <span style="color:#f92672">&amp;</span>buf, <span style="color:#f92672">&amp;</span>len) <span style="color:#f92672">!=</span> READALL_OK)
		errExit(<span style="color:#e6db74">&#34;readall&#34;</span>);
	fclose(f);

	<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>delimiter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\r\n</span><span style="color:#e6db74">&#34;</span>;
	<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>line <span style="color:#f92672">=</span> strtok(buf, delimiter);
	<span style="color:#66d9ef">while</span> (line <span style="color:#f92672">!=</span> NULL) {
		<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>pre <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Cap&#34;</span>;
		<span style="color:#66d9ef">if</span> (strncmp(pre, line, strlen(pre)) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
			printf(<span style="color:#e6db74">&#34;%s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, line);
		line <span style="color:#f92672">=</span> strtok(NULL, delimiter);
	}
}

<span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span>
<span style="color:#a6e22e">update_map</span>(<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>mapping, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>map_file)
{
	<span style="color:#66d9ef">int</span> fd, j;
	size_t map_len <span style="color:#f92672">=</span> map_len <span style="color:#f92672">=</span> strlen(mapping);

	fd <span style="color:#f92672">=</span> open(map_file, O_RDWR);
	<span style="color:#66d9ef">if</span> (fd <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
	{
		fprintf(stderr, <span style="color:#e6db74">&#34;open %s: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, map_file, strerror(errno));
		exit(EXIT_FAILURE);
	}
	<span style="color:#66d9ef">if</span> (write(fd, mapping, map_len) <span style="color:#f92672">!=</span> map_len)
	{
		fprintf(stderr, <span style="color:#e6db74">&#34;write %s: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, map_file, strerror(errno));
		exit(EXIT_FAILURE);
	}
	close(fd);
}

<span style="color:#66d9ef">struct</span> child_args {
	<span style="color:#66d9ef">int</span> pipe_fd[<span style="color:#ae81ff">2</span>]; <span style="color:#75715e">/* Pipe used to synchronize parent and child */</span>
};

<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> test_scripts[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-c&#34;</span>,
	<span style="color:#e6db74">&#34;echo &#39;&gt;&gt;&gt;&#39; 01.当前进程ID &amp;&amp; echo $$ &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 02.查看当前进程 Caps &amp;&amp; cat /proc/self/status | grep Cap &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 03.当前进程身份 &amp;&amp; id &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 04.执行 ps -ef &amp;&amp; ps -ef &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 05.执行 ls -al / &amp;&amp; ls -al / &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 06.执行 ls -al &amp;&amp; ls -al &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 07.执行 ls -al &amp;&amp; ls -al &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 08.写入 abc 到 testFile 并查看 &amp;&amp; echo &#39;abc&#39; &gt; testFile &amp;&amp; cat testFile &amp;&amp; echo \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;&gt;&gt;&gt;&#39; 09.sudo 更改 testFile owner 为 root &amp;&amp; sudo chown root:root testFile &amp;&amp; ls -al testFile &amp;&amp; echo \
</span><span style="color:#e6db74">	&#34;</span>,
	NULL};

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args) {
	<span style="color:#66d9ef">struct</span> child_args <span style="color:#f92672">*</span>typedArgs <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> child_args <span style="color:#f92672">*</span>)args;

	<span style="color:#66d9ef">char</span> ch;
	close(typedArgs<span style="color:#f92672">-&gt;</span>pipe_fd[<span style="color:#ae81ff">1</span>]);
	<span style="color:#66d9ef">if</span> (read(typedArgs<span style="color:#f92672">-&gt;</span>pipe_fd[<span style="color:#ae81ff">0</span>], <span style="color:#f92672">&amp;</span>ch, <span style="color:#ae81ff">1</span>) <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>) {
        fprintf(stderr, <span style="color:#e6db74">&#34;Failure in child: read from pipe returned != 0</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
        exit(EXIT_FAILURE);
    }

	printf(<span style="color:#e6db74">&#34;时序 05: 打印进程 B 的 Caps、 进程 ID 和 用户 ID</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	print_caps();
	printf(<span style="color:#e6db74">&#34;pid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, getpid());
	printf(<span style="color:#e6db74">&#34;uid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, getuid());
	printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);

	printf(<span style="color:#e6db74">&#34;时序 06: 尝试更改测试文件 owner</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	<span style="color:#66d9ef">if</span> (chown(testFileName, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
		errExit(<span style="color:#e6db74">&#34;chown-root&#34;</span>);
	<span style="color:#66d9ef">if</span> (chown(testFileName, getuid(), getuid()) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
		errExit(<span style="color:#e6db74">&#34;chown-uid&#34;</span>);
	printf(<span style="color:#e6db74">&#34;成功</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);

	printf(<span style="color:#e6db74">&#34;时序 07: 重新挂载 /proc</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);
	<span style="color:#66d9ef">if</span> (mount(NULL, <span style="color:#e6db74">&#34;/&#34;</span>, NULL, MS_SLAVE <span style="color:#f92672">|</span> MS_REC, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#75715e">// 阻止挂载事件传播到其他 Mount Namespace
</span><span style="color:#75715e"></span>		errExit(<span style="color:#e6db74">&#34;mount-MS_SLAVE&#34;</span>);
	<span style="color:#66d9ef">if</span> (mount(<span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#e6db74">&#34;/proc&#34;</span>, <span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#ae81ff">0</span>, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-proc&#34;</span>);

	printf(<span style="color:#e6db74">&#34;时序 08: 执行测试脚本</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	execv(test_scripts[<span style="color:#ae81ff">0</span>], test_scripts);

	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[]) {

	printf(<span style="color:#e6db74">&#34;时序 01: 打印进程 A 的 Caps 和 进程 ID</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	print_caps();
	printf(<span style="color:#e6db74">&#34;pid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, getpid());
	printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);

	printf(<span style="color:#e6db74">&#34;时序 02: 创建一个测试文件</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);
	<span style="color:#66d9ef">int</span> f <span style="color:#f92672">=</span> open(testFileName, O_WRONLY <span style="color:#f92672">|</span> O_CREAT <span style="color:#f92672">|</span> O_TRUNC, <span style="color:#ae81ff">0644</span>);
	<span style="color:#66d9ef">if</span> (f <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
		errExit(<span style="color:#e6db74">&#34;open-testFile&#34;</span>);

	printf(<span style="color:#e6db74">&#34;时序 03: 创建一个新进程 B，这个进程位于新的 User、Mount、PID Namespace</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	<span style="color:#66d9ef">struct</span> child_args args;
	<span style="color:#66d9ef">if</span> ( pipe(args.pipe_fd) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;pipe&#34;</span>);
	<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
							 PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
							 MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
							 <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
		errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
	pid_t pid <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWUSER <span style="color:#f92672">|</span> CLONE_NEWPID <span style="color:#f92672">|</span> CLONE_NEWNS, <span style="color:#f92672">&amp;</span>args);
	<span style="color:#66d9ef">if</span> (pid <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
		errExit(<span style="color:#e6db74">&#34;clone&#34;</span>);
	printf(<span style="color:#e6db74">&#34;pid: %d</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>, getpid());

	printf(<span style="color:#e6db74">&#34;时序 04: 配置子进程的 id map</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);

	<span style="color:#66d9ef">char</span> map_path[<span style="color:#ae81ff">128</span>];
	sprintf(map_path, <span style="color:#e6db74">&#34;/proc/%d/uid_map&#34;</span>, pid);
	update_map(<span style="color:#e6db74">&#34;0 0 4294967295&#34;</span>, map_path);
	sprintf(map_path, <span style="color:#e6db74">&#34;/proc/%d/gid_map&#34;</span>, pid);
	update_map(<span style="color:#e6db74">&#34;0 0 4294967295&#34;</span>, map_path);
	close(args.pipe_fd[<span style="color:#ae81ff">1</span>]);

	<span style="color:#66d9ef">if</span> (waitpid(pid, NULL, <span style="color:#ae81ff">0</span>) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
		errExit(<span style="color:#e6db74">&#34;pid&#34;</span>);
	printf(<span style="color:#e6db74">&#34;时序 09: 子进程 B 退出，并清理现场</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);
	unlink(testFileName);
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<h3 id="实验输出">实验输出</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">时序 01: 打印进程 A 的 Caps 和 进程 ID
CapInh: 0000000000000000
CapPrm: 00000000800000c2
CapEff: 00000000800000c2
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
pid: 17255

时序 02: 创建一个测试文件

时序 03: 创建一个新进程 B，这个进程位于新的 User、Mount、PID Namespace
pid: 17255

时序 04: 配置子进程的 id map

时序 05: 打印进程 B 的 Caps、 进程 ID 和 用户 ID
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
pid: 1
uid: 1000

时序 06: 尝试更改测试文件 owner
成功

时序 07: 重新挂载 /proc

时序 08: 执行测试脚本
&gt;&gt;&gt; 01.当前进程ID
1

&gt;&gt;&gt; 02.查看当前进程 Caps
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

&gt;&gt;&gt; 03.当前进程身份
用户id=1000(rectcircle) 组id=1000(rectcircle) 组=1000(rectcircle),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev),112(bluetooth)

&gt;&gt;&gt; 04.执行 ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
rectcir+       1       0  0 15:12 pts/4    00:00:00 /bin/bash -c echo &#39;&gt;&gt;&gt;&#39; 01.当前进程ID
rectcir+       5       1  0 15:12 pts/4    00:00:00 ps -ef

&gt;&gt;&gt; 05.执行 ls -al /
总用量 68
drwxr-xr-x  18 root root  4096  2月 13  2022 .
drwxr-xr-x  18 root root  4096  2月 13  2022 ..
lrwxrwxrwx   1 root root     7  2月 13  2022 bin -&gt; usr/bin
drwxr-xr-x   3 root root  4096  2月 13  2022 boot
drwxr-xr-x  17 root root  3140 10月 13 19:49 dev
drwxr-xr-x  79 root root  4096 10月 15 11:36 etc
drwxr-xr-x   3 root root  4096  2月 13  2022 home
lrwxrwxrwx   1 root root    31  2月 13  2022 initrd.img -&gt; boot/initrd.img-5.10.0-11-amd64
lrwxrwxrwx   1 root root    31  2月 13  2022 initrd.img.old -&gt; boot/initrd.img-5.10.0-10-amd64
lrwxrwxrwx   1 root root     7  2月 13  2022 lib -&gt; usr/lib
lrwxrwxrwx   1 root root     9  2月 13  2022 lib32 -&gt; usr/lib32
lrwxrwxrwx   1 root root     9  2月 13  2022 lib64 -&gt; usr/lib64
lrwxrwxrwx   1 root root    10  2月 13  2022 libx32 -&gt; usr/libx32
drwx------   2 root root 16384  2月 13  2022 lost+found
drwxr-xr-x   3 root root  4096  2月 13  2022 media
drwxr-xr-x   2 root root  4096  2月 13  2022 mnt
drwxr-xr-x   2 root root  4096  2月 13  2022 opt
dr-xr-xr-x 155 root root     0 10月 15 15:12 proc
drwx------   5 root root  4096  9月 18 23:23 root
drwxr-xr-x  17 root root   580 10月 15 00:01 run
lrwxrwxrwx   1 root root     8  2月 13  2022 sbin -&gt; usr/sbin
drwxr-xr-x   2 root root  4096  2月 13  2022 srv
dr-xr-xr-x  13 root root     0 10月 13 19:49 sys
drwxrwxrwt  14 root root  4096 10月 15 15:12 tmp
drwxr-xr-x  14 root root  4096  2月 13  2022 usr
drwxr-xr-x  12 root root  4096  3月 15  2022 var
lrwxrwxrwx   1 root root    28  2月 13  2022 vmlinuz -&gt; boot/vmlinuz-5.10.0-11-amd64
lrwxrwxrwx   1 root root    28  2月 13  2022 vmlinuz.old -&gt; boot/vmlinuz-5.10.0-10-amd64

&gt;&gt;&gt; 06.执行 ls -al
总用量 60
drwxr-xr-x  5 rectcircle rectcircle 12288 10月 15 15:12 .
drwxr-xr-x 14 rectcircle rectcircle  4096 10月 15 15:00 ..
-rwxr-xr-x  1 rectcircle rectcircle 18520 10月 15 15:12 a.out
drwxr-xr-x  6 rectcircle rectcircle  4096  3月  8  2022 data
-rw-r--r--  1 rectcircle rectcircle   259  9月 18 23:18 go.mod
-rw-r--r--  1 rectcircle rectcircle   843  9月 18 23:18 go.sum
-rw-r--r--  1 rectcircle rectcircle   192  2月 23  2022 README.md
drwxr-xr-x  5 rectcircle rectcircle  4096  2月 27  2022 src
-rw-r--r--  1 rectcircle rectcircle     0 10月 15 15:12 testFile
drwxr-xr-x  2 rectcircle rectcircle  4096 10月 13 21:46 .vscode

&gt;&gt;&gt; 07.执行 ls -al
总用量 60
drwxr-xr-x  5 rectcircle rectcircle 12288 10月 15 15:12 .
drwxr-xr-x 14 rectcircle rectcircle  4096 10月 15 15:00 ..
-rwxr-xr-x  1 rectcircle rectcircle 18520 10月 15 15:12 a.out
drwxr-xr-x  6 rectcircle rectcircle  4096  3月  8  2022 data
-rw-r--r--  1 rectcircle rectcircle   259  9月 18 23:18 go.mod
-rw-r--r--  1 rectcircle rectcircle   843  9月 18 23:18 go.sum
-rw-r--r--  1 rectcircle rectcircle   192  2月 23  2022 README.md
drwxr-xr-x  5 rectcircle rectcircle  4096  2月 27  2022 src
-rw-r--r--  1 rectcircle rectcircle     0 10月 15 15:12 testFile
drwxr-xr-x  2 rectcircle rectcircle  4096 10月 13 21:46 .vscode

&gt;&gt;&gt; 08.写入 abc 到 testFile 并查看
abc

&gt;&gt;&gt; 09.sudo 更改 testFile owner 为 root
-rw-r--r-- 1 root root 4 10月 15 15:12 testFile

时序 09: 子进程 B 退出，并清理现场</pre></div>
<h3 id="proc-问题">/proc 问题</h3>

<p>从文章 <a href="/posts/linux-process-permission/#实例：容器进程权限限制">Linux 进程权限</a> 可以得知，docker 默认是有 <code>CAP_SETUID,CAP_SETGID,CAP_SETFCAP,CAP_DAC_OVERRIDE</code> 这四个权限。似乎上述代码可以在 Docker/k8s 中运行。但是实测，这个程序并不能在 默认的 Docker/k8s 容器中运行。</p>

<p>在 Linux 虚拟机中执行 <code>mount | grep /proc</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=30,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=10609)</pre></div>
<p>在 docker 容器中执行 <code>mount | grep /proc</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
proc on /proc/bus type proc (ro,relatime)
proc on /proc/fs type proc (ro,relatime)
proc on /proc/irq type proc (ro,relatime)
proc on /proc/sys type proc (ro,relatime)
proc on /proc/sysrq-trigger type proc (ro,relatime)
tmpfs on /proc/asound type tmpfs (ro,relatime)
tmpfs on /proc/acpi type tmpfs (ro,relatime)
tmpfs on /proc/kcore type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/keys type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/timer_list type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/sched_debug type tmpfs (rw,nosuid,size=65536k,mode=755)</pre></div>
<p>通过查阅 docker 代码可以看出，这是有 <code>HostConfig</code> 的 <a href="https://github.com/moby/moby/blob/8d193d81af9cbbe800475d4bb8c529d67a6d8f14/api/types/container/host_config.go#L458"><code>MaskedPaths</code></a> 和 <a href="https://github.com/moby/moby/blob/8d193d81af9cbbe800475d4bb8c529d67a6d8f14/api/types/container/host_config.go#L461"><code>ReadonlyPaths</code></a> 字段配置的，默认值参见：<a href="https://github.com/moby/moby/blob/968a0bcd636b3720d2178d5dfed691c00c68e4a1/oci/defaults.go#L87">docker 源码</a>。更多参见： runc 对应的是<a href="https://github.com/opencontainers/runc/blob/bd69483df53570df10040b6b21f4cf798b9f6d3d/libcontainer/rootfs_linux.go#L1022">实现源码</a>。</p>

<p>通过 runc 的 <a href="https://github.com/opencontainers/runc/issues/1658">Issue</a> 可以看出，这是 <a href="https://github.com/opencontainers/runc/issues/1658#issuecomment-375750981">Linux 内核的一个限制</a>：当 <code>/proc</code> 存在被遮蔽的目录时，mount proc 将报错。因此，上面代码的 <code>mount(&quot;proc&quot;, &quot;/proc&quot;, &quot;proc&quot;, 0, NULL)</code> 行将报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Operation not permitted</pre></div>
<p>有人提了一个 <a href="https://lists.linuxfoundation.org/pipermail/containers/2018-April/038840.html">PR</a> 其修复该问题，但是并未合入。</p>

<p>如果需要解决该问题，有如下两种方案：</p>

<ul>
<li>开启特权模式。</li>
<li>关闭 <code>/proc</code> 的遮蔽（未测试）：

<ul>
<li>k8s： <code>spec.containers[*].securityContext.procMount: &quot;Unmasked&quot;</code> (安装集群时，需配置开启该特性门 <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/">https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/</a>)</li>
<li>docker：需要配置 <code>MaskedPaths</code>，只能通过客户端配置， cli 不支持，参见：<a href="https://github.com/moby/moby/blob/8d193d81af9cbbe800475d4bb8c529d67a6d8f14/api/types/container/host_config.go#L458">源码</a>。</li>
</ul></li>
</ul>

<p>上述方式都不是我们正常的容器使用方式，带来了额外的复杂度。因此：在默认配置的容器中创建 User + Mount + PID Namespace 的进程来进行一定的隔离是不可能的。</p>

<h2 id="rootless">Rootless</h2>

<p>默认情况下 Docker 和 k8s 并没有使用 User Namespace。</p>

<p>在容器技术中，rootless 容器才会使用 User Namespace （如： <a href="https://docs.docker.com.zh.xy2401.com/engine/security/rootless/">Docker rootless 模式</a>），其整体实现原理类似上述过程。</p>

<p>目前：Rootless 容器在挂载 /proc、网络和 OverlayFS 上存在一定的限制。</p>

<p>更多关于 rootless 容器，参见： <a href="https://rootlesscontaine.rs/">https://rootlesscontaine.rs/</a> 。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://man7.org/linux/man-pages/man7/user_namespaces.7.html">user_namespaces(7) — Linux manual page</a></li>
<li><a href="https://lwn.net/Articles/539940/">实验源码参考：userns_child_exec.c</a></li>
<li><a href="https://blog.lucode.net/linux/intro-Linux-namespace-6.html">User Namespace 子进程写入 id map 文件的例子：Linux namespace 简介 part 6 - USER</a></li>
<li><a href="https://www.361shipin.com/blog/1554013022308007936">包含一系列 lwn.net 示例：Namespaces in operation part 5: User namespaces</a></li>
<li><a href="https://rootlesscontaine.rs/">rootless container</a></li>
<li><a href="https://docs.docker.com/engine/security/rootless/">docker rootless</a></li>
</ul>
]]></description></item><item><title>Linux 进程权限</title><link>https://www.rectcircle.cn/posts/linux-process-permission/</link><pubDate>Sat, 24 Sep 2022 19:25:59 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-process-permission/</guid><description type="html"><![CDATA[

<h2 id="实验代码">实验代码</h2>

<p>github: <a href="https://github.com/rectcircle/learn-linux-process-permission">rectcircle/learn-linux-process-permission</a>。</p>

<h2 id="进程的身份">进程的身份</h2>

<blockquote>
<p><a href="https://man7.org/linux/man-pages/man7/credentials.7.html">credentials(7) — Linux manual page</a></p>
</blockquote>

<h3 id="进程的用户-组和附属组">进程的用户、组和附属组</h3>

<p>在 Linux 中，用来标识一个进程身份的属性为：用户 ID、组 ID、附属组 ID 列表。</p>

<ul>
<li><p>用户 ID 和 组 ID，在进程中存在多种类型，如：</p>

<ul>
<li>真实用户（组） ID （ruid），表示进程的 Owner。</li>
<li>有效用户（组） ID （euid），表示真正进行权限校验的 ID。</li>
<li>保存的设置用户（组） ID （suid），参见下文。</li>
<li>文件系统用户（组） ID （Linux 特有，本文不介绍）</li>
</ul>

<p>用户和组 ID 都存在这些类型，基本逻辑一致，因此本文将以，用户 ID 为例，介绍这些 ID 的作用。</p></li>

<li><p>这些 ID，控制的是进程对文件系统文件的操作权限（读、写、执行）。</p></li>

<li><p>一般情况下，ruid、euid、suid 都是一致的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#define _GNU_SOURCE
</span><span style="color:#75715e">#include</span><span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span><span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span><span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">printTheProccessIdentifiers</span>() {
gid_t supplementaryGids[<span style="color:#ae81ff">32</span>]; <span style="color:#75715e">// sysconf(_SC_NGROUPS_MAX)
</span><span style="color:#75715e"></span>uid_t ruid, euid, suid;
gid_t rgid, egid, sgid;

<span style="color:#75715e">// uid_t ruid = getuid();   // 获取真实用户 ID
</span><span style="color:#75715e">// uid_t euid = geteuid();  // 获取有效用户 ID
</span><span style="color:#75715e">// gid_t rgid = getgid();   // 获取真实组 ID
</span><span style="color:#75715e">// gid_t egid = getegid();  // 获取有效组 ID
</span><span style="color:#75715e"></span>getresuid(<span style="color:#f92672">&amp;</span>ruid, <span style="color:#f92672">&amp;</span>euid, <span style="color:#f92672">&amp;</span>suid);                               <span style="color:#75715e">// 非 POSIX.1 标准
</span><span style="color:#75715e"></span>getresgid(<span style="color:#f92672">&amp;</span>rgid, <span style="color:#f92672">&amp;</span>egid, <span style="color:#f92672">&amp;</span>sgid);                               <span style="color:#75715e">// 非  POSIX.1 标准
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> supplementaryGidsSize <span style="color:#f92672">=</span> getgroups(<span style="color:#ae81ff">32</span>, supplementaryGids); <span style="color:#75715e">// 获取附属组 ID 列表
</span><span style="color:#75715e"></span>
printf(<span style="color:#e6db74">&#34;ruid: %d, euid: %d, suid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ruid, euid, suid);
printf(<span style="color:#e6db74">&#34;rgid: %d, egid: %d, sgid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, rgid, egid, sgid);
printf(<span style="color:#e6db74">&#34;supplementary group ids: &#34;</span>);
<span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> supplementaryGidsSize; i<span style="color:#f92672">++</span>)
{
    printf(<span style="color:#e6db74">&#34;%d&#34;</span>, supplementaryGids[i]);
    <span style="color:#66d9ef">if</span> (i <span style="color:#f92672">!=</span> supplementaryGidsSize <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)
    {
        printf(<span style="color:#e6db74">&#34;, &#34;</span>);
    }
}
printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>);
}


<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
printTheProccessIdentifiers();
<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div></li>
</ul>

<p>编译执行 <code>gcc 01-get-uid-gid.c &amp;&amp; ./a.out &amp;&amp; sudo ./a.out</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ruid: 1000, euid: 1000, suid: 1000
rgid: 1000, egid: 1000, sgid: 1000
supplementary group ids: 24, 25, 29, 30, 44, 46, 109, 112, 1000

ruid: 0, euid: 0, suid: 0
rgid: 0, egid: 0, sgid: 0
supplementary group ids: 0</pre></div>
<h3 id="进程身份的继承">进程身份的继承</h3>

<p>Linux 通过 fork-exec 启动一个新进程：</p>

<ul>
<li>fork 阶段，子进程完全复制父进程的身份标识符。</li>
<li>exec 阶段，多数情况（例外情况参见下文），身份标识（ruid、euid、suid 等）不会发生变化。</li>
</ul>

<p>如下场景，子进程的的身份标识直接继承父进程的身份标识。</p>

<ul>
<li>在交互式 Shell 中执行普通的应用程序，如 ls、cat 等。</li>
<li>在编程语言中（如C、Go、Java、Python 等等）中，不进行特殊配置的调用标准库的创建子进程的 API。</li>
</ul>

<h3 id="root-进程创建普通进程">root 进程创建普通进程</h3>

<p>Linux 在启动过程，一定会使用 root 用户 (uid = 0) 启动 1 号进程（如 systemd），然后 1 号进程会根据配置（如 systemd 的 service 文件）启动各种后台服务进程，根据权限最小化原则，这些进程的所有者用户（ruid）不一定是 root。此外，ssh server 进程 sshd 是以 root 用户运行，而我们却可以通过普通用户登录，拿到普通用户运行的 shell。</p>

<p>在如上这些场景，我们需要从 root 进程创建一个进程，且这个进程的身份是普通的用户，此时可以通过 <code>setuid</code>、<code>setgid</code> 等系统调用来实现。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#define _GNU_SOURCE
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;grp.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">printTheProccessIdentifiers</span>() {
  <span style="color:#75715e">// 参见上文
</span><span style="color:#75715e"></span>  <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
    printf(<span style="color:#e6db74">&#34;before: </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    printTheProccessIdentifiers();

    <span style="color:#75715e">// 子进程 fork 完成后，配置身份标识
</span><span style="color:#75715e"></span>    setgid(<span style="color:#ae81ff">1000</span>);
    initgroups(<span style="color:#e6db74">&#34;rectcircle&#34;</span>, <span style="color:#ae81ff">1000</span>); <span style="color:#75715e">// 非 POSIX.1 标准，读取 /etc/group，并调用 setgroups 系统调用。
</span><span style="color:#75715e"></span>    setuid(<span style="color:#ae81ff">1000</span>); <span style="color:#75715e">// uid 要最后设置，否则前面两个讲没有权限。
</span><span style="color:#75715e"></span>    printf(<span style="color:#e6db74">&#34;after: </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    printTheProccessIdentifiers();
    <span style="color:#75715e">// 开始执行 exec
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>编译执行 <code>gcc 02-set-uid-gid.c &amp;&amp; sudo ./a.out</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">before: 
ruid: <span style="color:#ae81ff">0</span>, euid: <span style="color:#ae81ff">0</span>, suid: <span style="color:#ae81ff">0</span>
rgid: <span style="color:#ae81ff">0</span>, egid: <span style="color:#ae81ff">0</span>, sgid: <span style="color:#ae81ff">0</span>
supplementary group ids: <span style="color:#ae81ff">0</span>

after: 
ruid: <span style="color:#ae81ff">1000</span>, euid: <span style="color:#ae81ff">1000</span>, suid: <span style="color:#ae81ff">1000</span>
rgid: <span style="color:#ae81ff">1000</span>, egid: <span style="color:#ae81ff">1000</span>, sgid: <span style="color:#ae81ff">1000</span>
supplementary group ids: <span style="color:#ae81ff">24</span>, <span style="color:#ae81ff">25</span>, <span style="color:#ae81ff">29</span>, <span style="color:#ae81ff">30</span>, <span style="color:#ae81ff">44</span>, <span style="color:#ae81ff">46</span>, <span style="color:#ae81ff">109</span>, <span style="color:#ae81ff">112</span>, <span style="color:#ae81ff">1000</span></code></pre></div>
<p>注意：</p>

<ul>
<li>set 用户身份相关系统调用，在该场景（例外参见下文），要求调用时用户身份必须为 root。</li>
<li>一旦通过 <code>setuid</code> 将进程从 root 切换为普通用户后，该进程将无法再通过 setuid 恢复为 root 进程。</li>
</ul>

<h3 id="从普通进程创建-root-进程">从普通进程创建 root 进程</h3>

<p>上文可以看出 Linux 定义了多种 uid，ruid、euid、suid，上文场景这几个值都是一样的。在本场景中，可以看出 Linux 定义这些 uid 的用意。</p>

<p>通过进程身份的继承、root 进程创建普通进程，可以满足绝大多数对进程身份的设置。但是某些场景并不能满足。如：</p>

<ul>
<li>普通用户，想通过 passwd 命令设置当前用户的密码时，需要更新属于 /etc/passwd 或 /etc/shadow。</li>
<li>普通用户，通过 sudo 切换到以 root 或其他用户的身份执行启动进程。</li>
</ul>

<p>因此，需要普通用户的进程在运行某个程序文件时，通过某些机制，可以创建一个身份为 root(或其他用户) 的进程。</p>

<p>在 Linux 中进程的可执行文件存储在文件系统中，Linux 文件系统中，每个文件都有一个 12 位的来控制文件的权限。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">11 10 9 8 7 6 5 4 3 2 1 0
S  G  T r w x r w x r w x</pre></div>
<ul>
<li>大家熟悉的是其中的低 9 位的 <code>rwxrwxrwx</code>，分别为：所有者读、写、执行，组读、写、执行，其他人读、写、执行。</li>
<li>第 11 位 <code>S</code>，为设置用户 ID 位。</li>
<li>第 10 位 <code>G</code>，为设置组 ID 位。</li>
</ul>

<p>当一个可执行文件属性，设置用户 ID 为被设置，当改程序被加载时（<a href="https://man7.org/linux/man-pages/man2/execve.2.html">execve</a> 系统调用），该进程的 euid 将被设置为这个文件的所有者（设置组 ID 为同理）。</p>

<p>因此，当我们想让一个可执行文件可以以其他用户的身份运行时，则可以：</p>

<ul>
<li>将该可执行文件的 owner 设置为指定用户如 root （<code>chown user filepath</code>）。</li>
<li>设置该可执行文件的 设置用户 ID 位 （<code>chmod u+s filepath</code>）。</li>
</ul>

<p>一个示例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#75715e">#define _GNU_SOURCE
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;grp.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
    uid_t oruid, oeuid, osuid;
    getresuid(<span style="color:#f92672">&amp;</span>oruid, <span style="color:#f92672">&amp;</span>oeuid, <span style="color:#f92672">&amp;</span>osuid); <span style="color:#75715e">// 非 POSIX.1 标准
</span><span style="color:#75715e"></span>    uid_t ruid, euid, suid;

    printf(<span style="color:#e6db74">&#34;origin</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    getresuid(<span style="color:#f92672">&amp;</span>ruid, <span style="color:#f92672">&amp;</span>euid, <span style="color:#f92672">&amp;</span>suid); <span style="color:#75715e">// 非 POSIX.1 标准
</span><span style="color:#75715e"></span>    printf(<span style="color:#e6db74">&#34;ruid: %d, euid: %d, suid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ruid, euid, suid);
    printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);

    printf(<span style="color:#e6db74">&#34;setuid(origin ruid = %d)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, oruid);
    setuid(oruid);
    getresuid(<span style="color:#f92672">&amp;</span>ruid, <span style="color:#f92672">&amp;</span>euid, <span style="color:#f92672">&amp;</span>suid); <span style="color:#75715e">// 非 POSIX.1 标准
</span><span style="color:#75715e"></span>    printf(<span style="color:#e6db74">&#34;ruid: %d, euid: %d, suid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ruid, euid, suid);
    printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);

    printf(<span style="color:#e6db74">&#34;setuid(origin suid = %d)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, osuid);
    setuid(osuid);
    getresuid(<span style="color:#f92672">&amp;</span>ruid, <span style="color:#f92672">&amp;</span>euid, <span style="color:#f92672">&amp;</span>suid); <span style="color:#75715e">// 非 POSIX.1 标准
</span><span style="color:#75715e"></span>    printf(<span style="color:#e6db74">&#34;ruid: %d, euid: %d, suid: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ruid, euid, suid);
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}</code></pre></div>
<p>首先进行编译 <code>sudo gcc 03-set-uid-bit.c</code>。</p>

<p>然后将开启该文件的设置用户 id 位，并将该可执行文件的 owner 设置为 root，并执行该测试程序 <code>sudo chown root:root a.out &amp;&amp; sudo chmod u+s a.out &amp;&amp; ./a.out</code>，输出如下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">origin
ruid: 1000, euid: 0, suid: 0

setuid(origin ruid = 1000)
ruid: 1000, euid: 1000, suid: 1000

setuid(origin suid = 0)
ruid: 1000, euid: 1000, suid: 1000</pre></div>
<p>然后将开启该文件的设置用户 id 位，并将该可执行文件的 owner 设置为 普通用户，并执行该测试程序 <code>sudo chown root:root a.out &amp;&amp; sudo chmod u+s a.out &amp;&amp; ./a.out</code>，输出如下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">origin
ruid: 1000, euid: 1001, suid: 1001

setuid(origin ruid = 1000)
ruid: 1000, euid: 1000, suid: 1001

setuid(origin suid = 1001)
ruid: 1000, euid: 1001, suid: 1001</pre></div>
<p>总结：</p>

<ul>
<li>当可执行文件的开启了设置用户 id 位后，执行该文件后 （execve），该进程的 euid 和 suid 将被设置为该文件的 owner。</li>
<li>如果当前进程的 euid 是 root，则 setuid 会设置所有的 ruid、euid 和 suid。</li>
<li>如果当前进程的 euid 不是 root，则 setuid 只会设置 euid，且这个 euid 的可选值只能是 ruid 或 suid，也就是说，一个由开启了设置用户 id 位的可执行文件启动的进程，这个进程 euid 可以在 ruid 和 suid 之间来回切换。</li>
</ul>

<h3 id="setuid-不生效原因">setuid 不生效原因</h3>

<p>在某些时候（如：使用 runc 启动容器时），可能发生 setuid 无效的情况，表现为 sudo 命令输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the &#39;nosuid&#39; option set or an NFS file system without root privileges?</pre></div>
<p>遇到如上情况，有如下可能性：</p>

<ul>
<li><p><code>/usr/bin/sudo</code> 文件没有设置 setuid 位： <code>stat /usr/bin/sudo</code>。权限字段如果以 4 开头则没有问题比（如 <code>(4755/-rwsr-xr-x)</code>）。如果不是，使用 root 权限执行如下命令。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">chmod u+s /usr/bin/sudo</code></pre></div></li>

<li><p>确认当前进程的 CapBnd 包含 <code>setuid</code> 位。先使用 root 安装 <code>capsh</code> 命令（<code>apt-get install libcap2-bin</code>），然后在出问题的普通用户执行 <code>capsh --print</code>。</p>

<ul>
<li><p>如果输出 Bounding set 包含 <code>cap_setuid</code>，则说明没有问题。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap</pre></div></li>

<li><p>如果没有且使用的 runc 启动的容器，检查 <code>capabilities.bounding</code> 是否包含 <code>CAP_SETUID</code>。</p></li>
</ul></li>

<li><p>检查文件所在文件系统挂载点在挂载时，是否使用了 <code>nosuid</code> 选项。通过 <code>mount | grep nosuid</code> 命令查看。如果，使用了该选项，检查挂载位置，删除该选项。</p></li>

<li><p>检查 NoNewPrivs 是否被设置，通过 <code>cat /proc/$$/status | grep NoNewPrivs</code>，如果输出 <code>NoNewPrivs:  0</code>，标识没有问题。否则，说明通过 prctl 设置了 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>PR_SET_NO_NEW_PRIVS</code></a> 为 1，此时，如果是 runc 启动的，设置 <a href="https://github.com/opencontainers/runc/blob/2da0194236d2bf20a52751139d479ebd05512fc2/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go#L58"><code>process.noNewPrivileges</code></a> 为 false（runc <a href="https://github.com/opencontainers/runc/blob/eddf35e5462e2a9f24d8279874a84cfc8b8453c2/libcontainer/setns_init_linux.go#L57">源码</a> | <a href="https://github.com/opencontainers/runc/issues/641">issue</a>）。</p></li>
</ul>

<h2 id="进程的能力">进程的能力</h2>

<blockquote>
<p><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">capabilities(7) — Linux manual page</a></p>
</blockquote>

<h3 id="概述">概述</h3>

<p>另一方面，某些系统调用是很危险的，只有系统的管理员才能调用。而上述的进程身份，主要解决了进程对文件系统操作的权限控制，并不能解决该问题。</p>

<p>针对这个问题，在 Linux 中，我们熟悉的解决办法是：将系统调用分为两类，普通系统调用和特权系统调用，普通用户的进程只能调用普通系统调用，root 用户的进程才能调用特权系统调用。</p>

<p>但是，这种划分太过简单粗暴，不符合最小权限原则。比如某个进程，只需要某一个特权系统调用（如 Nginx 只需要一个绑定 80 端口特权系统调用），但是我们不得不以 root 权限运行这个程序，给这个进程调用全部特权系统调用的权限。</p>

<p>为了解决这个问题，Linux 将特权系统调用进一步进行分类，划分为了 41 中能力（capabilities）（截止 Linux 5.13）。</p>

<p>和上文 进程的身份 的设置用户 ID 位类似，Linux 从两个方面提供了对一个进程有权的能力（capabilities）进行设置：</p>

<ul>
<li>从文件系统角度，对于可执行文件，通过文件系统的扩展属性 <code>security.capability</code> 声明该可执行文件需要的特权能力列表（通过 <a href="https://man7.org/linux/man-pages/man2/setxattr.2.html"><code>setxattr(2)</code> 系统调用</a> 或 <a href="https://man7.org/linux/man-pages/man8/setcap.8.html"><code>setcap(8)</code> 命令</a> 可以进行设置），可以通过 <a href="https://man7.org/linux/man-pages/man8/getcap.8.html"><code>getcap(8) 命令</code></a> 可以查看一个可执行文件的特权能力列表（如：<code>sudo getcap $(which ping)</code> 和 <code>sudo getcap -r / 2&gt;/dev/null</code>）。最终可执行过文件的这些配置，会在 <a href="https://man7.org/linux/man-pages/man2/execve.2.html">execve</a> 系统调用执行阶段，根据一定规则应用到进程中。</li>
<li>从进程角度，可以通过相关系统调用来主动配置该进程的特权能力列表：<a href="https://man7.org/linux/man-pages/man2/capset.2.html"><code>capset(2)</code> 系统调用</a> 、 <a href="https://man7.org/linux/man-pages/man2/capget.2.html"><code>capget(2)</code> 系统调用</a>、<a href="https://man7.org/linux/man-pages/man3/cap_get_proc.3.html"><code>cap_get_proc(3)</code> 系列库函数</a>、<a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl(2)</code> 系统调用</a>。</li>
</ul>

<p>通过如上的手段可以做到：</p>

<ul>
<li>限制 root 用户进程的权限，让 root 用户进程按需使用权限。</li>
<li>让普通用户进程拥有部分特权，可以调用某些特权系统调用。</li>
</ul>

<p>目前目前最大的应用场景是：（Docker）容器进程权限限制。下文将介绍该场景的实现示例。</p>

<p>Linux 进程 capabilities 的细节还是非常多的，本文不会全部涉及，想了解详细的细节，参见： <a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">capabilities</a></p>

<h3 id="进程和可执行文件-capabilities-相关属性">进程和可执行文件 capabilities 相关属性</h3>

<p>在 Linux 进程的 capabilities 由多种 capabilities 集合和标志位决定。</p>

<p>进程有如下五个与 capabilities 相关的属性：</p>

<ul>
<li><code>P(permitted)</code> 类型为 capabilities 集合。控制 <code>capset</code> 系统调用可以的 <code>P(effective)</code> 或 <code>P(inheritable)</code> 的项目。</li>
<li><code>P(effective)</code> 类型为 capabilities 集合。进程在进行特权系统调用时，会依据该属性进行检查。</li>
<li><code>P(inheritable)</code> 类型为 capabilities 集合。控制接受哪些可执行文件配置的 <code>F(inheritable)</code>。</li>
<li><code>P(bounding)</code> 类型为 capabilities 集合。控制接受哪些可执行文件配置的 <code>F(permitted)</code>。</li>
<li><code>P(ambient)</code> 类型为 capabilities 集合，Linux 4.3 新增。</li>
</ul>

<p>可执行文件有三个与 capabilities 相关的属性：</p>

<ul>
<li><code>F(permitted)</code> 类型为 capabilities 集合。声明该程序需要的能力。</li>
<li><code>F(inheritable)</code> 类型为 capabilities 集合。声明该要从父进程继承的能力。</li>
<li><code>F(effective)</code> 类型为标志位。声明自动的将当前进程 <code>P(effective)</code> 设置为其计算出的 <code>P(permitted)</code>（不是父进程的）。</li>
</ul>

<p>如上进程的相关 capabilities 的属性在在如下场景会发生变化：</p>

<ul>
<li><code>setuid(2)</code> 和 <code>setresuid(2)</code> 系统调用被调用。

<ul>
<li>如果 ruid、euid、suid 有一个是 0，被修改后这个值都是非 0，则 <code>P(permitted)</code>, <code>P(effective)</code>, <code>P(ambient)</code> 都将被清除。</li>
<li>如果 euid 从 0 变为 非 0，则 <code>P(effective)</code> 将被清空。</li>
<li>如果 euid 从非 0 变为 0，则 <code>P(effective)</code> 将被设置为 <code>P(permitted)</code>。</li>
</ul></li>
<li>通过 <code>capset(2)</code> 系统调用，通过编程的方式修改。</li>

<li><p><code>execve(2)</code> 系统调用执行后，可以用公式来表示，下文中 P 表示 execve 之前，P&rsquo; 表示 execve 之后，F 表示可执行文件的属性。可以分为两种情况：</p>

<ul>
<li><p>一般情况：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">P&#39;(ambient)     = (file is privileged) ? 0 : P(ambient)

P&#39;(permitted)   = (P(inheritable) &amp; F(inheritable)) |
                    (F(permitted) &amp; P(bounding)) | P&#39;(ambient)

P&#39;(effective)   = F(effective) ? P&#39;(permitted) : P&#39;(ambient)

P&#39;(inheritable) = P(inheritable)    [i.e., unchanged]

P&#39;(bounding)    = P(bounding)       [i.e., unchanged]</pre></div>
<p>file is privileged 表示如下情况之一的：</p>

<ul>
<li>这个可执行文件有配置 capabilities 相关属性。</li>
<li>这个可执行文件的 set-user-ID 或 set-group-ID 启用。</li>
</ul></li>

<li><p>兼容 UNIX 规范情况，即当前进程的 euid 为 0 （当前进程为 root 或，可执行文件的 owner 为 root 且启用了设置用户 ID 位（参见上文））:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">P&#39;(permitted)   = P(inheritable) | P(bounding)

P&#39;(effective)   = P&#39;(permitted)</pre></div>
<p>只有如上两个属性和一般情况不同，其他属性和一般情况相同。</p>

<p>该场景具体实例：</p>

<ul>
<li>root 进程 fork-exec 了新的进程。</li>
<li>类似于 <code>sudo</code> 的命令执行。</li>
</ul></li>
</ul></li>
</ul>

<p>通过 <code>cat /proc/1/status | grep Cap</code> 和 <code>sudo -u root sh -c 'cat /proc/$$/status | grep Cap'</code>， 观察 1 号进程和普通 root 进程的 capabilities 相关属性（可以通过 <code>sudo capsh --decode=00000000a80425fb</code> 解码、<code>sudo apt-get install libcap2-bin</code>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000</pre></div>
<p>通过 <code>sudo -u nobody sh -c 'cat /proc/$$/status | grep Cap'</code>， 观察普通用户进程的 capabilities 相关属性：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">CapInh: <span style="color:#ae81ff">0000000000000000</span>
CapPrm: <span style="color:#ae81ff">0000000000000000</span>
CapEff: <span style="color:#ae81ff">0000000000000000</span>
CapBnd: 000001ffffffffff
CapAmb: <span style="color:#ae81ff">0000000000000000</span></code></pre></div>
<p>通过如上输出，可以看出：</p>

<ul>
<li>root 进程的 <code>P(permitted)</code>、<code>P(bounding)</code>、<code>P(effective)</code> 均为拥有全部能力。</li>
<li>普通进程，<code>P(bounding)</code> 拥有全部能力，其他均为 0。</li>
<li>在 sudo 时，按照如上公式，确实做到了普通进程在 execve 后，变成了 root。</li>
</ul>

<h3 id="相关编程接口和命令行工具">相关编程接口和命令行工具</h3>

<ul>
<li>系统调用：Linux glibc 并没有提供对 cap 的封装，需要使用 <code>syscall</code> 调用，更多参考：<a href="https://man7.org/linux/man-pages/man2/capset.2.html">capget(2) 和 capset(2)</a>。</li>
<li>C 语言函数库：参见 <a href="https://man7.org/linux/man-pages/man3/libcap.3.html">libcap(3)</a>，默认没有安装，可通过 <code>sudo apt-get install libcap-dev</code> 安装，源码参见：<a href="https://git.kernel.org/pub/scm/libs/libcap/libcap.git/tree/">git</a>。</li>
<li>Go 语言函数库： <a href="https://github.com/syndtr/gocapability">syndtr/gocapability</a>，<a href="https://github.com/opencontainers/runc">runc</a> 使用的库。</li>
<li>命令工具：<a href="https://man7.org/linux/man-pages/man1/capsh.1.html">capsh</a> 等，可通过 <code>sudo apt-get install libcap2-bin</code> 安装。</li>
</ul>

<h2 id="实例-容器进程权限限制">实例：容器进程权限限制</h2>

<p>按照如上类似的命令，观察 docker 容器按照默认方式启动容器，其 1 号进程的 capabilities 相关属性：</p>

<ul>
<li><p>root 权限 <code>docker run --user=root -it busybox sh -c 'cat /proc/1/status | grep Cap'</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">CapInh: 00000000a80425fb
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000</pre></div></li>

<li><p>非 root 权限 <code>docker run --user=nobody -it busybox sh -c 'cat /proc/1/status | grep Cap'</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">CapInh: 00000000a80425fb
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000</pre></div></li>
</ul>

<p>结合 <a href="https://github.com/moby/moby/blob/master/oci/caps/defaults.go#L6-L19">docker 源码</a>可以看出，docker 容器的进程开放了如下能力：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">DefaultCapabilities</span>() []<span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> []<span style="color:#66d9ef">string</span>{
		<span style="color:#e6db74">&#34;CAP_CHOWN&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_DAC_OVERRIDE&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_FSETID&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_FOWNER&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_MKNOD&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_NET_RAW&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_SETGID&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_SETUID&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_SETFCAP&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_SETPCAP&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_NET_BIND_SERVICE&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_SYS_CHROOT&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_KILL&#34;</span>,
		<span style="color:#e6db74">&#34;CAP_AUDIT_WRITE&#34;</span>,
	}
}</code></pre></div>
<p>根据上文内容可以推测出 docker 启动容器进程过程中，关于 capabilities 的相关配置：</p>

<ul>
<li>root 权限，fork 进程。</li>
<li>root 权限，子进程调用 capset 系统调用，清除危险的能力，默认情况只开启部分必要的不危险的权限。</li>
<li>如果启动的用户不是 root，通过 setuid 等命令，切换到普通用户/组/附属组。</li>
</ul>

<p>下文，使用 go 语言模拟上述后面两个步骤：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>

	<span style="color:#e6db74">&#34;github.com/syndtr/gocapability/capability&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;the kernal support %d caps: &#34;</span>, len(<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">List</span>()))
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">List</span>() {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;%s, &#34;</span>, <span style="color:#a6e22e">c</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#a6e22e">caps</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">NewPid2</span>(<span style="color:#ae81ff">0</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">Load</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;the origin root proc caps: &#34;</span>, <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#75715e">// 设置当前进程的 caps
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">Clear</span>(<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">EFFECTIVE</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">PERMITTED</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">BOUNDING</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">INHERITABLE</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">AMBIENT</span>)
	<span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">EFFECTIVE</span>|<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">PERMITTED</span>|<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">BOUNDING</span>|<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">INHERITABLE</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_CHOWN</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_DAC_OVERRIDE</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_FSETID</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_FOWNER</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_MKNOD</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_NET_RAW</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_SETGID</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_SETUID</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_SETFCAP</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_SETPCAP</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_NET_BIND_SERVICE</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_SYS_CHROOT</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_KILL</span>,
		<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">CAP_AUDIT_WRITE</span>,
	)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">Apply</span>(<span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">EFFECTIVE</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">PERMITTED</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">BOUNDING</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">INHERITABLE</span> | <span style="color:#a6e22e">capability</span>.<span style="color:#a6e22e">AMBIENT</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;the root proc new caps: &#34;</span>, <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">String</span>())
	<span style="color:#75715e">// 切换到普通用户
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Setuid</span>(<span style="color:#ae81ff">1000</span>)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">Load</span>() <span style="color:#75715e">// 重新加载
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;the nonroot proc new caps: &#34;</span>, <span style="color:#a6e22e">caps</span>.<span style="color:#a6e22e">String</span>())
}</code></pre></div>
<p>编译并执行 <code>sudo go run ./05-mock-docker-cap/</code>，输出如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">the kernal support 41 caps: chown, dac_override, dac_read_search, fowner, fsetid, kill, setgid, setuid, setpcap, linux_immutable, net_bind_service, net_broadcast, net_admin, net_raw, ipc_lock, ipc_owner, sys_module, sys_rawio, sys_chroot, sys_ptrace, sys_pacct, sys_admin, sys_boot, sys_nice, sys_resource, sys_time, sys_tty_config, mknod, lease, audit_write, audit_control, setfcap, mac_override, mac_admin, syslog, wake_alarm, block_suspend, audit_read, perfmon, bpf, checkpoint_restore, 
the origin root proc caps:  { effective=&#34;full&#34; permitted=&#34;full&#34; inheritable=&#34;empty&#34; bounding=&#34;full&#34; }
the root proc new caps:  { effective=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; permitted=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; inheritable=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; bounding=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; }
the nonroot proc new caps:  { effective=&#34;empty&#34; permitted=&#34;empty&#34; inheritable=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; bounding=&#34;chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap&#34; }</pre></div>
<h2 id="参考">参考</h2>

<ul>
<li><a href="http://www.apuebook.com/">Unix 环境高级编程中文第三版（APUE）</a>，4.4 设置用户 ID 和设置组 ID，8.11 更改用户 ID 和更改组 ID。</li>
<li><a href="https://gohalo.me/post/linux-capabilities-introduce.html">Linux Capabilites 机制详细介绍</a> 该文有 C 语言的示例程序。</li>
<li><a href="https://icloudnative.io/posts/linux-capabilities-why-they-exist-and-how-they-work/">Linux Capabilities 入门教程：概念篇</a>。</li>
<li><a href="https://icloudnative.io/posts/linux-capabilities-in-practice-1/">Linux Capabilities 入门教程：基础实战篇</a>。</li>
<li><a href="https://icloudnative.io/posts/linux-capabilities-in-practice-2/">Linux Capabilities 入门教程：进阶实战篇</a>。</li>
<li><a href="https://hustcat.github.io/docker-config-capabilities/">Docker解析：配置与权限管理</a>。</li>
</ul>
]]></description></item><item><title>容器核心技术（七） Network Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-7-namespace-net/</link><pubDate>Mon, 19 Sep 2022 00:01:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-7-namespace-net/</guid><description type="html"><![CDATA[

<blockquote>
<p>手册页面：<a href="https://man7.org/linux/man-pages/man7/network_namespaces.7.html">network namespaces</a>。</p>
</blockquote>

<h2 id="背景知识">背景知识</h2>

<p>Linux 网络话题非常庞大，在阅读 Network Namespace 之前，建议阅读 Linux 网络相关的系列文章：</p>

<ul>
<li><a href="https://www.rectcircle.cn/posts/learn-ipv6-by-ipv4-diff/">通过和 IPv4 对比，学习 IPv6</a></li>
<li><a href="/posts/learn-net-proto-stack-by-linux-api-1-overview/">通过 Linux API 学习网络协议栈（一）概览</a></li>
<li><a href="/posts/learn-net-proto-stack-by-linux-api-2-ip/">通过 Linux API 学习网络协议栈（二）IP 协议</a></li>
<li><a href="/posts/linux-net-virual-01-overview/">Linux 网络虚拟化技术（一）概览</a></li>
<li><a href="/posts/linux-net-virual-02-veth/">Linux 网络虚拟化技术（二）veth 虚拟设备</a></li>
<li><a href="/posts/linux-net-virual-03-bridge/">Linux 网络虚拟化技术（三）bridge 虚拟设备</a></li>
<li><a href="/posts/linux-net-virual-04-iptables/">Linux 网络虚拟化技术（四）iptables</a></li>
<li><a href="/posts/linux-net-virual-05-tunnel/">Linux 网络虚拟化技术（五）隧道技术</a></li>
</ul>

<h2 id="描述">描述</h2>

<p>网络名字空间提供了如下网络相关资源的隔离：</p>

<ul>
<li>网络设备（veth、bridge 等）</li>
<li>ipv4、ipv6 协议栈</li>
<li>ip 路由表、防火墙规则 (netfilters/iptables)</li>
<li><code>/proc/net</code> 目录（指向 <code>/proc/PID/net</code> 的软链）、<code>/sys/class/net</code> 目录、<code>/proc/sys/net</code> 下的各种文件、端口号 (socket) 等等。</li>
<li>UNIX domain abstract socket （注意是 abstract、是 Linux 特有的一种 Unix domain socket 类型，即绑定的路径不会再真正的文件系统中呈现， <a href="https://unix.stackexchange.com/questions/206386/what-does-the-symbol-denote-in-the-beginning-of-a-unix-domain-socket-path-in-l">ls 看不到</a>，解决了 socket 文件可能被误删的问题）。</li>
</ul>

<p>当一个网络名字空间释放后：</p>

<ul>
<li>该网络名字空间中的<strong>物理网络设备</strong>将会被移动回初始的网络名字空间（而非父进程）。</li>
<li>该网络名字空间中的<strong>虚拟网络设备</strong>（<a href="https://man7.org/linux/man-pages/man4/veth.4.html">veth(4)</a>）将会被销毁。</li>
</ul>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<p>在业界的容器实现中，用到的网络模型，在容器内部和 docker bridge 网络模式类似。即：在容器内外通过一对 veth 相连。在容器外部的 veth 通过可插拔网络驱动（如 docker 的采用 bridge、k8s flannel 采用 vxlan 等）来实现定制化的网络拓扑模型。</p>

<p>在 <a href="/posts/linux-net-virual-04-iptables/#实例-docker-bridge-网络模拟实现">Linux 网络虚拟化技术（四）iptables - 实例：docker bridge 网络模拟实现</a> ，已经进行了相关分析以及 shell 的示例代码。</p>

<p>本文将用 Go，实现一遍 docker bridge 网络模型。此外，因为本文重点关注的是网络名字空间，将忽略 docker bridge 网络模拟实现中的 bridge 以及 iptables 相关内容，仅介绍：</p>

<ul>
<li>如何创建 Network Namespace。</li>
<li>如何将 veth 的一端加入一个 Network Namespace。</li>
<li>如何配置加入到 Network Namespace 中的 veth 的 ip 地址、网关等。</li>
</ul>

<p>具体实现效果是：容器（新的网络名字空间）可以通过 veth ping 通宿主机（根网络名字空间）。</p>

<p>实现上述内容的需要的核心 api 为：</p>

<ul>
<li>父进程通过 clone 系统调用一个子进程，并绑定一个新的 Network Namespace。</li>
<li>父进程通过 netlink api 创建一对 veth，并配置在父进程 Network Namespace 这一端的 ip、子网 等。</li>
<li>父进程通过 netlink api 将 veth 的一端加入到新的 Network Namespace</li>
<li>父进程通过 setns 系统调用，进入 Network Namespace。通过 netlink api 设置加入新的 Network Namespace 的这一端 veth 的 ip、子网、gateway等。</li>
</ul>

<h3 id="源码">源码</h3>

<h4 id="go-语言描述">Go 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run src/go/01-namespace/05-network/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;runtime&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>

	<span style="color:#e6db74">&#34;github.com/vishvananda/netlink&#34;</span>
	<span style="color:#e6db74">&#34;github.com/vishvananda/netns&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#a6e22e">tip</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">script</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">tip</span>)
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-cx&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccess</span>() (<span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>, <span style="color:#66d9ef">int</span>) {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWNET</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Wait</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>() <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 时序 1: 刚创建的 Network Namespace， ip addr 只能看到 lo 接口
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;(1) === new namespace process ===&#34;</span>, <span style="color:#e6db74">&#34;ip addr&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// 时序 3: 此时已经配置好了 veth，ip addr 可以看到 veth 接口
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;(3) === new namespace process ===&#34;</span>, <span style="color:#e6db74">&#34;ip addr &amp;&amp; ip route&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
	<span style="color:#75715e">// 时序 4: ping veth 另一端
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;(4) === new namespace process ===&#34;</span>, <span style="color:#e6db74">&#34;ping -c 1 172.16.0.1&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">oldNamespaceProccess</span>(<span style="color:#a6e22e">pid</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// 时序 2: 配置 veth
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">configVeth</span>(<span style="color:#a6e22e">pid</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;(2) === old namespace process ===&#34;</span>, <span style="color:#e6db74">&#34;ip addr show veth0&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">configVeth</span>(<span style="color:#a6e22e">pid</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">const</span> (
		<span style="color:#a6e22e">vethName</span>     = <span style="color:#e6db74">&#34;veth0&#34;</span>
		<span style="color:#a6e22e">vethPeerName</span> = <span style="color:#e6db74">&#34;veth0container&#34;</span>
		<span style="color:#a6e22e">vethNet</span>      = <span style="color:#e6db74">&#34;172.16.0.1/16&#34;</span>
		<span style="color:#a6e22e">gatewayIP</span>    = <span style="color:#e6db74">&#34;172.16.0.1&#34;</span>
		<span style="color:#a6e22e">vethPeerNet</span>  = <span style="color:#e6db74">&#34;172.16.0.2/16&#34;</span>
	)
	<span style="color:#75715e">// 1. 创建并配置位于根 Network Namespace 的一侧
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    a. 创建 veth
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">la</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkAttrs</span>()
	<span style="color:#a6e22e">la</span>.<span style="color:#a6e22e">Name</span> = <span style="color:#a6e22e">vethName</span> <span style="color:#75715e">// 当前 veth 的命令
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// la.MasterIndex = br.Attrs().Index  // 如果是要和 bridge 连接，可以配置该属性
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAdd</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Veth</span>{
		<span style="color:#a6e22e">LinkAttrs</span>: <span style="color:#a6e22e">la</span>,
		<span style="color:#a6e22e">PeerName</span>:  <span style="color:#a6e22e">vethPeerName</span>, <span style="color:#75715e">// 当前 veth 另一端的名字
</span><span style="color:#75715e"></span>	}); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#a6e22e">ipNet</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">ParseIPNet</span>(<span style="color:#a6e22e">vethNet</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">//    b. 给一侧 veth 设置 ip
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">AddrAdd</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">vethName</span>}), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Addr</span>{<span style="color:#a6e22e">IPNet</span>: <span style="color:#a6e22e">ipNet</span>})
	<span style="color:#75715e">//    c. 启动一侧 veth
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetUp</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">vethName</span>}))

	<span style="color:#75715e">// 2. 将 veth 的另一侧加入新的 Network Namespace
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     a. 获取到要加入到新的 Network Namespace 的 veth 的另一侧
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">peerLink</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkByName</span>(<span style="color:#a6e22e">vethPeerName</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">//     b. 获取到新的 Network Namespace 的 proc 上的引用
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">f</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">OpenFile</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;/proc/%d/ns/net&#34;</span>, <span style="color:#a6e22e">pid</span>), <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">O_RDONLY</span>, <span style="color:#ae81ff">0</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#75715e">//     c. 将 veth 的另一侧加入新的 Network Namespace
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetNsFd</span>(<span style="color:#a6e22e">peerLink</span>, int(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Fd</span>())); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}

	<span style="color:#75715e">// 3. 让当前的进程 (父进程) 进入新的 Network Namespace
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     a. 记录当前的 Network Namespace
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">origns</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">netns</span>.<span style="color:#a6e22e">Get</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">origns</span>.<span style="color:#a6e22e">Close</span>()
	<span style="color:#75715e">//     b. 后文 netns.Set 利用的是 setns 系统调用配置的线程，因此需要禁止 go 将当前协程调度到其他操作系统线程中。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">LockOSThread</span>()
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">UnlockOSThread</span>()
	<span style="color:#75715e">//     c. 当前进程 (父进程) 加入到新的 Network Namespace 中。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netns</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">netns</span>.<span style="color:#a6e22e">NsHandle</span>(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Fd</span>())); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">//     d. 在当前函数执行完成后，恢复现场
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">netns</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">origns</span>)

	<span style="color:#75715e">// 4. 当前进程已经在新的 Network Namespace 中了，去配置已经在新的 Network Namespace 中的另一侧 veth
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     a. veth 配置 ip、子网
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ipNet</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">ParseIPNet</span>(<span style="color:#a6e22e">vethPeerNet</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">AddrAdd</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">vethPeerName</span>}), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Addr</span>{<span style="color:#a6e22e">IPNet</span>: <span style="color:#a6e22e">ipNet</span>}); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#75715e">//     b. 启动 veth 和 lo 设备
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetUp</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">vethPeerName</span>})); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetUp</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;lo&#34;</span>})); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}
	<span style="color:#75715e">//     c. 配置新的 Network Namespace 的路由表
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">cidr</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">ParseCIDR</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>)
	<span style="color:#a6e22e">gwIP</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">ParseIP</span>(<span style="color:#a6e22e">gatewayIP</span>)
	<span style="color:#a6e22e">defaultRoute</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Route</span>{
		<span style="color:#a6e22e">LinkIndex</span>: <span style="color:#a6e22e">peerLink</span>.<span style="color:#a6e22e">Attrs</span>().<span style="color:#a6e22e">Index</span>,
		<span style="color:#a6e22e">Gw</span>:        <span style="color:#a6e22e">gwIP</span>,
		<span style="color:#a6e22e">Dst</span>:       <span style="color:#a6e22e">cidr</span>,
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">RouteAdd</span>(<span style="color:#a6e22e">defaultRoute</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 Network Namespace 的进程
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r1</span>, <span style="color:#a6e22e">pid</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccess</span>()
		<span style="color:#75715e">// 2. 在根 Network Namespace 中执行。
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">oldNamespaceProccess</span>(<span style="color:#a6e22e">pid</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err2</span>)
		}
		<span style="color:#a6e22e">err1</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r1</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err1</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;(5) === old namespace process ===&#34;</span>, <span style="color:#e6db74">&#34;ip addr show veth0 || true&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err</span>)
		}
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
		<span style="color:#75715e">// 2. 该进程执行 newNamespaceProccessFunc，binding 文件系统，并执行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div>
<h3 id="输出及分析">输出及分析</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">(1) === new namespace process ===
+ ip addr
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

(2) === old namespace process ===
+ ip addr show veth0
22: veth0@if21: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 7a:2d:96:17:8d:bc brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.16.0.1/16 brd 172.16.255.255 scope global veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::c85a:16ff:fe7a:26e3/64 scope link tentative 
       valid_lft forever preferred_lft forever

(3) === new namespace process ===
+ ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
21: veth0container@if22: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 36:74:22:99:43:5a brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.16.0.2/16 brd 172.16.255.255 scope global veth0container
       valid_lft forever preferred_lft forever
    inet6 fe80::3474:22ff:fe99:435a/64 scope link tentative 
       valid_lft forever preferred_lft forever
+ ip route
default via 172.16.0.1 dev veth0container 
172.16.0.0/16 dev veth0container proto kernel scope link src 172.16.0.2 

(4) === new namespace process ===
+ ping -c 1 172.16.0.1
PING 172.16.0.1 (172.16.0.1) 56(84) bytes of data.
64 bytes from 172.16.0.1: icmp_seq=1 ttl=64 time=0.053 ms

--- 172.16.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.053/0.053/0.053/0.000 ms

(5) === old namespace process ===
+ ip addr show veth0
Device &#34;veth0&#34; does not exist.
+ true</pre></div>
<ul>
<li>子进程刚进入 Network Namespace 时，该进程只有一个未启动 lo 设备。</li>
<li>父进程在完成了配置后，在父进程中可以看到 veth0。</li>
<li>子进程再看网络设备，可以看到 lo 设备和 veth0container 都配置正确。</li>
<li>子进程 ping 网关也可以 ping 通。</li>
<li>最后子程序退出后，veth 全部消失了，和 man 手册描述的一致。</li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://weread.qq.com/web/reader/a8932240721e42b5a89f479ka5b325d0225a5bfc9e0772d">自己动手写 Docker - 第六章 容器网络</a></li>
<li><a href="https://github.com/xianlubird/mydocker/blob/master/network/network.go#L288">xianlubird/mydocker</a></li>
<li><a href="http://www.hyuuhit.com/2019/03/23/netns/">Linux network namespace 简单解读</a></li>
<li><a href="https://leon-wtf.github.io/distributed%20system/2020/10/11/docker-kubernetes-network-model/">Docker和Kubernetes网络模型</a></li>
<li><a href="https://www.kubernetes.org.cn/6908.html">从零开始入门 K8s | 理解 CNI 和 CNI 插件</a></li>
<li><a href="https://segmentfault.com/a/1190000040860373">15.kubernetes笔记 CNI网络插件(一) Flannel</a></li>
</ul>
]]></description></item><item><title>Openresty 使用</title><link>https://www.rectcircle.cn/posts/openresty/</link><pubDate>Fri, 02 Sep 2022 20:35:35 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/openresty/</guid><description type="html"><![CDATA[

<h2 id="简介">简介</h2>

<p>按照官方的介绍： OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台，其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。</p>

<p>简单来讲， OpenResty = Nginx + LuaJIT + OpenResty Lua 内置模块 + 第三方 Lua 模块。首次安装好的 OpenResty 包含 Nginx + LuaJIT + OpenResty Lua 内置模块。如果官方的模块无法满足需求，可以自己实现或者安装第三方 Lua 模块，以实现更高自由度的扩展和定制。</p>

<p>关于 Lua 参见：<a href="/posts/luajit-and-lua5.1/">LuaJIT 和 Lua 5.1</a></p>

<h2 id="快速开始">快速开始</h2>

<h3 id="安装">安装</h3>

<blockquote>
<p>官方文档：<a href="https://openresty.org/cn/installation.html">安装</a></p>
</blockquote>

<p>Mac(brew) 安装</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install openresty/brew/openresty</code></pre></div>
<p>其他平台参见：</p>

<ul>
<li><a href="https://openresty.org/cn/linux-packages.html">OpenResty® Linux 包</a></li>
</ul>

<p>以 Linux 平台为例，安装的内容 (<code>dpkg -L openresty</code>) 如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">/usr/local/openresty/bin
/usr/local/openresty/luajit
/usr/local/openresty/lualib
/usr/local/openresty/nginx
/usr/local/openresty/nginx/conf
/usr/local/openresty/nginx/html
/usr/local/openresty/nginx/logs
/usr/local/openresty/nginx/sbin/nginx
/usr/local/openresty/site

/etc/init.d/openresty
/lib/systemd/system/openresty.service

/usr/share/doc/openresty
/etc/openresty
/usr/bin/openresty</pre></div>
<p>通过 brew 在 Mac 上安装，目录结构和 Linux 类似，对应关系为：</p>

<ul>
<li>/usr/local/openresty 对应 /usr/local/Cellar/openresty/x.y.z</li>
<li>/etc/openresty 对应 /usr/local/etc/openresty/</li>
<li>/usr/bin/openresty 对应 /usr/local/bin/openresty</li>
<li>/lib/systemd/system/openresty.service 对应 /usr/local/Cellar/openresty/x.y.z/homebrew.mxcl.openresty.plist</li>
</ul>

<p>需要特别注意的是，默认的配置文件位于：<code>/usr/local/etc/openresty/nginx.conf</code> （Linux 为 <code>/etc/openresty/nginx.conf</code>）。</p>

<h3 id="运行">运行</h3>

<p>手动启动</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo openresty -g <span style="color:#e6db74">&#39;daemon off; master_process on;&#39;</span> <span style="color:#75715e"># 可以通过 -c 指定 nginx 配置文件</span></code></pre></div>
<p>Mac(brew) 开机自启</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 后台启动并设置开机自启</span>
sudo brew services start openresty/brew/openresty
<span style="color:#75715e"># 停止并取消后台启动</span>
sudo brew services stop openresty/brew/openresty</code></pre></div>
<p>Linux(systemd) 开机自启</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 设置开机自启</span>
sudo systemctl enable openresty
<span style="color:#75715e"># 取消开机自启</span>
sudo systemctl disable openresty
<span style="color:#75715e"># 启动服务</span>
sudo systemctl start openresty</code></pre></div>
<p>浏览器打开 <a href="http://127.0.0.1">http://127.0.0.1</a> 即可看到默认页面。</p>

<h3 id="配置">配置</h3>

<blockquote>
<p>官方文档：<a href="https://openresty.org/cn/getting-started.html">Getting Started</a></p>
</blockquote>

<p>修改 <code>/usr/local/etc/openresty/nginx.conf</code> (Linux 为 <code>/etc/openresty/nginx.conf</code>) 为如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">worker_processes</span> <span style="color:#ae81ff">1</span>;
<span style="color:#66d9ef">error_log</span> <span style="color:#e6db74">/dev/stderr</span> <span style="color:#e6db74">info</span>;
<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span> <span style="color:#ae81ff">1024</span>;
}
<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;
    <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/dev/stdout</span> <span style="color:#e6db74">main</span>;
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;hello,</span> <span style="color:#e6db74">world&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>
<span style="color:#960050;background-color:#1e0010">}</span></code></pre></div>
<p>加载配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo openresty -s reload</code></pre></div>
<p>执行 <code>curl http://127.0.0.1</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">&lt;<span style="color:#f92672">p</span>&gt;hello, world&lt;/<span style="color:#f92672">p</span>&gt;</code></pre></div>
<h2 id="示例">示例</h2>

<blockquote>
<p>以下示例仅供参考，未在生产环境验证。</p>
</blockquote>

<h3 id="基于-redis-动态路由">基于 Redis 动态路由</h3>

<blockquote>
<p>参考：<a href="https://openresty.org/cn/dynamic-routing-based-on-redis.html">官方文档</a></p>
</blockquote>

<p>修改 <code>/usr/local/etc/openresty/nginx.conf</code> (Linux 为 <code>/etc/openresty/nginx.conf</code>) 为如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">worker_processes</span> <span style="color:#ae81ff">1</span>;
<span style="color:#66d9ef">error_log</span> <span style="color:#e6db74">/dev/stderr</span> <span style="color:#e6db74">info</span>;
<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span> <span style="color:#ae81ff">1024</span>;
}
<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;
    <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/dev/stdout</span> <span style="color:#e6db74">main</span>;
    <span style="color:#75715e"># 模拟两个服务 8001 和 8002
</span><span style="color:#75715e"></span>    <span style="color:#75715e"># 正常情况，这两个服务应该部署在自己的机器上
</span><span style="color:#75715e"></span>    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8001</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8001</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8002</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8002</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#75715e"># 将 redis get 命令转换为一个 http 请求
</span><span style="color:#75715e"></span>    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
        <span style="color:#f92672">location</span> = <span style="color:#e6db74">/redis</span> {
            <span style="color:#75715e"># 这里为了直观的可以看出的输出，先公开
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># 正常情况，应该放开注释的
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># internal;
</span><span style="color:#75715e"></span>            <span style="color:#f92672">set_unescape_uri</span> $key $arg_key;
            <span style="color:#75715e"># redis2 相关指令，参见 redis2-nginx-module: https://github.com/openresty/redis2-nginx-module
</span><span style="color:#75715e"></span>            <span style="color:#f92672">redis2_query</span> <span style="color:#e6db74">get</span> $key;
            <span style="color:#f92672">redis2_pass</span> 127.0.0.1:<span style="color:#ae81ff">6379</span>;
        }

        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">set</span> $target <span style="color:#e6db74">&#39;&#39;</span>;
            <span style="color:#75715e"># access_by_lua_block 在访问的时候会被执行
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># 更多参见： https://github.com/openresty/lua-nginx-module#access_by_lua_block
</span><span style="color:#75715e"></span>            <span style="color:#f92672">access_by_lua_block</span> {
                <span style="color:#f92672">local</span> <span style="color:#e6db74">key</span> = <span style="color:#e6db74">ngx.var.arg_key</span>
                <span style="color:#e6db74">print(&#34;key:</span> <span style="color:#e6db74">&#34;,</span> <span style="color:#e6db74">key)</span>

                <span style="color:#e6db74">--</span> <span style="color:#e6db74">调用</span> <span style="color:#e6db74">Nginx</span> <span style="color:#e6db74">内部路径</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">res</span> = <span style="color:#e6db74">ngx.location.capture(</span>
                    <span style="color:#e6db74">&#34;/redis&#34;,</span> { <span style="color:#f92672">args</span> = { <span style="color:#f92672">key</span> = <span style="color:#e6db74">key</span> <span style="color:#960050;background-color:#1e0010">}</span> <span style="color:#960050;background-color:#1e0010">}</span>
                <span style="color:#e6db74">)</span>

                <span style="color:#e6db74">if</span> <span style="color:#e6db74">res.status</span> ~<span style="color:#e6db74">=</span> <span style="color:#ae81ff">200</span> <span style="color:#e6db74">then</span>
                    <span style="color:#e6db74">ngx.log(ngx.ERR,</span> <span style="color:#e6db74">&#34;redis</span> <span style="color:#e6db74">server</span> <span style="color:#e6db74">returned</span> <span style="color:#e6db74">bad</span> <span style="color:#e6db74">status:</span> <span style="color:#e6db74">&#34;,</span>
                        <span style="color:#e6db74">res.status)</span>
                    <span style="color:#e6db74">ngx.exit(res.status)</span>
                <span style="color:#e6db74">end</span>

                <span style="color:#e6db74">if</span> <span style="color:#e6db74">not</span> <span style="color:#e6db74">res.body</span> <span style="color:#e6db74">then</span>
                    <span style="color:#e6db74">ngx.log(ngx.ERR,</span> <span style="color:#e6db74">&#34;redis</span> <span style="color:#e6db74">returned</span> <span style="color:#e6db74">empty</span> <span style="color:#e6db74">body&#34;)</span>
                    <span style="color:#e6db74">ngx.exit(500)</span>
                <span style="color:#e6db74">end</span>

                <span style="color:#e6db74">--</span> <span style="color:#e6db74">解析</span> <span style="color:#e6db74">redis</span> <span style="color:#e6db74">返回</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">parser</span> = <span style="color:#e6db74">require</span> <span style="color:#e6db74">&#34;redis.parser&#34;</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">server,</span> <span style="color:#e6db74">typ</span> = <span style="color:#e6db74">parser.parse_reply(res.body)</span>
                <span style="color:#e6db74">if</span> <span style="color:#e6db74">typ</span> ~<span style="color:#e6db74">=</span> <span style="color:#e6db74">parser.BULK_REPLY</span> <span style="color:#e6db74">or</span> <span style="color:#e6db74">not</span> <span style="color:#e6db74">server</span> <span style="color:#e6db74">then</span>
                    <span style="color:#e6db74">ngx.log(ngx.ERR,</span> <span style="color:#e6db74">&#34;bad</span> <span style="color:#e6db74">redis</span> <span style="color:#e6db74">response:</span> <span style="color:#e6db74">&#34;,</span> <span style="color:#e6db74">res.body)</span>
                    <span style="color:#e6db74">ngx.exit(500)</span>
                <span style="color:#e6db74">end</span>
                <span style="color:#e6db74">print(&#34;server:</span> <span style="color:#e6db74">&#34;,</span> <span style="color:#e6db74">server)</span>
                
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">将返回的</span> <span style="color:#e6db74">server</span> <span style="color:#e6db74">设置给变量</span> <span style="color:#e6db74">target</span>
                <span style="color:#e6db74">ngx.var.target</span> = <span style="color:#e6db74">server</span>
            <span style="color:#960050;background-color:#1e0010">}</span>

            <span style="color:#e6db74">proxy_pass</span> <span style="color:#e6db74">http://</span>$target;
        }
    }
}</code></pre></div>
<p>加载配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo openresty -s reload</code></pre></div>
<p>写入路由到 redis 里面</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;set service-8001 &#39;127.0.0.1:8001&#39;\nset service-8002 &#39;127.0.0.1:8002&#39;&#34;</span> | redis-cli</code></pre></div>
<p>验证：</p>

<ul>
<li><p><code>curl 'http://127.0.0.1/redis?key=service-8001'</code> 可以看到刚刚设置的到 redis 中的信息。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">$14
127.0.0.1:8001</pre></div></li>

<li><p><code>curl 'http://127.0.0.1/?key=service-8001'</code> 从输出 <code>&lt;p&gt;service: 8001&lt;/p&gt;</code>，可以看到路由到了 8001 端口。</p></li>

<li><p><code>curl 'http://127.0.0.1/?key=service-8002'</code> 从输出 <code>&lt;p&gt;service: 8002&lt;/p&gt;</code>，可以看到路由到了 8002 端口。</p></li>
</ul>

<h3 id="基于-redis-进行服务发现和负载均衡">基于 Redis 进行服务发现和负载均衡</h3>

<ul>
<li>服务通过注册到 Redis 中的名为服务名的 hash 中，key 该实例的 host, value 为该实例的一些元信息，在本例中为一致性 hash 的权重。</li>
<li>每个请求过来后，在 Openresty 中，通过 Redis lua 库查询当前服务信息，并通过一定策略选取一个 host</li>
</ul>

<p>修改 <code>/usr/local/etc/openresty/nginx.conf</code> (Linux 为 <code>/etc/openresty/nginx.conf</code>) 为如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">worker_processes</span> <span style="color:#ae81ff">1</span>;
<span style="color:#66d9ef">error_log</span> <span style="color:#e6db74">/dev/stderr</span> <span style="color:#e6db74">info</span>;
<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span> <span style="color:#ae81ff">1024</span>;
}
<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;
    <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/dev/stdout</span> <span style="color:#e6db74">main</span>;
    <span style="color:#75715e"># 模拟两个服务 8001 和 8002
</span><span style="color:#75715e"></span>    <span style="color:#75715e"># 正常情况，这两个服务应该部署在自己的机器上
</span><span style="color:#75715e"></span>    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8001</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8001</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>

    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8002</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8002</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>

    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">set</span> $target <span style="color:#e6db74">&#39;&#39;</span>;
            <span style="color:#f92672">access_by_lua_block</span> {
                <span style="color:#f92672">local</span> <span style="color:#e6db74">redis</span> = <span style="color:#e6db74">require</span> <span style="color:#e6db74">&#34;resty.redis&#34;</span>

                <span style="color:#e6db74">local</span> <span style="color:#e6db74">red</span> = <span style="color:#e6db74">redis:new()</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">ok,</span> <span style="color:#e6db74">err</span> = <span style="color:#e6db74">red:connect(&#34;127.0.0.1&#34;,</span> <span style="color:#ae81ff">6379</span><span style="color:#e6db74">)</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">res,</span> <span style="color:#e6db74">err</span> = <span style="color:#e6db74">red:hgetall(&#39;my-service&#39;)</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">res</span> <span style="color:#e6db74">为一个数组</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=1,</span> <span style="color:#e6db74">v=127.0.0.1:8001</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=2,</span> <span style="color:#e6db74">v=1</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=3,</span> <span style="color:#e6db74">v=127.0.0.1:8002</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=4,</span> <span style="color:#e6db74">v=1</span>
                <span style="color:#e6db74">red:set_keepalive(10000,</span> <span style="color:#ae81ff">100</span><span style="color:#e6db74">)</span>
                <span style="color:#e6db74">ngx.var.target</span> = <span style="color:#e6db74">res[2</span> <span style="color:#e6db74">*</span> <span style="color:#e6db74">math.random(</span><span style="color:#75715e">#res / 2) - 1]
</span><span style="color:#75715e"></span>            <span style="color:#960050;background-color:#1e0010">}</span>
            <span style="color:#e6db74">proxy_pass</span> <span style="color:#e6db74">http://</span>$target;
        }
    }
}</code></pre></div>
<p>加载配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo openresty -s reload</code></pre></div>
<p>假设我们一个服务名为 my-service，包含两个实例 <code>127.0.0.1:8001</code> 和 <code>127.0.0.1:8002</code>，通过 redis-cli 注册这两个实例。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;HMSET my-service &#39;127.0.0.1:8001&#39; &#39;1&#39; &#39;127.0.0.1:8002&#39; &#39;1&#39;&#34;</span> | redis-cli
echo <span style="color:#e6db74">&#34;HGETALL my-service&#34;</span> | redis-cli</code></pre></div>
<p>验证：</p>

<ul>
<li>多次执行 <code>curl 'http://127.0.0.1/</code> 发现随机返回 service: 8001 和 service: 8002。</li>
</ul>

<p>说明：</p>

<ul>
<li>本例中直接使用了底层的 <a href="https://github.com/openresty/lua-resty-redis">openresty/lua-resty-redis</a>，而没有使用 <a href="https://github.com/openresty/redis2-nginx-module">openresty/redis2-nginx-module</a>。</li>
<li>如果想使用 Nginx Upstream 相关的重试能力，进行如下优化：

<ul>
<li>在 <code>access_by_lua_block</code>，如果有返回的是 host 不是 ip，还需要在此进行手动的 DNS（如 <a href="https://github.com/Kong/lua-resty-dns-client">Kong/lua-resty-dns-client</a> 库），最后，将结果记录到 <code>ngx.ctx</code> 中。</li>
<li>在 updstream 中通过 <a href="https://github.com/openresty/lua-nginx-module#balancer_by_lua_block">balancer_by_lua_block</a> ，来设置 upstream。需要注意的是，对 Redis 的请求涉及到了 socket 请求，无法在 <code>balancer_by_lua_block</code> 中使用，更多参见：<a href="https://github.com/openresty/lua-resty-redis/issues/119">issue</a>。</li>
<li>具体参见下一个例子。</li>
</ul></li>
</ul>

<h3 id="支持动态更新的一致性-hash-负载均衡">支持动态更新的一致性 hash 负载均衡</h3>

<p>假设一个后端服务集群有多台实例，并使用 OpenResty (Nginx) 作为网关。该服务由一个特性，如果某一类请求能打到同一台实例，这项性能最优。此时可以通过 OpenResty 来实现这类要求的基本思路为：</p>

<ul>
<li>这些后端服务会注册到某注册中心 (本例中为 redis)。</li>
<li>每次请求均查询 OpenResty，并通过一致性 Hash 来选择一台实例。</li>
</ul>

<p>安装 <code>jojohappy/lua-resty-balancer</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/openresty/lua-resty-balancer.git
cd lua-resty-balancer
git checkout v0.04
make <span style="color:#f92672">&amp;&amp;</span> sudo make install
cd ../ <span style="color:#f92672">&amp;&amp;</span> rm -rf lua-resty-balancer
<span style="color:#75715e"># 安装位置为</span>
<span style="color:#75715e"># /usr/local/lib/lua/librestychash.dylib  (linux 为 librestychash.so)</span>
<span style="color:#75715e"># /usr/local/lib/lua/resty</span></code></pre></div>
<p>修改 <code>/usr/local/etc/openresty/nginx.conf</code> (Linux 为 <code>/etc/openresty/nginx.conf</code>) 为如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">worker_processes</span> <span style="color:#ae81ff">1</span>;
<span style="color:#66d9ef">error_log</span> <span style="color:#e6db74">/dev/stderr</span> <span style="color:#e6db74">info</span>;
<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span> <span style="color:#ae81ff">1024</span>;
}
<span style="color:#66d9ef">http</span> {

    <span style="color:#75715e"># 添加搜索路径
</span><span style="color:#75715e"></span>    <span style="color:#f92672">lua_package_path</span> <span style="color:#e6db74">&#34;/usr/local/lib/lua/?.lua</span>;;<span style="color:#f92672">&#34;</span>;
    <span style="color:#f92672">lua_package_cpath</span> <span style="color:#e6db74">&#34;/usr/local/lib/lua/?.dylib</span>;;<span style="color:#f92672">&#34;</span>; <span style="color:#75715e"># Mac
</span><span style="color:#75715e"></span>    <span style="color:#75715e"># lua_package_cpath &#34;/usr/local/lib/lua/?.so;;&#34;; # Linux
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                    <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;
    <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/dev/stdout</span> <span style="color:#e6db74">main</span>;
    <span style="color:#75715e"># 模拟两个服务 8001 和 8002
</span><span style="color:#75715e"></span>    <span style="color:#75715e"># 正常情况，这两个服务应该部署在自己的机器上
</span><span style="color:#75715e"></span>    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8001</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8001</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>

    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8002</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">default_type</span> <span style="color:#e6db74">text/html</span>;
            <span style="color:#f92672">content_by_lua_block</span> {
                <span style="color:#f92672">ngx.say(&#34;&lt;p&gt;service:</span> <span style="color:#ae81ff">8002</span><span style="color:#e6db74">&lt;/p&gt;&#34;)</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>

    <span style="color:#e6db74">upstream</span> <span style="color:#e6db74">myService</span> {
        <span style="color:#f92672">server</span> <span style="color:#ae81ff">0</span><span style="color:#e6db74">.0.0.1</span>;   <span style="color:#75715e"># 一个无效的地址作为占位符
</span><span style="color:#75715e"></span>        <span style="color:#f92672">balancer_by_lua_block</span> {
            <span style="color:#f92672">--</span> <span style="color:#e6db74">调用一致性</span> <span style="color:#e6db74">hash</span> <span style="color:#e6db74">算法选取一个实例</span>
            <span style="color:#e6db74">--</span> <span style="color:#e6db74">参见:</span> <span style="color:#e6db74">https://github.com/openresty/lua-resty-balancer</span><span style="color:#75715e">#find
</span><span style="color:#75715e"></span>            <span style="color:#e6db74">local</span> <span style="color:#e6db74">chash_up</span> = <span style="color:#e6db74">package.loaded.my_chash_up</span>
            <span style="color:#e6db74">local</span> <span style="color:#e6db74">host</span> = <span style="color:#e6db74">chash_up:find(ngx.var.arg_key)</span>
            <span style="color:#e6db74">--</span> <span style="color:#e6db74">调用</span> <span style="color:#e6db74">OpenResty</span> <span style="color:#e6db74">的</span> <span style="color:#e6db74">balancer</span> <span style="color:#e6db74">相关能力，设置一个目标</span> <span style="color:#e6db74">host</span>
            <span style="color:#e6db74">--</span> <span style="color:#e6db74">参见:</span> <span style="color:#e6db74">https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md</span>
            <span style="color:#e6db74">local</span> <span style="color:#e6db74">b</span> = <span style="color:#e6db74">require</span> <span style="color:#e6db74">&#34;ngx.balancer&#34;</span>
            <span style="color:#e6db74">assert(b.set_current_peer(host))</span>
        <span style="color:#960050;background-color:#1e0010">}</span>
    <span style="color:#960050;background-color:#1e0010">}</span>

    <span style="color:#e6db74">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">access_by_lua_block</span> {
                <span style="color:#f92672">local</span> <span style="color:#e6db74">resty_chash</span> = <span style="color:#e6db74">require</span> <span style="color:#e6db74">&#34;resty.chash&#34;</span>

                <span style="color:#e6db74">local</span> <span style="color:#e6db74">function</span> <span style="color:#e6db74">discoveryService(serviceName)</span>
                    <span style="color:#e6db74">local</span> <span style="color:#e6db74">redis</span> = <span style="color:#e6db74">require</span> <span style="color:#e6db74">&#34;resty.redis&#34;</span>
                    <span style="color:#e6db74">local</span> <span style="color:#e6db74">red</span> = <span style="color:#e6db74">redis:new()</span>
                    <span style="color:#e6db74">local</span> <span style="color:#e6db74">ok,</span> <span style="color:#e6db74">err</span> = <span style="color:#e6db74">red:connect(&#34;127.0.0.1&#34;,</span> <span style="color:#ae81ff">6379</span><span style="color:#e6db74">)</span>
                    <span style="color:#e6db74">local</span> <span style="color:#e6db74">res,</span> <span style="color:#e6db74">err</span> = <span style="color:#e6db74">red:hgetall(serviceName)</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">res</span> <span style="color:#e6db74">为一个数组</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=1,</span> <span style="color:#e6db74">v=127.0.0.1:8001</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=2,</span> <span style="color:#e6db74">v=1</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=3,</span> <span style="color:#e6db74">v=127.0.0.1:8002</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">i=4,</span> <span style="color:#e6db74">v=1</span>
                    <span style="color:#e6db74">local</span> <span style="color:#e6db74">server_list</span> = {}
                    <span style="color:#f92672">for</span> <span style="color:#e6db74">i</span> = <span style="color:#ae81ff">1</span><span style="color:#e6db74">,</span> <span style="color:#75715e">#res, 2 do
</span><span style="color:#75715e"></span>                        <span style="color:#e6db74">server_list[res[i]]</span> = <span style="color:#e6db74">res[i</span> <span style="color:#e6db74">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">]</span>
                    <span style="color:#e6db74">end</span>
                    <span style="color:#e6db74">table.sort(server_list)</span>
                    <span style="color:#e6db74">return</span> <span style="color:#e6db74">server_list</span>
                <span style="color:#e6db74">end</span>

                <span style="color:#e6db74">local</span> <span style="color:#e6db74">function</span> <span style="color:#e6db74">serviceChanged(a,</span> <span style="color:#e6db74">b)</span>
                    <span style="color:#e6db74">if</span> <span style="color:#e6db74">a</span> == <span style="color:#e6db74">nil</span> <span style="color:#e6db74">or</span> <span style="color:#e6db74">b</span> == <span style="color:#e6db74">nil</span> <span style="color:#e6db74">then</span>
                        <span style="color:#e6db74">return</span> <span style="color:#e6db74">true</span>
                    <span style="color:#e6db74">end</span>
                    <span style="color:#e6db74">if</span> <span style="color:#75715e">#a ~= #b then
</span><span style="color:#75715e"></span>                        <span style="color:#e6db74">return</span> <span style="color:#e6db74">true</span>
                    <span style="color:#e6db74">end</span>
                    <span style="color:#e6db74">for</span> <span style="color:#e6db74">k,</span> <span style="color:#e6db74">value</span> <span style="color:#e6db74">in</span> <span style="color:#e6db74">pairs(a)</span> <span style="color:#e6db74">do</span>
                        <span style="color:#e6db74">if</span> <span style="color:#e6db74">value</span> ~<span style="color:#e6db74">=</span> <span style="color:#e6db74">b[k]</span> <span style="color:#e6db74">then</span>
                            <span style="color:#e6db74">return</span> <span style="color:#e6db74">true</span>
                        <span style="color:#e6db74">end</span>
                    <span style="color:#e6db74">end</span>
                    <span style="color:#e6db74">return</span> <span style="color:#e6db74">false</span>
                <span style="color:#e6db74">end</span>

                <span style="color:#e6db74">--</span> <span style="color:#e6db74">发现服务</span>
                <span style="color:#e6db74">local</span> <span style="color:#e6db74">server_list</span> = <span style="color:#e6db74">discoveryService(&#34;my-service&#34;)</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">服务是否有变化</span>
                <span style="color:#e6db74">if</span> <span style="color:#e6db74">not</span> <span style="color:#e6db74">serviceChanged(server_list,</span> <span style="color:#e6db74">package.loaded.my_servers)</span> <span style="color:#e6db74">then</span>
                    <span style="color:#e6db74">return</span>
                <span style="color:#e6db74">end</span>
                <span style="color:#e6db74">if</span> <span style="color:#e6db74">not</span> <span style="color:#e6db74">package.loaded.my_chash_up</span> <span style="color:#e6db74">then</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">如果是第一次执行，则创建</span> <span style="color:#e6db74">chash</span>
                    <span style="color:#e6db74">package.loaded.my_chash_up</span> = <span style="color:#e6db74">resty_chash:new(server_list)</span>
                <span style="color:#e6db74">else</span>
                    <span style="color:#e6db74">--</span> <span style="color:#e6db74">如果是不是第一次执行，重新初始化</span>
                    <span style="color:#e6db74">package.loaded.my_chash_up:reinit(server_list)</span>
                <span style="color:#e6db74">end</span>
                <span style="color:#e6db74">--</span> <span style="color:#e6db74">保存服务列表</span>
                <span style="color:#e6db74">package.loaded.my_servers</span> = <span style="color:#e6db74">server_list</span>
            <span style="color:#960050;background-color:#1e0010">}</span>
            <span style="color:#e6db74">proxy_pass</span> <span style="color:#e6db74">http://myService</span>;
        }
    }
}</code></pre></div>
<p>加载配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo openresty -s reload</code></pre></div>
<p>假设我们一个服务名为 my-service，包含两个实例 <code>127.0.0.1:8001</code> 和 <code>127.0.0.1:8002</code>，通过 redis-cli 注册这两个实例。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#34;del my-service\nHMSET my-service &#39;127.0.0.1:8001&#39; &#39;1&#39; &#39;127.0.0.1:8002&#39; &#39;1&#39;&#34;</span> | redis-cli
echo <span style="color:#e6db74">&#34;HGETALL my-service&#34;</span> | redis-cli</code></pre></div>
<p>验证：</p>

<ul>
<li>多次执行 <code>curl 'http://127.0.0.1/?key=xxx</code> 可以发现当 key 不变的情况下，始终指向同一个实例。</li>
</ul>

<p>说明：</p>

<ul>
<li>和官方 <a href="https://github.com/openresty/lua-resty-balancer#synopsis">openresty/lua-resty-balancer</a> 的示例不同，本例中对 <code>resty_chash</code> 的初始化放到了可 <code>access_by_lua_block</code> 块中，原因如下：

<ul>
<li>希望可以动态的感知实例的变化，每次请求都执行一次服务发现。</li>
<li><code>init_by_lua_block</code> 和 <code>balancer_by_lua_block</code> 无法调用 redis 相关函数，参见：<a href="https://github.com/openresty/lua-resty-redis/issues/119">issue</a>。</li>
</ul></li>
<li>本例忽略了对 redis 的压力，忽略了 Nginx 错误重试相关机制。</li>
<li>Redis 如果返回的是 Host 而不是 IP，则还需要进行手动 DNS，如 <a href="https://github.com/Kong/lua-resty-dns-client">Kong/lua-resty-dns-client</a> 库，Kong/lua-resty-dns-client 库是由 <code>luarocks</code> 管理，因此需要为 OpenResty 安装 luarocks（具体，参见：<a href="https://segmentfault.com/a/1190000008658146">博客</a>）。</li>
</ul>
]]></description></item><item><title>LuaJIT 和 Lua 5.1</title><link>https://www.rectcircle.cn/posts/luajit-and-lua5.1/</link><pubDate>Fri, 02 Sep 2022 18:46:13 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/luajit-and-lua5.1/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>Lua 在众多领域使用广泛，如游戏等。本文主要面向学习和使用 OpenResty 开发者。因此本文介绍的 Lua 版本是 OpenResty 使用的 Lua 解释器，是兼容 Lua 5.1 语法 LuaJIT。</p>

<p>此外需要注意的是 OpenResty 使用的是其自己维护的 LuaJIT 的<a href="https://github.com/openresty/luajit2/tree/v2.1-agentzh">分支</a> （总体来看，在特性上：<code>OpenResty LuaJIT 分支 &gt; LuaJIT &gt; Lua 5.1</code>）。</p>

<p>示例代码参见 github： <a href="https://github.com/rectcircle/lua-learn">rectcircle/lua-learn</a></p>

<h2 id="开发环境搭建">开发环境搭建</h2>

<h3 id="编译安装">编译安装</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://luajit.org/git/luajit.git
cd luajit
MACOSX_DEPLOYMENT_TARGET<span style="color:#f92672">=</span><span style="color:#ae81ff">12</span>.5.1 make <span style="color:#f92672">&amp;&amp;</span> sudo make install
sudo ln -sf luajit-2.1.0-beta3 /usr/local/bin/luajit
luajit --help
cd ..
rm -rf luajit</code></pre></div>
<p>说明：</p>

<ul>
<li><a href="https://github.com/LuaJIT/LuaJIT/issues/563">官方建议</a>：始终使用 v2.1 分支的代码编译。</li>
<li>Mac 编译需要 <code>MACOSX_DEPLOYMENT_TARGET</code> 环境变量，指向系统版本（系统偏好设置 &gt; 软件更新查看）</li>
<li>本例使用的代码版本为： <a href="https://github.com/LuaJIT/LuaJIT/tree/03080b795aa3496ed62d4a0697c9f4767e7ca7e5">03080b795aa3496ed62d4a0697c9f4767e7ca7e5</a>。</li>
<li>不使用 <code>PREFIX=/home/myself/lj2</code> 参数，将安装到 <code>/usr/local</code> 相关目录下。</li>
<li>卸载使用 <code>sudo make uninstall</code> 命令。</li>
</ul>

<p>更多参见：<a href="http://luajit.org/install.html">官方安装手册</a>。</p>

<h3 id="vscode-扩展">VSCode 扩展</h3>

<ul>
<li>语言服务器 <a href="https://marketplace.visualstudio.com/items?itemName=sumneko.lua">sumneko.lua</a>。更多参见：官方 <a href="https://github.com/sumneko/lua-language-server/wiki">wiki</a>。</li>
<li>调试器 <a href="https://marketplace.visualstudio.com/items?itemName=actboy168.lua-debug">actboy168.lua-debug</a>。更多参见：官方 <a href="https://github.com/actboy168/lua-debug/wiki">wiki</a>。</li>
</ul>

<p><code>cmd + ,</code>， 配置使用 luajit 版本：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;Lua.telemetry.enable&#34;</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#f92672">&#34;Lua.runtime.version&#34;</span>: <span style="color:#e6db74">&#34;LuaJIT&#34;</span>,
    <span style="color:#f92672">&#34;lua.debug.settings.luaVersion&#34;</span>: <span style="color:#e6db74">&#34;jit&#34;</span>,
}</code></pre></div>
<h3 id="运行测试">运行测试</h3>

<p><code>hello.lua</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#e6db74">&#39;hello&#39;</span>)</code></pre></div>
<ul>
<li>方式 1：命令行执行 <code>luajit hello.lua</code>。</li>
<li>方式 2：VSCode 调试，按 <code>F5</code>。</li>
</ul>

<h2 id="语言特性">语言特性</h2>

<h3 id="定位">定位</h3>

<p>Lua 是一个语法简单的，被定位为嵌入到 C 语言中，作为动态配置、数据处理等业务场景的脚本语言。</p>

<p>本部分仅介绍 Lua 语言自身的部分，不介绍 C 和 Lua 的交互。</p>

<h3 id="注释">注释</h3>

<p>和 sql 语言类似，使用 <code>--</code> 开启一个注释。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#75715e">-- 短注释，以 -- 开头</span></code></pre></div>
<p>Lua 还支持一种长注释。<code>--[[内容支持换行]]</code> 或 <code>--[==[内容支持换行]==]</code>，这里的 <code>=</code> 可以有多个，只要能对的上，且长度匹配都可以。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#75715e">--[[
</span><span style="color:#75715e">    长注释，--紧接着长括号，本例中为 0 级长括号
</span><span style="color:#75715e">]]</span>
<span style="color:#75715e">--[==[
</span><span style="color:#75715e">    长注释，--紧接着长括号，本例中为 2 级长括号
</span><span style="color:#75715e">]==]</span></code></pre></div>
<h3 id="类型和操作符">类型和操作符</h3>

<p>Lua 一共有 8 种数据类型，分别是： nil, boolean, number, string, function, userdata, thread, table。</p>

<p>下文将使用了两个内置函数介绍这 8 种数据类型：</p>

<ul>
<li><code>print</code> 打印 0 到多个值到标准输出。</li>
<li><code>type</code> 获取一个值的类型名。</li>
</ul>

<h4 id="nil">nil</h4>

<p>nil 即空类型（和其他语言的 null/nil 类型），只有一种值 nil。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(<span style="color:#66d9ef">nil</span>), <span style="color:#66d9ef">nil</span>)
<span style="color:#75715e">-- nil     nil</span></code></pre></div>
<h4 id="boolean">boolean</h4>

<p>boolean 值只有两种值 true、false。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(<span style="color:#66d9ef">true</span>), <span style="color:#66d9ef">true</span>)
print(type(<span style="color:#66d9ef">false</span>), <span style="color:#66d9ef">false</span>)
<span style="color:#75715e">-- boolean true</span>
<span style="color:#75715e">-- boolean false</span></code></pre></div>
<p>和 Python 类似， Lua 采用 <code>and</code>、 <code>or</code>、 <code>not</code> 作为逻辑运算符。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#66d9ef">true</span> <span style="color:#f92672">and</span> <span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">true</span> <span style="color:#f92672">or</span> <span style="color:#66d9ef">false</span>, <span style="color:#f92672">not</span> <span style="color:#66d9ef">false</span>)
<span style="color:#75715e">-- false   true    true</span></code></pre></div>
<h4 id="number">number</h4>

<p>number 类型，实现为双精度浮点数。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(<span style="color:#ae81ff">1.1</span>), <span style="color:#ae81ff">1.1</span>)
print(type(<span style="color:#ae81ff">1</span>), <span style="color:#ae81ff">1</span>)
print(<span style="color:#e6db74">&#39;字面量: 数字&#39;</span>, <span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">3.0</span>, <span style="color:#ae81ff">3.1416</span>, <span style="color:#ae81ff">314.16e-2</span>, <span style="color:#ae81ff">0.31416E1</span>, <span style="color:#ae81ff">0xff</span>, <span style="color:#ae81ff">0x56</span>)
<span style="color:#75715e">-- number  1.1</span>
<span style="color:#75715e">-- number  1</span>
<span style="color:#75715e">-- 字面量: 数字    3       3       3.1416  3.1416  3.1416  255     86</span></code></pre></div>
<p>和其他语言类似，Lua 支持 <code>+</code>、 <code>-</code>、 <code>*</code>、 <code>/</code>、 <code>%</code> 算数运算符，除此之外，Lua 还原生支持 <code>^</code> 次幂运算符。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#ae81ff">1</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span><span style="color:#f92672">*</span><span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">1</span><span style="color:#f92672">/</span><span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">5</span><span style="color:#f92672">%</span><span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">2</span><span style="color:#f92672">^</span><span style="color:#ae81ff">10</span>)
<span style="color:#75715e">-- 2       1       6       0.5     1       1024</span></code></pre></div>
<p>同样， Lua 支持关系运算符， <code>==</code>、 <code>~=</code>、 <code>&lt;=</code>、 <code>&gt;=</code>、 <code>&lt;</code>、 <code>&gt;</code>。这里需要特别说明的是：不等于使用的时 <code>~=</code> 而不是常见的 <code>!=</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#ae81ff">1</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;1&#39;</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">~=</span> <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">2</span>)
<span style="color:#75715e">-- true    false   true    true    false   true    false</span></code></pre></div>
<p>需要特别说明的时，Lua 并没有提供原生的位运算运算符，如需使用，可以搜索一些第三方库。</p>

<h4 id="string">string</h4>

<p>string 类型，即字节数组。需要特别说明的是 Lua 不关心 string 的字符集。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(<span style="color:#e6db74">&#39;string&#39;</span>), <span style="color:#e6db74">&#39;string&#39;</span>)
<span style="color:#75715e">-- string  string</span></code></pre></div>
<p>在 Lua 中，字符串字面量支持单引号、双引号以及长括号。长括号形如 <code>[[内容]]</code> 或 <code>[==[内容]==]</code>，这里的<code>内容</code>支持换行等任意特殊字符，<code>=</code>支持 0 个或多个。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#e6db74">[[支持单引号，使用 \ 转义]]</span>, <span style="color:#e6db74">&#39;alo</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">123&#34;&#39;</span>)
print(<span style="color:#e6db74">[[支持双引号，使用 \ 转义]]</span>, <span style="color:#e6db74">&#34;alo</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">123</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">&#34;</span>)
print(<span style="color:#e6db74">[[\数字字面量，转移 ascii 码]]</span>, <span style="color:#e6db74">&#39;</span><span style="color:#ae81ff">\97</span><span style="color:#e6db74">lo</span><span style="color:#ae81ff">\10\049</span><span style="color:#e6db74">23&#34;&#39;</span>)
print(<span style="color:#e6db74">&#39;[[ ]] 支持多行字符串（0 级长括号）&#39;</span>,<span style="color:#e6db74">[[alo
</span><span style="color:#e6db74">123&#34;]]</span>)
print(<span style="color:#e6db74">&#39;[==[ ]==] 支持多行字符串（可以有多个=号）（2 级长括号）&#39;</span>, <span style="color:#e6db74">[==[
</span><span style="color:#e6db74">alo
</span><span style="color:#e6db74">123&#34;]==]</span>)

<span style="color:#75715e">-- 支持单引号，使用 \ 转义 alo</span>
<span style="color:#75715e">-- 123&#34;</span>
<span style="color:#75715e">-- 支持双引号，使用 \ 转义 alo</span>
<span style="color:#75715e">-- 123&#34;</span>
<span style="color:#75715e">-- \数字字面量，转移 ascii 码      alo</span>
<span style="color:#75715e">-- 123&#34;</span>
<span style="color:#75715e">-- [[ ]] 支持多行字符串（0 级长括号）      alo</span>
<span style="color:#75715e">-- 123&#34;</span>
<span style="color:#75715e">-- [==[ ]==] 支持多行字符串（可以有多个=号）（2 级长括号） alo</span>
<span style="color:#75715e">-- 123&#34;</span></code></pre></div>
<h4 id="function">function</h4>

<p>Lua 本质上以一种长得很像 C 语言的函数式编程语言，因此 Lua 的函数也是第一公民，函数也是一种数据类型。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Add</span>(a, b)
    <span style="color:#66d9ef">return</span> a <span style="color:#f92672">+</span> b
<span style="color:#66d9ef">end</span>
print(type(Add), Add)
<span style="color:#75715e">-- function        function: 0x010564a058</span></code></pre></div>
<p>更多关于 function 的详细介绍，参见<a href="#函数">下文</a>。</p>

<h4 id="userdata">userdata</h4>

<p>userdata 类型，该类型的具体类型由通过 C 语言定义，并提供 Lua 可以调用的相关函数。该数据类型涉及到与 C 语言互操作，本文不多介绍。</p>

<h4 id="thread">thread</h4>

<p>thread 类型，即 coroutine，协同程序，协程。和操作系统线程相比相同点事，拥有独立的堆栈、局部变量、和指令指针。但是不同点在于：</p>

<ul>
<li>协程切换只能通过代码来实现，而不是操作系统线程是由操作系统来控制。</li>

<li><p>协程是串行的，而不是并发的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">T <span style="color:#f92672">=</span> coroutine.create(<span style="color:#66d9ef">function</span>() print(<span style="color:#e6db74">&#39;thread running&#39;</span>) <span style="color:#66d9ef">end</span>)
print(type(T), T)
coroutine.resume(T)
<span style="color:#75715e">-- thread  thread: 0x01053a07e0</span>
<span style="color:#75715e">-- thread running</span></code></pre></div></li>
</ul>

<p>更多关于 thread 的详细介绍，参见下文。</p>

<h4 id="table">table</h4>

<p>table 类型，可以用来实现对应其他语言的 array 和 map 相同的能力。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type({}), {})
<span style="color:#75715e">-- table   table: 0x010539e4c0</span></code></pre></div>
<p>table 作为数组使用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">for</span> i, v <span style="color:#66d9ef">in</span> ipairs({<span style="color:#e6db74">&#39;a&#39;</span>, <span style="color:#e6db74">&#39;b&#39;</span>, <span style="color:#ae81ff">3</span>}) <span style="color:#66d9ef">do</span>
    print(i, v)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- 1       a</span>
<span style="color:#75715e">-- 2       b</span>
<span style="color:#75715e">-- 3       3</span>

<span style="color:#66d9ef">for</span> i, v <span style="color:#66d9ef">in</span> ipairs({ a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>, [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;aa&#39;</span>, [<span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;bb&#39;</span>, [<span style="color:#ae81ff">2</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;cc&#39;</span>, [<span style="color:#ae81ff">4</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span> }) <span style="color:#66d9ef">do</span>
    print(i, v)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- 1       bb</span>
<span style="color:#75715e">-- 2       cc</span></code></pre></div>
<p>可以看出：</p>

<ul>
<li>table 作为数组使用时，下标是从 1 开始的，这和大多数主流编程语言都不一样。</li>
<li>在遍历数组时，如果 table 中，有数字非数字的，不是从 1 连续的，都会被忽略。</li>
<li>在遍历数组时，一定会按照从 1 开始从小到大依次遍历，因此可以使用 table.sort 对数组进行排序。</li>
</ul>

<p>table 作为 map 使用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">for</span> i, v <span style="color:#66d9ef">in</span> pairs({ a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>, [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;aa&#39;</span>, [<span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;bb&#39;</span>, [<span style="color:#ae81ff">2</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;cc&#39;</span>, [<span style="color:#ae81ff">4</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span> }) <span style="color:#66d9ef">do</span>
    print(i, v)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- 0       aa</span>
<span style="color:#75715e">-- 1       bb</span>
<span style="color:#75715e">-- 2       cc</span>
<span style="color:#75715e">-- a       1</span>
<span style="color:#75715e">-- 4       3</span></code></pre></div>
<p>需要特别说明的是，table 作为 map 时，遍历是 key 的顺序时不保证的，如需保证顺序，需转换为数组再排序。</p>

<p>table 元素的访问。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">T <span style="color:#f92672">=</span> { a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>, [<span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;b&#39;</span>, [<span style="color:#e6db74">&#39;1c&#39;</span>]<span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> }
print(T[<span style="color:#e6db74">&#39;a&#39;</span>], T[<span style="color:#ae81ff">1</span>], T[<span style="color:#e6db74">&#39;1c&#39;</span>])
print(T.a)
<span style="color:#75715e">-- 1       b       2</span>
<span style="color:#75715e">-- 1</span></code></pre></div>
<p>即支持两种模式：</p>

<ul>
<li><code>T['a']</code> 中括号，支持访问任意元素。</li>
<li><code>T.a</code> 点号，只支持下标为符合 lua 标识符规则的元素，如上的 1 和 <code>1c</code> 都不行。</li>
</ul>

<h4 id="自动类型转换">自动类型转换</h4>

<p>Lua 在进行 string 和 number 的操作时，会进行自动类型转换。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(<span style="color:#ae81ff">1</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;2&#34;</span>))  <span style="color:#75715e">-- 自动转为数字</span>
print(type(<span style="color:#ae81ff">1</span> <span style="color:#f92672">..</span> <span style="color:#e6db74">&#34;2&#34;</span>)) <span style="color:#75715e">-- 自动转为字符串</span>
<span style="color:#75715e">-- number</span>
<span style="color:#75715e">-- string</span></code></pre></div>
<p>当然，也可以手动进行转换</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(type(tonumber(<span style="color:#e6db74">&#39;1&#39;</span>)))
print(type(tostring(<span style="color:#ae81ff">1</span>)))
<span style="color:#75715e">-- number</span>
<span style="color:#75715e">-- string</span></code></pre></div>
<h4 id="运算符"><code>#</code> 运算符</h4>

<p>可用于获取</p>

<ul>
<li>字符串的字节长度。</li>

<li><p>table 的 下标为数字 1 开始的连续的下标的最大值。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#f92672">#</span><span style="color:#e6db74">&#39;abc&#39;</span>, <span style="color:#f92672">#</span>{<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">2</span>,<span style="color:#ae81ff">3</span>}, <span style="color:#f92672">#</span>{a<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, b<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>}, <span style="color:#f92672">#</span>{a<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, b<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>}, <span style="color:#f92672">#</span>{[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, [<span style="color:#ae81ff">2</span>]<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, [<span style="color:#ae81ff">4</span>]<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span>})
<span style="color:#75715e">-- 3       3       0       3       4</span>
<span style="color:#75715e">-- 注意：这里最后一个实测返回 4，有些奇怪。</span></code></pre></div></li>
</ul>

<h3 id="变量">变量</h3>

<p>Lua 的变量分为全局变量和局部变量。如下所示：</p>

<ul>
<li>定义一个全局变量的方式为：<code>变量名 = 表达式</code>，表达式为必选。</li>

<li><p>定义一个局部变量的方式为：<code>local 变量名 = 表达式</code>，表达式为可选。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">A <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
<span style="color:#66d9ef">local</span> b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
<span style="color:#66d9ef">local</span> c
print(A, b, c)
<span style="color:#75715e">-- 1       2       nil</span></code></pre></div></li>
</ul>

<p>关于 Lua 变量，需要注意的是：</p>

<ul>
<li><p>Lua 的全局变量的作用域是整个程序，也就是说，假如在一个函数中定一个了全局变量，在调用后，该变量在函数外面仍能够访问到。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">F1</span>()
    D1 <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
<span style="color:#66d9ef">end</span>
F1()
print(D1)
<span style="color:#75715e">-- 1</span></code></pre></div></li>

<li><p>Lua 的局部变量的表现和其他语言类似，脱离作用域后，将不存在。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">F2</span>()
    <span style="color:#66d9ef">local</span> d2 <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
<span style="color:#66d9ef">end</span>
F2()
print(d2)
    <span style="color:#75715e">-- nil</span></code></pre></div></li>

<li><p>Lua 是动态类型的，也就是说，Lua 变量不会和类型绑定，不同类型的值可以复制给同一个变量。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">E <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
print(type(E), E)
E <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;string&#39;</span>
print(type(E), E)
    <span style="color:#75715e">-- number  1</span>
<span style="color:#75715e">-- string  string</span></code></pre></div></li>

<li><p>Lua 的同一个名字的局部变量可以定义多次，后面定义的会隐藏前面定义的。</p></li>
</ul>

<h3 id="流程控制">流程控制</h3>

<h4 id="if-elseif-else">if elseif else</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">IfNumber</span>(i)
    <span style="color:#66d9ef">if</span> i <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">then</span>
        print(<span style="color:#e6db74">&#39;&gt;0&#39;</span>)
    <span style="color:#66d9ef">elseif</span> i <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">then</span>  <span style="color:#75715e">-- 可选</span>
        print(<span style="color:#e6db74">&#39;&lt;0&#39;</span>)
    <span style="color:#66d9ef">else</span>               <span style="color:#75715e">-- 可选</span>
        print(<span style="color:#e6db74">&#39;==0&#39;</span>)
    <span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">end</span>
IfNumber(<span style="color:#ae81ff">1</span>)
IfNumber(<span style="color:#ae81ff">0</span>)
IfNumber(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
<span style="color:#75715e">-- &gt;0</span>
<span style="color:#75715e">-- ==0</span>
<span style="color:#75715e">-- &lt;0</span></code></pre></div>
<h4 id="while-和-until">while 和 until</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
<span style="color:#66d9ef">while</span> i <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">10</span> <span style="color:#66d9ef">do</span>
    <span style="color:#66d9ef">if</span> i <span style="color:#f92672">==</span> <span style="color:#ae81ff">5</span> <span style="color:#66d9ef">then</span>
        <span style="color:#66d9ef">break</span>
    <span style="color:#66d9ef">end</span>
    print(<span style="color:#e6db74">&#39;while&#39;</span>, i)
    i <span style="color:#f92672">=</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
<span style="color:#66d9ef">end</span>

i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
<span style="color:#66d9ef">repeat</span>
    print(<span style="color:#e6db74">&#39;until&#39;</span>, i)
    i <span style="color:#f92672">=</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
<span style="color:#66d9ef">until</span> i <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">5</span>

<span style="color:#75715e">-- while   0</span>
<span style="color:#75715e">-- while   1</span>
<span style="color:#75715e">-- while   2</span>
<span style="color:#75715e">-- while   3</span>
<span style="color:#75715e">-- while   4</span>
<span style="color:#75715e">-- until   0</span>
<span style="color:#75715e">-- until   1</span>
<span style="color:#75715e">-- until   2</span>
<span style="color:#75715e">-- until   3</span>
<span style="color:#75715e">-- until   4</span></code></pre></div>
<p>需要特别说明的是，Lua 不支持 continue。</p>

<h4 id="for">for</h4>

<p>数字循环 <code>for i = start, end, step do ... end</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">for</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">do</span>  <span style="color:#75715e">-- 起始(包括), 结束(包括), 步长(可以省略, 默认为 1)</span>
    print(<span style="color:#e6db74">&#39;for-num&#39;</span>, i)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- for-num 1</span>
<span style="color:#75715e">-- for-num 2</span>
<span style="color:#75715e">-- for-num 3</span></code></pre></div>
<p>table (数组) 通过 <code>ipairs</code> 函数返回的迭代器进行遍历。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">for</span> k, v <span style="color:#66d9ef">in</span> ipairs({ <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">&#34;two&#34;</span>, <span style="color:#e6db74">&#34;three&#34;</span> }) <span style="color:#66d9ef">do</span>
    print(<span style="color:#e6db74">&#39;for-it&#39;</span>, k, v)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- for-it  1       one</span>
<span style="color:#75715e">-- for-it  2       two</span>
<span style="color:#75715e">-- for-it  3       three</span></code></pre></div>
<p>以上等价于：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">do</span>
    <span style="color:#66d9ef">local</span> f, s, var <span style="color:#f92672">=</span> ipairs({ <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">&#34;two&#34;</span>, <span style="color:#e6db74">&#34;three&#34;</span> })
    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">true</span> <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">local</span> k, v <span style="color:#f92672">=</span> f(s, var)
        var <span style="color:#f92672">=</span> k
        <span style="color:#66d9ef">if</span> var <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> <span style="color:#66d9ef">then</span> <span style="color:#66d9ef">break</span> <span style="color:#66d9ef">end</span>
        print(<span style="color:#e6db74">&#39;for-mock&#39;</span>, k, v)
    <span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- for-mock        1       one</span>
<span style="color:#75715e">-- for-mock        2       two</span>
<span style="color:#75715e">-- for-mock        3       three</span></code></pre></div>
<p>table (map) 通过 <code>pairs</code> 函数返回的迭代器进行遍历。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">for</span> k, v <span style="color:#66d9ef">in</span> pairs({a<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, b<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, c<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>}) <span style="color:#66d9ef">do</span>
    print(<span style="color:#e6db74">&#39;for-it&#39;</span>, k, v)
<span style="color:#66d9ef">end</span>
<span style="color:#75715e">-- for-it  c       3</span>
<span style="color:#75715e">-- for-it  a       1</span>
<span style="color:#75715e">-- for-it  b       2</span></code></pre></div>
<h3 id="函数">函数</h3>

<h4 id="函数定义">函数定义</h4>

<p>在 Lua 中，函数也是一种数据类型，因此也分全局和局部函数。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">GlobalAdd1</span>(a, b) <span style="color:#75715e">-- 全局函数（方式 1）</span>
    <span style="color:#66d9ef">return</span> a <span style="color:#f92672">+</span> b
<span style="color:#66d9ef">end</span>
GlobalAdd2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>(a, b) <span style="color:#75715e">-- 全局函数（方式 2）</span>
    <span style="color:#66d9ef">return</span> a <span style="color:#f92672">+</span> b
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">local</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">localAdd1</span>(a, b) <span style="color:#75715e">-- 局部函数（方式 1）</span>
    <span style="color:#66d9ef">return</span> a <span style="color:#f92672">+</span> b
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">local</span> localAdd2;
localAdd2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>(a, b) <span style="color:#75715e">-- 局部函数（方式 2）</span>
    <span style="color:#66d9ef">return</span> a <span style="color:#f92672">+</span> b
<span style="color:#66d9ef">end</span>

print(<span style="color:#e6db74">&#39;add&#39;</span>, GlobalAdd1(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1</span>), GlobalAdd2(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1</span>), localAdd1(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1</span>), localAdd2(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1</span>))

<span style="color:#75715e">-- add     2       2       2       2</span></code></pre></div>
<h4 id="定义到-table-中">定义到 table 中</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> t1 <span style="color:#f92672">=</span> { name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;abc&#34;</span> }
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">t1</span>.<span style="color:#a6e22e">PrintName1</span>() <span style="color:#75715e">-- 方式 1</span>
    print(<span style="color:#e6db74">&#34;print t1.name 1&#34;</span>, t1.name)
<span style="color:#66d9ef">end</span>
t1.PrintName2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>() <span style="color:#75715e">-- 方式 2</span>
    print(<span style="color:#e6db74">&#34;print t1.name 2&#34;</span>, t1.name)
<span style="color:#66d9ef">end</span>

t1.PrintName1()
t1.PrintName2()

<span style="color:#75715e">-- print t1.name 1 abc</span>
<span style="color:#75715e">-- print t1.name 2 abc</span></code></pre></div>
<h4 id="函数返回值">函数返回值</h4>

<p>Lua 函数支持返回 0 到多个返回值。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> nilReturn <span style="color:#f92672">=</span> (<span style="color:#66d9ef">function</span>() <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">end</span>)()
print(<span style="color:#e6db74">&#34;nil return&#34;</span>, nilReturn)

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">r</span>() <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span> <span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">local</span> rr1 <span style="color:#f92672">=</span> r()
print(<span style="color:#e6db74">&#39;返回多个值, 接收 1 个&#39;</span>, rr1)
<span style="color:#66d9ef">local</span> rr1, rr2 <span style="color:#f92672">=</span> r()
print(<span style="color:#e6db74">&#39;返回多个值, 接收 2 个&#39;</span>, rr1, rr2)
<span style="color:#66d9ef">local</span> rr1, rr2, rr3 <span style="color:#f92672">=</span> r()
print(<span style="color:#e6db74">&#39;返回多个值, 接收 3 个&#39;</span>, rr1, rr2, rr3)

<span style="color:#75715e">-- nil return      nil</span>
<span style="color:#75715e">-- 返回多个值, 接收 1 个   1</span>
<span style="color:#75715e">-- 返回多个值, 接收 2 个   1       2</span>
<span style="color:#75715e">-- 返回多个值, 接收 3 个   1       2       3</span></code></pre></div>
<h4 id="函数参数">函数参数</h4>

<p>Lua 函数参数在调用时：</p>

<ul>
<li>如果传递的参数少于函数声明的数目，则填充 nil。</li>
<li>如果传递的参数多余函数声明的数目，多余的将被忽略。</li>

<li><p>如果将一个函数的调用作为函数参数，且这个函数有多个返回值。</p>

<ul>
<li>如果函数调用作为函数的最后一个参数，则所有返回值都会作为参数都会传递。</li>

<li><p>如果函数调用不是作为函数的最后一个参数，则之后将第一个返回值作为参数传递。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">r</span>() <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span> <span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">f</span>(a, b, c) print(<span style="color:#e6db74">&#39;f(a, b, c) params:&#39;</span>, a, b, c) <span style="color:#66d9ef">end</span>

f(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>)
f(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">5</span>)
f(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">5</span>, <span style="color:#ae81ff">6</span>)
f(r())
f(r(), <span style="color:#ae81ff">10</span>)
f(<span style="color:#ae81ff">10</span>, r())

<span style="color:#75715e">-- f(a, b, c) params:      3       4       nil</span>
<span style="color:#75715e">-- f(a, b, c) params:      3       4       5</span>
<span style="color:#75715e">-- f(a, b, c) params:      3       4       5</span>
<span style="color:#75715e">-- f(a, b, c) params:      1       2       3</span>
<span style="color:#75715e">-- f(a, b, c) params:      1       10      nil</span>
<span style="color:#75715e">-- f(a, b, c) params:      10      1       2</span></code></pre></div></li>
</ul></li>
</ul>

<p>Lua 支持可变参数 <code>...</code>。</p>

<ul>
<li>在函数形参列表的最后可以通过 <code>...</code> 声明可变参数。</li>
<li>可以通过 <code>{...}</code> 将可变参数转换为一个 table (数组)。</li>
<li>可以通过 <code>unpack</code> 函数，将一个 table (数组) 作为可变参数进行传递。</li>
<li><code>...</code> 可以直接传递给其他函数的可变参数。</li>

<li><p>通过 <code>select</code> 函数可以获取可变参数的长度或者截取从 index 开始到之后的可变参数。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">r</span>() <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span> <span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">g</span>(a, b, ...) print(<span style="color:#e6db74">&#39;params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :&#39;</span>, a, b, { ... }, select(<span style="color:#e6db74">&#39;#&#39;</span>, ...), <span style="color:#f92672">#</span>{select(<span style="color:#ae81ff">2</span>, ...)}, ...) <span style="color:#66d9ef">end</span>

g(<span style="color:#ae81ff">3</span>)
g(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>)
g(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">5</span>, <span style="color:#ae81ff">8</span>)
g(<span style="color:#ae81ff">5</span>, r())
g(unpack({ <span style="color:#e6db74">&#39;a&#39;</span>, <span style="color:#e6db74">&#39;b&#39;</span>, <span style="color:#e6db74">&#39;c&#39;</span>, <span style="color:#e6db74">&#39;d&#39;</span>, <span style="color:#e6db74">&#39;e&#39;</span> }))
<span style="color:#75715e">-- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :    3       nil     table: 0x010818d160     0       0</span>
<span style="color:#75715e">-- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :    3       4       table: 0x010818d6f8     0       0</span>
<span style="color:#75715e">-- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :    3       4       table: 0x010818d518     2       1       5       8</span>
<span style="color:#75715e">-- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :    5       1       table: 0x0108184c80     2       1       2       3</span>
<span style="color:#75715e">-- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :    a       b       table: 0x01081854f8     3       2       c       d       e</span></code></pre></div></li>
</ul>

<h4 id="方法">方法</h4>

<p>调用或定义 table 的一个函数时，可以使用 <code>:</code> 语法糖，以实现类似其他语言方法的能力：</p>

<ul>
<li>定义时使用 <code>:</code>，则可以在函数体里面隐含一个 <code>self</code> 变量，指向调用者。</li>

<li><p>调用时使用 <code>:</code>，则会将调用对象作为参数传递到函数的第一个参数的位置。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> t2 <span style="color:#f92672">=</span> { total <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> }
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">t2</span>:<span style="color:#a6e22e">Add1</span>(a) <span style="color:#75715e">-- 语法糖，隐含一个 self 变量，等价于下方 t2</span>
self.total <span style="color:#f92672">=</span> self.total <span style="color:#f92672">+</span> a
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">t2</span>.<span style="color:#a6e22e">Add2</span>(self, a)
self.total <span style="color:#f92672">=</span> self.total <span style="color:#f92672">+</span> a
<span style="color:#66d9ef">end</span>

t2:Add1(<span style="color:#ae81ff">1</span>) <span style="color:#75715e">-- 语法糖，隐含一个 self 变量传递</span>
t2.Add1(t2, <span style="color:#ae81ff">1</span>)
t2:Add2(<span style="color:#ae81ff">1</span>) <span style="color:#75715e">-- 语法糖，隐含一个 self 变量传递</span>
t2.Add2(t2, <span style="color:#ae81ff">1</span>)
print(<span style="color:#e6db74">&#39;t2.total = &#39;</span>, t2.total)
<span style="color:#75715e">-- t2.total =      4</span></code></pre></div></li>
</ul>

<h3 id="错误处理">错误处理</h3>

<h4 id="产生错误">产生错误</h4>

<p>通过 assert 或者 error 函数可以产生一个错误。如果没有捕捉的话，错误会中断整个程序的执行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">assert(<span style="color:#ae81ff">1</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#39;断言函数的消息, 如果第一个参数是 false, 则触发 error&#39;</span>)
<span style="color:#75715e">-- error (message [, level]) -- 抛出异常</span></code></pre></div>
<h4 id="捕获错误">捕获错误</h4>

<p>通过 pcall 和 xpcall 可以捕捉错误。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">print(<span style="color:#e6db74">&#39;pcall has error&#39;</span>, pcall(<span style="color:#66d9ef">function</span>() error(<span style="color:#e6db74">&#34;my error&#34;</span>) <span style="color:#66d9ef">end</span>))
print(<span style="color:#e6db74">&#39;pcall success&#39;</span>, pcall(<span style="color:#66d9ef">function</span>() <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">end</span>))
print(<span style="color:#e6db74">&#39;xpcall success&#39;</span>, pcall(<span style="color:#66d9ef">function</span>() <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">end</span>, <span style="color:#66d9ef">function</span>(err) print(err) <span style="color:#66d9ef">end</span>))
print(<span style="color:#e6db74">&#39;xpcall 1&#39;</span>, xpcall(<span style="color:#66d9ef">function</span>() error(<span style="color:#e6db74">&#34;my error&#34;</span>) <span style="color:#66d9ef">end</span>, <span style="color:#66d9ef">function</span>(err) print(err) <span style="color:#66d9ef">end</span>))
print(<span style="color:#e6db74">&#39;xpcall 2&#39;</span>, xpcall(<span style="color:#66d9ef">function</span>() error(<span style="color:#e6db74">&#34;my error&#34;</span>) <span style="color:#66d9ef">end</span>, <span style="color:#66d9ef">function</span>(err) print(err) <span style="color:#66d9ef">return</span> err <span style="color:#66d9ef">end</span>))
<span style="color:#75715e">-- pcall has error false   hello.lua:1: my error</span>
<span style="color:#75715e">-- pcall success   true    1</span>
<span style="color:#75715e">-- xpcall success  true    1</span>
<span style="color:#75715e">-- hello.lua:4: my error</span>
<span style="color:#75715e">-- xpcall 1        false   nil</span>
<span style="color:#75715e">-- hello.lua:5: my error</span>
<span style="color:#75715e">-- xpcall 2        false   hello.lua:5: my error</span></code></pre></div>
<h3 id="协程">协程</h3>

<h4 id="创建协程">创建协程</h4>

<p>通过 <code>coroutine.wrap</code> 和 <code>coroutine.create</code> 可以创建一个协程。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> cof1 <span style="color:#f92672">=</span> coroutine.wrap(<span style="color:#66d9ef">function</span>()
    print(<span style="color:#e6db74">&#39;coroutine.wrap called&#39;</span>)
<span style="color:#66d9ef">end</span>)

<span style="color:#66d9ef">local</span> co1 <span style="color:#f92672">=</span> coroutine.create(<span style="color:#66d9ef">function</span>()
    print(<span style="color:#e6db74">&#39;coroutine.create called&#39;</span>)
<span style="color:#66d9ef">end</span>)</code></pre></div>
<h4 id="启动协程">启动协程</h4>

<p>通过 <code>coroutine.wrap</code> 创建的协程，可以通过函数调用的方式启动。</p>

<p>通过 <code>coroutine.create</code> 创建的协程，可以通过 <code>coroutine.resume</code> 方式启动。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">cof1(<span style="color:#e6db74">&#39;cof1&#39;</span>)
coroutine.resume(co1, <span style="color:#e6db74">&#39;co1&#39;</span>)
<span style="color:#75715e">-- coroutine.wrap called   cof1</span>
<span style="color:#75715e">-- coroutine.create called co1</span></code></pre></div>
<h4 id="协程-yield-和-返回值">协程 yield 和 返回值</h4>

<ul>
<li>在协程内部，可以通过 <code>coroutine.yield</code> 函数：

<ul>
<li>暂停该协程的执行。</li>
<li>对该协程 <code>coroutine.resume</code> 的调用将返回，第一个返回值是 bool，表示协程是否没有发生错误。

<ul>
<li>如果第一个返回值为 false，则第二个返回值为错误信息。</li>
<li>如果第一个返回值为 true，第二个极其之后的返回值，是 <code>coroutine.yield</code> 函数传递的内容。</li>
</ul></li>
</ul></li>
<li>当协程函数返回后， 对该协程 <code>coroutine.resume</code> 的调用将返回，返回内容似乎协程函数的返回值。</li>

<li><p>对协程调用 <code>coroutine.resume</code> 时：</p>

<ul>
<li>如果协程没有启动过，<code>coroutine.resume</code> 的参数将作为协程函数的参数进行传递。</li>

<li><p>如果协程启动过，并通过 <code>coroutine.yield</code> 暂停执行，<code>coroutine.resume</code> 的参数将作为 <code>coroutine.yield</code> 的返回值返回。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> co2 <span style="color:#f92672">=</span> coroutine.create(<span style="color:#66d9ef">function</span>(a, msg)
print(<span style="color:#e6db74">&#39;[co2] a, msg: &#39;</span>, a, msg)
<span style="color:#66d9ef">local</span> a3, msg3 <span style="color:#f92672">=</span> coroutine.yield(<span style="color:#ae81ff">2</span>, <span style="color:#e6db74">&#39;coroutine.yield 被调用&#39;</span>)
print(<span style="color:#e6db74">&#39;[co2] a3, msg3: &#39;</span>, a3, msg3)
<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">4</span>, <span style="color:#e6db74">&#39;返回&#39;</span>
<span style="color:#66d9ef">end</span>)

<span style="color:#66d9ef">local</span> ok2, a2, msg2 <span style="color:#f92672">=</span> coroutine.resume(co2, <span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#39;coroutine.resume 第一次调用&#39;</span>)
print(<span style="color:#e6db74">&#39;[main] ok2, a2, msg2: &#39;</span>, ok2, a2, msg2)
<span style="color:#66d9ef">local</span> ok4, a4, msg4 <span style="color:#f92672">=</span> coroutine.resume(co2, <span style="color:#ae81ff">3</span>, <span style="color:#e6db74">&#39;coroutine.resume 第二次调用&#39;</span>)
print(<span style="color:#e6db74">&#39;[main] ok4, a4, msg4: &#39;</span>, ok4, a4, msg4)
<span style="color:#75715e">-- [co2] a, msg:   1       coroutine.resume 第一次调用</span>
<span style="color:#75715e">-- [main] ok2, a2, msg2:   true    2       coroutine.yield 被调用</span>
<span style="color:#75715e">-- [co2] a3, msg3:         3       coroutine.resume 第二次调用</span>
<span style="color:#75715e">-- [main] ok4, a4, msg4:   true    4       返回</span></code></pre></div></li>
</ul></li>
</ul>

<h4 id="其他协程函数">其他协程函数</h4>

<ul>
<li><code>coroutine.status(thread)</code> 获取给定协程对象的状态， dead, running, suspend, normal。</li>
<li><code>coroutine.running()</code> 获取当前函数所在协程对象，main 协程将返回 nil。</li>
</ul>

<h3 id="元表">元表</h3>

<p>在 Lua 中，8 种数据类型值的各种操作，如 + - * / . [] 等，都是通过一种称为 metatable 的机制实现的。</p>

<ul>
<li>除了 userdata、table 类型外，其他每种类型的所有值，都共享一套内建的 metatable。</li>
<li>userdata 和 table，每个对象（实例），都可以配置绑定一个自定义的 metatable。</li>
<li>只有 table 的 metatable 可以 Lua 代码更改，其他只能通过 C 语言修改。即通过 <code>function setmetatable(table: table, metatable?: table) -&gt; table</code> 函数，可以设置一个 table 的元表。

<ul>
<li>table 参数，要自定义元表的 table 类型的值。</li>
<li>metatable 要给 table 绑定的元表，如果为 nil 表示清楚元表。</li>
<li>返回 table 参数。</li>
</ul></li>
</ul>

<p>通过给 table 自定义 metatable ，可以实现类似 Python 、C++ 的运算符重载特性。</p>

<p>一个复数的例子如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">NewComplex</span>(r, i)
    <span style="color:#66d9ef">local</span> o <span style="color:#f92672">=</span> { r <span style="color:#f92672">=</span> r, i <span style="color:#f92672">=</span> i }
    <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">o</span>:<span style="color:#a6e22e">print</span>()
        print(r <span style="color:#f92672">..</span> <span style="color:#e6db74">&#34;+&#34;</span> <span style="color:#f92672">..</span> i <span style="color:#f92672">..</span> <span style="color:#e6db74">&#34;i&#34;</span>)
    <span style="color:#66d9ef">end</span>

    setmetatable(o, {
        __add <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>(a, b)
            <span style="color:#66d9ef">return</span> NewComplex(a.r <span style="color:#f92672">+</span> b.r, a.i <span style="color:#f92672">+</span> b.i)
        <span style="color:#66d9ef">end</span>
    })
    <span style="color:#66d9ef">return</span> o
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">local</span> a <span style="color:#f92672">=</span> NewComplex(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>)
<span style="color:#66d9ef">local</span> b <span style="color:#f92672">=</span> NewComplex(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>)
<span style="color:#66d9ef">local</span> c <span style="color:#f92672">=</span> a <span style="color:#f92672">+</span> b
c.print()
<span style="color:#75715e">-- 4+6i</span></code></pre></div>
<p>元表如果包含key <code>__metatable</code>，则表示：</p>

<ul>
<li>如果某个 table 一旦绑定该元表，则不再允许通过 <code>setmetatable</code> 修改，如果修改，抛出错误。</li>

<li><p>通过 <code>getmetatable</code> 获取到的值为 <code>__metatable</code> 对应的 value。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">D <span style="color:#f92672">=</span> {}
setmetatable(D, {})
setmetatable(D, { __metatable <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;not allow change metatable&#34;</span> }) 
<span style="color:#75715e">-- setmetatable(d, {})  -- 将报错</span>
print(getmetatable(D)) <span style="color:#75715e">-- 获取 metatable 只会返回 __metatable 的值。</span>
<span style="color:#75715e">-- not allow change metatable</span></code></pre></div></li>
</ul>

<p>上文介绍了元表 key <code>__add</code> 对应的是 <code>+</code> 运算符，Lua 中的所有运算符都有对应的 key，如 <code>.</code> 对应 <code>__index</code>，需要注意的是元表的 value 不一定是函数，也可能是其他类型，比如 <code>__index</code> 可以是函数也可以是 table。详细说明参见：<a href="https://www.lua.org/manual/5.1/manual.html#2.8">官方手册</a>。</p>

<p>最后，元表除了可以实现运算符重载外，还可以对 table 的垃圾回收进行配置，更多参见：<a href="https://www.lua.org/manual/5.1/manual.html#2.10">官方手册</a>。</p>

<h3 id="env">env</h3>

<p>回顾一下上文的<a href="#变量">变量</a>章节。 Lua 的变量，分为全局变量和局部变量。其中全局变量一旦被定义，则在后续的所有代码中，都可以通过该变量名直接访问。</p>

<p>实际上，在 Lua 中，全局变量（也包括全局函数标准库）实际上是存储在一个被称为 env 的 table 中的，全局变量名为该表的 key。每个函数都会和一张 env 表绑定。</p>

<ul>
<li>Lua 入口脚本可以理解为一个函数，Lua 解释器会在执行脚本前，创建一张 env 表，并和入口脚本绑定，这个 env 表中包含了标准库中的各种函数如 <code>print</code> 等，需要特别说明的是，这个 env 表中包含一个 <code>_G</code> 指向 env 表自身。</li>
<li>当在入口脚本定义一个函数时，Lua 会该函数的 env 设置为函数定义所在位置的函数绑定 env 表，也就是说当前函数和待调用函数共用一张 env 表。这就是为什么全局函数在函数中定义后，在函数调用结束后，任然可以访问的原因。</li>
<li>在函数中，可以通过 <code>getfenv</code> 获取当前函数绑定的 env 表，可以通过 <code>setfenv</code> 给当前函数设置一张新的 env 表。</li>
</ul>

<p>描述调用一个全局函数的过程（以 <code>print(&quot;hello&quot;)</code> 为例）：</p>

<ul>
<li>获取到当前函数绑定的 env 表，假设这个表为 <code>E</code>，后续操作等价于 <code>E.print(&quot;hello&quot;)</code>，即触发 <code>gettable_event</code> 的行为。</li>
<li>查找 <code>E</code> 中是否存在 key <code>print</code>，如果存在则直接返回并调用函数。否则继续执行后续流程。</li>

<li><p>获取 <code>E</code> 的 metatable 判断是否存在 key <code>__index</code>，如果存在</p>

<ul>
<li>如果是 table 类型，则对该 table 继续触发 <code>gettable_event</code> 行为。</li>

<li><p>如果是函数类型，则返回 <code>__index(E, 'print')</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">F1</span>()
E3 <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Ef</span>()
print(<span style="color:#e6db74">&#34;C&#34;</span>, C)
<span style="color:#75715e">-- 第一个参数和 getfenv 类似，第二个参数为要设置的表</span>
<span style="color:#75715e">-- 可以通过 setmetatable 继承上层环境，形成类似链表的结构</span>
<span style="color:#66d9ef">local</span> newEnv <span style="color:#f92672">=</span> {}
setmetatable(newEnv, { __index <span style="color:#f92672">=</span> getfenv(<span style="color:#ae81ff">1</span>) })
setfenv(<span style="color:#ae81ff">1</span>, newEnv)
E1 <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span> <span style="color:#75715e">-- E1 不会逃逸到全局环境中了</span>
print(<span style="color:#e6db74">&#34;C&#34;</span>, C) <span style="color:#75715e">-- 会查找旧的环境</span>
print(<span style="color:#e6db74">&#34;getfenv&#34;</span>, getfenv) <span style="color:#75715e">-- 会递归的查找 _G</span>
F1()  <span style="color:#75715e">-- F1 定义在顶层，所以 env 仍然是全局 env，所以在外层仍然存在</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">F2</span>() <span style="color:#75715e">-- 该函数定义在 newEnv ，所以 env 是 newEnv</span>
E4 <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>
<span style="color:#66d9ef">end</span>
F2()
<span style="color:#75715e">-- 环境表不设置 _G 的，则找不到全局变量和函数</span>
setfenv(<span style="color:#ae81ff">1</span>, { print <span style="color:#f92672">=</span> print })
print(<span style="color:#e6db74">&#34;C&#34;</span>, C) <span style="color:#75715e">-- 可以看出已经找不到上层定义的 C 函数了。</span>
print(<span style="color:#e6db74">&#34;getfenv&#34;</span>, getfenv) <span style="color:#75715e">-- 可以看出已经找不到 getfenv 全局函数了。</span>
E2 <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> <span style="color:#75715e">-- E1 不会逃逸到全局环境中了</span>
<span style="color:#66d9ef">end</span>

Ef()
print(<span style="color:#e6db74">&#39;E1&#39;</span>, E1)
print(<span style="color:#e6db74">&#39;E2&#39;</span>, E2)
print(<span style="color:#e6db74">&#39;E3&#39;</span>, E3)
print(<span style="color:#e6db74">&#39;E4&#39;</span>, E4)

<span style="color:#75715e">-- C       nil</span>
<span style="color:#75715e">-- C       nil</span>
<span style="color:#75715e">-- getfenv function: builtin#10</span>
<span style="color:#75715e">-- C       nil</span>
<span style="color:#75715e">-- getfenv nil</span>
<span style="color:#75715e">-- E1      nil</span>
<span style="color:#75715e">-- E2      nil</span>
<span style="color:#75715e">-- E3      3</span>
<span style="color:#75715e">-- E4      nil</span></code></pre></div></li>
</ul></li>
</ul>

<p>最后，需要特别说明的是：</p>

<ul>
<li>从函数视角看，每个函数都会绑定一个 env 表。</li>
<li>函数默认 env 表的确定，发生在函数定义阶段，而非调用阶段。</li>
<li>通过 <code>setfenv(f: integer|fun(), table: table) -&gt; function</code> 可以手动设置一个函数的 env 表。第一个参数为要配置的函数，可以函数或者数字，1 表示当前函数，2 表示调用当前函数的函数，以此类推。</li>
<li>同样的通过 <code>getfenv(f?: integer|fun()) -&gt; table</code> 可以获取当前函数的 env 表。</li>
</ul>

<h3 id="模块和包">模块和包</h3>

<p>上文介绍的都是单个 Lua 脚本文件，Lua 也提供了模块和包相关能力，可以通过目录和文件来组织代码。</p>

<p>Lua 模块支持 C 语言编写的动态链接库和 Lua 脚本，本文仅介绍 Lua 脚本。</p>

<h4 id="动态执行代码">动态执行代码</h4>

<p>再介绍模块和包之前，先介绍 Lua 动态执行代码相关的能力。</p>

<p>Lua 是动态的解释型语言，因此也提供了在运行时执行，内容为 lua 代码的字符串或者文件的能力。</p>

<ul>
<li><code>function dofile(filename?: string) -&gt; ...any</code> 将执行 filename 文件的内容，并获取返回值。</li>
<li><code>function load(chunk: string|function, chunkname?: string, mode?: &quot;b&quot;|&quot;bt&quot;|&quot;t&quot;, env?: table) -&gt; function?, 2. error_message: string?</code> 加载一个代码块。如果 chunk 是一个字符串，代码块指这个字符串。 如果 chunk 是一个函数， load 不断地调用它获取代码块的片断。 每次对 chunk 的调用都必须返回一个字符串紧紧连接在上次调用的返回串之后。 当返回空串、nil、或是不返回值时，都表示代码块结束。最终，代码不会执行，而是封住到函数里面返回。</li>
<li><code>function loadstring(text: string, chunkname?: string) -&gt; function?, error_message: string?</code> 将字符串作为 lua 代码到一个函数里面。</li>
<li><code>function loadfile(filename?: string, mode?: &quot;b&quot;|&quot;bt&quot;|&quot;t&quot;, env?: table) -&gt; function?, error_message: string?</code> 和 load 类似。</li>
</ul>

<h4 id="模块定义">模块定义</h4>

<p>在 Lua 中，一个模块就是一个 lua 代码文件，可以通过如下方式导出函数或变量：</p>

<ul>
<li>通过 <code>return</code> 导出。</li>
<li>全局变量 （不推荐）。</li>
</ul>

<p><code>08-module-declare/2.lua</code> 通过 return 导出变量。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> module <span style="color:#f92672">=</span> {}

<span style="color:#75715e">-- 导出的函数</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">module</span>.<span style="color:#a6e22e">PrintModule</span>()
    print(<span style="color:#e6db74">&#39;my module 2&#39;</span>)
<span style="color:#66d9ef">end</span>

<span style="color:#75715e">-- 导出模块</span>
<span style="color:#66d9ef">return</span> module</code></pre></div>
<p><code>08-module-declare/4_1.lua</code> 通过全局变量导出（方式 1），注意如果没有 <code>package.seeall</code>，将无法使用标准库相关函数，如 <code>print</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">module(<span style="color:#e6db74">&#34;mymod4_1&#34;</span>, package.seeall) <span style="color:#75715e">-- 等价于： 08-module-declare/4_2.lua</span>

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">PrintModule</span>()
    print(<span style="color:#e6db74">&#39;my module 4_1&#39;</span>)
<span style="color:#66d9ef">end</span></code></pre></div>
<p><code>08-module-declare/4_2.lua</code> 通过全局变量导出（方式 2）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua">mymod4_2 <span style="color:#f92672">=</span> {}
setmetatable(mymod4_2, {__index<span style="color:#f92672">=</span>getfenv(<span style="color:#ae81ff">1</span>)})
setfenv(<span style="color:#ae81ff">1</span>, mymod4_2)

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">PrintModule</span>()
    print(<span style="color:#e6db74">&#39;my module 4_2&#39;</span>)
<span style="color:#66d9ef">end</span></code></pre></div>
<h4 id="导入模块">导入模块</h4>

<p>通过 <code>function require(modname: string) -&gt; ...</code> 函数可以导入一个模块。</p>

<ul>
<li>参数为模块名，影响代码文件的搜索过程，参见下文。</li>

<li><p>返回值为模块代码的 <code>return</code> 语句的内容。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> mymod2 <span style="color:#f92672">=</span> require(<span style="color:#e6db74">&#39;08-module-declare.2&#39;</span>)
mymod2.PrintModule()
require(<span style="color:#e6db74">&#39;08-module-declare.4_1&#39;</span>)
mymod4_1.PrintModule()
require(<span style="color:#e6db74">&#39;08-module-declare.4_2&#39;</span>)
mymod4_2.PrintModule()
<span style="color:#75715e">-- my module 2</span>
<span style="color:#75715e">-- my module 4_1</span>
<span style="color:#75715e">-- my module 4_2</span></code></pre></div></li>
</ul>

<p><code>require</code> 的执行过程如下所示：</p>

<ul>
<li>依次调用 <code>package.loaders</code> 数组中的模块加载器函数（声明和 <code>require</code> 函数一致），默认情况包含 4 个加载函数。

<ul>
<li>第 1 个加载器会从 <code>package.preload</code> 表中查找</li>
<li>第 2 个加载器会从从 <code>package.path</code> 中查找对应的 lua 文件。

<ul>
<li>如 <code>package.path = ./?.lua;/usr/local/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua</code>。</li>
<li>此时，会将 require 参数的 <code>.</code> 替换为 <code>/</code> 然后替换 package.path 参数中的 ?，到对应的文件中去加载模块。</li>
</ul></li>
<li>第 3 个加载器会从 <code>package.cpath</code> 中查找对应的 C 动态库。并调用 <code>luaopen_</code> + 模块名最后一个 - 的后面的字符串并将 . 替换为 _（如 <code>a.v1-b.c</code> -&gt; <code>luaopen_b_c</code>）。</li>
<li>第 4 个加载器，如 <code>a.b.c</code> 模块，会搜索 <code>a</code> 库，并调用 <code>luaopen_a_b_c</code> 函数。</li>
</ul></li>
<li>加载完成后，结果记录将到 <code>package.loaded[modname]</code> 中，下次再次 <code>require</code> 将直接从这个变量中取。</li>
<li>如果 reqire 找不到，将抛出错误。</li>
</ul>

<h2 id="标准库">标准库</h2>

<p>标准库也是 Lua 语言的一部分，上文提到的函数实际上都是标准库的一部分，更多关于标准库的详解，参见：<a href="https://www.lua.org/manual/5.1/manual.html#5">官方手册</a>。</p>

<h2 id="高级用法">高级用法</h2>

<h3 id="面相对象风格">面相对象风格</h3>

<p>Lua 原生没有提供面向对象的能力，但是可以通过元表实现类似的效果，一个示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#75715e">-- 通过 table 和 metatable 实现面向对象</span>

<span style="color:#75715e">-- 类、抽象父类、子类、单继承、实例化。</span>

<span style="color:#75715e">-- 抽象父类</span>
Shape <span style="color:#f92672">=</span> { type<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;none&#39;</span> }
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Shape</span>:<span style="color:#a6e22e">new</span>(type) <span style="color:#75715e">-- 父类构造函数</span>
    <span style="color:#66d9ef">local</span> o <span style="color:#f92672">=</span> {type<span style="color:#f92672">=</span>type}
    setmetatable(o, { __index <span style="color:#f92672">=</span> Shape })
    <span style="color:#66d9ef">return</span> o
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Shape</span>:<span style="color:#a6e22e">print</span>() <span style="color:#75715e">-- 父类方法</span>
    print(<span style="color:#e6db74">&#34;type: &#34;</span>, self.type , <span style="color:#e6db74">&#34;area:&#34;</span>, self:getArea())
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Shape</span>:<span style="color:#a6e22e">getArea</span>() <span style="color:#75715e">-- 待子类实现的方法</span>
    error(<span style="color:#e6db74">&#39;no impl&#39;</span>)
<span style="color:#66d9ef">end</span>

<span style="color:#75715e">-- 子类</span>
Rectangle <span style="color:#f92672">=</span> { length <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, breadth <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> }
setmetatable(Rectangle, { __index <span style="color:#f92672">=</span> Shape }) <span style="color:#75715e">-- 继承父类</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Rectangle</span>:<span style="color:#a6e22e">new</span>(length, breadth) <span style="color:#75715e">-- 子类构造函数</span>
    <span style="color:#66d9ef">local</span> o <span style="color:#f92672">=</span> Shape:new(<span style="color:#e6db74">&#39;Rectangle&#39;</span>)
    o.length <span style="color:#f92672">=</span> length
    o.breadth <span style="color:#f92672">=</span> breadth
    setmetatable(o, { __index <span style="color:#f92672">=</span> Rectangle })
    <span style="color:#66d9ef">return</span> o
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Rectangle</span>:<span style="color:#a6e22e">getArea</span>() <span style="color:#75715e">-- 子类实现父类方法 getArea</span>
    <span style="color:#66d9ef">return</span> self.length <span style="color:#f92672">*</span> self.breadth
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Rectangle</span>:<span style="color:#a6e22e">diagonal</span>()  <span style="color:#75715e">-- 子类自己的方法</span>
    <span style="color:#66d9ef">return</span> (self.length<span style="color:#f92672">^</span><span style="color:#ae81ff">2</span> <span style="color:#f92672">+</span> self.breadth<span style="color:#f92672">^</span><span style="color:#ae81ff">2</span>)<span style="color:#f92672">^</span>(<span style="color:#ae81ff">0.5</span>)
<span style="color:#66d9ef">end</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Rectangle</span>:<span style="color:#a6e22e">print</span>() <span style="color:#75715e">-- 子类覆写方法 print</span>
    print(<span style="color:#e6db74">&#39;length: &#39;</span>, self.length, <span style="color:#e6db74">&#39;breadth: &#39;</span>, self.breadth)
    Shape.print(self) <span style="color:#75715e">-- 子类调用父类的方法</span>
    print(<span style="color:#e6db74">&#39;and diagonal: &#39;</span>, self:diagonal())
<span style="color:#66d9ef">end</span>

<span style="color:#75715e">-- 实例化</span>
<span style="color:#66d9ef">local</span> r <span style="color:#f92672">=</span> Rectangle:new(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>)
r:print()</code></pre></div>
<h3 id="推荐的模块写法">推荐的模块写法</h3>

<p>定义模块</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#75715e">-- square.lua 长方形模块</span>
<span style="color:#66d9ef">local</span> _M <span style="color:#f92672">=</span> {}           <span style="color:#75715e">-- 局部的变量</span>
_M._VERSION <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;1.0&#39;</span>     <span style="color:#75715e">-- 模块版本</span>

<span style="color:#66d9ef">local</span> mt <span style="color:#f92672">=</span> { __index <span style="color:#f92672">=</span> _M }

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">_M</span>.<span style="color:#a6e22e">new</span>(self, width, height)
    <span style="color:#66d9ef">return</span> setmetatable({ width<span style="color:#f92672">=</span>width, height<span style="color:#f92672">=</span>height }, mt)
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">_M</span>.<span style="color:#a6e22e">get_square</span>(self)
    <span style="color:#66d9ef">return</span> self.width <span style="color:#f92672">*</span> self.height
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">_M</span>.<span style="color:#a6e22e">get_circumference</span>(self)
    <span style="color:#66d9ef">return</span> (self.width <span style="color:#f92672">+</span> self.height) <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span>
<span style="color:#66d9ef">end</span>

<span style="color:#66d9ef">return</span> _M</code></pre></div>
<p>导入模块</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-lua" data-lang="lua"><span style="color:#66d9ef">local</span> square <span style="color:#f92672">=</span> require <span style="color:#e6db74">&#34;square&#34;</span>

<span style="color:#66d9ef">local</span> s1 <span style="color:#f92672">=</span> square:new(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>)
print(s1:get_square())          <span style="color:#75715e">--output: 2</span>
print(s1:get_circumference())   <span style="color:#75715e">--output: 6</span></code></pre></div>
<p>更多参见：</p>

<ul>
<li><a href="https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/not_use_module.html">抵制使用 module() 定义模块</a></li>
<li><a href="https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/module_is_evil.html">module 是邪恶的</a></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://blog.51cto.com/u_14861909/5441626">LuaJIT分支和标准Lua有什么不同？</a></li>
<li><a href="https://groups.google.com/g/OpenResty/c/0ftsRpgC5kE">开源项目Luajit是否有“死亡”的风险？</a></li>
<li><a href="https://github.com/LuaJIT/LuaJIT/issues/563">LuaJIT 如何发行？什么样的代码是稳定的？</a></li>
<li><a href="http://luajit.org/install.html">LuaJIT 官方安装手册</a>。</li>
<li><a href="https://www.codingnow.com/2000/download/lua_manual.html">Lua 5.1 参考手册（中文翻译）</a></li>
<li><a href="https://www.lua.org/manual/5.1/">Lua 5.1 Reference Manual</a></li>
<li><a href="https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/main.html">OpenResty最佳实践 - Lua 入门</a></li>
</ul>
]]></description></item><item><title>Nreal Air 开箱体验</title><link>https://www.rectcircle.cn/posts/nreal-air-out-of-the-box/</link><pubDate>Sun, 28 Aug 2022 15:10:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/nreal-air-out-of-the-box/</guid><description type="html"><![CDATA[

<h2 id="开箱">开箱</h2>

<p>2022-08-25 在天猫预订，08-27 付尾款，顺丰发货，次日 08-28 到货。</p>

<p><img src="/image/nreal-12.jpeg" alt="image" /></p>

<p><img src="/image/nreal-11.jpeg" alt="image" /></p>

<p><img src="/image/nreal-10.jpeg" alt="image" /></p>

<p><img src="/image/nreal-09.jpeg" alt="image" /></p>

<p><img src="/image/nreal-08.jpeg" alt="image" /></p>

<p><img src="/image/nreal-07.jpeg" alt="image" /></p>

<p><img src="/image/nreal-06.jpeg" alt="image" /></p>

<p>眼镜特写</p>

<p><img src="/image/nreal-04.jpeg" alt="image" /></p>

<p><img src="/image/nreal-05.jpeg" alt="image" /></p>

<h2 id="初体验">初体验</h2>

<h3 id="网页激活">网页激活</h3>

<p>打开 <a href="https://www.nreal.cn/support/activationGlasses/">https://www.nreal.cn/support/activationGlasses/</a> 选择浏览器激活。</p>

<h3 id="直连-macbook-pro-空中投屏">直连 MacBook Pro （空中投屏）</h3>

<p>刚开始折腾半天不亮以为是坏的，后面找了一台支持 DP 输出的安卓设备，安装 App，重新激活了下，然后 app 会进行几次固件升级。</p>

<p>升级完成后，又尝试直连 MacBook Pro，发现还是不亮，后来观察了下眼镜内侧，发下有一个距离传感器（猜测），才恍然大悟。遮住这个距离传感器终于亮了。说明书竟然也没有相关说明。</p>

<p>如果遇到类似的情况，建议：</p>

<ul>
<li>先遮住距离传感器看是否亮屏。</li>
<li>如果不行找一台支持 DP 输出的安卓设备重新激活并升级固件。升级完成后再直连电脑，再遮住距离传感器。</li>
</ul>

<p>点亮后的效果如下：</p>

<ul>
<li><p>正面，即第三人视角，可以看出别人是可以看到你的眼睛在发光的，隐私性一般（似乎光波导技术不会有这种情况）。</p>

<p><img src="/image/nreal-01.jpeg" alt="image" /></p></li>

<li><p>背面，佩戴者视角。</p>

<p><img src="/image/nreal-02.jpeg" alt="image" /></p></li>

<li><p>背面，单眼特写。</p>

<p><img src="/image/nreal-03.jpeg" alt="image" /></p></li>
</ul>

<p>显示效果上，指的一提的是，远远强于 Pico Neo 3 VR 设备，没有颗粒感（感觉原因是：AR 视场角远小于 VR，相当于屏幕更小了，像素密度因此就更高）。此外，由于有外部光线，因此长时间使用眼睛也不会难受。</p>

<p>需要注意的是：</p>

<ul>
<li>画面大小观感，相当于距眼睛 74 厘米（用皮尺测量的）的 27 寸显示器的大小，用于办公，没有现象中的大，只能说够用。</li>
<li>只支持 0DoF，无法固定到某个固定个位置，无法虚拟出多块副屏（官方论坛说: <a href="https://bbs.nreal.cn/post/b2eae1a4a642415fbde1295b7d866ca5?csr=1">Mac M1 9 月会支持 3DoF</a>）。</li>
</ul>

<h3 id="ar-空间">AR 空间</h3>

<p>AR 空间需要安装 <a href="https://www.nreal.cn/nrealapp">Nreal App</a> 实测 小米11 不支持，插进去没反应。</p>

<p>刚好有一台支持 USB 3 以及 DP 输出的安卓平板（联想小新 Pad Pro 2021），安装 App 后连接，可以正常进入。</p>

<p>体验感受如下：</p>

<ul>
<li>追踪还是挺准的。</li>
<li>可以将窗口固定到固定位置，体验明显比空间投屏好。</li>
<li>好像支持 XR 无线串流，利用这个应该可以投电脑的画面并固定到固定位置（未测试）。</li>
</ul>

<p>直观的体验，可以去 bilibili 搜索各大 Up 主的视频。</p>

<h2 id="问题和建议">问题和建议</h2>

<ul>
<li>希望完善生态，尽快开发 Mac、Windows 甚至 Linux 版的 App，以支持多虚拟显示器并可以固定到固定位置，这样才能充分发挥眼镜在生产力（办公）方面的能力。</li>
<li>关于距离传感器的问题，建议在说明书里面标识出来。另外，希望可以通过 App 配置关闭距离传感器自动灭屏的功能，甚至默认设置为关闭。原因是，用户刚购买时，没有配镜片，如果内部再带一个近视眼镜的话，经常误触发，忽亮忽灭，会以为设备有问题。</li>
<li>建议即使不支持 USB3 以及 DP 的手机设备，App 可以不支持 AR 空间，但是也要支持连接眼镜进行激活、固件升级和配置等相关能力（这几个能力又不依赖 USB3 的高带宽）。</li>
<li>鼻托卡明显是设计缺陷，插拔太难了。</li>

<li><p>AR 空间的触摸板建议添加新的操控模式：</p>

<ul>
<li>不使用陀螺仪</li>
<li>单指滑动光笔移动</li>
<li>双支滑动滑动页面</li>
<li>三指滑动等可以参考 mac 的触摸板</li>
</ul>

<p>这个需要这个模式的原因是：我用的是 Pad 拿起来去操控太累了。即使是手机，动辄 200g 的重量感觉也不轻松。</p></li>

<li><p>不知道是否有边充电边连接眼镜的转接头，感觉眼镜耗电还是挺多的，安卓设备只有一个接口，有续航焦虑很难受。</p></li>

<li><p>AR 空间支持打开普通的 Android App，并固定到某个位置，这样可玩性就大大增加了（比如利用 AirScreen 将电脑投屏到 AR 空间的一个位置中）。</p></li>
</ul>
]]></description></item><item><title>frp 源码浅析</title><link>https://www.rectcircle.cn/posts/frp-source/</link><pubDate>Sat, 27 Aug 2022 16:12:13 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/frp-source/</guid><description type="html"><![CDATA[

<h2 id="frp-介绍">frp 介绍</h2>

<p>frp 是一套通用的，基于 C/S 架构的内网穿透软件。软件分为两个部分：client (frpc) 和 server (frps)。</p>

<p>该软件的基本使用流程：</p>

<ul>
<li>部署：首先需要将 frps 部署到具有公网 IP 的主机上（远端主机）；</li>
<li>暴露端口：然后在需要暴露端口内网的设备上（本地主机），运行 frpc，并通过配置文件指定指定：

<ul>
<li>要暴露的本地主机的本地端口；</li>
<li>要暴露端口映射到的远端主机的远端端口；</li>
</ul></li>
<li>访问端口：在任意一台设备，通过远端主机公网 IP 和 暴露到远端端口即可访问到本地主机上的本地端口。</li>
</ul>

<p>更详细的使用教程参见：<a href="https://gofrp.org/docs/">官方文档</a>。</p>

<p>本文介绍 frp 版本为：<a href="https://github.com/fatedier/frp/tree/v0.44.0">v0.44.0</a>。</p>

<h2 id="frp-架构图">frp 架构图</h2>

<p><img src="/image/frp-architecture.png" alt="image" /></p>

<p>（图片来源 <a href="https://github.com/fatedier/frp/tree/v0.44.0#architecture">github</a>）</p>

<p>该图主要描述的是访问端口的流量情况，用户流量经过部署在具有公网 ip 的主机上的 frps 中转，发送到位于任意内网主机上的 frpc，frpc 再将流量转发到内网主机上的端口。</p>

<p>下文，从 frps 的源码，本部分仅介绍暴露和访问 tcp 端口的流程，忽略鉴权和插件等细节。</p>

<h2 id="frps-启动流程">frps 启动流程</h2>

<ul>
<li><a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/cmd/frps/main.go#L27">cmd/frps/main.go:27</a>: frps main 函数，最终会调用 <code>rootCmd.RunE</code>。</li>
<li><a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/cmd/frps/root.go#L105"><code>rootCmd.RunE</code></a>:

<ul>
<li>首先，读取从命令行和配置文件读取配置。</li>
<li>最终，调用 <code>svr.Run</code></li>
</ul></li>
<li><a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/service.go#L305"><code>svr.Run</code></a>: 调用 <code>svr.HandleListener</code>，接收来自客户端的请求，支持多种协议：kcp、websocket、TCP(tls)、TCP，下文主要介绍的是基于 TCP 协议。</li>
<li><a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/service.go#L382"><code>svr.HandleListener</code></a>: Accept 等待客户端的连接。具体连接处理流程参见下文。</li>
</ul>

<h2 id="frps-连接处理">frps 连接处理</h2>

<p>该小结主要介绍 frpc 和 frps 之间进行端口暴露的流程。</p>

<h3 id="tcp-mux">tcp_mux</h3>

<p>当 frpc 启动后，在需要暴露端口时，会建立和 frps 建立一个连接。此时 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/service.go#L382"><code>svr.HandleListener</code></a> Accept 会返回 <code>net.Conn</code>，并会启动一个协程来处理请求，处理流程分为两种情况：</p>

<ul>
<li><code>common.tcp_mux</code> 配置如果为 true （<a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/pkg/config/server.go#L133">默认</a>），该 <code>net.Conn</code> 将会通过 <code>yamux</code> 进行建立一个 <strong>Server</strong> 侧的 <code>yamux.Session</code>，并等待客户端的在该 Session 建立逻辑连接，<code>yamux.Session</code> 获取到 <code>net.Conn</code>  后，会启动一个协程调用 <code>svr.handleConnection</code> 进行处理。</li>
<li><code>common.tcp_mux</code> 配置如果为 false，该 <code>net.Conn</code> 会直接调用 <code>svr.handleConnection</code> 进行处理。</li>
</ul>

<p>说明：这里很关键，如果 <code>tcp_mux</code> 位 true， frpc 和 frps 间每个暴露的端口，只建立一个 tcp 连接，访问该端口的所有的逻辑连接的流量都在该物理连接上利用 <code>yamux</code> 多路复用起在该 tcp 连接上进行传输。也就是说，在操作系统层面，只能看到一个连接 TCP 连接。否则，每个请求 frpc 和 frps 之间会都会建立一个 tcp 连接。</p>

<h3 id="连接类型">连接类型</h3>

<p>从 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/service.go#L319">svr.handleConnection</a> 可以看出，每个链接建立后，客户端会发送一个握手消息，这个消息标识了，frpc 和 frps 间的三种连接类型：</p>

<ul>
<li><code>msg.Login</code> 控制连接</li>
<li><code>msg.NewWorkConn</code> 工作连接</li>
<li><code>msg.NewVisitorConn</code> 访问连接</li>
</ul>

<p>其中，访问连接应该是为了实现端到端加密场景使用的（<a href="https://gofrp.org/docs/concepts/">stcp、sudp</a>），在此不多深究了。</p>

<h3 id="控制连接">控制连接</h3>

<p>针对 <code>msg.Login</code> 控制连接，会进入 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/service.go#L436"><code>svr.RegisterControl</code></a> 进行处理，主要流程为：</p>

<ul>
<li>进行权限校验</li>
<li>构造并启动一个控制器 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L189"><code>ctl.Start</code></a>：

<ul>
<li>回复 frpc 登录成功。</li>
<li>通过该控制连接给 frpc 发送命令，让 frpc 建立多个工作连接（即工作连接池）。</li>
<li>启动 <code>ctl.manager</code> 协程，该协程会读取 frpc 通过该控制连接发送给 frps 的一些消息，并处理，细节参见下文。</li>
<li>启动 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L318"><code>ctl.reader</code></a> 协程，读取控制连接发送的原始数据流，解析成控制命令，并通过 chan 交由 <code>ctl.manager</code> 处理。</li>
<li>启动 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L347"><code>ctl.stoper</code></a> 协程，等待关闭信号，用于关闭并清理资源。</li>
</ul></li>
</ul>

<p>在此，重点介绍 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L405"><code>ctl.manager</code></a> ，即控制器的核心逻辑，主要处理三种类型消息：</p>

<ul>
<li><code>msg.NewProxy</code> 新建一个 Proxy，frpc 在接收到登录成功，会根据暴露端口的配置，告知 frps 创建一个指定类型的 proxy，逻辑位于 ctl.RegisterProxy，参见下文。</li>
<li><code>msg.CloseProxy</code> 关闭一个 Proxy。</li>
<li><code>msg.Ping</code> 心跳消息。</li>
</ul>

<p>建立 proxy 的 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L501"><code>ctl.RegisterProxy</code></a> 主要流程如下：</p>

<ul>
<li><code>proxy.NewProxy</code> 初始化一个 Proxy 对象，以 TCP 为例，将构造一个 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/proxy/tcp.go#L25"><code>proxy.TCPProxy</code></a> 对象（其中核心参数为 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L231"><code>ctl.GetWorkConn</code></a>，后文有介绍）。</li>
<li>然后调用 <code>pxy.Run()</code> ，以 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/proxy/tcp.go#L32"><code>proxy.TCPProxy</code></a> 为例：会在 frps 所在主机上监听一个 TCP 端口，并启动一个协程，该协程会 Accept 连接到该端口的 TCP 连接。这个端口就是提供给用户访问的端口，处理逻辑参见：<a href="#访问-frps-暴露的端口">访问 frps 暴露的端口</a>.</li>
</ul>

<h3 id="工作连接">工作连接</h3>

<p>针对 <code>msg.NewWorkConn</code> 控制连接，会进入 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L208"><code>svr.RegisterWorkConn</code></a> 进行处理，主要流程为：</p>

<ul>
<li>将该工作连接发送给 <code>ctl.workConnCh</code> 通道。</li>
<li><code>ctl.WorkConnCh</code> 通道的接受处理函数位于 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L231"><code>ctl.GetWorkConn</code></a>，细节参见：<a href="#访问-frps-暴露的端口">访问 frps 暴露的端口</a>。</li>
</ul>

<h2 id="访问-frps-暴露的端口">访问 frps 暴露的端口</h2>

<p>上文 <a href="#frps-连接处理">连接处理</a> 介绍了 frpc 和 frps 之间进行暴露端口的流程。本部分将介绍，端口暴露到 frps 的主机后，用户访问该端口的流程（以 TCP 为例）。</p>

<p>如上文提到，这个端口的处理函数函数位于： <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/proxy/proxy.go#L151"><code>pxy.startListenHandler</code></a> 当用户建立连接后，会调用：<a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/proxy/proxy.go#L252"><code>HandleUserTCPConnection</code></a> 进行处理：</p>

<ul>
<li>调用 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/proxy/proxy.go#L96"><code>pxy.GetWorkConnFromPool</code></a> 函数，获取一条和 frpc 之间的<a href="#工作连接">工作连接</a>。上文可以得知，该函数的实现位于 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L231"><code>ctl.GetWorkConn</code></a>。

<ul>
<li>从 <code>ctl.WorkConnCh</code> 获取 <a href="#工作连接">工作连接</a> 该连接是由 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/server/control.go#L208"><code>svr.RegisterWorkConn</code></a> 发送的。

<ul>
<li>如果能获取到，返回该工作连接（说明连接池中存在可用的连接）。</li>
<li>否则通过<a href="#控制连接">控制连接</a>，让 frpc 建立一条 <a href="#工作连接">工作连接</a>（默认需 10 秒钟内建立成功，配置项为 <code>common.user_conn_timeout</code>，超时将失败）。</li>
</ul></li>
<li>调用 <a href="https://github.com/fatedier/golib/blob/dev/io/io.go"><code>frpIo.Join</code></a> 相互拷贝两个连接，完成流量从 frps 转发到 frpc。</li>
</ul></li>
</ul>

<p>frpc 创建好<a href="#工作连接">工作连接</a>后，会调用 <a href="https://github.com/fatedier/frp/blob/8888610d8339bb26bbfe788d4e8edfd6b3dc9ad6/client/proxy/proxy.go#L720">HandleTCPWorkConnection</a> 函数，最终使用 <code>net.Dial</code> 打开一个访问 127.0.0.1 对应本地端口的本地连接。并调用 <a href="https://github.com/fatedier/golib/blob/dev/io/io.go"><code>frpIo.Join</code></a> 进行<a href="#工作连接">工作连接</a>和该本地连接的进行相互拷贝。</p>

<p>至此流量就进入了本地的服务中了。</p>

<h2 id="总结">总结</h2>

<p><img src="/image/frp-tcp-flow.svg" alt="image" /></p>

<p>上图介绍了一个 tcp 端口暴露到公网以及访问的主要流程，需要注意的是：</p>

<ul>
<li>忽略鉴权和插件相关细节。</li>
<li>假设没有启用连接池的场景。</li>
<li><code>2.3</code> 多条工作连接强调的是存在并发访问时，会创建多条工作连接，但：

<ul>
<li>每个 <code>2.2 请求建立工作连接</code> 只会建立一个连接。</li>
<li>一个请求只会用到一个工作连接。</li>
</ul></li>
<li>如果开启了 <code>tcp_mux</code>（默认），上图控制连接和工作连接都是跑在 yamux 封装的一条 TCP 连接。</li>
</ul>

<h2 id="其他说明">其他说明</h2>

<ul>
<li>Go 的 io.Copy 只会单向拷贝，在双向拷贝的场景可以参考：<a href="https://github.com/fatedier/golib/blob/dev/io/io.go"><code>frpIo.Join</code></a>。</li>
<li><code>tcp_mux</code> 是多路复用，具体细节参见 <a href="https://github.com/hashicorp/yamux">hashicorp/yamux</a> 实现。</li>
<li><code>tcp_mux</code> 可能存在无法跑满带宽的问题，具体参见：<a href="https://github.com/fatedier/frp/issues/2987">issue</a>。</li>
<li>如何想把 websocket 连接作为原生的 <code>net.Conn</code> 处理，建议使用 <a href="https://pkg.go.dev/golang.org/x/net/websocket"><code>golang.org/x/net/websocket</code></a> 库，而非 <a href="https://pkg.go.dev/github.com/gorilla/websocket">github.com/gorilla/websocket</a>。</li>
</ul>
]]></description></item><item><title>Consul 服务发现</title><link>https://www.rectcircle.cn/posts/consul-service-decovery/</link><pubDate>Thu, 18 Aug 2022 18:35:09 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/consul-service-decovery/</guid><description type="html"><![CDATA[

<blockquote>
<p>Version: 1.31.1</p>
</blockquote>

<h2 id="概述">概述</h2>

<p>Consul 被官方定义为多网络工具，提供功能齐全的服务网格解决方案。Consul 提供了一种软件驱动的路由和分段方法。它还带来了额外的好处，例如故障处理、重试和网络可观察性。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建完整的服务网格并实现零信任安全。</p>

<p>最早 Consul 的就是一个高可用的服务注册和发现的注册中心，近些年来，Consul 引入了服务网格（service mesh）。从其官方网站来开，官方希望 Consul 可以提供一整套服务网格的解决方案。</p>

<p>本文，主要从使用者的角度，来介绍 Consul 服务发现相关的特性的基本使用以及安装和部署。其他关于 kv 存储、服务网格等特性，本文暂不涉及。此外，文本也不重点介绍 Consul 底层的高可用的原理和算法。</p>

<!--
本文将，先从最小化安装和使用 Consul 开始（[快速开始](#快速开始)）；然后从如何部署运维 ([安装和部署](#安装和部署))、如何使用 ([核心特性](#核心特性))，两个角度详细介绍 Consul。

-->

<h2 id="快速开始">快速开始</h2>

<h3 id="单机运行">单机运行</h3>

<p>由于 Consul 是 Go 实现的，而具有 Go 优秀的跨平台特性，因此在任何平台安装和运行 Consul 非常容易，只需从其 <a href="https://www.consul.io/downloads">官方下载站点</a> ，下载相应平台的 zip 包，解压后即可直接运行 <code>./consul</code> 命令即可运行（或加入 <code>PATH</code>），关于下载，更多参见：<a href="https://learn.hashicorp.com/tutorials/consul/get-started-install">Install Consul</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd ~/Downloads
unzip consul_1.13.1_darwin_amd64.zip
sudo mv consul /usr/local/bin</code></pre></div>
<p>安装完成后，通过如下命令，以 dev 模式运行：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">consul agent -dev -node machine</code></pre></div>
<p>核心输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">==&gt; Starting Consul agent...
           Version: &#39;1.13.1&#39;
        Build Date: &#39;2022-08-11 19:07:00 +0000 UTC&#39;
           Node ID: &#39;d4f7830e-00e8-9405-fa38-ec873c85b295&#39;
         Node name: &#39;machine&#39;
        Datacenter: &#39;dc1&#39; (Segment: &#39;&lt;all&gt;&#39;)
            Server: true (Bootstrap: false)
       Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
      Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false</pre></div>
<ul>
<li>Node Name：是 Agent 的唯一名称。默认情况下，这是机器的主机名，但您可以使用 <code>-node</code> 标志对其进行自定义。</li>
<li>Datacenter：配置代理运行的数据中心。对于单 DC 配置，代理将默认为 <code>dc1</code>，您可以使用 -datacenter 配置。 Consul 对多个数据中心具有一流的支持，但配置每个节点以报告其数据中心可提高代理效率。</li>
<li>Server：表明 Agent 是在 Server 还是 Client 模式下运行。在服务器模式下运行代理需要额外的开销。这是因为它们参与了共识仲裁、存储集群状态并处理查询。服务器也可能处于  &ldquo;bootstrap&rdquo; 模式，这使服务器能够选举自己作为 Raft 领导者。多个服务器不能处于引导模式，因为它会使集群处于不一致的状态。</li>
<li>Client Addr：这是用于 Client 连接的接口的地址。这包括 HTTP 和 DNS 接口的端口。默认情况下，这仅绑定到 localhost。如果更改此地址或端口，则在运行 consul members 等命令时指定 <code>-http-addr</code> 以指示如何访问代理。其他应用程序也可以使用 HTTP 地址和端口来控制 Consul。</li>
<li>Cluster Addr：这是用于集群中 Consul Agent 之间通信的地址和端口集。并不要求集群中的所有 Consul Agent 都必须使用相同的端口，但所有其他节点必须可以访问此地址。</li>
</ul>

<p>注意：如果使用 systemd 运行 Consul 且配置了 -join 时，service 配置文件需配置 <a href="https://askubuntu.com/questions/1120023/how-to-use-systemd-notify">Type=notify</a>。</p>

<p>该模式将启用单个 Server Agent，并打印 Debug 日志，不能在真实生产环境使用，如何在生产模式部署，参见：<a href="#安装和部署">下文</a>。</p>

<p>此时，通过浏览器打开 <a href="http://127.0.0.1:8500">http://127.0.0.1:8500</a> 即可打开 Consul 的 WebUI。</p>

<p>下文将，通过 <a href="http://localhost:8500">http://localhost:8500</a> 端口的 HTTP API 注册和查询服务。需要注意的是：</p>

<ul>
<li>注册和取消服务，需使用 <code>/v1/agent</code> 接口。</li>
<li>发现服务，需使用 <code>/v1/catalog</code> 和 <code>/v1/health</code> 接口。</li>
</ul>

<h3 id="基本使用">基本使用</h3>

<p>本部分主要介绍如何通过 HTTP API 进行 Consul 的服务注册和发现的基本使用方法。</p>

<p>假设我们有两个服务，分别是 <code>test-service-1</code> 和 <code>test-service-2</code>。</p>

<p><code>test-service-1</code> 包含两个实例，一个是健康的 (<code>9000</code>)，一个是未启动的 (<code>9001</code>)。<code>test-service-2</code> 有两个健康实例 (<code>9010</code> 和 <code>9011</code>)，使用 nc 命令模拟这两个服务。</p>

<ul>
<li><code>while true; do echo -e &quot;HTTP/1.1 200 OK\n\ntest-service-1 (instance1): $(date)&quot; | nc -l 9000; if [ $? -ne 0 ]; then break; fi; done</code></li>
<li><code>while true; do echo -e &quot;HTTP/1.1 200 OK\n\ntest-service-2 (instance1): $(date)&quot; | nc -l 9010; if [ $? -ne 0 ]; then break; fi; done</code></li>
<li><code>while true; do echo -e &quot;HTTP/1.1 200 OK\n\ntest-service-2 (instance2): $(date)&quot; | nc -l 9011; if [ $? -ne 0 ]; then break; fi; done</code></li>
</ul>

<h4 id="注册服务">注册服务</h4>

<blockquote>
<p>更多参见： <a href="https://www.consul.io/api-docs/agent/service#register-service">Service - Agent HTTP API - 注册服务</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 注册第 1 个服务的第 1 个实例</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-1-instance-1&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-1&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9000,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9000&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register

<span style="color:#75715e"># 注册第 1 个服务的第 2 个实例</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-1-instance-2&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-1&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9001,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9001&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register

<span style="color:#75715e"># 注册第 2 个服务的第 1 个实例</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-2-instance-1&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-2&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9010,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9010&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register

<span style="color:#75715e"># 注册第 2 个服务的第 2 个实例</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-2-instance-2&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-2&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9011,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9011&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register</code></pre></div>
<h4 id="发现服务">发现服务</h4>

<blockquote>
<p>参见：<a href="https://learn.hashicorp.com/tutorials/consul/get-started-service-discovery?in=consul/getting-started#query-services">Query services</a></p>
</blockquote>

<p><strong>方式 1：通过 DNS</strong></p>

<p>只返回健康的实例。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">dig @127.0.0.1 -p <span style="color:#ae81ff">8600</span> test-service-1.service.consul SRV</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">; &lt;&lt;&gt;&gt; DiG 9.10.6 &lt;&lt;&gt;&gt; @127.0.0.1 -p 8600 test-service-1.service.consul SRV
; (1 server found)
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 9930
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;test-service-1.service.consul. IN      SRV

;; ANSWER SECTION:
test-service-1.service.consul. 0 IN     SRV     1 1 9000 7f000001.addr.dc1.consul.

;; ADDITIONAL SECTION:
7f000001.addr.dc1.consul. 0     IN      A       127.0.0.1
machine.node.dc1.consul. 0      IN      TXT     &#34;consul-network-segment=&#34;

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Sun Aug 21 14:58:43 CST 2022
;; MSG SIZE  rcvd: 167</pre></div>
<p><strong>方式 2：catalog HTTP API</strong></p>

<p>返回所有实例（细节参见：<a href="https://www.consul.io/api-docs/catalog#list-nodes-for-service">List Nodes for Service</a> | <a href="https://www.consul.io/api-docs/features/filtering">Filtering</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl http://localhost:8500/v1/catalog/service/test-service-1</code></pre></div>
<p>输出部分内容如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">[
    {
        <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;3033c057-b976-28bb-1666-02fbf3dd00d2&#34;</span>,
        <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
        <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
        <span style="color:#f92672">&#34;Datacenter&#34;</span>: <span style="color:#e6db74">&#34;dc1&#34;</span>,
        <span style="color:#f92672">&#34;ServiceID&#34;</span>: <span style="color:#e6db74">&#34;test-service-1-instance-1&#34;</span>,
        <span style="color:#f92672">&#34;ServiceName&#34;</span>: <span style="color:#e6db74">&#34;test-service-1&#34;</span>,
        <span style="color:#f92672">&#34;ServiceTags&#34;</span>: [],
        <span style="color:#f92672">&#34;ServicePort&#34;</span>: <span style="color:#ae81ff">9000</span>,
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
    },
    {
        <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;3033c057-b976-28bb-1666-02fbf3dd00d2&#34;</span>,
        <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
        <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
        <span style="color:#f92672">&#34;Datacenter&#34;</span>: <span style="color:#e6db74">&#34;dc1&#34;</span>,
        <span style="color:#f92672">&#34;ServiceID&#34;</span>: <span style="color:#e6db74">&#34;test-service-1-instance-2&#34;</span>,
        <span style="color:#f92672">&#34;ServiceName&#34;</span>: <span style="color:#e6db74">&#34;test-service-1&#34;</span>,
        <span style="color:#f92672">&#34;ServiceTags&#34;</span>: [],
        <span style="color:#f92672">&#34;ServicePort&#34;</span>: <span style="color:#ae81ff">9001</span>,
        <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
    }
]</code></pre></div>
<p><strong>方式 3：health HTTP API</strong></p>

<p>返回所有实例，并可以获取到健康检查的状态（细节参见：<a href="https://www.consul.io/api-docs/catalog#list-nodes-for-service">List Nodes for Service</a> | <a href="https://www.consul.io/api-docs/features/filtering">Filtering</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl http://127.0.0.1:8500/v1/health/service/test-service-1</code></pre></div>
<p>输出部分内容如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">[
    {
        <span style="color:#f92672">&#34;Node&#34;</span>: {
            <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;3033c057-b976-28bb-1666-02fbf3dd00d2&#34;</span>,
            <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
            <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
            <span style="color:#f92672">&#34;Datacenter&#34;</span>: <span style="color:#e6db74">&#34;dc1&#34;</span>,
            <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
        },
        <span style="color:#f92672">&#34;Service&#34;</span>: {
            <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;test-service-1-instance-1&#34;</span>,
            <span style="color:#f92672">&#34;Service&#34;</span>: <span style="color:#e6db74">&#34;test-service-1&#34;</span>,
            <span style="color:#f92672">&#34;Tags&#34;</span>: [],
            <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
            <span style="color:#f92672">&#34;Port&#34;</span>: <span style="color:#ae81ff">9000</span>,
        },
        <span style="color:#f92672">&#34;Checks&#34;</span>: [
            {
                <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
                <span style="color:#f92672">&#34;CheckID&#34;</span>: <span style="color:#e6db74">&#34;serfHealth&#34;</span>,
                <span style="color:#f92672">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Serf Health Status&#34;</span>,
                <span style="color:#f92672">&#34;Status&#34;</span>: <span style="color:#e6db74">&#34;passing&#34;</span>,
                <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
            },
            {
                <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
                <span style="color:#f92672">&#34;CheckID&#34;</span>: <span style="color:#e6db74">&#34;service:test-service-1-instance-1&#34;</span>,
                <span style="color:#f92672">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Service &#39;test-service-1&#39; check&#34;</span>,
                <span style="color:#f92672">&#34;Status&#34;</span>: <span style="color:#e6db74">&#34;passing&#34;</span>,
                <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
            }
        ]
    },
    {
        <span style="color:#f92672">&#34;Node&#34;</span>: {
            <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;3033c057-b976-28bb-1666-02fbf3dd00d2&#34;</span>,
            <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
            <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
            <span style="color:#f92672">&#34;Datacenter&#34;</span>: <span style="color:#e6db74">&#34;dc1&#34;</span>,
            <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
        },
        <span style="color:#f92672">&#34;Service&#34;</span>: {
            <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;test-service-1-instance-2&#34;</span>,
            <span style="color:#f92672">&#34;Service&#34;</span>: <span style="color:#e6db74">&#34;test-service-1&#34;</span>,
            <span style="color:#f92672">&#34;Tags&#34;</span>: [],
            <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
            <span style="color:#f92672">&#34;Port&#34;</span>: <span style="color:#ae81ff">9001</span>,
        },
        <span style="color:#f92672">&#34;Checks&#34;</span>: [
            {
                <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
                <span style="color:#f92672">&#34;CheckID&#34;</span>: <span style="color:#e6db74">&#34;serfHealth&#34;</span>,
                <span style="color:#f92672">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Serf Health Status&#34;</span>,
                <span style="color:#f92672">&#34;Status&#34;</span>: <span style="color:#e6db74">&#34;passing&#34;</span>,
                <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
            },
            {
                <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;machine&#34;</span>,
                <span style="color:#f92672">&#34;CheckID&#34;</span>: <span style="color:#e6db74">&#34;service:test-service-1-instance-2&#34;</span>,
                <span style="color:#f92672">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Service &#39;test-service-1&#39; check&#34;</span>,
                <span style="color:#f92672">&#34;Status&#34;</span>: <span style="color:#e6db74">&#34;critical&#34;</span>,
                <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">...</span>
            }
        ]
    }
]</code></pre></div>
<h4 id="取消注册">取消注册</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl --request PUT http://127.0.0.1:8500/v1/agent/service/deregister/test-service-1-instance-1
curl --request PUT http://127.0.0.1:8500/v1/agent/service/deregister/test-service-1-instance-2
curl --request PUT http://127.0.0.1:8500/v1/agent/service/deregister/test-service-2-instance-1
curl --request PUT http://127.0.0.1:8500/v1/agent/service/deregister/test-service-2-instance-2</code></pre></div>
<h2 id="安装和部署">安装和部署</h2>

<blockquote>
<p>参考： <a href="https://learn.hashicorp.com/tutorials/consul/deployment-overview?in=consul/production-deploy">数据中心部署</a></p>
</blockquote>

<h3 id="架构和说明">架构和说明</h3>

<blockquote>
<p>参考： <a href="https://learn.hashicorp.com/tutorials/consul/get-started?in=consul/getting-started">What is Consul?</a> | <a href="https://learn.hashicorp.com/tutorials/consul/reference-architecture?in=consul/production-deploy">部署架构</a></p>
</blockquote>

<p>Consul 是一个分布式系统，在 Consul 的概念中，一个 Consul 集群被称为数据中心 (datacenter)。</p>

<p>一个 Consul 数据中心由多个节点（被称为 Agent）组成，这些节点可以部署在物理机、虚拟机、或容器中。这些 Agent 可以分为如下两类：</p>

<ul>
<li>3~5 台 Server Agent，包括一个 Leader 和多个 Follower，部署独立的性能强劲的机器上。</li>
<li>0~n 台 Client Agent，部署在每一台需要部署业务应用的机器上。</li>
</ul>

<p>在 Consul 中，Server 还是 Client 只是 consul 这个二进制文件 agent 子命令的一个模式。区别在于：</p>

<ul>
<li>Server Agent

<ul>
<li>跟踪可用服务、它们的 IP 地址以及它们当前的运行状况和状态。</li>
<li>跟踪可用节点、它们的 IP 地址以及它们当前的运行状况和状态。</li>
<li>构建一个了解服务和节点可用性的服务目录 (DNS)。</li>
<li>维护和更新 K/V 存储。</li>
<li>采用 gossip protocol 协议向所有 Agent 传达更新。</li>
<li>对通过该 Agent 注册的服务进行健康检查。</li>
</ul></li>
<li>Client Agent

<ul>
<li>将请求通过 RPC 转发到 Server Agent，以及一些缓存策略。</li>
<li>对通过该 Agent 注册的服务进行健康检查。</li>
</ul></li>
</ul>

<p>也就是说，Client Agent 的健康检查和 Server Agent 能力是相同的，其他的 API 请求都会转发到 ServerAgent 中。</p>

<p>在 API 层面。在使用者看来，不需要区分 Client 和 Server，不管连到 Client 还是 Server，都可以使用 Consul 的全部的功能。</p>

<p>另外，一个只有 Server 的 Consul 集群也是可以正常工作的，但是 Consul 的推荐架构是为每个服务所在的节点 (如 k8s node) 部署一个 Client Agent 原因在于（来自：<a href="https://groups.google.com/g/consul-tool/c/VI1xd8wG-0w">网络</a>）：</p>

<ul>
<li>Agent 可以减轻 Server 的健康检查的压力。</li>
<li>对应用层隐藏 Server Agent 的分布式复杂性：应用层只需要知道服务发现的地址永远是 localhost:8500。</li>
<li>当 Server Agent 故障时， Client Agent 可以利用缓存继续提供服务。</li>
<li>Client Agent 的缓存机制，极大提高了集群的吞吐和性能。</li>
</ul>

<p>因此一个推荐的单数据中心的 Consul 集群的架构如下图所示（图片来源：<a href="http://www.liuhaihua.cn/archives/546262.html">一篇文章了解Consul服务发现实现原理</a>）：</p>

<p><img src="/image/single-dc-consul-arch.jpeg" alt="image" /></p>

<p>Consul Agent 会暴露很多个服务地址，可以分为两类 Client 和 Cluster，默认端口为(详见：<a href="https://www.consul.io/docs/install/ports">Required Ports</a>)：</p>

<ul>
<li>Client （给应用程序使用）

<ul>
<li>8500(tcp): HTTP api 和 WebUI</li>
<li>8600(tcp/udp): DNS 服务</li>
<li>8501(tcp): HTTPS（默认不开启）</li>
<li>8502(tcp): gRPC API（默认不开启）</li>
</ul></li>
<li>Cluster （集群 Agent 使用，通过 <a href="https://github.com/hashicorp/serf">serf</a> 库实现）

<ul>
<li>8300(tcp)： Server RPC 地址。</li>
<li>8301(tcp/udp)：集群内部 Client &amp; Server Agent 间相互通讯协调的端口。</li>
<li>8302(tcp/udp)：跨集群（跨数据中心） 的 Server Agent 间相互通讯协调的端口。</li>
</ul></li>
</ul>

<p>consul agent 的配置可以通过 <a href="https://www.consul.io/docs/agent/config/cli-flags">命令行参数</a> (<code>consul agent --help</code>) 和 <a href="https://www.consul.io/docs/agent/config/config-files">配置文件</a>。本部分仅介绍部分常用的命令行参数：</p>

<table>
<thead>
<tr>
<th>参数</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_datacenter"><code>-datacenter=&lt;value&gt;</code></a></td>
<td>dc1</td>
<td>同一个集群的数据中心名应该是一致的</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_server"><code>-server</code></a></td>
<td></td>
<td>agent 模式是否为 server</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_bootstrap_expect"><code>-bootstrap-expect=&lt;value&gt;</code></a></td>
<td></td>
<td>server 模式有效，当 server agent 达到该数值后，集群开始引导启动，需要注意的是，这个集群中所有 server agent 的该参数的值必须一致</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_node"><code>-node=&lt;value&gt;</code></a></td>
<td>主机的 hostname</td>
<td>此节点的名称。 在集群中必须是唯一的。</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_bind"><code>-bind=&lt;value&gt;</code></a></td>
<td><code>0.0.0.0</code></td>
<td>Cluster 端口绑定的地址，可以通过 <a href="https://godoc.org/github.com/hashicorp/go-sockaddr/template">go-sockaddr</a> 语法运行时获取。如果为默认值，通告地址（指的是别的节点访问本节点时的 ip 地址）将为该机器的私有地址，因此如果有多个私有地址将报错</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_ui"><code>-retry-join</code></a></td>
<td></td>
<td>需要加入的集群中的其他节点的地址，会一致重试直到成功，可以通过 <a href="https://godoc.org/github.com/hashicorp/go-sockaddr/template">go-sockaddr</a> 语法运行时获取，支持域名，可以指定多次。</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_data_dir"><code>-data-dir=&lt;value&gt;</code></a></td>
<td></td>
<td>数据持久化目录，需要保证在 agent 挂掉数据仍然是持久化。 client 和 server 模式都需要，client 用来实现停止后重新注册服务。</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_client"><code>-client=&lt;value&gt;</code></a></td>
<td><code>127.0.0.1</code></td>
<td>Client 端口绑定的地址，可以通过 <a href="https://godoc.org/github.com/hashicorp/go-sockaddr/template">go-sockaddr</a> 语法运行时获取。</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_ui"><code>-ui</code></a></td>
<td></td>
<td>是否启用 webui</td>
</tr>

<tr>
<td><a href="https://www.consul.io/docs/agent/config/cli-flags#_domain"><code>-domain</code></a></td>
<td><code>consul.</code></td>
<td>通过 DNS 做服务发现时的域名。</td>
</tr>
</tbody>
</table>

<h3 id="手动部署">手动部署</h3>

<p>上文 <a href="#单机运行">快速开始 - 单机运行</a> 介绍了如何启动一个开发模式（单 Server Agent 节点）的 Consul 集群。本部分，介绍如何手动部署一个 Consul 集群。集群规划如下：</p>

<ul>
<li>3 台机器：搭建一个包含 3 个 Server Agent 节点的 Consul 集群。</li>
<li>2 台机器：模拟用于部署服务的节点，在这两台机器上部署 Client Agent 进程。</li>
</ul>

<p>需要特别说明的是，Consul 所有 Agent 节点必须是相互可通的同一内网。</p>

<p>简单起见，使用 Docker 容器模拟这 5 台机器（参见：<a href="https://learn.hashicorp.com/tutorials/consul/docker-container-agents">官方镜像说明</a>，配置文件内容可以通过 <code>CONSUL_LOCAL_CONFIG</code> 配置）。</p>

<h4 id="创建网络">创建网络</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker network create consul-cluster</code></pre></div>
<h4 id="部署-server-agent">部署 Server Agent</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 启动第 1 个 server</span>
docker run --network<span style="color:#f92672">=</span>consul-cluster --hostname<span style="color:#f92672">=</span>consul-server-1 --name<span style="color:#f92672">=</span>consul-server-1 -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    consul agent -server -bootstrap-expect<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> -retry-join<span style="color:#f92672">=</span>consul-server-1 -retry-join<span style="color:#f92672">=</span>consul-server-2 -retry-join<span style="color:#f92672">=</span>consul-server-3
<span style="color:#75715e"># 启动第 2 个 server</span>
docker run --network<span style="color:#f92672">=</span>consul-cluster --hostname<span style="color:#f92672">=</span>consul-server-2 --name<span style="color:#f92672">=</span>consul-server-2 -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    consul agent -server -bootstrap-expect<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> -retry-join<span style="color:#f92672">=</span>consul-server-1 -retry-join<span style="color:#f92672">=</span>consul-server-2 -retry-join<span style="color:#f92672">=</span>consul-server-3
<span style="color:#75715e"># 启动第 3 个 server</span>
docker run --network<span style="color:#f92672">=</span>consul-cluster --hostname<span style="color:#f92672">=</span>consul-server-3 --name<span style="color:#f92672">=</span>consul-server-3 -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    consul agent -server -bootstrap-expect<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> -retry-join<span style="color:#f92672">=</span>consul-server-1 -retry-join<span style="color:#f92672">=</span>consul-server-2 -retry-join<span style="color:#f92672">=</span>consul-server-3</code></pre></div>
<p>注意：</p>

<ul>
<li>可以添加 <code>-client 0.0.0.0 -ui</code> 简单起见使用默认值。</li>
<li>生产环境一定要指定 <code>-data-dir</code> 持久化数据。</li>
</ul>

<h4 id="部署-client-agent">部署 Client Agent</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 启动第 1 个 client (为了方便观察，启用 webui、并将 http api 和 DNS 暴露到宿主机)</span>
docker run --network<span style="color:#f92672">=</span>consul-cluster --hostname<span style="color:#f92672">=</span>consul-client-1 --name<span style="color:#f92672">=</span>consul-client-1 -p <span style="color:#ae81ff">8500</span>:8500 -p <span style="color:#ae81ff">8600</span>:8600 -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    consul agent -retry-join<span style="color:#f92672">=</span>consul-server-1 -retry-join<span style="color:#f92672">=</span>consul-server-2 -retry-join<span style="color:#f92672">=</span>consul-server-3 -client<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>.0.0.0 -ui
<span style="color:#75715e"># 启动第 2 个 client</span>
docker run --network<span style="color:#f92672">=</span>consul-cluster --hostname<span style="color:#f92672">=</span>consul-client-2 --name<span style="color:#f92672">=</span>consul-client-2 -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    consul agent -retry-join<span style="color:#f92672">=</span>consul-server-1 -retry-join<span style="color:#f92672">=</span>consul-server-2 -retry-join<span style="color:#f92672">=</span>consul-server-3</code></pre></div>
<h4 id="观察测试">观察测试</h4>

<ul>
<li>打开 client 1 的 webui ( <a href="http://localhost:8500">http://localhost:8500</a> ) 可以看到

<ul>
<li>Services 面板： 3 个 server 组成了 consul 服务。</li>
<li>Node 面板： 5 个 agent 全部健康。</li>
</ul></li>

<li><p>在 client 1 上注册一个测试服务。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 不健康的服务</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-1-instance-1&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-1&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9000,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9000&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register
<span style="color:#75715e"># 健康的服务（不启用健康检查）</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-1-instance-2&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-1&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9001
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register</code></pre></div></li>

<li><p>服务发现</p>

<ul>
<li>通过 DNS： <code>dig @127.0.0.1 +tcp -p 8600 test-service-1.service.consul SRV</code>（注意 <code>+tcp</code>），可以返回 9001 的服务。</li>
<li>通过 Catalog API： <code>curl http://localhost:8500/v1/catalog/service/test-service-1</code>，返回全部两个服务。</li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，返回全部两个服务以及状态。</li>
</ul></li>

<li><p>服务发现 (在其他 client 中)</p>

<ul>
<li>进入 client2 <code>docker exec -it consul-client-2 sh</code></li>
<li>通过 Catalog API： <code>curl http://localhost:8500/v1/catalog/service/test-service-1</code>，返回全部两个服务。</li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，返回全部两个服务以及状态。</li>
<li>通过 <a href="https://www.consul.io/api-docs/agent/service#get-local-service-health">Service Local Health API</a>：<code>curl http://localhost:8500/v1/agent/health/service/name/test-service-1</code> 找不到，可以看出该 API 只能查找到注册到当前 Node 上的服务。</li>
</ul></li>

<li><p>Client Agent 正常退出</p>

<ul>
<li>停止 client1 <code>docker stop consul-client-1</code></li>
<li>进入 client2 <code>docker exec -it consul-client-2 sh</code></li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，观察状态可知，服务已经被取消注册了。</li>
<li>重新启动 client1 <code>docker stop consul-client-1</code></li>
<li>进入 client2 <code>docker exec -it consul-client-2 sh</code></li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，观察状态可知，服务已经又被重新注册了。</li>
</ul></li>

<li><p>Client Agent 异常退出</p>

<ul>
<li>停止 client1 <code>docker kill -9 consul-client-1</code></li>
<li>进入 client2 <code>docker exec -it consul-client-2 sh</code></li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，观察状态可知，服务仍然存在，但是 Checks 状态可以看出 Node 退出了。</li>
<li>重新启动 client1 <code>docker start consul-client-1</code></li>
<li>进入 client2 <code>docker exec -it consul-client-2 sh</code></li>
<li>通过 Health API： <code>curl http://localhost:8500/v1/health/service/test-service-1</code>，观察状态可知，服务已经又被重新注册了。</li>
</ul></li>

<li><p>Server Agent (Leader) 异常退出</p>

<ul>
<li>停止 server1 (从 webui 中找到 leader)： <code>docker kill consul-server-1</code></li>
<li>打开 webui，经过一段时间后，可以看出 Consul 集群 2 个 server 仍然正常工作</li>
<li>重新启动 server2：<code>docker start consul-server-1</code></li>
<li>打开 webui，经过一段时间后，可以看出 Consul 集群 3 个 server 仍然正常工作</li>
</ul></li>

<li><p>Server Agent (Leader) 退出</p>

<ul>
<li>停止 server2 (从 webui 中找到 leader)： <code>docker stop consul-server-2</code></li>
<li>打开 webui 可以看出 Consul 集群 2 个 server 仍然正常工作</li>
<li>重新启动 server2：<code>docker start consul-server-2</code></li>
<li>打开 webui 可以看出 Consul 集群 3 个 server 仍然正常工作</li>
</ul></li>

<li><p>Server Agent (follower) 退出</p>

<ul>
<li>停止 server3 (从 webui 中找到非 leader)： <code>docker stop consul-server-3</code></li>
<li>打开 webui 可以看出 Consul 集群 2 个 server 仍然正常工作</li>
<li>重新启动 server2：<code>docker start consul-server-3</code></li>
<li>打开 webui 可以看出 Consul 集群 3 个 server 仍然正常工作</li>
</ul></li>
</ul>

<h4 id="清理现场">清理现场</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker rm -f consul-client-1 consul-client-2 consul-server-1 consul-server-2 consul-server-3</code></pre></div>
<h3 id="云原生部署">云原生部署</h3>

<p>在官方的将 <a href="https://www.consul.io/docs/k8s">Consul 部署到 Kubernetes</a> 的文档中，重点介绍的是 Service Mesh 相关的教程。</p>

<p>本部分，不会介绍 Service Mesh 相关的内容，而介绍如何在 Kubernetes 部署一套仅提供服务发现注册中心能力的 Consul 集群。可能的规划如下：</p>

<ul>
<li>使用 Kubernetes StatefulSet 部署具有 3~5 个 Consul Server Agent 的 Consul 集群。</li>
<li>该 Consul 集群的 Client 的部署有如下两种选择：

<ul>
<li>(推荐) 使用 Kubernetes DaemonSet 为 Kubernete 集群的每个节点，部署 Consul Client Agent。</li>
<li>如果没有 Kubernetes 集群 DaemonSet 的权限，则可以使用 Kubernetes StatefulSet 部署一个 Consul Client Agent 集群，并通过 Kubernetes 的 Service (type=Cluster) 提供服务。</li>
</ul></li>

<li><p>需要使用 Consul 服务注册和发现能力的 Pod，针对如上 Consul 集群 Client 的部署方式的不同，有不同的使用方式：</p>

<ul>
<li><p>DaemonSet：</p>

<ul>
<li>（推荐）挂载宿主机的文件（Consul Client Agent 的配置文件添加 <code>addresses { http = &quot;0.0.0.0 unix:///var/run/consul/socket/http.sock&quot;}</code>）。

<ul>
<li>挂载宿主机 <code>/var/run/consul/socket</code>  目录</li>
<li>导出环境变量 <code>CONSUL_HTTP_ADDR=unix:///var/run/consul/socket/http.sock</code></li>
</ul></li>

<li><p>使用 host ip。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">        env:
        - name: HOST_IP
          valueFrom:
            fieldRef:
            apiVersion: v1
            fieldPath: status.hostIP
        - name: CONSUL_HTTP_ADDR
          value: http://$(HOST_IP):<span style="color:#ae81ff">8500</span></code></pre></div></li>
</ul></li>

<li><p>StatefulSet + Service (type=Cluster)：导出环境变量 <code>CONSUL_HTTP_ADDR=http://$ConsulClientAgentService.$Namespace.svc.cluster.local</code>。</p></li>
</ul></li>
</ul>

<p>本部分示例选择：以 DaemonSet 的方式部署 Consul Client Agent，通过挂载宿主机文件 unix daemon socket 文件的方式给其他 pod 提供服务。</p>

<p>执行 <code>kubectl apply -f consul.yaml</code>，其中 <code>consul.yaml</code> 内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># Namespace: consul</span>
apiVersion: v1
kind: Namespace
metadata:
  name: consul
---
<span style="color:#75715e"># Headless Service: consul-server</span>
<span style="color:#75715e"># 为 StatefulSet 的 consul-server 准备</span>
apiVersion: v1
kind: Service
metadata:
  name: consul-server
  namespace: consul
spec:
  clusterIP: None
  selector:
    app: consul-server
  ports:
    - name: http
      port: <span style="color:#ae81ff">8500</span>
      targetPort: <span style="color:#ae81ff">8500</span>
    - name: dns-udp
      protocol: <span style="color:#e6db74">&#34;UDP&#34;</span>
      port: <span style="color:#ae81ff">8600</span>
      targetPort: <span style="color:#ae81ff">8600</span>
    - name: dns-tcp
      protocol: <span style="color:#e6db74">&#34;TCP&#34;</span>
      port: <span style="color:#ae81ff">8600</span>
      targetPort: <span style="color:#ae81ff">8600</span>
    - name: server
      port: <span style="color:#ae81ff">8300</span>
      targetPort: <span style="color:#ae81ff">8300</span>
    - name: serflan-tcp
      protocol: <span style="color:#e6db74">&#34;TCP&#34;</span>
      port: <span style="color:#ae81ff">8301</span>
      targetPort: <span style="color:#ae81ff">8301</span>
    - name: serflan-udp
      protocol: <span style="color:#e6db74">&#34;UDP&#34;</span>
      port: <span style="color:#ae81ff">8301</span>
      targetPort: <span style="color:#ae81ff">8301</span>
    - name: serfwan-tcp
      protocol: <span style="color:#e6db74">&#34;TCP&#34;</span>
      port: <span style="color:#ae81ff">8302</span>
      targetPort: <span style="color:#ae81ff">8302</span>
    - name: serfwan-udp
      protocol: <span style="color:#e6db74">&#34;UDP&#34;</span>
      port: <span style="color:#ae81ff">8302</span>
      targetPort: <span style="color:#ae81ff">8302</span>
---
<span style="color:#75715e"># StatefulSet: consul-server</span>
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: consul-server
  namespace: consul
spec:
  selector:
    matchLabels:
      app: consul-server
  serviceName: <span style="color:#e6db74">&#34;consul-server&#34;</span>
  replicas: <span style="color:#ae81ff">3</span>  <span style="color:#75715e"># TODO: 节点数目根据情况而定</span>
  template:
    metadata:
      labels:
        app: consul-server
    spec:
      containers:
      - name: consul-server
        image: <span style="color:#e6db74">&#34;consul:1.13.1&#34;</span>
        args:
          - <span style="color:#e6db74">&#34;agent&#34;</span>
          - <span style="color:#e6db74">&#34;-server&#34;</span>
          - <span style="color:#e6db74">&#34;-bootstrap-expect=3&#34;</span>  <span style="color:#75715e"># TODO: 节点数目根据情况而定</span>
          <span style="color:#75715e"># https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/statefulset/#stable-network-id</span>
          <span style="color:#75715e"># url 为 $podName-{0..N-1}.$(serviceName).$(namespace).svc.cluster.local</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-0.consul-server.consul.svc.cluster.local&#34;</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-1.consul-server.consul.svc.cluster.local&#34;</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-2.consul-server.consul.svc.cluster.local&#34;</span>  <span style="color:#75715e"># TODO: 节点数目根据情况而定</span>
          <span style="color:#75715e"># TODO: 其他参数根据自身情况修改</span>
        volumeMounts:
          - name: consul-server-data
            mountPath: /consul/data
  volumeClaimTemplates:
  - metadata:
      name: consul-server-data
      namespace: consul
    spec:
      accessModes: [ <span style="color:#e6db74">&#34;ReadWriteOnce&#34;</span> ]
      storageClassName: <span style="color:#e6db74">&#34;my-storage-class&#34;</span>  <span style="color:#75715e"># TODO: 根据自身情况修改</span>
      resources:
        requests:
          storage: 5Gi                      <span style="color:#75715e"># TODO: 根据自身情况修改</span>
---
<span style="color:#75715e"># DaemonSet: consul-client</span>
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: consul-client
  namespace: consul
spec:
  selector:
    matchLabels:
      name: consul-client
  template:
    metadata:
      labels:
        name: consul-client
    spec:
      initContainers:
      - name: init-mount
        image: <span style="color:#e6db74">&#34;consul:1.13.1&#34;</span>
        command: [<span style="color:#e6db74">&#39;/bin/sh&#39;</span>, <span style="color:#e6db74">&#39;-c&#39;</span>, <span style="color:#e6db74">&#39;chown -R consul:consul /var/run/consul/socket&#39;</span>]
        volumeMounts:
        - name: consul-client-socket
          mountPath: /var/run/consul/socket
      containers:
      - name: consul-client
        image: <span style="color:#e6db74">&#34;consul:1.13.1&#34;</span>
        args:
          - <span style="color:#e6db74">&#34;agent&#34;</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-0.consul-server.consul.svc.cluster.local&#34;</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-1.consul-server.consul.svc.cluster.local&#34;</span>
          - <span style="color:#e6db74">&#34;-retry-join=consul-server-2.consul-server.consul.svc.cluster.local&#34;</span>
          <span style="color:#75715e"># TODO: 其他参数根据自身情况修改</span>
          - <span style="color:#e6db74">&#34;-ui&#34;</span>
        env:
        - name: CONSUL_LOCAL_CONFIG
          value: <span style="color:#e6db74">&#39;{ &#34;addresses&#34;: { &#34;http&#34;: &#34;0.0.0.0 unix:///var/run/consul/socket/http.sock&#34; } }&#39;</span>
        volumeMounts:
        - name: consul-client-data
          mountPath: /consul/data
        - name: consul-client-socket
          mountPath: /var/run/consul/socket
      volumes:
      - name: consul-client-data
        hostPath:
          path: /var/run/consul/data
          type: DirectoryOrCreate
      - name: consul-client-socket
        hostPath:
          path: /var/run/consul/socket
          type: DirectoryOrCreate</code></pre></div>
<p>通过 <code>kubectl port-forward pods/consul-client-4mtdb 8500:8500 -n consul</code> 将 <code>consul-client</code> 的 8500 端口进行端口转发，并打开 WebUI <code>http://localhost:8500</code>，观察集群情况。</p>

<p>执行 <code>kubectl create -f test-consul-pod.yaml</code> 创建测试 pod，其中 <code>test-consul-pod.yaml</code> 内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  volumes:
  - name: consul-client-socket
    hostPath:
      path: /var/run/consul/socket
  containers:
  - name: busybox
    image: busybox:<span style="color:#ae81ff">1.28.4</span>
    command: [ <span style="color:#e6db74">&#34;sleep&#34;</span>, <span style="color:#e6db74">&#34;100000000&#34;</span>]
    imagePullPolicy: IfNotPresent
    env:
    - name: CONSUL_HTTP_ADDR
      value: unix:///var/run/consul/socket/http.sock
    volumeMounts:
    - name: consul-client-socket
      mountPath: /var/run/consul/socket
  restartPolicy: Always</code></pre></div>
<p>启动测试服务：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kubectl exec -it busybox -- /bin/sh
<span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span> echo -e <span style="color:#e6db74">&#34;HTTP/1.1 200 OK\n\ntest-service-1 (instance1): </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> | nc -l -p <span style="color:#ae81ff">9000</span>; <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $? -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> break; <span style="color:#66d9ef">fi</span>; <span style="color:#66d9ef">done</span></code></pre></div>
<p>注册测试服务：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kubectl exec -it busybox -- /bin/sh
ip addr
<span style="color:#75715e"># 将如下的 127.0.0.1 替换为 ip addr 的输出</span>
curl  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --request PUT <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --unix-socket <span style="color:#e6db74">${</span>CONSUL_HTTP_ADDR#*//<span style="color:#e6db74">}</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --data <span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">        {
</span><span style="color:#e6db74">            &#34;ID&#34;: &#34;test-service-1-instance-1&#34;,
</span><span style="color:#e6db74">            &#34;Name&#34;: &#34;test-service-1&#34;,
</span><span style="color:#e6db74">            &#34;Address&#34;: &#34;127.0.0.1&#34;,
</span><span style="color:#e6db74">            &#34;Port&#34;: 9000,
</span><span style="color:#e6db74">            &#34;Check&#34;: {
</span><span style="color:#e6db74">                &#34;HTTP&#34;: &#34;http://127.0.0.1:9000&#34;,
</span><span style="color:#e6db74">                &#34;Interval&#34;: &#34;10s&#34;
</span><span style="color:#e6db74">            }
</span><span style="color:#e6db74">        }
</span><span style="color:#e6db74">    &#39;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    http://localhost:8500/v1/agent/service/register</code></pre></div>
<p>通过 WebUI 观察：<a href="http://localhost:8500/ui/dc1/services">http://localhost:8500/ui/dc1/services</a> 。</p>

<!-- 

### 多数据中心

## 核心特性

### 高可用

故障恢复 https://www.consul.io/docs/agent#failures-and-crashes

### API 设计

https://discuss.hashicorp.com/t/what-is-the-different-between-catalog-service-and-agent-service/37155

### 服务发现

### 健康检查

### 服务网格

-->

<h2 id="参考">参考</h2>

<ul>
<li>代码库 <a href="https://github.com/hashicorp/consul">hashicorp/consul</a></li>
<li><a href="https://www.consul.io/">官方网站</a></li>
<li><a href="http://www.liuhaihua.cn/archives/546262.html">一篇文章了解Consul服务发现实现原理</a></li>
<li><a href="https://www.zhihu.com/question/68005259">Consul的client mode把请求转向server，那么client的作用是什么？</a></li>
<li><a href="https://groups.google.com/g/consul-tool/c/VI1xd8wG-0w">What is purpose and intent of Consul Agents running in Client mode?</a></li>
</ul>
]]></description></item><item><title>Go Test 详解</title><link>https://www.rectcircle.cn/posts/go-test/</link><pubDate>Sat, 06 Aug 2022 23:41:09 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-test/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>本文主要介绍：</p>

<ul>
<li>Go 标准库 <a href="https://pkg.go.dev/testing@go1.18">testing</a> 包 和 go test <a href="https://pkg.go.dev/cmd/go@go1.18">命令</a>。</li>
<li>Go 官方维护的 <a href="https://github.com/golang/mock">Mock</a> 库。</li>
<li>Go 社区最主流的测试库 <a href="https://github.com/stretchr/testify#mock-package">Testify</a>。</li>
</ul>

<p>本文使用的 Go 版本为 1.18，示例代码位于 <a href="https://github.com/rectcircle/go-test-demo">rectcircle/go-test-demo</a>。</p>

<h2 id="go-标准库-testing-包-和-go-test-命令">Go 标准库 testing 包 和 <code>go test</code> 命令</h2>

<p>Go 通过标准库的 testing 包和 Go 命令行工具 test 相关命令，在语言层面，提供了一整套全面的测试机制。</p>

<p>本小结主要介绍如何使用 testing 包编写各种类型的测试函数。</p>

<h3 id="常规测试">常规测试</h3>

<p>一个被测函数位于 <code>01-testing/01-testfunc.go</code> 文件：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">IntAbs</span>(<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">a</span> &lt; <span style="color:#ae81ff">0</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span><span style="color:#a6e22e">a</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>
}</code></pre></div>
<p>测试函数位于 <code>01-testing/01-testfunc_test.go</code> 文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;testing&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestIntAbs</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">got</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">IntAbs</span>(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#ae81ff">1</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Abs(-1) = %d; want 1&#34;</span>, <span style="color:#a6e22e">got</span>)
	}
	<span style="color:#a6e22e">got</span> = <span style="color:#a6e22e">IntAbs</span>(<span style="color:#ae81ff">1</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#ae81ff">1</span> {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Abs(1) = %d; want 1&#34;</span>, <span style="color:#a6e22e">got</span>)
	}
}</code></pre></div>
<p>测试函数编写的基本要求为：</p>

<ul>
<li>测试源码的文件名以 <code>_test.go</code> 结尾。</li>
<li>测试函数的函数名以 <code>Test</code> 开头。</li>
<li>函数签名为 <code>func (t *testing.T)</code>。</li>
</ul>

<p>通过 <code>go test -run ^TestIntAbs$ ./01-testing</code> 命令，可以运行测试函数。</p>

<p><a href="https://pkg.go.dev/testing@go1.18#T"><code>*testing.T</code> 类型</a></p>

<ul>
<li><p>常用方法如下：</p>

<ul>
<li><code>func (c *T) Fail()</code> 将测试函数标记为失败，但仍继续执行。</li>
<li><code>func (c *T) FailNow()</code> 将测试函数标记为失败，并调用 <code>runtime.Goexit</code>，终止该协程。</li>
<li><code>func (c *T) Log(args ...any)</code> 打印日志，类似于 Println，仅当运行测试添加 <code>-v</code> 标志，或者测试失败时，才打印日志。</li>
<li><code>func (c *T) Logf(format string, args ...any)</code> 打印日志，类似于 <code>Printf</code>，仅当运行测试添加 <code>-v</code> 标志，或者测试失败时，才打印日志。</li>
<li><code>func (c *T) Error(args ...any)</code> 等价于调用 <code>Log</code> 后跟 <code>Fail</code>。</li>
<li><code>func (c *T) Errorf(format string, args ...any)</code> 等价于 <code>Logf</code> 后跟 <code>Fail</code>。</li>
<li><code>func (c *T) Fatal(args ...any)</code> 等价于调用 <code>Log</code> 后跟 <code>FailNow</code>。</li>
<li><code>func (c *T) Fatalf(format string, args ...any)</code> 等价于 <code>Logf</code> 后跟 <code>FailNow</code>。</li>
<li><code>func (c *T) SkipNow()</code> 将测试标记为已被跳过，并通过调用 <code>runtime.Goexit</code> 停止执行。如果测试失败（参见 <code>Error</code>, <code>Errorf</code>, <code>Fail</code>）然后被跳过，它仍然被认为是失败的。另请参阅 <code>FailNow</code>。 <code>SkipNow</code> 必须从运行测试的 goroutine 调用，而不是从测试期间创建的其他 goroutine 调用。调用 <code>SkipNow</code> 不会停止其他 goroutine。</li>
<li><code>func (c *T) Skip(args ...any)</code> 等价于 <code>Log</code> 后跟 <code>SkipNow</code>。</li>

<li><p><code>func (c *T) Skipf(format string, args ...any)</code> 等价于 <code>Logf</code> 后跟 <code>SkipNow</code>。</p></li>

<li><p><code>func (c *T) Cleanup(f func())</code> 注册清理函数，调用顺序为，后添加，先调用。</p></li>

<li><p><code>func (t *T) Parallel()</code> 表示该测试将与（并且仅与）其他并行测试并行运行。（当使用 <code>-count</code> 或 <code>-cpu</code> 多次运行测试时，单个测试的多个实例永远不会彼此并行运行）</p></li>

<li><p><code>func (t *T) Run(name string, f func(t*T)) bool</code> 运行 t 的子测试，名为 name ，测试函数 f 。它在单独的 goroutine 中运行 f 并阻塞，直到 f 返回或调用 <code>t.Parallel</code>。 Run 报告 f 是否成功（或者至少在调用 <code>t.Parallel</code> 之前没有失败）。可以从多个 goroutine 同时调用 Run，但所有此类调用都必须在外部测试函数 t 返回之前返回。</p></li>
</ul></li>

<li><p>其他方法如下：</p>

<ul>
<li><code>func (c *T) Name() string</code> 返回当前测试/子测试函数的名称，如果存在同名的，将自动添加一个后缀。</li>
<li><code>func (c *T) Skipped() bool</code> 是否被跳过。</li>
<li><code>func (c *T) TempDir() string</code> 返回一个临时目录供测试使用。当测试及其所有子测试完成时，<code>Cleanup</code> 会自动删除该目录。对 <code>t.TempDir</code> 的每次后续调用都会返回一个唯一的目录；如果目录创建失败，TempDir 通过调用 <code>Fatal</code> 终止测试。</li>
<li><code>func (c *T) Helper()</code> 标记该函数为辅助函数，在测试失败或打印日志时，将不会打印该函数的调用栈或日志。</li>
<li><code>func (t *T) Deadline() (deadline time.Time, ok bool)</code> 返回运行测试时 <code>-timeout</code> 设置的时间，默认为 0 (永不超时)。</li>
<li><code>func (t *T) Setenv(key, value string)</code> 调用 <code>os.Setenv(key, value)</code> 并使用 <code>Cleanup</code> 将环境变量恢复到测试后的原始值（这不能用于并行测试）。</li>
</ul></li>
</ul>

<h3 id="基准测试">基准测试</h3>

<p>假设我们希望测试一个函数的性能，此时可以通过 Go 提供的基准测试来实现（基本原理为：多次循环调用待测函数，计算平均耗时等指标）。</p>

<p><code>01-testing/02-benchmark_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;bytes&#34;</span>
	<span style="color:#e6db74">&#34;html/template&#34;</span>
	<span style="color:#e6db74">&#34;math/rand&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">BenchmarkRandInt</span>(<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">B</span>) {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">N</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
		<span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Int</span>()
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">BenchmarkTemplateParallel</span>(<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">B</span>) {
	<span style="color:#a6e22e">templ</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">template</span>.<span style="color:#a6e22e">Must</span>(<span style="color:#a6e22e">template</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;test&#34;</span>).<span style="color:#a6e22e">Parse</span>(<span style="color:#e6db74">&#34;Hello, {{.}}!&#34;</span>))
	<span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">RunParallel</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">pb</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">PB</span>) {
		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">buf</span> <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>
		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">Next</span>() {
			<span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">Reset</span>()
			<span style="color:#a6e22e">templ</span>.<span style="color:#a6e22e">Execute</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">buf</span>, <span style="color:#e6db74">&#34;World&#34;</span>)
		}
	})
}</code></pre></div>
<p>基准测试编写的基本要求为：</p>

<ul>
<li>源码的文件名以 <code>_test.go</code> 结尾。</li>
<li>函数名以 <code>Benchmark</code> 开头。</li>
<li>函数签名为 <code>func (b *testing.B)</code>。</li>
</ul>

<p>通过 <code>go test -run=^$ -benchmem -bench ^BenchmarkRandInt$ ./01-testing</code> 和 <code>go test -run=^$ -benchmem -bench ^BenchmarkTemplateParallel$ ./01-testing</code> 命令，可以运行如上两个基准测试函数。</p>

<p>和常规测试不同，基准测试的日志总是会被打印出来</p>

<p>第一个基准测试，输出如下（忽略设备信息）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">BenchmarkRandInt-8      77098495                15.57 ns/op            0 B/op          0 allocs/op</pre></div>
<p>输出含义如下：</p>

<ul>
<li>BenchmarkRandInt-8 <code>测试名-GOMAXPROCS</code>。</li>
<li>77098495 表示一共执行了 77098495 次，即 <code>b.N</code> 的值。</li>
<li>15.57 ns/op 表示平均下来，for 循环每次花费了 15.57 ns。</li>
<li>0 B/op 表示平均下来，for 循环每次申请了 0 Byte 内存 （需启用 <code>-benchmem</code> 标志）。</li>
<li>0 allocs/op 表示平均下来，for 循环每次申请了 0 次内存（需启用 <code>-benchmem</code> 标志）。</li>
</ul>

<p><a href="https://pkg.go.dev/testing@go1.18#B"><code>*testing.B</code> 类型</a></p>

<ul>
<li>导出的字段：

<ul>
<li><code>N int</code> 迭代次数。和常规测试不同，基准测试会被调用多次，每次调用，需要迭代的次数记录在 <code>N</code> 中，<code>N</code> 从 1 开始，如果基准测试函数在 1 秒(默认值)内就完成，则 <code>N</code> 增加，并再次运行基准测试函数。</li>
</ul></li>
<li>方法如下：

<ul>
<li>上文 <a href="https://pkg.go.dev/testing@go1.18#T"><code>*testing.T</code> 类型</a> <code>func (c *T) Xxx</code> 相关方法，如 <code>FailNow</code>, <code>Fatal</code>, <code>Fatalf</code>、<code>Error</code> 等。</li>
<li><code>func (b *B) ReportAllocs()</code> 为此基准启用 malloc 统计信息。等价于设置 <code>-benchmem</code>，只对当前基准函数生效。</li>
<li><code>func (b *B) ReportMetric(n float64, unit string)</code> 报告自定义指标，参见：<a href="https://pkg.go.dev/testing@go1.18#example-B.ReportMetric">示例</a>。</li>
<li><code>func (b *B) StartTimer()</code> StartTimer 开始计时测试。此函数在基准测试开始前自动调用，但也可用于在调用 StopTimer 后恢复计时。</li>
<li><code>func (b *B) StopTimer()</code> StopTimer 停止计时测试。这可用于在执行您不想测量的复杂初始化时暂停计时器。</li>
<li><code>func (b *B) ResetTimer()</code> ResetTimer 将经过的基准测试时间和内存分配计数器归零并删除用户报告的指标。它不影响计时器是否正在运行。</li>
<li><code>func (b *B) Run(name string, f func(b *B)) bool</code> 运行一个子基准。注意，<code>b.Run</code> 仅在 <code>b.N</code> 为 1 时才会被调用真正调用，另外 <code>Run</code> 函数自身的耗时不会被统计。</li>
<li><code>func (b *B) RunParallel(body func(*PB))</code> 并行运行基准测试。它创建多个 goroutine 并在它们之间分配 b.N 次迭代。 goroutine 的数量默认为 GOMAXPROCS。要增加非 CPU 绑定基准的并行度，请在 RunParallel 之前调用 SetParallelism。 RunParallel 通常与 go test -cpu 标志一起使用。body 函数将在独立的 goroutine 中运行。它应该设置任何 goroutine-local 状态，然后迭代直到 pb.Next 返回 false。它不应使用 StartTimer、StopTimer 或 ResetTimer 函数，因为它们具有全局效果。它也不应该调用 Run。参见：<a href="https://pkg.go.dev/testing@go1.18#example-B.RunParallel">示例</a>。</li>
<li><code>func (b *B) SetBytes(n int64)</code> SetBytes 记录单个操作中处理的字节数。如果调用它，基准将报告 ns/op 和 MB/s。</li>
<li><code>func (b *B) SetParallelism(p int)</code> SetParallelism 将 RunParallel 使用的 goroutine 的数量设置为 p*GOMAXPROCS。对于受 CPU 限制的基准测试，通常不需要调用 SetParallelism。如果 p 小于 1，则此调用将无效。</li>
</ul></li>
</ul>

<h3 id="example">Example</h3>

<p>假设一个包，导出了如下函数 <code>01-testing/03-example.go</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Hello</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;hello&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Salutations</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;hello, and&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;goodbye&#34;</span>)
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">T</span> <span style="color:#66d9ef">struct</span>{}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">M</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;t.m()&#34;</span>)
}</code></pre></div>
<p>希望给这些类型编写一些示例代码，这些示例代码会打印一些内容，并校验这些文本的是否符合预期。</p>

<p><code>01-testing/03-example_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Example</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;This is a package example&#34;</span>)
	<span style="color:#75715e">// Output: This is a package example
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Example_a01</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;This is a package example&#34;</span>)
	<span style="color:#75715e">// Output: This is a package example
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleHello</span>() {
	<span style="color:#a6e22e">Hello</span>()
	<span style="color:#75715e">// Output: hello
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleHello_a01</span>() {
	<span style="color:#a6e22e">Hello</span>()
	<span style="color:#75715e">// Output: hello
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleSalutations</span>() {
	<span style="color:#a6e22e">Salutations</span>()
	<span style="color:#75715e">// Output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// hello, and
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// goodbye
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleT_M</span>() {
	<span style="color:#a6e22e">t</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">T</span>{}
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">M</span>()
	<span style="color:#75715e">// Output: t.m()
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleT_M_a01</span>() {
	<span style="color:#a6e22e">t</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">T</span>{}
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">M</span>()
	<span style="color:#75715e">// Output: t.m()
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>Example 编写的基本要求为：</p>

<ul>
<li>源码的文件名以 <code>_test.go</code> 结尾。</li>
<li>函数名以 <code>Example</code> 开头：

<ul>
<li>包 Example 为： <code>Example</code>。</li>
<li>包多个 Example 为： <code>Example_suffix</code>。</li>
<li>函数/类型 Example 为： <code>ExampleT</code>、<code>ExampleF</code>。</li>
<li>函数/类型多个 Example 为： <code>ExampleT_suffix</code>、<code>ExampleF_suffix</code>。</li>
<li>方法 Example 为： <code>ExampleT_M</code>。</li>
<li>方法多个 Example 为： <code>ExampleT_M_suffix</code>。</li>
</ul></li>
<li>函数签名为 <code>func ()</code>。</li>

<li><p>对 Example 的输出进行校验，在函数体的最后添加如下注释：</p>

<ul>
<li><p>一般输出</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleHello</span>() {
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;hello&#34;</span>)
    <span style="color:#75715e">// Output: hello
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleSalutations</span>() {
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;hello, and&#34;</span>)
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;goodbye&#34;</span>)
    <span style="color:#75715e">// Output:
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// hello, and
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// goodbye
</span><span style="color:#75715e"></span>}</code></pre></div></li>

<li><p>无序输出</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePerm</span>() {
    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">value</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">Perm</span>(<span style="color:#ae81ff">5</span>) {
        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">value</span>)
    }
    <span style="color:#75715e">// Unordered output: 4
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 1
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 0
</span><span style="color:#75715e"></span>}</code></pre></div></li>
</ul></li>
</ul>

<p>通过类似于 <code>go test -run ^Example$ ./01-testing</code> 的命令可以运行 Exmaple。</p>

<p>注意：Example 函数除了可以用 <code>go test</code> 进行测试外，还可以通过 <code>go doc</code> 命令生成到 go doc 文档中。</p>

<h3 id="fuzzing-测试">Fuzzing 测试</h3>

<blockquote>
<p>更多参见：<a href="/posts/go-1-18-features#fuzzing-单元测试">Go 1.18 新特性 - Fuzzing 单元测试</a>。</p>
</blockquote>

<p>假设有一个待测函数：字符串翻转，位于 <code>01-testing/04-fuzzing.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;unicode/utf8&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">s</span>) {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;input is not valid UTF-8&#34;</span>)
	}
	<span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> []rune(<span style="color:#a6e22e">s</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">r</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">r</span>)<span style="color:#f92672">/</span><span style="color:#ae81ff">2</span>; <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> = <span style="color:#a6e22e">i</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
		<span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">j</span>] = <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">j</span>], <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">i</span>]
	}
	<span style="color:#66d9ef">return</span> string(<span style="color:#a6e22e">r</span>), <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p>通过 Go 1.18 提供的 Fuzzing 测试，可以进行随机输入测试，位于 <code>01-testing/04-fuzzing_test.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;testing&#34;</span>
	<span style="color:#e6db74">&#34;unicode/utf8&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">FuzzReverse</span>(<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">F</span>) {
	<span style="color:#75715e">// 1. 提供默认情况下的测试样例
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2. 告诉驱动器参数的类型
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">testcases</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;Hello, world&#34;</span>, <span style="color:#e6db74">&#34; &#34;</span>, <span style="color:#e6db74">&#34;!12345&#34;</span>}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">tc</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">testcases</span> {
		<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">tc</span>) <span style="color:#75715e">// Use f.Add to provide a seed corpus
</span><span style="color:#75715e"></span>	}
	<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Fuzz</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">orig</span> <span style="color:#66d9ef">string</span>) { <span style="color:#75715e">// 2~n 个参数需要和上面 f.Add 类型一致
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">rev</span>, <span style="color:#a6e22e">err1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">orig</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span>
		}
		<span style="color:#a6e22e">doubleRev</span>, <span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">rev</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span>
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">orig</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">doubleRev</span> {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Before: %q, after: %q&#34;</span>, <span style="color:#a6e22e">orig</span>, <span style="color:#a6e22e">doubleRev</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">orig</span>) <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">rev</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Reverse produced invalid UTF-8 string %q&#34;</span>, <span style="color:#a6e22e">rev</span>)
		}
	})
}</code></pre></div>
<p>Fuzz 测试编写的基本要求为：</p>

<ul>
<li>源码的文件名以 <code>_test.go</code> 结尾。</li>
<li>函数名以 <code>Fuzz</code> 开头。</li>
<li>函数签名为 <code>func (f *testing.F)</code>。</li>
</ul>

<p>通过 <code>go test -fuzz=^FuzzReverse$ -fuzztime 2s -run ^$ ./01-testing</code>  可以运行该测试，失败的 case 将写入 <code>testdata/fuzz</code> 目录中。</p>

<p><a href="https://pkg.go.dev/testing@go1.18#F"><code>*testing.F</code> 类型</a> 导出的方法：</p>

<ul>
<li>上文 <a href="https://pkg.go.dev/testing@go1.18#T"><code>*testing.T</code> 类型</a> <code>func (c *T) Xxx</code> 相关方法，如 <code>FailNow</code>, <code>Fatal</code>, <code>Fatalf</code>、<code>Error</code> 等。</li>
<li><code>func (f *F) Add(args ...any)</code> 将参数添加到种子语料库以进行模糊测试。如果在 fuzz 目标之后或内部调用，这将是一个空操作，并且 args 必须与 fuzz 目标的参数匹配。Go 还会自动的读取  <code>testdata/fuzz</code> 目录中的种子语料库。</li>
<li><code>func (f *F) Fuzz(ff any)</code> Fuzz 运行 fuzz 函数 ff 进行模糊测试。如果 ff 对于一组参数失败，这些参数将被添加到种子语料库中。

<ul>
<li>ff 必须是一个没有返回值的函数，其第一个参数是 <code>*T</code>。其余参数是要模糊测试的类型，例如：<code>f.Fuzz(func(t*testing.T, b []byte, i int) { ... })</code>。允许使用以下类型：<code>[]byte</code>、<code>string</code>、<code>bool</code>、<code>byte</code>、<code>rune</code>、<code>float32</code>、<code>float64</code>、<code>int</code>、<code>int8</code>、<code>int16</code>、<code>int32</code>、<code>int64</code>、<code>uint</code>、<code>uint8</code>、<code>uint16</code>、<code>uint32</code>、<code>uint64</code>。未来可能会支持更多类型。</li>
<li>ff 不得调用任何 <code>*F</code> 方法，例如 <code>(*F).Log</code>, <code>(*F).Error</code>, <code>(*F).Skip</code>。请改用相应的 <code>*T</code> 方法。 <code>(*F).Fuzz</code> 函数中唯一允许的 <code>*F</code> 方法是 <code>(*F).Failed</code> 和 <code>(*F).Name</code>。</li>
<li>ff 函数应该是快速和确定的，并且它的行为不应该依赖于共享状态。在模糊函数的执行之间不应保留可变的输入参数或指向它们的指针，因为支持它们的内存可能会在后续调用期间发生变化。 ff 不得修改模糊引擎提供的参数的基础数据。</li>
<li>进行模糊测试时，F.Fuzz 直到发现问题、时间用完（使用 -fuzztime 设置）或测试过程被信号中断才返回。 F.Fuzz 应该只调用一次，除非事先调用了 F.Skip 或 F.Fail。</li>
</ul></li>
</ul>

<h3 id="skipping-方法">Skipping 方法</h3>

<p>通过调用 <code>*T</code> 或 <code>*B</code> 的 <code>Skip</code> 方法，可以在运行时跳过测试或基准测试：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestTimeConsuming</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">Short</span>() {
        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Skip</span>(<span style="color:#e6db74">&#34;skipping test in short mode.&#34;</span>)
    }
    <span style="color:#f92672">...</span>
}</code></pre></div>
<p>如果输入无效，<code>*T</code> 的 Skip 方法可用于模糊目标，但不应将其视为失败输入。例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">FuzzJSONMarshalling</span>(<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">F</span>) {
    <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Fuzz</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">b</span> []<span style="color:#66d9ef">byte</span>) {
        <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">v</span> <span style="color:#66d9ef">interface</span>{}
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>(<span style="color:#a6e22e">b</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">v</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Skip</span>()
        }
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">v</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;Marshal: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
        }
    })
}</code></pre></div>
<h3 id="子测试和子基准">子测试和子基准</h3>

<p>可以通过 <code>Run</code> 函数，为常规测试和基准测试，添加一个子测试和子基准测试，示例参见 <code>01-testing/06-subtest_test.go</code> 文件。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestFoo</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#75715e">// &lt;setup code&gt;
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;A=1&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {})
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;A=2&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {})
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;B=1&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {})
	<span style="color:#75715e">// &lt;tear-down code&gt;
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestGroupedParallel</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">tests</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">struct</span> {
		<span style="color:#a6e22e">Name</span> <span style="color:#66d9ef">string</span>
	}{
		{
			<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;A=3&#34;</span>,
		},
	}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">tc</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">tests</span> {
		<span style="color:#a6e22e">tc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tc</span> <span style="color:#75715e">// capture range variable
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">tc</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
		})
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestTeardownParallel</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#75715e">// This Run will not return until the parallel tests finish.
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;group&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;Test1&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
			<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span>)
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Test1&#34;</span>)
		})
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;Test2&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
			<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span>)
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Test2&#34;</span>)
		})
		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;Test3&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
			<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span>)
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Test3&#34;</span>)
		})
	})
	<span style="color:#75715e">// &lt;tear-down code&gt;
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>运行指定测试命令如下：</p>

<ul>
<li><code>go test -run '' ./01-testing</code>       运行该包的所有测试。</li>
<li><code>go test -run Foo ./01-testing</code>      运行该包匹配 Foo 的顶级测试如 &ldquo;TestFoo&rdquo;。</li>
<li><code>go test -run Foo/A= ./01-testing</code>   运行该包匹配 Foo 的顶级测试，以及匹配 &ldquo;A=&rdquo; 的子测试。</li>
<li><code>go test -run /A=1 ./01-testing</code>     运行该包所有顶级测试，以及匹配 &ldquo;A=1&rdquo; 的子测试。</li>
<li><code>go test -fuzz FuzzFoo ./01-testing</code> Fuzz 匹配 &ldquo;FuzzFoo&rdquo; 的目标。</li>
<li><code>go test -run=FuzzFoo/9ddb952d9814</code>  -run 参数还可用于运行种子语料库中的特定值，以进行调试。</li>
</ul>

<h3 id="testmain">TestMain</h3>

<p>测试或基准程序有时需要在执行之前或之后进行额外的设置或拆卸。有时还需要控制哪些代码在主线程上运行。为了支持这些和其他情况，如果测试文件包含一个 <code>TestMain</code> 函数，<code>01-testing/07-testmain_test.go</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testingdemo</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestMain</span>(<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">M</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++ TestMain +++&#34;</span>)
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">Run</span>())
}</code></pre></div>
<p>TestMain 编写的基本要求为：</p>

<ul>
<li>源码的文件名以 <code>_test.go</code> 结尾。</li>
<li>函数名固定为 <code>TestMain</code>。</li>
<li>函数签名为 <code>func (m *testing.M)</code>。</li>
<li>在 <code>m.Run()</code> 调用之前进行一些准备工作。</li>
<li>在 <code>m.Run()</code> 调用之后做一些回收工作。</li>
<li>最后调用 <code>os.Exit</code>，其参数为 <code>m.Run()</code> 的返回值。</li>
</ul>

<p>TestMain 是一个低级原语，对于常规测试功能就足够了的临时测试需求，不应该是必需的。</p>

<h3 id="其他说明">其他说明</h3>

<ul>
<li><code>_test.go</code> 中定义的类型和函数，只能被同一个包中的文件引用，不允许跨包导入，参见：<a href="https://github.com/golang/go/issues/39565">issue</a>。</li>
<li><code>_test.go</code> 文件的包名有两种选择

<ul>
<li>测试源代码的包名和源代码文件的包名相同，如上文示例中的 <code>package testingdemo</code>，此时可以直接对<strong>未导出</strong>函数、方法进行测试。</li>
<li>测试源代码的包名为源代码文件的包名加 <code>_test</code> 后缀，如上文示例中的 <code>testingdemo</code>，测试包可以为 <code>testingdemo_test</code>，此时，只能测试包和源代码属于不同的包，因此只能对<strong>导出</strong>函数、方法进行测试。该场景适合：

<ul>
<li>不需要测试<strong>未导出</strong>函数、方法的场景</li>
<li>可能导致循环引用的场景</li>
</ul></li>
</ul></li>
<li>go test 运行时，测试函数和进程、协程的关系为（测试代码参见下文）：

<ul>
<li>同一个包的所有测试函数，都在同一个进程中执行，不同包的测试函数在不同的进程中执行。</li>
<li><code>TestMain</code> 在 1 号协程中执行，对于测试函数

<ul>
<li>如果测试函数全都不是 <code>Parallel</code> 的，则串行的在 2 号协程中执行。</li>
<li>如果是 <code>Parallel</code> 的，则测试函数会并根据 <code>Parallel</code> 的情况再不同的协程并行执行。</li>
</ul></li>
</ul></li>
</ul>

<p><code>01-testing/a/a_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">a</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;runtime&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestA1</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;A1 Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;A1 Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestA2</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;A2 Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;A2 Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestMain</span>(<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">M</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;A TestMain Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;A TestMain Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">Run</span>())
}</code></pre></div>
<p><code>01-testing/b/b_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">b</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;runtime&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestB1</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;B1 Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;B1 Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestB2</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parallel</span>()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;B2 Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;B2 Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestMain</span>(<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">M</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++&#34;</span>, <span style="color:#e6db74">&#34;B TestMain Goroutine Num&#34;</span>, <span style="color:#a6e22e">runtime</span>.<span style="color:#a6e22e">NumGoroutine</span>(), <span style="color:#e6db74">&#34;B TestMain Pid&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">Run</span>())
}</code></pre></div>
<p>运行 <code>go test -run '' ./01-testing/a ./01-testing/b -v</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+++ A TestMain Goroutine Num 1 A TestMain Pid 98270
=== RUN   TestA1
+++ A1 Goroutine Num 2 A1 Pid 98270

--- PASS: TestA1 (0.00s)
=== RUN   TestA2
+++ A2 Goroutine Num 2 A2 Pid 98270

--- PASS: TestA2 (0.00s)
PASS
ok      github.com/rectcircle/go-test-demo/01-testing/a 1.174s
+++ B TestMain Goroutine Num 1 B TestMain Pid 98271
=== RUN   TestB1
=== PAUSE TestB1
=== RUN   TestB2
=== PAUSE TestB2
=== CONT  TestB1
+++ B1 Goroutine Num 3 B1 Pid 98271

--- PASS: TestB1 (0.00s)
=== CONT  TestB2
+++ B2 Goroutine Num 2 B2 Pid 98271

--- PASS: TestB2 (0.00s)
PASS
ok      github.com/rectcircle/go-test-demo/01-testing/b 1.698s</pre></div>
<h3 id="go-test-命令"><code>go test</code> 命令</h3>

<p><code>go test</code> 有如下两种模式：</p>

<ul>
<li><code>cd packagexxx &amp;&amp; go test</code> 本地目录模式，即直接运行当前目录下的包，即 <code>packagexxx</code> 目录下的包的测试。</li>
<li><code>go test ./packagexxx</code> 包列表模式，运行指定包下的测试，<code>./packagexxx</code> 可以指定多个（如 <code>./a /.b</code>），也可以可以使用 <code>./xxx/...</code>、<code>./...</code>，测试该目录下的所有包，在该模式下，<code>go test</code> 会使用缓存，可以通过 <code>go clean -testcache</code> 清理缓存，或者通过手动指定 <code>-count 1</code> 来禁用缓存。</li>
</ul>

<p><code>go test</code> 常见标志如下所示：</p>

<ul>
<li>选择测试目标的标志

<ul>
<li><code>-run regexp</code> 只运行与正则表达式匹配的常规测试、Example、Fuzz 的种子语料库。默认值为 <code>''</code>，即运行所有测试。regexp 会按照不带括号的 <code>/</code> 分割为多个正则表达式，并且测试标识符的每个部分都必须匹配序列中的相应元素。注意，对于 <code>-run=X/Y</code> 这种情况，如果 <code>X</code> 存在 <code>X/Y</code> 不存在，则 <code>X</code> 仍会被执行，因为必须运行 <code>X</code>，才能查找到到 <code>X/Y</code> 是否存在。</li>
<li><code>-bench regexp</code> 仅运行与正则表达式匹配的基准。默认情况下，不运行任何基准测试。要运行所有基准测试，请使用 <code>'-bench .'</code> 或 <code>'-bench=.'</code>。 正则表达式由不带括号的斜杠 (/) 字符拆分为一系列正则表达式，并且基准标识符的每个部分都必须匹配序列中的相应元素（如果有）。 匹配的可能父项以 <code>b.N=1</code> 运行以识别子基准。 例如，给定 <code>-bench=X/Y</code>，匹配 <code>X</code> 的顶级基准以 <code>b.N=1</code> 运行，以找到匹配 Y 的任何子基准，然后完整运行。</li>
<li><code>-fuzz regexp</code> 模糊测试的方式，运行 Fuzz 测试，默认情况下不进行模糊测试。指定时，命令行参数必须与主模块中的一个包完全匹配，而正则表达式必须与该包中的一个模糊测试完全匹配 ，模糊测试将在常规测试、基准测试、其他模糊测试的种子语料库和 Example 完成后进行。</li>
<li><code>-list regexp</code> 列出所有符合正则表达式的顶层测试，不会运行任何测试。</li>
</ul></li>
<li>通用参数

<ul>
<li><code>-v</code> 输出测试细节，包括测试手动打的日志等</li>
<li><code>-timeout d</code> 超时时间，默认为 10 分钟 (10m)。</li>
<li><code>-short</code> 告诉长时间运行的测试以缩短它们的运行时间。它默认关闭，但在 all.bash 期间设置，以便安装 Go 树可以运行健全性检查，但不能花时间运行详尽的测试 (这一句不理解，这个 <a href="https://go.dev/src/all.bash">all.bash</a>？) 。</li>
<li><code>-vet list</code> 在测试前调用 <code>go vet</code></li>
<li><code>-failfast</code> 在第一次测试失败后不要开始新的测试，立即失败。</li>
<li><code>-json</code> 以 JSON 格式记录详细输出和测试结果。这提出了与机器可读格式的 -v 标志相同的信息。</li>
<li><code>-parallel n</code> 指 <code>t.Parallel</code> 调用后允许产生的做到并行运行的测试数目。 在进行模糊测试时，该标志的值是可以同时调用模糊函数的最大子进程数，而不管是否调用了 <code>t.Parallel</code>。 默认情况下，-parallel 设置为 GOMAXPROCS 的值。 将 -parallel 设置为高于 GOMAXPROCS 的值可能会由于 CPU 争用而导致性能下降，尤其是在模糊测试时。 请注意，-parallel 仅适用于单个测试二进制文件（包）。 根据 -p 标志的设置，<code>go test</code> 命令也可以并行运行不同包的测试（参见 <code>go help build</code>）。</li>
<li><code>-shuffle off,on,N</code> 随机测试执行顺序，默认为 off，<code>N</code> 为指定一个随机数种子。</li>
</ul></li>
<li>对 <code>-run</code>、<code>-bench</code> 匹配的测试的配置

<ul>
<li><code>-count n</code>，对 <code>-fuzz</code> 不生效。默认为 1 并在包列表模式（测试缓存）。手动指定 1 将禁用测试缓存。该参数仅用来指定测试运行的次数，如果设置了 -cpu，则为每个 GOMAXPROCS 值运行 n 次。</li>
<li><code>-cpu 1,2,4</code> 指定运行测试的 GOMAXPROCS 列表，默认值为当前 GOMAXPROCS 值，每个测试函数会针对每一个 cpu 值运行一次。</li>
</ul></li>
<li>对 <code>-bench</code>  匹配的测试的配置

<ul>
<li><code>-benchtime t</code> 对每个基准运行足够的迭代以获取指定的 t 作为 time.Duration（例如，<code>-benchtime 1h30s</code>）。默认值为 1 秒 (<code>1s</code>)。特殊语法 Nx 表示运行准 N 次（例如，<code>-benchtime 100x</code>）。</li>
<li><code>-benchmem</code> 打印基准测试的内存分配统计信息。</li>
</ul></li>
<li>对 <code>-fuzz</code> 匹配的测试的配置

<ul>
<li><code>-fuzztime t</code> 和 <code>-benchtime t</code> 类似。</li>
<li><code>-fuzzminimizetime t</code> 和 <code>-fuzztime t</code> 类似，表示最小值。</li>
</ul></li>
<li>覆盖率相关

<ul>
<li><code>-cover</code> 启用覆盖率统计</li>
<li><code>-covermode set,count,atomic</code>  设置正在测试的包的覆盖率分析模式。默认值为 <code>set</code>，如果启用 <code>-race</code>，默认值为 <code>atomic</code>。

<ul>
<li>set: bool: 这个语句是否运行。</li>
<li>count: int: 这个语句运行了多少次。</li>
<li>atomic: int: count，但在多线程测试中是精确的；但是代价更高。</li>
</ul></li>
<li><code>-coverpkg pattern1,pattern2,pattern3</code> 在每个测试中对匹配模式的包应用覆盖率分析。默认情况下，每个测试只分析正在测试的包。有关包模式的描述，请参阅 <code>go help packages</code>。</li>
<li><code>-coverprofile cover.out</code> 在所有测试通过后，将覆盖率配置文件写入的文件。</li>
</ul></li>
<li>性能监控相关（参见：<a href="https://pkg.go.dev/cmd/go#hdr-Testing_flags">原文</a>）

<ul>
<li><code>-blockprofile block.out</code></li>
<li><code>-blockprofilerate n</code></li>
<li><code>-cpuprofile cpu.out</code></li>
<li><code>-memprofile mem.out</code></li>
<li><code>-memprofilerate n</code></li>
<li><code>-mutexprofile mutex.out</code></li>
<li><code>-mutexprofilefraction n</code></li>
<li><code>-outputdir directory</code></li>
<li><code>-trace trace.out</code></li>
</ul></li>
<li>编译构建相关标志

<ul>
<li><code>go help build</code> 相关标志</li>
<li><code>-args</code> 将命令行的其余部分（-args 之后的所有内容）传递给测试二进制文件，未经解释且未更改。 因为这个标志占用了命令行的剩余部分，所以包列表（如果存在）必须出现在这个标志之前。</li>
<li><code>-c</code> 将测试二进制文件编译为 <code>pkg.test</code> 但不要运行它（其中 pkg 是包导入路径的最后一个元素）。 可以使用 -o 标志更改文件名。（一个例子 <code>go test ./01-testing -c</code>）</li>
<li><code>-o file</code> 将测试二进制文件编译到指定文件。测试仍然运行（除非指定了 -c 或 -i）。</li>
<li><code>-exec xprog</code> 使用 xprog 运行测试二进制文件，详见：<code>go help run</code>。</li>
<li><code>-i</code> 略，已废弃。</li>
</ul></li>
</ul>

<h2 id="go-官方维护的-mock-库">Go 官方维护的 Mock 库</h2>

<blockquote>
<p>版本：<a href="https://pkg.go.dev/github.com/mock/mockgen@v1.6.0">v1.6.0</a></p>
</blockquote>

<h3 id="示例场景">示例场景</h3>

<p>假设我们在开发一个博客后端的 article 模块，包含如下两层：</p>

<ul>
<li>service 业务逻辑，会调用 repository 层的函数，及 repository 是 service 的依赖。</li>
<li>repository 数据操纵层，对数据库等外部数据存储的操作的封装。</li>
</ul>

<p>模型和接口声明： <code>02-mock/domain/</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// article.go
</span><span style="color:#75715e"></span><span style="color:#f92672">package</span> <span style="color:#a6e22e">domain</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Article</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">ID</span>      <span style="color:#66d9ef">int64</span>
	<span style="color:#a6e22e">Author</span>  <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">Title</span>   <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">Tags</span>    []<span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">Content</span> <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ArticleRepository</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">FindByID</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">int64</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>)
	<span style="color:#a6e22e">Create</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">Article</span>) (<span style="color:#66d9ef">int64</span>, <span style="color:#66d9ef">error</span>)
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ArticleService</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">Publish</span>(<span style="color:#a6e22e">author</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">title</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">tags</span> []<span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">content</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>)
	<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">int64</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>)
}

<span style="color:#75715e">// error.go
</span><span style="color:#75715e"></span><span style="color:#f92672">package</span> <span style="color:#a6e22e">domain</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;errors&#34;</span>

<span style="color:#66d9ef">var</span> (
	<span style="color:#a6e22e">ErrRecordNotFound</span>   = <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;record not found&#34;</span>)
)</code></pre></div>
<p>service 的实现：<code>02-mock/article/service.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">article</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/rectcircle/go-test-demo/02-mock/domain&#34;</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">service</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">repository</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ArticleRepository</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewService</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ArticleRepository</span>) (<span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ArticleService</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">service</span>{
		<span style="color:#a6e22e">repository</span>: <span style="color:#a6e22e">r</span>,
	}, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">service</span>) <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">int64</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">repository</span>.<span style="color:#a6e22e">FindByID</span>(<span style="color:#a6e22e">id</span>)
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">service</span>) <span style="color:#a6e22e">Publish</span>(<span style="color:#a6e22e">author</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">title</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">tags</span> []<span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">content</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">id</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">repository</span>.<span style="color:#a6e22e">Create</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>{
		<span style="color:#a6e22e">ID</span>:      <span style="color:#ae81ff">0</span>,
		<span style="color:#a6e22e">Author</span>:  <span style="color:#a6e22e">author</span>,
		<span style="color:#a6e22e">Title</span>:   <span style="color:#a6e22e">title</span>,
		<span style="color:#a6e22e">Tags</span>:    <span style="color:#a6e22e">tags</span>,
		<span style="color:#a6e22e">Content</span>: <span style="color:#a6e22e">content</span>,
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">id</span>)
}</code></pre></div>
<h3 id="为什么需要-mock">为什么需要 Mock</h3>

<p>此时，假设想要编写测试用例，测试 service 层的函数，如果使用 repository 的实现的话，我们为每次测试，需要准备一个测试数据库，并编写 sql 将准备数据。这样做有如下问题：</p>

<ul>
<li>数据库等外部依赖安装复杂，成本高，数据准备麻烦。</li>
<li>假设 service 调用外部函数，没有测试环境，或者无法做到无状态，此时 service 的测试就无法进行。</li>
<li>对 service 的测试必须依赖 repository 就绪才能进行，而 repository 的开发可能由其他人员负责，存在依赖关系。</li>
</ul>

<p>针对这种情况，我们就需要 Mock（模拟） 待测函数的依赖。</p>

<h3 id="mock-的前提条件">Mock 的前提条件</h3>

<p>首先对待测函数的测试不能修改待测函数。这就要求，需要 Mock 的待测函数必须是可插拔的。</p>

<p>在上面的例子中，在 Go 语言中，这就要求 repository 必须是一个接口而不能是一个具体的类型。此时我们就可以写一个 repository 的 Mock 实现，在测试时准备阶段，使用 Mock 对象构造 service，然后就可以编写测试 case 了。</p>

<h3 id="mock-库的核心能力">Mock 库的核心能力</h3>

<p>当然，可以手动编写一个 repository 接口的 Mock 实现，但是会存在如下问题：针对每一个 service 的 case，都需要定义一个 Mock 实现，在测时覆盖率足够高的情况下，Mock 的数量会非常多，这会产生大量的样板代码。</p>

<p>因此，为了消除样板代码，可以抽象出一个 Mock 工具库，该工具有如下能力：</p>

<ul>
<li>根据接口生成且仅生成一个 Mock 实现的代码。</li>
<li>可以通过编程的方式，定制这个接口 Mock 实现的每个函数在什么样的参数下返回什么样的结果（打桩）。</li>
<li>可以通过编程的方式，断言这个接口 Mock 实现的每个函数在会调用多少次，是否会被调用。从被测函数的依赖函数的角度，测试被测函数的行为是否符合预期（打桩）。</li>
</ul>

<p><a href="https://github.com/golang/mock">golang/mock</a> 就实现了如上能力。</p>

<h3 id="使用-golang-mock-https-github-com-golang-mock-示例">使用 <a href="https://github.com/golang/mock">golang/mock</a> 示例</h3>

<h4 id="安装代码生成器">安装代码生成器</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">go install github.com/golang/mock/mockgen@v1.6.0</pre></div>
<h4 id="生成-mock-代码">生成 Mock 代码</h4>

<p>通过 <code>go:generate</code> 注释，快速生成代码。</p>

<p>在 <code>02-mock/domain/article.go</code> 添加如下注释：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">//go:generate mockgen -destination=./mock/mock_article_repository.go -package=mock github.com/rectcircle/go-test-demo/02-mock/domain ArticleRepository</pre></div>
<p>执行 <code>mkdir -p 02-mock/domain/mock &amp;&amp;  go generate ./...</code> 生成代码。</p>

<p>代码将生成到 <code>02-mock/domain/mock/mock_article_repository.go</code> 文件中。</p>

<h4 id="编写测试-case">编写测试 Case</h4>

<p><code>02-mock/article/service_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">article</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;reflect&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>

	<span style="color:#e6db74">&#34;github.com/golang/mock/gomock&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/go-test-demo/02-mock/domain&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/go-test-demo/02-mock/domain/mock&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Test_service_Get</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">want</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>{
		<span style="color:#a6e22e">ID</span>:      <span style="color:#ae81ff">1</span>,
		<span style="color:#a6e22e">Author</span>:  <span style="color:#e6db74">&#34;author&#34;</span>,
		<span style="color:#a6e22e">Title</span>:   <span style="color:#e6db74">&#34;title&#34;</span>,
		<span style="color:#a6e22e">Tags</span>:    []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;go&#34;</span>},
		<span style="color:#a6e22e">Content</span>: <span style="color:#e6db74">&#34;content&#34;</span>,
	}

	<span style="color:#75715e">// 准备 Mock 控制器。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ctrl</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">NewController</span>(<span style="color:#a6e22e">t</span>)
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Finish</span>()
	<span style="color:#75715e">// 构造一个 Mock 的 ArticleRepository 接口的实现 m。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   该实现的代码由 mockgen 命令生成
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   该实现的 mock 的函数的返回值通过 m.EXPECT() 方法构造
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">m</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">mock</span>.<span style="color:#a6e22e">NewMockArticleRepository</span>(<span style="color:#a6e22e">ctrl</span>)
	<span style="color:#75715e">// 声明，使用 1 调用 m.FindByID 时，返回 want。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">EXPECT</span>().<span style="color:#a6e22e">FindByID</span>(<span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">Eq</span>(int64(<span style="color:#ae81ff">1</span>))).<span style="color:#a6e22e">Return</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">want</span>, <span style="color:#66d9ef">nil</span>)
	<span style="color:#75715e">// 声明，使用非 1 调用 m.FindByID 时，返回 没有发现错误。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">EXPECT</span>().<span style="color:#a6e22e">FindByID</span>(<span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">Not</span>(int64(<span style="color:#ae81ff">1</span>))).<span style="color:#a6e22e">Return</span>(<span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrRecordNotFound</span>)

	<span style="color:#75715e">// 构造待测实例，将 mock 对象 m 传递给该实例
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewService</span>(<span style="color:#a6e22e">m</span>)
	<span style="color:#75715e">// 执行测试
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;success&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
		<span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#ae81ff">1</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;s.Get(1) err want nil, got %s&#34;</span>, <span style="color:#a6e22e">err</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">DeepEqual</span>(<span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">want</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;s.Get(1) want %+v, got %+v&#34;</span>, <span style="color:#a6e22e">want</span>, <span style="color:#a6e22e">got</span>)
		}
	})
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;notFound&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#ae81ff">2</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;s.Get(2) err want %s, got nil&#34;</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrRecordNotFound</span>)
		}
	})
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Test_service_Publish</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">want</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>{
		<span style="color:#a6e22e">ID</span>:      <span style="color:#ae81ff">1</span>,
		<span style="color:#a6e22e">Author</span>:  <span style="color:#e6db74">&#34;author&#34;</span>,
		<span style="color:#a6e22e">Title</span>:   <span style="color:#e6db74">&#34;title&#34;</span>,
		<span style="color:#a6e22e">Tags</span>:    []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;go&#34;</span>},
		<span style="color:#a6e22e">Content</span>: <span style="color:#e6db74">&#34;content&#34;</span>,
	}

	<span style="color:#a6e22e">ctrl</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">NewController</span>(<span style="color:#a6e22e">t</span>)
	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Finish</span>()
	<span style="color:#a6e22e">m</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">mock</span>.<span style="color:#a6e22e">NewMockArticleRepository</span>(<span style="color:#a6e22e">ctrl</span>)
	<span style="color:#a6e22e">data</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">int64</span>]<span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>{}
	<span style="color:#a6e22e">id</span> <span style="color:#f92672">:=</span> int64(<span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">EXPECT</span>().<span style="color:#a6e22e">FindByID</span>(<span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">Any</span>()).<span style="color:#a6e22e">DoAndReturn</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">int64</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>, <span style="color:#66d9ef">error</span>) {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">id</span>]; <span style="color:#a6e22e">ok</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">a</span>, <span style="color:#66d9ef">nil</span>
		} <span style="color:#66d9ef">else</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrRecordNotFound</span>
		}
	})
	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">EXPECT</span>().<span style="color:#a6e22e">Create</span>(<span style="color:#a6e22e">gomock</span>.<span style="color:#a6e22e">Any</span>()).<span style="color:#a6e22e">DoAndReturn</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">Article</span>) (<span style="color:#66d9ef">int64</span>, <span style="color:#66d9ef">error</span>) {
		<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">ID</span> = <span style="color:#a6e22e">id</span>
		<span style="color:#a6e22e">id</span> <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
		<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">ID</span>] = <span style="color:#f92672">*</span><span style="color:#a6e22e">a</span>
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#66d9ef">nil</span>
	})

	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewService</span>(<span style="color:#a6e22e">m</span>)
	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#e6db74">&#34;success&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
		<span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Publish</span>(<span style="color:#a6e22e">want</span>.<span style="color:#a6e22e">Author</span>, <span style="color:#a6e22e">want</span>.<span style="color:#a6e22e">Title</span>, <span style="color:#a6e22e">want</span>.<span style="color:#a6e22e">Tags</span>, <span style="color:#a6e22e">want</span>.<span style="color:#a6e22e">Content</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;s.Publish(1) err want nil, got %s&#34;</span>, <span style="color:#a6e22e">err</span>)
		}
		<span style="color:#a6e22e">want</span>.<span style="color:#a6e22e">ID</span> = <span style="color:#a6e22e">got</span>.<span style="color:#a6e22e">ID</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">DeepEqual</span>(<span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">want</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;s.Publish(1) want %+v, got %+v&#34;</span>, <span style="color:#a6e22e">want</span>, <span style="color:#a6e22e">got</span>)
		}
	})
}</code></pre></div>
<ul>
<li><code>ctrl := gomock.NewController(t)</code> 用来实现：从被测函数的依赖函数的角度，测试被测函数的行为是否符合预期，也就是说如果 <code>m</code> 中的方法被调用次数和被调用的参数不符合 <code>m.EXPECT()</code> 的声明，<code>ctrl</code> 将调用 <code>t</code> 的相关方法，标记本测试失败。</li>
<li><code>m.EXPECT()</code> 返回一个配置对象，可以配置：某个方法期望调用的参数列表、返回值、调用次数等（打桩）。</li>
<li>以上准备完成后，编写 Case 即可。</li>
</ul>

<h3 id="golang-mock-https-github-com-golang-mock-命令行说明"><a href="https://github.com/golang/mock">golang/mock</a> 命令行说明</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="color:#a6e22e">mockgen</span> <span style="color:#a6e22e">有两种操作模式</span>: <span style="color:#a6e22e">source</span> <span style="color:#a6e22e">和</span> <span style="color:#a6e22e">reflect</span><span style="color:#960050;background-color:#1e0010">。</span>

<span style="color:#a6e22e">当使用</span> <span style="color:#f92672">-</span><span style="color:#a6e22e">source</span> <span style="color:#a6e22e">标识时</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">启用</span> <span style="color:#a6e22e">source</span> <span style="color:#a6e22e">模式</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">该模式通过源代码文件来生成接口的</span> <span style="color:#a6e22e">mock</span> <span style="color:#a6e22e">实现</span><span style="color:#960050;background-color:#1e0010">。</span>
<span style="color:#f92672">-</span><span style="color:#a6e22e">imports</span> <span style="color:#a6e22e">和</span> <span style="color:#f92672">-</span><span style="color:#a6e22e">aux_files</span> <span style="color:#a6e22e">可以在</span> <span style="color:#a6e22e">Source</span> <span style="color:#a6e22e">模式下使用</span><span style="color:#960050;background-color:#1e0010">。</span>
<span style="color:#a6e22e">示例</span><span style="color:#960050;background-color:#1e0010">：</span>
        <span style="color:#a6e22e">mockgen</span> <span style="color:#f92672">-</span><span style="color:#a6e22e">source</span>=<span style="color:#a6e22e">foo</span>.<span style="color:#66d9ef">go</span> [<span style="color:#a6e22e">other</span> <span style="color:#a6e22e">options</span>]


<span style="color:#a6e22e">当传递两个非标示的参数时</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">启用</span> <span style="color:#a6e22e">reflect</span> <span style="color:#a6e22e">模式</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">该模式通过反射理解接口来生成接口的</span> <span style="color:#a6e22e">mock</span> <span style="color:#a6e22e">实现</span><span style="color:#960050;background-color:#1e0010">。</span>
<span style="color:#a6e22e">这两个参数分别是</span><span style="color:#960050;background-color:#1e0010">：</span><span style="color:#a6e22e">导入路径和通过逗号分隔符号列表</span><span style="color:#960050;background-color:#1e0010">。</span>
<span style="color:#a6e22e">示例</span><span style="color:#960050;background-color:#1e0010">：</span>
        <span style="color:#a6e22e">mockgen</span> <span style="color:#a6e22e">database</span><span style="color:#f92672">/</span><span style="color:#a6e22e">sql</span><span style="color:#f92672">/</span><span style="color:#a6e22e">driver</span> <span style="color:#a6e22e">Conn</span>,<span style="color:#a6e22e">Driver</span>

  <span style="color:#f92672">-</span><span style="color:#a6e22e">aux_files</span> <span style="color:#66d9ef">string</span>
        (<span style="color:#a6e22e">source</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">逗号分隔的</span> <span style="color:#a6e22e">pkg</span>=<span style="color:#a6e22e">path</span> <span style="color:#a6e22e">表示</span> <span style="color:#a6e22e">auxiliary</span> <span style="color:#a6e22e">Go</span> <span style="color:#a6e22e">源代码文件</span><span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">每太理解</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">可以看</span><span style="color:#960050;background-color:#1e0010">：</span><span style="color:#a6e22e">https</span>:<span style="color:#75715e">//github.com/golang/mock/issues/181）。
</span><span style="color:#75715e"></span>  <span style="color:#f92672">-</span><span style="color:#a6e22e">build_flags</span> <span style="color:#66d9ef">string</span>
        (<span style="color:#a6e22e">reflect</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">额外的</span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">build</span> <span style="color:#a6e22e">参数</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">copyright_file</span> <span style="color:#66d9ef">string</span>
        <span style="color:#a6e22e">Copyright</span> <span style="color:#a6e22e">文件将添加到生成的文件头</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">debug_parser</span>
        <span style="color:#a6e22e">只打印解析器结果</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">destination</span> <span style="color:#66d9ef">string</span>
        <span style="color:#a6e22e">输出到的文件</span><span style="color:#960050;background-color:#1e0010">；</span><span style="color:#a6e22e">默认输出到</span> <span style="color:#a6e22e">stdout</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">exec_only</span> <span style="color:#66d9ef">string</span>
        (<span style="color:#a6e22e">reflect</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">如果设置</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">执行这个反射程序源码文件</span><span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">参见</span><span style="color:#960050;background-color:#1e0010">：</span><span style="color:#f92672">-</span><span style="color:#a6e22e">prog_only</span><span style="color:#960050;background-color:#1e0010">）。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">imports</span> <span style="color:#66d9ef">string</span>
        (<span style="color:#a6e22e">source</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">逗号分隔的</span> <span style="color:#a6e22e">name</span>=<span style="color:#a6e22e">path</span> <span style="color:#a6e22e">表示要使用的显式导入</span><span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">不理解</span><span style="color:#960050;background-color:#1e0010">）。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">mock_names</span> <span style="color:#66d9ef">string</span>
        <span style="color:#a6e22e">逗号分隔的</span> <span style="color:#a6e22e">interfaceName</span>=<span style="color:#a6e22e">mockName</span> <span style="color:#a6e22e">表示生成的结构体名</span><span style="color:#960050;background-color:#1e0010">。</span><span style="color:#a6e22e">默认为</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">Mock</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#f92672">+</span> <span style="color:#a6e22e">接口名</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#f92672">package</span> <span style="color:#66d9ef">string</span>
        <span style="color:#a6e22e">生成代码的包名</span><span style="color:#960050;background-color:#1e0010">；</span><span style="color:#a6e22e">默认为</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">mock_</span><span style="color:#960050;background-color:#1e0010">&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">当前包名</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">prog_only</span>
        (<span style="color:#a6e22e">reflect</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">只生成反射程序源码</span><span style="color:#960050;background-color:#1e0010">；</span><span style="color:#a6e22e">把它写入</span> <span style="color:#a6e22e">stdout</span> <span style="color:#a6e22e">并退出</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">self_package</span> <span style="color:#66d9ef">string</span>
        <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">full</span> <span style="color:#f92672">package</span> <span style="color:#f92672">import</span> <span style="color:#a6e22e">path</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">generated</span> <span style="color:#a6e22e">code</span>. <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">purpose</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">flag</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">prevent</span> <span style="color:#f92672">import</span> <span style="color:#a6e22e">cycles</span> <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">generated</span> <span style="color:#a6e22e">code</span> <span style="color:#a6e22e">by</span> <span style="color:#a6e22e">trying</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">include</span> <span style="color:#a6e22e">its</span> <span style="color:#a6e22e">own</span> <span style="color:#f92672">package</span>. <span style="color:#a6e22e">This</span> <span style="color:#a6e22e">can</span> <span style="color:#a6e22e">happen</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">mock</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">s</span> <span style="color:#f92672">package</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">set</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">one</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">its</span> <span style="color:#a6e22e">inputs</span> (<span style="color:#a6e22e">usually</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">main</span> <span style="color:#a6e22e">one</span>) <span style="color:#a6e22e">and</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">output</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">stdio</span> <span style="color:#a6e22e">so</span> <span style="color:#a6e22e">mockgen</span> <span style="color:#a6e22e">cannot</span> <span style="color:#a6e22e">detect</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">final</span> <span style="color:#a6e22e">output</span> <span style="color:#f92672">package</span>. <span style="color:#a6e22e">Setting</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">flag</span> <span style="color:#a6e22e">will</span> <span style="color:#a6e22e">then</span> <span style="color:#a6e22e">tell</span> <span style="color:#a6e22e">mockgen</span> <span style="color:#a6e22e">which</span> <span style="color:#f92672">import</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">exclude</span>.<span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">不理解</span><span style="color:#960050;background-color:#1e0010">）</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">source</span> <span style="color:#66d9ef">string</span>
        (<span style="color:#a6e22e">source</span> <span style="color:#a6e22e">模式</span>) <span style="color:#a6e22e">输入的</span> <span style="color:#a6e22e">Go</span> <span style="color:#a6e22e">源代码文件</span><span style="color:#960050;background-color:#1e0010">；</span><span style="color:#a6e22e">启用</span> <span style="color:#a6e22e">source</span> <span style="color:#a6e22e">模式</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">version</span>
        <span style="color:#a6e22e">打印版本</span><span style="color:#960050;background-color:#1e0010">。</span>
  <span style="color:#f92672">-</span><span style="color:#a6e22e">write_package_comment</span>
        <span style="color:#a6e22e">如果为</span> <span style="color:#66d9ef">true</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">则写入包文档注释</span> (<span style="color:#a6e22e">godoc</span>)<span style="color:#960050;background-color:#1e0010">。</span> <span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">默认为</span> <span style="color:#66d9ef">true</span><span style="color:#960050;background-color:#1e0010">）。</span></pre></div>
<ul>
<li>source 模式：利用 Go 标准库的 <code>&quot;go/parser&quot;</code>。</li>
<li>反射模式：先生成一个 main 函数源码，然后编译运行这个函数。这个函数会通过反射获取到接口的信息，并生成代码。</li>
</ul>

<p>这里推荐优先使用 source 模式，如果有问题，可以回退到反射模式：</p>

<ul>
<li>source 模式性能高，生成速度快。</li>
<li>source 模式生成的代码可以保留参数名信息，有利于编写桩代码。</li>
<li>source 模式的缺点：

<ul>
<li>从 <a href="https://github.com/golang/mock/issues">issue</a> 来看，有挺多问题的。</li>
<li>无法指定生成某个接口，-source 中如果包含多个接口，都会被生成。</li>
</ul></li>
</ul>

<h3 id="golang-mock-https-github-com-golang-mock-api"><a href="https://github.com/golang/mock">golang/mock</a> API</h3>

<ul>
<li><code>MockXxx.EXPECT()</code> 返回 MockXxxRecorder 类型指针。</li>
<li><code>MockXxxRecorder.方法名(...)</code>

<ul>
<li>参数为 nil、精确值 或者 <a href="https://pkg.go.dev/github.com/golang/mock@v1.6.0/gomock#Matcher"><code>gomock.Matcher</code></a>  参数匹配与断言，如果被测函数调用时，没有匹配到，将失败。

<ul>
<li><code>All</code> 匹配所有条件</li>
<li><code>AssignableToTypeOf</code> 匹配类型</li>
<li><code>Eq</code> 精确值</li>
<li><code>InAnyOrder</code> 任意顺序的集合</li>
<li><code>Len</code> 数组长度</li>
<li><code>Nil</code> 为 nil</li>
<li><code>Not</code> 不为某个值</li>
<li>修改失败 Got 和 Want 是的输出格式，参见： <a href="https://github.com/golang/mock#modifying-failure-messages">README</a></li>
</ul></li>
<li>返回值为 <a href="https://pkg.go.dev/github.com/golang/mock@v1.6.0/gomock#Call"><code>*gomock.Call</code></a> 声明函数被调用时的一些行为或者断言。

<ul>
<li><code>After</code> 期望调用顺序。</li>
<li><code>AnyTimes</code>、<code>Times</code>、<code>MaxTimes</code>、<code>MinTimes</code> 期望调用的次数的值、最大值、最小值、等。</li>
<li><code>Return</code> 定义返回值。</li>
<li><code>Do</code>、<code>DoAndReturn</code> 被调用时，执行函数并返回。</li>
<li><code>SetArg</code> 修改函数调用的参数，应该发生在之后。</li>
<li>通过源码可知，如果 <code>Return</code>、<code>DoAndReturn</code> 被调用了多次，则函数的返回值以最后一个的返回值为准。</li>
</ul></li>
</ul></li>
</ul>

<h2 id="go-社区主流的测试库-testify">Go 社区主流的测试库 Testify</h2>

<blockquote>
<p>版本：<a href="https://pkg.go.dev/github.com/stretchr/testify@v1.8.0">v1.8.0</a></p>
</blockquote>

<p>Testify 是 Go 社区主流的测试工具集。包含如下特性：</p>

<ul>
<li>易用的断言</li>
<li>Mock</li>
<li>测试套件接口和函数</li>
</ul>

<h3 id="assert-包">assert 包</h3>

<p>提供了移动的断言函数，示例 <code>03-testify/assert_test.go</code> 如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testifydemo_test</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;testing&#34;</span>

	<span style="color:#e6db74">&#34;github.com/stretchr/testify/assert&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestSomething</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#75715e">// 相等断言
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">t</span>, <span style="color:#ae81ff">123</span>, <span style="color:#ae81ff">123</span>, <span style="color:#e6db74">&#34;they should be equal&#34;</span>)

	<span style="color:#75715e">// 不等断言
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">NotEqual</span>(<span style="color:#a6e22e">t</span>, <span style="color:#ae81ff">123</span>, <span style="color:#ae81ff">456</span>, <span style="color:#e6db74">&#34;they should not be equal&#34;</span>)

	<span style="color:#75715e">// nil 断言
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">Nil</span>(<span style="color:#a6e22e">t</span>, <span style="color:#66d9ef">nil</span>)

	<span style="color:#75715e">// 非 nil 断言
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">got2</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;Something&#34;</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">NotNil</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">got2</span>) {
		<span style="color:#75715e">// 现在 got2 不是 nil
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 可以安全地进行进一步的断言而不会导致任何错误
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">t</span>, <span style="color:#e6db74">&#34;Something&#34;</span>, <span style="color:#a6e22e">got2</span>)
	}
}</code></pre></div>
<p>stretchr/testify 的 <a href="https://pkg.go.dev/github.com/stretchr/testify@v1.8.0/assert">assert 包</a>，提供了一系列函数，封装了常见的断言逻辑，当断言失败时，这些函数会友好的打印出失败的原因，代码行数等辅助信息。如 <code>assert.Equal(t, 123, 124, &quot;they should be equal&quot;)</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== RUN   TestSomething
    /Users/xxx/Workspace/personal/go-test-demo/03-testify/assert_test.go:11:
        	Error Trace:	/Users/xxx/Workspace/personal/go-test-demo/03-testify/assert_test.go:11
        	Error:      	Not equal:
        	            	expected: 123
        	            	actual  : 124
        	Test:       	TestSomething
        	Messages:   	they should be equal</pre></div>
<p>导出的断言函数如下：</p>

<table>
<thead>
<tr>
<th>函数</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>func ObjectsAreEqual(expected, actual interface{}) bool</code></td>
<td>不要使用，这不是一个断言函数，参见：<a href="https://github.com/stretchr/testify/issues/1180">issue</a></td>
</tr>

<tr>
<td><code>func ObjectsAreEqualValues(expected, actual interface{}) bool</code></td>
<td>不要使用，这不是一个断言函数，参见：<a href="https://github.com/stretchr/testify/issues/1180">issue</a></td>
</tr>

<tr>
<td><code>func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool</code></td>
<td>友好的打印失败信息，标记失败并退出当前协程，一般不需要直接使用</td>
</tr>

<tr>
<td><code>func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool</code></td>
<td>友好的打印失败信息，标记失败，一般不需要直接使用</td>
</tr>

<tr>
<td><code>func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言 <code>object</code> 是否实现了 <code>interfaceObject</code> 接口，如 <code>assert.Implements(t, (*MyInterface)(nil), new(MyObject))</code></td>
</tr>

<tr>
<td><code>func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言 <code>object</code> 的类型和 <code>expectedType</code> 的类型是否相同</td>
</tr>

<tr>
<td><code>func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言值是否相同，指针变量相等性是根据引用值的相等性确定的，函数类型总是失败，如 <code>assert.Equal(t, 123, 123)</code></td>
</tr>

<tr>
<td><code>func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Equal</code></td>
</tr>

<tr>
<td><code>func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言两个指针的类型相同，且指针地址相同</td>
</tr>

<tr>
<td><code>func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Same</code></td>
</tr>

<tr>
<td><code>func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言相等，或可转换为相同类型且相等，如 <code>assert.EqualValues(t, uint32(123), int32(123))</code> 返回 true</td>
</tr>

<tr>
<td><code>func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>EqualValues</code></td>
</tr>

<tr>
<td><code>func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言值和类型都相同（精确相等），如 <code>assert.Exactly(t, int32(123), int64(123))</code> 返回 false</td>
</tr>

<tr>
<td><code>func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言对象是否为 nil</td>
</tr>

<tr>
<td><code>func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Nil</code></td>
</tr>

<tr>
<td><code>func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言是 emtpy，例如 nil, &ldquo;&rdquo;, false, 0 或者 len == 0 的切片或 chan 都是 empty</td>
</tr>

<tr>
<td><code>func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Empty</code></td>
</tr>

<tr>
<td><code>func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool</code></td>
<td>断言指定的对象具有特定的长度。 如果对象是无法 <code>len()</code> 的会失败，如 <code>assert.Len(t, mySlice, 3)</code></td>
</tr>

<tr>
<td><code>func True(t TestingT, value bool, msgAndArgs ...interface{}) bool</code></td>
<td>断言对象是否为 true</td>
</tr>

<tr>
<td><code>func False(t TestingT, value bool, msgAndArgs ...interface{}) bool</code></td>
<td>断言对象是否为 false</td>
</tr>

<tr>
<td><code>func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>a) 断言字符串是否包含一个子串，如 <code>assert.Contains(t, &quot;Hello World&quot;, &quot;World&quot;)</code>；b) list(array, slice&hellip;) 是否包含一个元素，如 <code>assert.Contains(t, [&quot;Hello&quot;, &quot;World&quot;], &quot;World&quot;)</code>，c) map 是否包含一个元素 <code>assert.Contains(t, {&quot;Hello&quot;: &quot;World&quot;}, &quot;Hello&quot;)</code></td>
</tr>

<tr>
<td><code>func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Contains</code></td>
</tr>

<tr>
<td><code>func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)</code></td>
<td>断言 subset 是否是 list(array, slice&hellip;) 的子集</td>
</tr>

<tr>
<td><code>func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)</code></td>
<td>参见：<code>Subset</code></td>
</tr>

<tr>
<td><code>func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool)</code></td>
<td>断言两个 list (array, slice&hellip;) 的元素是否完全相同（忽略顺序）， 如 <code>assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2])</code> 为 true</td>
</tr>

<tr>
<td><code>func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool</code></td>
<td>断言 f 函数是否 panic（原理是 f 通过 <code>recover()</code> 接收）</td>
</tr>

<tr>
<td><code>func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Panics</code></td>
</tr>

<tr>
<td><code>func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool</code></td>
<td>断言 f 函数是否发生 panic 且 painc 接收的值和 excepted 相同 (<code>==</code>)，如：<code>assert.PanicsWithValue(t, &quot;crazy error&quot;, func(){ GoCrazy() })</code></td>
</tr>

<tr>
<td><code>func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs ...interface{}) bool</code></td>
<td>断言 f 函数是否发生 panic 且 panic 接收的值为 error 且 <code>error.Error()</code> 的值和 errString 想通，如：<code>assert.PanicsWithError(t, &quot;crazy error&quot;, func(){ GoCrazy() })</code></td>
</tr>

<tr>
<td><code>func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool</code></td>
<td>断言这两个时间相差时间是否在 delta 内，如 <code>assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second)</code></td>
</tr>

<tr>
<td><code>func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool</code></td>
<td>断言 actual 是否在 start 和 end 之间（包括）， 如 <code>assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))</code></td>
</tr>

<tr>
<td><code>func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool</code></td>
<td>断言这两个数字的差值的 delta 范围内，如 <code>assert.InDelta(t, math.Pi, 22/7.0, 0.01)</code></td>
</tr>

<tr>
<td><code>func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool</code></td>
<td>和 <code>InDelta</code> 类似，断言 expected, actual 切片的对应的两个数字元素的的差值在 delta 内</td>
</tr>

<tr>
<td><code>func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool</code></td>
<td>和 <code>InDelta</code> 类似，断言 expected, actual Map 的对应的两个数字元素的的差值在 delta 内</td>
</tr>

<tr>
<td><code>func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool</code></td>
<td>断言 <code>(abs(expected - actual) / abc(expected)) &lt;= epsilon</code></td>
</tr>

<tr>
<td><code>func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool</code></td>
<td>和 <code>InEpsilon</code> 类似，断言 expected, actual 切片的对应的两个数字元素满足 <code>InEpsilon</code></td>
</tr>

<tr>
<td><code>func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool</code></td>
<td>和 <code>InEpsilon</code> 类似，断言 expected, actual 切片的对应的两个数字元素满足 <code>InEpsilon</code></td>
</tr>

<tr>
<td><code>func Error(t TestingT, err error, msgAndArgs ...interface{}) bool</code></td>
<td>断言 err 是否不为 nil</td>
</tr>

<tr>
<td><code>func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>NoError</code></td>
</tr>

<tr>
<td><code>func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool</code></td>
<td>断言 <code>theError.Error()</code> 和 errorString 是否相等</td>
</tr>

<tr>
<td><code>func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool</code></td>
<td>断言 <code>theError.Error()</code> 是否包含 <code>contains</code> 是否相等</td>
</tr>

<tr>
<td><code>func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言字符串是否和正则表达式匹配，如 <code>assert.Regexp(t, regexp.MustCompile(&quot;start&quot;), &quot;it's starting&quot;)</code>、<code>assert.Regexp(t, &quot;start...$&quot;, &quot;it's not starting&quot;)</code></td>
</tr>

<tr>
<td><code>func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见： <code>Regexp</code></td>
</tr>

<tr>
<td><code>func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>断言 i 是否是零值</td>
</tr>

<tr>
<td><code>func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>Zero</code></td>
</tr>

<tr>
<td><code>func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool</code></td>
<td>断言文件是否存在，如果是目录将失败</td>
</tr>

<tr>
<td><code>func NoFileExists(t TestingT, path string, msgAndArgs ...interface{}) bool</code></td>
<td>参见： <code>FileExists</code></td>
</tr>

<tr>
<td><code>func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool</code></td>
<td>断言目录是否存在</td>
</tr>

<tr>
<td><code>func NoDirExists(t TestingT, path string, msgAndArgs ...interface{}) bool</code></td>
<td>参见： <code>DirExists</code></td>
</tr>

<tr>
<td><code>func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool</code></td>
<td>断言两个 JSON 字符窜是否相等，如 <code>assert.JSONEq(t, `{&quot;hello&quot;: &quot;world&quot;, &quot;foo&quot;: &quot;bar&quot;}`, `{&quot;foo&quot;: &quot;bar&quot;, &quot;hello&quot;: &quot;world&quot;}`)</code></td>
</tr>

<tr>
<td><code>func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool</code></td>
<td>断言两个 YAML 字符窜是否相等</td>
</tr>

<tr>
<td><code>func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool</code></td>
<td>该函数每经过 tick 时间，调用一次 <code>condition</code> 函数，如果在 <code>waitFor</code> 时间内返回 true 则断言成功，如 <code>assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond)</code></td>
</tr>

<tr>
<td><code>func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool</code></td>
<td>参见： <code>Eventually</code>，即在 <code>waitFor</code> 间内 condition 没有返回 true</td>
</tr>

<tr>
<td><code>func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool</code></td>
<td>通过 <code>errors.Is</code> 进行断言</td>
</tr>

<tr>
<td><code>func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool</code></td>
<td>参见：<code>ErrorIs</code></td>
</tr>

<tr>
<td><code>func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{})</code></td>
<td>通过 <code>errors.As</code> 进行断言</td>
</tr>
</tbody>
</table>

<h3 id="require-包">require 包</h3>

<p>类似于 <a href="https://pkg.go.dev/github.com/stretchr/testify@v1.8.0/assert">assert 包</a> ，不同点在于 <a href="https://pkg.go.dev/github.com/stretchr/testify@v1.8.0/require">require</a> 包会在断言失败后立即退出。</p>

<h3 id="mock-包">mock 包</h3>

<p>能力和上文提到的 <a href="https://github.com/golang/mock">golang/mock</a> 类似，在此不多介绍了。建议直接使用 golang/mock。</p>

<h3 id="suite-包">suite 包</h3>

<p>提供了类似面向对象语言的测试套件（如 junit），主流的 IDE （如 VSCode <a href="https://github.com/golang/vscode-go/blob/master/CHANGELOG.md#0684---29th-june-2018">Go 扩展</a>）对该包提供了原生的支持。</p>

<p>示例 <code>03-testify/suite_test.go</code> 如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">testifydemo_test</span>

<span style="color:#75715e">// Basic imports
</span><span style="color:#75715e"></span><span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;testing&#34;</span>

	<span style="color:#e6db74">&#34;github.com/stretchr/testify/assert&#34;</span>
	<span style="color:#e6db74">&#34;github.com/stretchr/testify/suite&#34;</span>
)

<span style="color:#75715e">// 定义测试套件结构体，嵌入一个 suite.Suite，该结构体包含一个 T() 方法可以返回原生的 *testing.T
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ExampleTestSuite</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">suite</span>.<span style="color:#a6e22e">Suite</span>
}

<span style="color:#75715e">// 运行套件内所有测试函数前，执行且只执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">SetupSuite</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++SetupSuite+++&#34;</span>)
}

<span style="color:#75715e">// 运行套件内所有测试函数后，执行且只执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">TearDownSuite</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++TearDownSuite+++&#34;</span>)
}

<span style="color:#75715e">// 运行套件内的每个测试前，都会执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">SetupTest</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++SetupTest+++&#34;</span>)
}

<span style="color:#75715e">// 运行套件内的每个测试后，都会执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">TearDownTest</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++TearDownTest+++&#34;</span>)
}

<span style="color:#75715e">// 运行套件内的每个测试前，都会执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">BeforeTest</span>(<span style="color:#a6e22e">suiteName</span>, <span style="color:#a6e22e">testName</span> <span style="color:#66d9ef">string</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;+++BeforeTest(suiteName=%s, testName=%s)+++\n&#34;</span>, <span style="color:#a6e22e">suiteName</span>, <span style="color:#a6e22e">testName</span>)
}

<span style="color:#75715e">// 运行套件内的每个测试后，都会执行一次该函数。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">AfterTest</span>(<span style="color:#a6e22e">suiteName</span>, <span style="color:#a6e22e">testName</span> <span style="color:#66d9ef">string</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;+++AfterTest(suiteName=%s, testName=%s)+++\n&#34;</span>, <span style="color:#a6e22e">suiteName</span>, <span style="color:#a6e22e">testName</span>)
}

<span style="color:#75715e">// 运行套件内所有测试函数后，执行且只执行一次该函数，可以获取执行结果（起止时间、是否通过）相关信息。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">HandleStats</span>(<span style="color:#a6e22e">suiteName</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">stats</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">suite</span>.<span style="color:#a6e22e">SuiteInformation</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;+++HandleStats(suiteName=%s, stats=%+v)+++\n&#34;</span>, <span style="color:#a6e22e">suiteName</span>, <span style="color:#a6e22e">stats</span>)
}

<span style="color:#75715e">// 测试套件内，所有以 Test 开头的方法都会作为测试运行
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">TestExample1</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++TestExample1+++&#34;</span>)
	<span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">True</span>(<span style="color:#a6e22e">suite</span>.<span style="color:#a6e22e">T</span>(), <span style="color:#66d9ef">true</span>)
}

<span style="color:#75715e">// 测试套件内，所有以 Test 开头的方法都会作为测试运行
</span><span style="color:#75715e">// 注意：可以使用 suite.Suite 导出的断言函数，以方便测试
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">suite</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ExampleTestSuite</span>) <span style="color:#a6e22e">TestExample2</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;+++TestExample1+++&#34;</span>)
	<span style="color:#a6e22e">suite</span>.<span style="color:#a6e22e">True</span>(<span style="color:#66d9ef">true</span>)
}

<span style="color:#75715e">// 为了让 go test 运行这个套件，我们需要创建一个正常的测试函数并将套件的指针传递给 suite.Run 函数
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestExampleTestSuite</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">suite</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">t</span>, new(<span style="color:#a6e22e">ExampleTestSuite</span>))
}</code></pre></div>
<p>使用 <code>go test -run ^TestExampleTestSuite$ github.com/rectcircle/go-test-demo/03-testify -v -testify.m ^TestExample1$</code> 命令可以运行该测试内的某个具体测试（通过 <code>-testify.m</code> 指定）。</p>

<p>使用 <code>go test -run ^TestExampleTestSuite$ github.com/rectcircle/go-test-demo/03-testify -v</code> 命令，可以运行该套件的所有测试，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== RUN   TestExampleTestSuite
+++SetupSuite+++
=== RUN   TestExampleTestSuite/TestExample1
+++SetupTest+++
+++BeforeTest(suiteName=ExampleTestSuite, testName=TestExample1)+++
+++TestExample1+++
+++AfterTest(suiteName=ExampleTestSuite, testName=TestExample1)+++
+++TearDownTest+++
=== RUN   TestExampleTestSuite/TestExample2
+++SetupTest+++
+++BeforeTest(suiteName=ExampleTestSuite, testName=TestExample2)+++
+++TestExample1+++
+++AfterTest(suiteName=ExampleTestSuite, testName=TestExample2)+++
+++TearDownTest+++
+++TearDownSuite+++
+++HandleStats(suiteName=ExampleTestSuite, stats=&amp;{Start:2022-08-14 00:30:45.337556 +0800 CST m=+0.003622966 End:2022-08-14 00:30:45.338018 +0800 CST m=+0.004085457 TestStats:map[TestExample1:0xc000262140 TestExample2:0xc000262190]})+++
--- PASS: TestExampleTestSuite (0.00s)
    --- PASS: TestExampleTestSuite/TestExample1 (0.00s)
    --- PASS: TestExampleTestSuite/TestExample2 (0.00s)
PASS
ok      github.com/rectcircle/go-test-demo/03-testify   1.193s</pre></div>
<p>通过如上示例可以看出：</p>

<ul>
<li><p>生命周期函数以及生命周期为（定义在：<a href="https://github.com/stretchr/testify/blob/master/suite/interfaces.go">suite/interfaces.go</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">启动测试套件
    |
    v
SetupSuite
    |
    v
 SetupTest       ---+
    |               |
    v               |
 BeforeTest      ---+
    |               |
    v               |
  TestXxx        ---+--&gt;  每个测试仅按此循序执行
    |               |
    v               |
 AfterTest       ---+
    |               |
    v               |
TearDownTest     ---+
    |
    v
TearDownSuite
    |
    v
HandleStats</pre></div></li>

<li><p>go test 通过 <code>-testify.m</code> 可以实现只测试某个测试函数。</p></li>

<li><p>suite.Suite 也导出了一系列断言函数 <code>suite.Xxx(...)</code>，等价于  <code>assert.Xxx(suite.T(), ...)</code></p></li>
</ul>
]]></description></item><item><title>Go 1.19 发行说明</title><link>https://www.rectcircle.cn/posts/go-1-19-release-notes/</link><pubDate>Sat, 06 Aug 2022 10:24:40 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-1-19-release-notes/</guid><description type="html"><![CDATA[

<h2 id="值得开发者关注的特性">值得开发者关注的特性</h2>

<ul>
<li>更新 <a href="https://go.dev/ref/mem">内存模型文档</a>，是其更易读。阅读改文章，可以避免一些并发内存访问不当，导致的非预期问题。</li>
<li><a href="https://pkg.go.dev/sync/atomic#pkg-types">sync/atomic</a> 新增了一些易用的类型，如 <code>atomic.Int64</code> 等。</li>
<li><a href="https://go.dev/doc/comment">文档注释</a>，终于支持链接、列表，以及明确的标题语法。</li>
<li>软内存限制，通过 <a href="/pkg/runtime/debug/#SetMemoryLimit"><code>runtime/debug.SetMemoryLimit</code></a> 或 <a href="/pkg/runtime/#hdr-Environment_Variables"><code>GOMEMLIMIT</code></a> 环境变量配置。在某些不期望内存占用过多，期望提早 GC 以及释放内存给 OS 的场景可以使用。</li>
</ul>

<h2 id="语言层面改变">语言层面改变</h2>

<p>基本上没有变化，仅修复了一个 <a href="https://github.com/golang/go/issues/52038">bug</a>。</p>

<h2 id="内存模型更新">内存模型更新</h2>

<p>2022 年 6 月 6 日，<a href="https://github.com/golang/go/commit/865911424d509184d95d3f9fc6a8301927117fdc?diff=split">更新</a>了 <a href="https://go.dev/ref/mem">Go 内存模型文档</a>（参见：<a href="https://github.com/golang/go/discussions/47141">讨论</a>），和 C、C++、Java、JavaScript、Rust 和 Swift 对齐。</p>

<p>Go 只提供<a href="https://en.wikipedia.org/wiki/Sequential_consistency">顺序一致</a>原子操作 (sequentially consistent atomics)，而不是其他语言中的任何更宽松的形式（<a href="https://www.yuque.com/gamergodot/kcfazr/ixxbyo">浅谈Happens-Before：以Go Memory Model为例</a>）。</p>

<p>在 Go 1.19，标准库的 <a href="https://go.dev/doc/go1.19#atomic_types">sync/atomic</a> 包添加了新的 atomic 类型，如：<a href="https://go.dev/pkg/sync/atomic/#Int64">atomic.Int64</a> 和 <a href="https://go.dev/pkg/sync/atomic/#Pointer">atomic.Pointer[T]</a> 等。</p>

<h2 id="端">端</h2>

<ul>
<li>支持 Linux <a href="https://loongson.github.io/LoongArch-Documentation/">龙芯架构</a> (<code>GOOS=linux</code>, <code>GOARCH=loong64</code>)。实现的 ABI 是 LP64D。支持的最低内核版本为 5.19。</li>
<li>riscv64 现在支持使用寄存器传递函数参数和结果。基准测试显示 riscv64 的典型性能提升 10% 或更多。</li>
</ul>

<h2 id="工具">工具</h2>

<h3 id="文档注释">文档注释</h3>

<p>添加了对链接、列表和更清晰标题的支持，在 2022-06-01 新增了一篇关于文档注释的<a href="https://go.dev/doc/comment">官方文档</a>。</p>

<ul>
<li><p>外部链接，和 <a href="https://spec.commonmark.org/0.30/#shortcut-reference-link">markdown 引用链接</a>类似。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="color:#75715e">// Package json implements encoding and decoding of JSON as defined in
</span><span style="color:#75715e">// [RFC 7159]. The mapping between JSON and Go values is described
</span><span style="color:#75715e">// in the documentation for the Marshal and Unmarshal functions.
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// For an introduction to this package, see the article
</span><span style="color:#75715e">// “[JSON and Go].”
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// [RFC 7159]: https://tools.ietf.org/html/rfc7159
</span><span style="color:#75715e">// [JSON and Go]: https://golang.org/doc/articles/json_and_go.html
</span><span style="color:#75715e"></span><span style="color:#f92672">package</span> <span style="color:#a6e22e">json</span></pre></div></li>

<li><p>文档链接，形如 <code>[Name1]</code>、<code>[Name1.Name2]</code>、<code>[pkg]</code>， <code>[example.com/sys.File]</code>，为了避免和 map list 的歧义，文档链接前后必须有空格或表单符号等字符。</p></li>

<li><p>列表和 markdown 类似。</p>

<ul>
<li><p>无需列表，以 <code>*</code>, <code>+</code>, <code>-</code>, <code>•</code>; <code>U+002A</code>, <code>U+002B</code>, <code>U+002D</code>, <code>U+2022</code> 开头</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="color:#f92672">package</span> <span style="color:#a6e22e">url</span>

<span style="color:#75715e">// PublicSuffixList provides the public suffix of a domain. For example:
</span><span style="color:#75715e">//   - the public suffix of &#34;example.com&#34; is &#34;com&#34;,
</span><span style="color:#75715e">//   - the public suffix of &#34;foo1.foo2.foo3.co.uk&#34; is &#34;co.uk&#34;, and
</span><span style="color:#75715e">//   - the public suffix of &#34;bar.pvt.k12.ma.us&#34; is &#34;pvt.k12.ma.us&#34;.
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">PublicSuffixList</span> <span style="color:#66d9ef">interface</span> {
    <span style="color:#f92672">...</span>
}</pre></div></li>

<li><p>有序列表，以数字开头</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="color:#f92672">package</span> <span style="color:#a6e22e">path</span>

<span style="color:#75715e">// Clean returns the shortest path name equivalent to path
</span><span style="color:#75715e">// by purely lexical processing. It applies the following rules
</span><span style="color:#75715e">// iteratively until no further processing can be done:
</span><span style="color:#75715e">//
</span><span style="color:#75715e">//  1. Replace multiple slashes with a single slash.
</span><span style="color:#75715e">//  2. Eliminate each . path name element (the current directory).
</span><span style="color:#75715e">//  3. Eliminate each inner .. path name element (the parent directory)
</span><span style="color:#75715e">//     along with the non-.. element that precedes it.
</span><span style="color:#75715e">//  4. Eliminate .. elements that begin a rooted path:
</span><span style="color:#75715e">//     that is, replace &#34;/..&#34; by &#34;/&#34; at the beginning of a path.
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Clean</span>(<span style="color:#a6e22e">path</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
    <span style="color:#f92672">...</span>
}</pre></div></li>
</ul></li>

<li><p>标题，以 <code>#</code> 开头，而不是之前的隐式的规则。</p></li>
</ul>

<h3 id="新的-unix-构建约束">新的 unix 构建约束</h3>

<p>现在可以在 <code>//go:build</code> 行中识别构建约束 <code>unix</code>。如果目标操作系统（也称为 GOOS）是 Unix 或类 Unix 系统，则满足约束。对于 1.19 版本，如果 GOOS 是 <code>aix</code>、<code>android</code>、<code>darwin</code>、<code>dragonfly</code>、<code>freebsd</code>、<code>hurd</code>、<code>illumos</code>、<code>ios</code>、<code>linux</code>、<code>netbsd</code>、<code>openbsd</code> 或 <code>solaris</code> 之一，则满足要求。在未来的版本中，unix 约束可能会匹配其他新支持的操作系统。</p>

<h3 id="go-command">Go command</h3>

<p>参见：<a href="https://go.dev/doc/go1.19#atomic_types#go-command">原文</a>。</p>

<h3 id="vet">Vet</h3>

<p>添加一个新的检查器，<code>errorsas</code>，当 <code>errors.As</code> 的第二个参数是 <code>*error</code> 类型时报错。</p>

<h2 id="运行时">运行时</h2>

<ul>
<li><p>通过 <a href="/pkg/runtime/debug/#SetMemoryLimit"><code>runtime/debug.SetMemoryLimit</code></a> 或 <a href="/pkg/runtime/#hdr-Environment_Variables"><code>GOMEMLIMIT</code></a> 环境变量，可以配置软内存限制(soft memory limit)。Go 运行时会尽力维持内存消耗小于该限制，此功能不保证：</p>

<ul>
<li>过小的内存限制不会遵守，约几十m以下，参见：<a href="https://go.dev/issue/52433">issue</a>。</li>
<li>不能 100% 消除 OOM。</li>
</ul>

<p>更多参见：<a href="https://go.dev/doc/gc-guide#Memory_limit">A Guide to the Go Garbage Collector</a></p></li>

<li><p>import os 会自动增加 可打开的最大文件描述符数量 到 hard limits，参见：<a href="https://github.com/golang/go/issues/46279">issue</a>。</p></li>

<li><p>除非 <code>GOTRACEBACK=system</code> 或 <code>crash</code>，否则不可恢复的致命错误（例如并发写 map 或解锁未锁定的 mutex）现在打印更简单的堆栈，不包括运行时元数据。无论 GOTRACEBACK 的值如何，运行时内部的致命错误回溯始终包含完整的元数据。</p></li>

<li><p>在 ARM64 上添加了对调试器注入函数调用的支持，使用户能够在使用经过更新以利用此功能的调试器时，在交互式调试会话中从其二进制文件调用函数（<a href="https://github.com/go-delve/delve/blob/master/CHANGELOG.md#190-2022-07-06">dlv 已支持</a>）。</p></li>
</ul>

<h2 id="编译器">编译器</h2>

<p>switch 字符串和数字使用<a href="https://en.wikipedia.org/wiki/Branch_table">跳表</a>实现，性能提升 20%。</p>

<p>其他：<a href="https://go.dev/doc/go1.19#compiler">略</a></p>

<h2 id="汇编器">汇编器</h2>

<p><a href="https://go.dev/doc/go1.19#assembler">略</a></p>

<h2 id="链接器">链接器</h2>

<p><a href="https://go.dev/doc/go1.19#linker">略</a></p>

<h2 id="核心库">核心库</h2>

<h3 id="新的-atomic-类型">新的 atomic 类型</h3>

<p>上文 <a href="#内存模型更新">内存模型更新</a> 已介绍，更多参见： <a href="https://pkg.go.dev/sync/atomic#pkg-types">go docs</a>。</p>

<h3 id="os-exec-可执行文件路径查找">os/exec 可执行文件路径查找</h3>

<p>出于<a href="https://go.dev/blog/path-security">安全原因</a>，<code>exec.Command(&quot;prog&quot;)</code> 默认将不会查找 work directory。这个改变可能破坏了现有程序，更多参见：<a href="https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory">os/exec go docs</a></p>

<h3 id="小的改变">小的改变</h3>

<p><a href="https://go.dev/doc/go1.19#minor_library_changes">略</a></p>
]]></description></item><item><title>暴露边缘服务简单设计和示例实现</title><link>https://www.rectcircle.cn/posts/design-and-demo-of-expose-edge-service/</link><pubDate>Sat, 23 Jul 2022 22:10:05 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/design-and-demo-of-expose-edge-service/</guid><description type="html"><![CDATA[

<h2 id="需求说明">需求说明</h2>

<p>在典型的微后端场景中，项目会被拆成多个可以独立运行的微服务，这些服务会通过相关协议（如 http/protobuf/thrift）相互调用。在实现上，这些服务一般会通过暴露 TCP 端口的方式提供服务。</p>

<p>这在网络层面隐含了一个假设：服务间的网络必须是双向可通的，即这些服务需要在同一个内网。这个假设在一般的机房内部很容易实现，但是在很多场景中，服务不一定是部署在同一个机房中，这就涉及了跨网络调用的问题。</p>

<p>本文设想一个场景：一个项目包含多个服务，这些服务可以分为两种类型：</p>

<ul>
<li>核心服务：部署于同一个数据中心的核心。</li>
<li>边缘服务：部署于任意网络环境中的边缘设备，简单起见，该服务只有一个实例。</li>
</ul>

<p>在网络上，边缘服务可以单向访问核心服务，但是核心服务不能单向访问边缘服务（因为边缘服务没有公网 IP）。</p>

<p>希望达到的效果是：一旦边缘服务上线，核心服务可以直接调用运行在边缘设备上的边缘服务，就像两者运行在同一个内网上的微服务一样。</p>

<h2 id="架构和实现">架构和实现</h2>

<h3 id="架构图">架构图</h3>

<p><img src="/image/design-and-demo-of-expose-edge-service.svg" alt="image" /></p>

<p>上图描述了一种暴露边缘服务给核心服务的架构。</p>

<h3 id="组件">组件</h3>

<p>包含如下几类组件，这些组件使用的技术如下所示：</p>

<table>
<thead>
<tr>
<th>组件</th>
<th>相关技术</th>
</tr>
</thead>

<tbody>
<tr>
<td>Exposer Server</td>
<td>Websocket Server、多路复用器 Client</td>
</tr>

<tr>
<td>Exposer Client</td>
<td>Websocket Client、多路复用器 Server</td>
</tr>

<tr>
<td>协议转换器</td>
<td>Websocket Client</td>
</tr>
</tbody>
</table>

<h3 id="具体流程">具体流程</h3>

<p>从按照时序上来说，需要访问边缘服务，需要先建立暴露边缘服务的长连接，在访问边缘服务两个流程。</p>

<h4 id="建立暴露边缘服务的长连接">建立暴露边缘服务的长连接</h4>

<ol>
<li>Exposer Client

<ol>
<li>读取配置（可能从本地配置文件或者相关服务），获取需要暴露的服务的元数据（设备 ID、服务 ID、协议类型、端口）。</li>
<li>为每个服务与 Exposer Server 的 Expose 端点，建立 Websocket 连接，并将服务的元数据通过 Header 传递给 Exposer Server（对应下文的 2.2）。</li>
<li>使用该 Websocket 连接，创建一个多路复用器 Server，记录当前多路复用器 Server 和服务元信息的映射关系，并 Accept 等待连接。</li>
</ol></li>
<li>Exposer Server 集群（Expose 端点）

<ol>
<li>等待来自 Exposer Client 的 Websocket 的连接。</li>
<li>接收到 Websocket 连接后，校验 Header 中的服务元数据，校验通过后建立连接。并将路由信息记录到全局路由表中（redis）：key 为 服务 ID + 设备 ID，value 为当前实例的 IP Port。</li>
<li>使用该 Websocket 连接，创建一个多路复用器 Client。并将该多路复用 Client 存储到内存中的会话表中，key 为服务 ID + 设备 ID，value 为多路复用器 Client。</li>
</ol></li>
</ol>

<h4 id="访问边缘服务">访问边缘服务</h4>

<p>微服务想要调用位于边缘设备中的边缘服务的接口（假设边缘服务协议为 HTTP）。</p>

<ol>
<li>微服务，像直接访问边缘服务一样访问 HTTP 协议转换器，并根据 HTTP 协议转换器的标准，通过 Header 传递需要访问的设备 ID 和服务 ID。</li>
<li>HTTP 协议转换器

<ol>
<li>根据 Header 中的设备 ID 和服务 ID，查找全局路由表（redis），获取需要连接的 Exposer Server 的 ip prot。</li>
<li>和上一步选取的 Exposer Server 实例的 Access 端点，建立一个 Websocket 连接，并根据 Exposer Server 协议转换器的标准，通过 Header 传递需要访问的设备 ID 和服务 ID。</li>
<li>使用 HTTP 反向代理库，将上一步建立的 Websocket 连接作为底层通道，对微服务的流量进行转发（即 http over websocket）。</li>
</ol></li>
<li>Exposer Server 集群（Access 端点）

<ol>
<li>等待来自 协议转换器 的 Websocket 的连接。</li>
<li>接收到 Websocket 连接后，根据 Header 中的设备 ID 和服务 ID，查找内存中的会话表，拿到对应的多路复用器 Client，并打开一个连接。</li>
<li>将流量转发到该多路复用连接上。</li>
</ol></li>
<li>Exposer Client

<ol>
<li>多路复用器的 Server Accept 到连接，根据当前多路复用器 Server 和服务元信息的映射关系，获取到边缘服务的协议类型和端口，发现协议类型是 HTTP，则与边缘服务所在端口建立 TCP 连接。</li>
<li>将流量准发到这个 TCP 连接上。</li>
</ol></li>
</ol>

<h2 id="其他说明">其他说明</h2>

<h3 id="为什么使用-websocket">为什么使用 Websocket</h3>

<p>Expose Client 和 Expose Server 需要建立一个长连接。主流的可以选择的协议有：</p>

<ul>
<li>Websocket</li>
<li>TCP</li>
<li>QUIC</li>
</ul>

<p>本方案选择 Websocket 的原因是普适性更强、开发成本更低。</p>

<ul>
<li>Websocket 可以复用目前 Web 后端领域的沉淀很久的基础设施，如 Nginx。</li>
<li>Websocket 可以在握手阶段传递设备 ID 和服务 ID 等元信息，减少开发成本。</li>
</ul>

<p>当然如果追求极致的性能，也可以选择 TCP 或 QUIC，自己实现一套握手协议即可。</p>

<h3 id="多路复用器概念原理">多路复用器概念原理</h3>

<p>在通讯领域，多路复用指的是，一条物理链路上可以建立多个 TCP 连接。</p>

<p>本文的多路复用器指的是软件上的多路复用器，即可以在一条底层连接，可以建立多条逻辑连接。</p>

<p>以 golang 的 <a href="https://github.com/hashicorp/yamux">hashicorp/yamux</a> 为例，底层连接即：任意实现 net.Conn 接口的类型，如 TCP 连接等。</p>

<p>特别说明的是，多路复用器的 Server 和 Client 不需要和底层连接的 Server 和 Client 对应，从上文可以看到：</p>

<ul>
<li>在 Exposer Client 中，多路复用器的底层连接是一个 Websocket 的 Client 侧连接，但是建立的是多路复用器的 Server。</li>
<li>在 Exposer Server 中，多路复用器的底层连接是一个 Websocket 的 Server 侧连接，但是建立的是多路复用器的 Client。</li>
</ul>

<h2 id="示例代码">示例代码</h2>

<p>上述架构，通过 Golang 进行了简单的实现，具体参见：github <a href="https://github.com/rectcircle/expose-edge-service-demo">rectcircle/expose-edge-service-demo</a>。</p>

<h2 id="本文未讨论部分">本文未讨论部分</h2>

<ul>
<li>Exposer Server 的权限校验等相关能力可以进一步拆分出来成为一个网关。</li>
<li>Exposer Client 和 Exposer Server 流量控制以及负载均衡需要更仔细的设计（比如：根据负载， Exposer Server 和 Exposer Client 建立多个 Expose 的 Websocket 连接）。</li>
<li>协议转换器和 Exposer Server 集群的通信协议，Websocket 不是必须的，可以使用任意性能更好的协议，只要是可靠的连接，并在握手阶段可以传递设备 ID 和服务 ID 即可。</li>
</ul>

<h2 id="具体应用场景">具体应用场景</h2>

<p>该场景核心就是所谓的内网穿透，本文给出的是一种支持横向扩容，可以云原生部署的方案。如下场景可以参考该设计：</p>

<ul>
<li>远程/云端调用 IoT 设备</li>
<li>混合云控制面调用私有云上的组件</li>
<li>面向个人提供的基于内网穿透的应用

<ul>
<li>内网端口暴露</li>
<li>远程桌面</li>
</ul></li>
<li>服务端推送给客户端推送消息</li>
</ul>
]]></description></item><item><title>理解和使用 GPG</title><link>https://www.rectcircle.cn/posts/understand-and-use-gpg/</link><pubDate>Sun, 17 Jul 2022 12:22:30 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/understand-and-use-gpg/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考：<a href="https://www.gnupg.org/">GPG 官方网站</a></p>
</blockquote>

<h2 id="基础知识">基础知识</h2>

<p>要理解 GPG 的设计和原理。需要具备加密算法的基本知识，参见： <a href="/posts/understand-ssl-tls/">理解 ssl/tls 协议: 从加密算法到 ssl/tls</a>。</p>

<h2 id="gpg-简介">GPG 简介</h2>

<p>学术界发明了各种加密算法，这些算法最早军事和情报领域得以应用。</p>

<p>随着信息革命的不断深化，加密算法在工业界也逐步落地，上面提到的 ssl/tls 就是一种应用场景。</p>

<p>ssl/tls 面向的是是个人和组织间安全通讯的标准，即 C/S 架构。而 GPG (GunPG) 提供的是一套基于加密算法实现的一套标准的安全工具包，该工具实现了 <a href="https://datatracker.ietf.org/wg/openpgp/documents/">IETF OpenPGP 标准</a>。</p>

<p>该工具主要提供了如下两个核心能力：</p>

<ul>
<li>key 管理（公私钥管理）</li>
<li>数据签名和校验、数据加密和解密</li>
</ul>

<p>该工具的应用场景如下所示：</p>

<ul>
<li>电子邮件加密传输</li>
<li>git commit 签名</li>
<li>管理 ssh key</li>
<li>文件加密存储</li>
</ul>

<h2 id="安装-gpg">安装 GPG</h2>

<p>Linux：常见的发行版已预装 GPG，如 Debian 系列、RHEL 系列 等.</p>

<p>MacOS：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install gpg</code></pre></div>
<h2 id="使用-gpg">使用 GPG</h2>

<h3 id="key-管理">Key 管理</h3>

<h4 id="核心概念">核心概念</h4>

<blockquote>
<p>参考：<a href="https://zhuanlan.zhihu.com/p/137801979">简明 GPG 概念</a></p>
</blockquote>

<p>Key 管理是 GPG 的核心功能，GPG Key 不能简单的理解为非对称加密的 Private Key / Public Key / Key Pair。GPG Key 由如下信息组成：</p>

<ul>
<li>Key ID: 该 GPG Key 的唯一标识，值为主公钥的指纹，支持多种格式(Fingerprint, Long key ID, Short key ID)，更多参见：<a href="https://superuser.com/questions/769452/what-is-a-openpgp-gnupg-key-id">What is a OpenPGP/GnuPG key ID?</a>。</li>
<li>UID: 1 个或多个，每个 UID 由 name、email、comment 组成，email 和 comment 可以为空。</li>
<li>Expire: 过期时间，可以为永久。</li>

<li><p>多个具有不同用途的非对称加密算法中的 Key 的集合。</p>

<ul>
<li>key 分类参见下表。</li>
</ul>

<table>
<thead>
<tr>
<th>类型</th>
<th>全名</th>
<th>缩写</th>
<th>用途 (Usage)</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>主私钥</td>
<td>Secret Key</td>
<td>sec</td>
<td>SC</td>
<td>每个 GPG Key 有且只有一个 主私钥，可以选择一种或多种 Usage</td>
</tr>

<tr>
<td>主公钥</td>
<td>Public Key</td>
<td>pub</td>
<td>SC</td>
<td>每个 GPG Key 有且只有一个 主公钥，可以选择一种或多种 Usage</td>
</tr>

<tr>
<td>子私钥</td>
<td>Secret Subkey</td>
<td>ssb</td>
<td>S/A/E</td>
<td>每个 GOG Key 可以有多个子私钥，每个子私钥可以选择一种或多种 Usage</td>
</tr>

<tr>
<td>子公钥</td>
<td>Public Subkey</td>
<td>sub</td>
<td>S/A/E</td>
<td>每个 GOG Key 可以有多个子公钥，每个子公钥可以选择一种或多种 Usage</td>
</tr>
</tbody>
</table>

<ul>
<li>主秘钥和主公钥（Primary Key）、子秘钥和子公钥（Sub Key）都是成对出现的，其用途也是一致的。</li>
<li>每一对都包含一个 key id 属性（为 public key 的指纹），其中主密钥/主公钥的 key id 就是当前 GPG Key 的 Key ID。</li>
<li>上面提到的用途，如下表所示：</li>
</ul>

<table>
<thead>
<tr>
<th>缩写</th>
<th>全名</th>
<th>用途</th>
</tr>
</thead>

<tbody>
<tr>
<td>C</td>
<td>Certificating</td>
<td>管理证书，如添加/删除/吊销子密钥/UID，修改过期时间。</td>
</tr>

<tr>
<td>S</td>
<td>Signing</td>
<td>签名，如文件数字签名、邮件签名、Git 提交。</td>
</tr>

<tr>
<td>A</td>
<td>Authenticating</td>
<td>身份验证，如登录。</td>
</tr>

<tr>
<td>E</td>
<td>Encrypting</td>
<td>加密，如文件和文本。</td>
</tr>
</tbody>
</table>

<ul>
<li>注意具有 <code>C</code> 的密钥是主密钥，只有这个密钥可以用于：

<ul>
<li>添加或吊销子密钥的用途</li>
<li>添加、更改或吊销密钥关联的身份（UID）</li>
<li>添加或更改本身或其他子密钥的到期时间</li>
<li>为了网络信任目的为其它密钥签名</li>
</ul></li>
</ul></li>

<li><p>GPG Public Key 说明如下：</p>

<ul>
<li>在非对称加密中的的公钥，公钥是需要公开发布的。同样的 GPG Public Key 也需要公开发布，但 GPG Public Key 由如下内容组成：

<ul>
<li>主公钥和所有子公钥的。</li>
<li>有效期和吊销信息。</li>
<li>所有 UID 信息。</li>
</ul></li>
<li>GPG Public Key 会使用 GPG 的主秘钥进行签名，以防止公钥被篡改。</li>
<li>删除和吊销 GPG Public Key 中的 Sub Key 和 UID。

<ul>
<li>删除，针对已公开的 GPG Public Key，如果其他人已经加载了这个 GPG Public Key。则这个删除将不会生效（原因是：GPG 在更新一个 GPG Public Key 是合并操作，因此删除是无效的），如果没有公开过，则删除生效。</li>
<li>吊销。标记 GPG Public Key 中 Public Subkey / UID，将添加一个“吊销”标记，这样如果其他人已经加载了这个 GPG Public Key，只要更新了，就会知道该 Public Subkey / UID 不可用了（参考： <a href="https://zhuanlan.zhihu.com/p/21267738">GPG应用指南</a>）。</li>
</ul></li>
</ul></li>
</ul>

<h4 id="生成-primary-key">生成 Primary Key</h4>

<p>执行如下命令，交互式的生成一个新的 Primary Key：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --full-gen-key
<span style="color:#75715e"># 请选择您要使用的密钥类型，您的选择是？：选择 (1) RSA 和 RSA ，兼容性最好。</span>
<span style="color:#75715e"># 您想要使用的密钥长度？： 4096，安全性较好。</span>
<span style="color:#75715e"># 密钥的有效期限是？： (0) 永久。</span>
<span style="color:#75715e"># 构建用户标识以辨认您的密钥</span>
<span style="color:#75715e">#    真实姓名： rectcircle</span>
<span style="color:#75715e">#    电子邮件地址： rectcircle96@gmail.com</span>
<span style="color:#75715e">#    注释：</span>
<span style="color:#75715e">#    您选定了此用户标识：</span>
<span style="color:#75715e">#        “rectcircle &lt;rectcircle96@gmail.com&gt;”</span>
<span style="color:#75715e"># 输入 Primary Key 保护秘钥。</span></code></pre></div>
<p>最后，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">gpg: ~/.gnupg/trustdb.gpg：建立了信任度数据库
gpg: 目录‘~/.gnupg/openpgp-revocs.d’已创建
gpg: 吊销证书已被存储为‘~/.gnupg/openpgp-revocs.d/89F61268D036DF3C3BA931C07C5D945596EE795B.rev’
公钥和私钥已经生成并被签名。

pub   rsa4096 2022-07-17 [SC]
      89F61268D036DF3C3BA931C07C5D945596EE795B
uid                      rectcircle &lt;rectcircle96@gmail.com&gt;
sub   rsa4096 2022-07-17 [E]</pre></div>
<h4 id="列出-key">列出 Key</h4>

<p>列出所有公钥 <code>gpg --list-keys --keyid-format long</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">~/.gnupg/pubring.kbx
-----------------------------------
pub   rsa4096/7C5D945596EE795B 2022-07-17 [SC]
      89F61268D036DF3C3BA931C07C5D945596EE795B
uid                   [ 绝对 ] rectcircle &lt;rectcircle96@gmail.com&gt;
sub   rsa4096/D50B14322C993EBE 2022-07-17 [E]</pre></div>
<p>列出所有私钥 <code>gpg --list-secret-keys --keyid-format long</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">~/.gnupg/pubring.kbx
-----------------------------------
sec   rsa4096/7C5D945596EE795B 2022-07-17 [SC]
      89F61268D036DF3C3BA931C07C5D945596EE795B
uid                   [ 绝对 ] rectcircle &lt;rectcircle96@gmail.com&gt;
ssb   rsa4096/D50B14322C993EBE 2022-07-17 [E]</pre></div>
<p>可以看出，上文生成 Primary Key 的命令生成了：</p>

<ul>
<li>主公钥(pub)和主私钥(sec): 用于签名和管理证书 (SC)</li>
<li>子公钥(sub)和子私钥(ssb)：用于加密 (E)</li>
</ul>

<h4 id="更新-uid">更新 UID</h4>

<p>进入编辑密钥，可以添加、设置主 UID、吊销和删除 UID。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --edit-key 89F61268D036DF3C3BA931C07C5D945596EE795B
<span style="color:#75715e"># 添加一个新的 UID</span>
adduid
<span style="color:#75715e"># 选中第一个 uid</span>
uid <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># 将选中的 uid 设置为主 uid</span>
primary
<span style="color:#75715e"># 取消选中第一个 uid</span>
uid <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># 选中第二个 uid</span>
uid <span style="color:#ae81ff">2</span>
<span style="color:#75715e"># 吊销选中的 uid</span>
revuid
<span style="color:#75715e"># 删除选中的 uid</span>
deluid
<span style="color:#75715e"># 保存，不保存所有操作都不会生效</span>
save</code></pre></div>
<h4 id="添加-sub-key">添加 Sub Key</h4>

<p>进入编辑密钥，可以 Sub Key。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># --expert 为专家模式，可以添加用于 Authenticate 的 SubKey</span>
gpg --expert --edit-key 89F61268D036DF3C3BA931C07C5D945596EE795B
<span style="color:#75715e"># 添加新的用于签名的 Sub Key，选择  (6) RSA（仅用于加密），其他和创建 Primary Key 一致</span>
addkey
<span style="color:#75715e"># 添加新的用于签名的 Sub Key，选择  (4) RSA（仅用于签名），其他和创建 Primary Key 一致</span>
addkey
<span style="color:#75715e"># 添加新的用于的 Sub Key，选择  (8) RSA（自定义用途），然后让，目前启用的功能只有，身份验证（Authenticate）</span>
<span style="color:#75715e"># 其他和创建 Primary Key 一致</span>
addkey
<span style="color:#75715e"># 保存，不保存所有操作都不会生效</span>
save</code></pre></div>
<p>最后，执行 <code>gpg --list-secret-keys --keyid-format long</code> 列出所有 Key，输出如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">~/.gnupg/pubring.kbx
-----------------------------------
sec   rsa4096/7C5D945596EE795B 2022-07-17 [SC]
      89F61268D036DF3C3BA931C07C5D945596EE795B
uid                   [ 绝对 ] rectcircle &lt;rectcircle96@gmail.com&gt;
ssb   rsa4096/D50B14322C993EBE 2022-07-17 [E]
ssb   rsa4096/50235B5E033D3535 2022-07-17 [S]
ssb   rsa4096/CA45E598414ADA5F 2022-07-17 [A]</pre></div>
<h4 id="删除-sub-key">删除 Sub Key</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --expert --edit-key 89F61268D036DF3C3BA931C07C5D945596EE795B
list
<span style="color:#75715e"># 选中的 key</span>
key <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># 删除选中的 key</span>
delkey
<span style="color:#75715e"># 如果需要应用，需执行 save 命令。</span>
<span style="color:#75715e"># save</span></code></pre></div>
<h4 id="吊销-sub-key">吊销 Sub Key</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --expert --edit-key 89F61268D036DF3C3BA931C07C5D945596EE795B
list
<span style="color:#75715e"># 选中的 key</span>
key <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># 删除选中的 key</span>
revkey
<span style="color:#75715e"># 如果需要应用，需执行 save 命令。</span>
<span style="color:#75715e"># save</span></code></pre></div>
<h4 id="导出-gpg-public-key">导出 GPG Public Key</h4>

<p>导出</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 打印 GPG Public Key 到标准输出</span>
gpg --armor --export 89F61268D036DF3C3BA931C07C5D945596EE795B
<span style="color:#75715e"># 打印 GPG Public Key 文件</span>
gpg --output my.pub --armor --export 89F61268D036DF3C3BA931C07C5D945596EE795B</code></pre></div>
<h4 id="导出-gpg-private-key">导出 GPG Private Key</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 保存 GPG Private Key (包含 主私钥和所有子私钥) 到文件</span>
gpg --output all.pri --armor --export-secret-keys 89F61268D036DF3C3BA931C07C5D945596EE795B
<span style="color:#75715e"># 保存 GPG 主私钥到文件 (注意后面的感叹号)</span>
gpg --output primary.pri --armor --export-secret-key 7C5D945596EE795B!
<span style="color:#75715e"># 保存 GPG 所有子私钥到文件</span>
gpg --output all-subkey.pri --armor --export-secret-subkeys 89F61268D036DF3C3BA931C07C5D945596EE795B</code></pre></div>
<h4 id="删除">删除</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 删除私钥（交互式的选择删除那些秘钥）</span>
gpg --delete-secret-keys UID 或 子秘钥 KeyID 或 主密钥 KeyID
<span style="color:#75715e"># 删除公钥 (如果存在对应个私钥，需先删除；如果该公钥对应的私钥没有主私钥，只有子私钥，该操作也会直接删除对应的子私钥，因此可能会导致子秘钥丢失)</span>
gpg --delete-keys UID 或 子秘钥 KeyID 或 主密钥 KeyID</code></pre></div>
<h4 id="导入">导入</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --import 导出的文件</code></pre></div>
<h4 id="公布-pgg-public-key">公布 PGG Public Key</h4>

<blockquote>
<p>参考：<a href="https://wiki.archlinux.org/title/GnuPG_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)#%E4%BD%BF%E7%94%A8%E5%85%AC%E9%92%A5%E6%9C%8D%E5%8A%A1%E5%99%A8">使用公钥服务器</a></p>
</blockquote>

<p>该操作会将 GPG Public Key 公布到 GPG Key Server 上，以便其他人可以使用你的 GPG Public Key。目前默认的 GPG Key Server 地址是：<code>hkps://hkps.pool.sks-keyservers.net</code> （<a href="https://github.com/gpg/gnupg/blob/master/configure.ac#L1981">源码</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">gpg --send-keys 89F61268D036DF3C3BA931C07C5D945596EE795B</code></pre></div>
<p><strong>注意：</strong>一旦发布，则无法删除，公钥中包含 UID，如果包含多个 UID，则可能存在隐私泄漏的风险，关于安全风险，参见：<a href="https://ulyc.github.io/2021/01/26/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8B/#%E5%AE%89%E5%85%A8%E9%A3%8E%E9%99%A9%E5%92%8C%E4%BA%89%E8%AE%AE-%E8%A2%AB%E7%8E%A9%E5%9D%8F%E7%9A%84keyserver">2021年，用更现代的方法使用PGP（下） - 安全风险和争议， 被玩坏的KeyServer</a></p>

<h3 id="文件加密解密">文件加密解密</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 文件加密（使用公钥）</span>
gpg --recipient D50B14322C993EBE --output input.txt.gpg --encrypt input.txt
<span style="color:#75715e"># 文件解密（使用私钥）</span>
gpg --output input2.txt --decrypt input.txt.gpg</code></pre></div>
<h3 id="文件签名验证">文件签名验证</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 文件签名 (使用私钥)，文件内容也包含到输出文件中（二进制）</span>
gpg --output input.txt.gpg --sign input.txt
<span style="color:#75715e"># 签名验证 (使用公钥)，文件内容也包含到输出文件中（ascii）</span>
gpg --output input2.txt --decrypt input.txt.gpg

<span style="color:#75715e"># 文件签名 (使用私钥)，文件内容也包含到输出文件中（二进制）</span>
gpg --output input.txt.gpg --clearsign input.txt
<span style="color:#75715e"># 签名验证 (使用公钥)，文件内容也包含到输出文件中（ascii）</span>
gpg --output input2.txt --decrypt input.txt.gpg

<span style="color:#75715e"># 文件签名 (使用私钥)，只生成签名文件，不包含原始内容（推荐）</span>
gpg --output input.txt.asc --armor --detach-sign input.txt
<span style="color:#75715e"># 签名验证 (使用公钥)</span>
gpg --verify input.txt.asc input.txt</code></pre></div>
<h3 id="git-commit-签名">git commit 签名</h3>

<blockquote>
<p>参考： <a href="https://docs.github.com/cn/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key">github docs</a> | <a href="https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/">gitlab docs</a></p>
</blockquote>

<p>按照上文，<a href="#添加-sub-key">添加 Sub Key</a>，添加一个仅用于签名的 RSA SubKey，进行如下配置：</p>

<ul>
<li>将 gpg public key 配置到代码托管平台。执行 <code>gpg --armor --export 89F61268D036DF3C3BA931C07C5D945596EE795B</code>，将输出的文本，上传到 Github/Gitlab 用户配置页的 GPG 秘钥配置项。</li>
<li>执行 <code>gpg --list-secret-keys --keyid-format long</code> 选取一个具有 <code>[S]</code> 用途的 key id 作为签名 key（即上面创建的用于签名的 sub key），执行 <code>git config --global user.signingkey 50235B5E033D3535</code>，告知 git 用于加密的 key。</li>

<li><p>如果是 Mac，需执行如下操作（zsh）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -r ~/.zshrc <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> echo <span style="color:#e6db74">&#39;export GPG_TTY=$(tty)&#39;</span> &gt;&gt; ~/.zshrc; <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span><span style="color:#66d9ef">else</span> echo <span style="color:#e6db74">&#39;export GPG_TTY=$(tty)&#39;</span> &gt;&gt; ~/.zprofile; <span style="color:#66d9ef">fi</span>
brew install pinentry-mac
echo <span style="color:#e6db74">&#34;pinentry-program </span><span style="color:#66d9ef">$(</span>which pinentry-mac<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> &gt;&gt; ~/.gnupg/gpg-agent.conf
killall gpg-agent</code></pre></div></li>
</ul>

<p>最后，如果想为 commit 签名，git commit 需添加 -S 参数，如 <code>git commit -S -m &quot;feat: 测试 git gpg 签名&quot;</code>。如果默认开启签名，通过 <code>git config --global commit.gpgsign true</code> 配置项配置。</p>

<h3 id="ssh-登录">SSH 登录</h3>

<ul>
<li>按照上文，<a href="#添加-sub-key">添加 Sub Key</a>，添加一个仅用于身份验证（Authenticate）的 RSA SubKey。</li>
<li>配置步骤，参见：<a href="https://ulyc.github.io/2021/01/18/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%AD/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">2021年，用更现代的方法使用PGP（中）</a>。</li>
</ul>

<h3 id="邮件加密">邮件加密</h3>

<p>目前，主流电子邮件提供商，已经开启了全量的 SSL 加密，因此 GPG 在电子邮件场景的进行加密显得有些上古了，在此不多阐述了。</p>

<h2 id="最佳实践">最佳实践</h2>

<h3 id="uid-规划">UID 规划</h3>

<p>在 GPG 中一个 Key 可以包含多个 UID，这就意味着，一个 Key 可以表示一个人的多种身份，比如，一个开发者，只有一个 Key，该 Key 包含如下 UID：</p>

<ul>
<li><code>&lt;公司用户名&gt; &lt;公司 Email&gt;</code></li>
<li><code>&lt;开源用户名&gt; &lt;个人 Email&gt;</code></li>
</ul>

<p>这种做法，是符合 GPG 的最佳实践的。但是，在现实中，这种做法可能存在一定的风险。</p>

<ul>
<li>隐私风险，GPG Public Key 是公开的，所有人都可以都可以从 GPG Public Key 中获取到 UID 列表，且不可撤销。</li>
<li>法律风险，从该公司离职了，要怎么做呢？吊销公司 UID，吊销公司用的 SubKey？</li>
</ul>

<p>因此，建议拆分成两个 Key，公司和个人的 GPG Key 分离。公司的 GPG Key 仅用于公司相关场景。个人的 GPG Key 仅用于个人非商业相关场景。</p>

<h3 id="sub-key-规划">Sub Key 规划</h3>

<p>GPG Key 中的 Primary Key，默认情况下具有两个能力，S 和 C，但按照 GPG 的设计，Primary Key 的作用仅仅用于管理 SubKey，而不用于具体的加密、签名、身份验证等具体场景。此外，尽量一个 SubKey 只做一个事情。</p>

<p>以上文 <a href="#添加-sub-key">添加 Sub Key</a> 为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">~/.gnupg/pubring.kbx
-----------------------------------
sec   rsa4096/7C5D945596EE795B 2022-07-17 [SC]
      89F61268D036DF3C3BA931C07C5D945596EE795B
uid                   [ 绝对 ] rectcircle &lt;rectcircle96@gmail.com&gt;
ssb   rsa4096/D50B14322C993EBE 2022-07-17 [E]
ssb   rsa4096/50235B5E033D3535 2022-07-17 [S]
ssb   rsa4096/CA45E598414ADA5F 2022-07-17 [A]</pre></div>
<p>最后，添加了三个 subkey，分别用于：加密、签名和身份认证。</p>

<h3 id="安全性和隐私">安全性和隐私</h3>

<blockquote>
<p>本部分主要参考： <a href="https://ulyc.github.io/2021/01/18/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%AD/#%E5%A4%87%E4%BB%BD%E7%AD%96%E7%95%A5">2021年，用更现代的方法使用PGP（中）</a></p>
</blockquote>

<h4 id="生成设备">生成设备</h4>

<p>个人的 GPG Key 建议使用非公司的个人设备生成，并避免联网。</p>

<h4 id="吊销证书备份">吊销证书备份</h4>

<p>在上文 <a href="#生成-primary-key">生成 Primary Key</a> 中可以看到，其命令生成了一个吊销证书的文件。</p>

<p>上文可以看到，在 Primary Key 未丢失的情况下，可以使用 Primary Key 吊销某个 SubKey 和 UID。</p>

<p>但是 GPG 提供了一种额外的安全机制，可以通过一个称为吊销证书的文件，来实现在 Primary Key 丢失的情况下，吊销整个 Public Key。</p>

<p>因此从安全性角度来看，吊销证书的重要性是大于 Primary Key，是整个系统最后的保证兜底手段。因此，吊销证书应该存在在比 Primary Key 更安全的地方，并多存储一份。</p>

<h4 id="主密钥备份">主密钥备份</h4>

<p>主要考虑到主密钥设备是用来管理 SubKey 的，如果其泄漏了，真个 Key 将都不可用了，影响范围过大。所以，基于前文提到的 Primary Key 不做加密、签名、身份验证等具体场景。建议：</p>

<ul>
<li>Primary Key 只离线保留一份。也就是说，建议生成完成 SubKey 备份完成后，彻底将 Primary Key 设备中删除。</li>
</ul>

<h4 id="主密钥子秘钥备份">主密钥子秘钥备份</h4>

<p>为了安全性，在完成 SubKey 的生成后，应该将 Primary Key 导出到安全的外部设备，如加密的 U 盘、云盘、私有 github 仓库等个人认为安全的地方。</p>

<p>Subkey 也需要备份，但是其安全等级相对来说低于 Primary Key。因为如果一个用于解密邮件的 SubKey 丢失了，可以用 Primary Key 直接吊销 SubKey，重新生成。</p>

<p>但是用于加密文件的 SubKey 丢失后果还是很严重的，将导致加密后的文件永远无法打开了。因此，SubKey 也至少备份一份。</p>

<h4 id="公布-gpg-public-key">公布 GPG Public Key</h4>

<p>按照上文说明，可以将 GPG Public Key 通过 Key Server 公布到网络上。也可以将 GPG Public Key 公布到个人博客中。</p>

<p>需要注意的是，某些 Key Server 一旦公布将不能撤销，因此需要注意 UID 是否会泄漏个人隐私。</p>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://www.ruanyifeng.com/blog/2013/07/gpg.html">GPG入门教程</a></li>
<li><a href="https://blog.zhanganzhi.com/zh-CN/2022/06/1c71f69657ed">GPG 入门教程</a></li>
<li><a href="https://ulyc.github.io/2021/01/13/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A/">2021年，用更现代的方法使用PGP（上）</a></li>
<li><a href="https://ulyc.github.io/2021/01/18/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%AD/">2021年，用更现代的方法使用PGP（中）</a></li>
<li><a href="https://ulyc.github.io/2021/01/26/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8B/">2021年，用更现代的方法使用PGP（下）</a></li>
<li><a href="https://wiki.archlinux.org/title/GnuPG_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)">archlinux GnuPG (简体中文)</a></li>
<li><a href="https://emacsist.github.io/2019/01/01/gnupg2%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8C%97">GnuPG2使用指北</a></li>
<li><a href="https://docs.github.com/cn/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key">Github 将您的签名密钥告知 Git</a></li>
<li><a href="https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/">Signing commits with GPG</a></li>
</ul>
]]></description></item><item><title>理解 ssl/tls 协议: 从加密算法到 ssl/tls</title><link>https://www.rectcircle.cn/posts/understand-ssl-tls/</link><pubDate>Sat, 16 Jul 2022 19:16:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/understand-ssl-tls/</guid><description type="html"><![CDATA[

<h2 id="加密算法">加密算法</h2>

<h3 id="定义">定义</h3>

<p>一般语境下，加密算法是对加密和解密两个过程的概括：</p>

<ul>
<li>加密过程，即通过 key (秘钥)，将 plaintext (明文) 转换 ciphertext (密文) 的过程。</li>
<li>解密过程，即通过 key，将 ciphertext 转换为明文数据 &lsquo;plaintext 的过程。</li>
<li>在上述两个过程中，需保证，对任意 plaintext ，满足 <code>plaintext = 'plaintext</code>。</li>
</ul>

<h3 id="分类">分类</h3>

<p>加密算法根据加密和解密过程使用的 key 是否是同一个分为对称加密算法和非对称加密算法。</p>

<ul>
<li>key 是同一个的，为对称加密算法。</li>
<li>key 是不同的两个的，为非对称加密算法。</li>
</ul>

<h4 id="对称加密算法">对称加密算法</h4>

<p>对称算法的加密和解密使用的 key 同一个。即：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">plaintext == decrypt(encrypt(plaintext, key)), key)</pre></div>
<h4 id="非对称加密算法">非对称加密算法</h4>

<p>非对称算法的加密和解密使用的 key 不是同一个。这两个 key 成对出现，被称为 key pair (秘钥对)。</p>

<p>假设一个 key pair 的两个 key ，分别为 <code>key1</code> 和 <code>key2</code>，则：</p>

<ul>
<li>使用 <code>key1</code> 加密得到的 ciphertext，可以使用 <code>key2</code> 解密；</li>
<li>使用 <code>key2</code> 加密得到的 ciphertext，可以使用 <code>key1</code> 解密。</li>
</ul>

<p>也就是说，<code>key1</code> 和 <code>key2</code> 是对等的 (对称的)，即：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">plaintext == decrypt(encrypt(plaintext, key1)), key2)
plaintext == decrypt(encrypt(plaintext, key2)), key1)</pre></div>
<p>虽然两个 key 是对等的，但在业界，为了性能和实际场景，会选取其中一个作为 private key (私钥)，另外一个作为 public key (公钥)。</p>

<p>其中，public key 是公开的，所有人都可以获得到。而 private key 是私有的，只有 key pair 所有者拥有，</p>

<h3 id="安全通讯">安全通讯</h3>

<p>在一次数据通讯过程，涉及两方；发送方和接收方。双方的数据通过通信信道传输。由于通信信道是基于公开的经典物理学原理，是不安全的，所以第三方可以很容易的做到如下几点：</p>

<ul>
<li>窃听，第三方只要了解数据调制方法，就可以在通信信道，随意窃听通信内容。</li>
<li>冒充，第三方冒充发送者，通过通讯信道，给接收者发送消息。</li>
<li>篡改，第三方截获到发送者的数据，篡改后发送给接收者。</li>
</ul>

<p>利用加密算法和合理的流程设计，可以实现安全的通讯，解决如上几个问题。</p>

<h4 id="对称加密实现安全通讯">对称加密实现安全通讯</h4>

<p>加密算法的可以解决这些问题：发送方将 plaintext 加密后得到的 ciphertext，通过不安全的通讯信道发送给接收方；接收方解密后，即可获取到 &lsquo;plaintext。在这整个过程中，即使数据在不安全的信道中被截获了，第三方也只能得到无法阅读的 ciphertext。</p>

<p>在非对称加密场景，流程为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">encrypt(plaintext, key) =============== ciphertext ===============&gt; decrypt(ciphertext, key)
    发送方                                  信道                              接收方</pre></div>
<p>以上流程解决了安全通讯的三个问题：</p>

<ul>
<li>（✅ 解决）窃听，第三方没有 key，通过窃听拿到 ciphertext，也无法获取到 plaintext；</li>
<li>（✅ 解决）冒充，第三方没有 key，无法发送合法的 ciphertext；</li>
<li>（✅ 解决）篡改，第三方没有 key，通过窃听拿到的 ciphertext，无法获取到 plaintext，也就无法篡改。</li>
</ul>

<p>但是，以上场景存在一个严重问题，即：通讯双方如何知道通讯中使用的 key 呢？首先，不能明文传输，因为第三方可以窃听， key 泄漏了，加密形同虚设。因此在不使用非对称加密算法的情况下，最安全的方式是，通讯双方线下交换 key。</p>

<h4 id="非对称加密解决窃听问题">非对称加密解决窃听问题</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">encrypt(plaintext, public_key_b) =============== ciphertext ===============&gt; decrypt(ciphertext, private_key_b)
    A(发送方)                                         信道                              B(接收方)</pre></div>
<p>上文提到，非对称算法的 public key 是公开的，所有人都可以获得到。而 private key 是私有的，只有 key pair 所有者拥有。评估如上流程：</p>

<ul>
<li>（✅ 解决）窃听，第三方没有接收方的 private key，通过窃听拿到 ciphertext，也无法获取到 plaintext；</li>
<li>（❌ 未解决）冒充，第三方可以获取到接收方 public key，可以冒充发送者给接收者发送 ciphertext；</li>
<li>（✅ 解决）篡改，第三方没有接收方的 private key，通过窃听拿到的 ciphertext，无法获取到 plaintext，也就无法篡改。</li>
</ul>

<h4 id="非对称加密解决冒充问题">非对称加密解决冒充问题</h4>

<p>上文提到，任意一个第三方，都可以通过 public key 通过信道给接收方发送消息，接收方就需要一种机制识别发送者的身份，解决冒充问题。</p>

<p>此时，可以利用加密算法并配合 hash 算法实现一个数字签名算法，可实现对发送方身份的认证。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">encrypt(hash(plaintext), private_key_a) =============== (plaintext, signature) ===============&gt; hash(plaintext) == decrypt(signature, public_key_a) ?
        A(发送方)                                                信道                                      B(接收方)</pre></div>
<ul>
<li>（❌ 未解决）窃听，未加密，第三发可以获取明文；</li>
<li>（✅ 解决）冒充，第三方没有发送方的 private key，所以无法生成 signature，因此无法通过接收方的身份验证；</li>
<li>（✅ 解决）篡改，第三方没有发送方的 private key，所以无法生成 signature，因此篡改了 plaintext，会导致 signature 失效，因此无法篡改。</li>
</ul>

<h4 id="非对称加密实现安全通讯">非对称加密实现安全通讯</h4>

<p>结合上文的两个流程，即可利用非对称加密实现安全通讯。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">encrypt(plaintext, public_key_b), encrypt(hash(plaintext), private_key_a) =============== (ciphertext, signature) ===============&gt; plaintext = decrypt(ciphertext, private_key_b), hash(plaintext) == decrypt(signature, public_key_a) ?
        A(发送方)                                                                                 信道                                                 B(接收方)</pre></div>
<p>评估如上流程</p>

<ul>
<li>（✅ 解决）窃听，第三方没有接收方的 private key，通过窃听拿到 ciphertext，也无法获取到 plaintext；</li>
<li>（✅ 解决）冒充，第三方没有发送方的 private key，所以无法生成 signature，因此无法通过接收方的身份验证；</li>
<li>（✅ 解决）篡改，第三方没有发送方的 private key，所以无法生成 signature；第三方没有接收方的 private key，通过窃听拿到的 ciphertext，无法获取到 plaintext，也就无法篡改。</li>
</ul>

<h2 id="ssl-tls-协议">ssl/tls 协议</h2>

<blockquote>
<p>参考: <a href="https://bbs.huaweicloud.com/blogs/335979">假如让你来设计SSL/TLS协议</a> | <a href="https://www.zhihu.com/question/37370216">浏览器如何验证HTTPS证书的合法性？</a></p>
</blockquote>

<p>上文，<a href="#非对称加密实现安全通讯">安全通讯 - 非对称加密实现安全通讯</a> 流程，存在两个问题：</p>

<ul>
<li>性能问题。非对称加密算法的性能很差，无法满足大规模数据传输的要求。</li>
<li>public key 分发问题。如果 public key 的分发是在建立连接时通过明文发送，那么一个第三方攻击者在通信信道上进行篡改，将 public key 篡改成攻击者的 public key，那么这个通讯链路对于攻击者来说就是非加密的（中间人攻击）。</li>
</ul>

<h3 id="性能问题">性能问题</h3>

<p>从上文的<a href="#对称加密实现安全通讯">对称加密实现安全通讯</a> 可以看出，只要解决了 key 分发的问题，对称加密也是足够安全的。</p>

<p>因此，<a href="#非对称加密实现安全通讯">非对称加密实现安全通讯</a> 仅用来传输对称加密的 key，即利用非对称加密来解决对称加密的 key 分发问题。</p>

<p>而对称加密的性能远好于非对称加密，这样，非对称加密实现安全通讯的性能问题就解决了，而安全性由对称加密保证。</p>

<h3 id="公钥分发问题">公钥分发问题</h3>

<p>为了解决公钥分发问题，需要一个通讯双方都信任的第三方来解决 public key 的信任问题，在 ssl/tls 协议中，这个第三方机构称为 CA。</p>

<p>ssl/tls 最常见的场景就是对 http 的支持，下文将以 http 为例，来讲解 ssl/tls 协议的如何解决公钥分发问题。</p>

<p><strong>服务端向申请 CA 证书</strong></p>

<p>服务端开发者注册一个域名如 <code>example.com</code>，并向 CA 机构申请一个 CA 证书，流程如下：</p>

<ul>
<li>服务端准备好用于非对称加密的 key pair</li>
<li>服务端提供如下信息：

<ul>
<li>域名，如 <code>www.example.com</code></li>
<li>server 的 public key</li>
</ul></li>

<li><p>CA 机构</p>

<ul>
<li>通过某些机制，验证申请者是该域名的主人</li>

<li><p>使用 CA 机构的 提供一个证书，这个这证书包含如下信息：</p>

<ul>
<li>证书所有者，即 <code>www.example.com</code></li>
<li>证书有效期</li>
<li>颁发者即 CA 机构的名称</li>
<li>服务端公钥</li>
<li>&hellip;</li>

<li><p>证书签名，由 CA 机构的 private key 生成</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">server_certificate = (domain, validity, issuer, public_key_server, ..., signature = (encrypt(hash(domain, validity, issuer, public_key_server, ...), private_key_ca)))</pre></div></li>
</ul></li>
</ul></li>
</ul>

<p>服务端获取到证书后，将证书和服务端的 private key 部署到服务器上，此时就可以对外提供服务。</p>

<p><strong>握手: 服务端将证书发送给客户端</strong></p>

<p>客户端（操作系统）内部会预设 CA 机构的 public key。</p>

<p>客户端请求该服务端时，服务端将证书发送给客户端，客户端接收到证书后，会使用 CA 机构的 public key 验证证书是否是伪造的：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">server =================== server_certificate ===================&gt; client: hash(domain, validity, issuer, public_key_server, ...) == decrypt(signature, public_key_ca) ?</pre></div>
<p><strong>握手: 客户端随机生成对称加密的 key</strong></p>

<p>客户端随机生成对称加密的 key 并使用证书中服务端的 public key 对 key 进行加密，发送给服务端。此后双方通讯的数据均通过该对称加密的 key 进行传输。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">client: encrypt(key, public_key_server) ==================== ciphertext ==================&gt; server: decrypt(ciphertext, private_key_server)</pre></div>
<h3 id="其他说明">其他说明</h3>

<ul>
<li>上文将介绍 ssl/tls 协议是如何解决如上问题的思路，真实的协议比上文描述的流程复杂一些。</li>
<li>上文介绍的是 ssl/tls 的单向认证，即 客户端对服务端的信任问题，实际上还有更严格的双向认证，参考：<a href="https://www.jianshu.com/p/2b2d1f511959">HTTPS双向认证指南</a>。</li>
<li>上文描述的，客户端对服务端证书验证仅涉及单个证书，实际场景存在证书链的概念：<a href="https://www.jianshu.com/p/46e48bc517d0">证书链-Digital Certificates</a></li>
</ul>
]]></description></item><item><title>ChromeOS 体验 (FydeOS)</title><link>https://www.rectcircle.cn/posts/chromeos-experience/</link><pubDate>Sun, 10 Jul 2022 00:31:38 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/chromeos-experience/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>作为一名开发人员，一直关注各种桌面级/移动级操作系统的进展，其中就包含 ChromeOS。</p>

<p>对于一个开发者（客户端、嵌入式、硬件开发者除外）而言，对于操作系统的要求如下：</p>

<ul>
<li>流畅、稳定而现代化的系统 UI。</li>
<li>完整的 Linux 环境。</li>
<li>好用的浏览器。</li>
<li>丰富的开发者和娱乐软件生态。</li>
</ul>

<p>对于这三个要求，ChromeOS，都可以比较完美的支持：</p>

<ul>
<li>ChromeOS 是后发的桌面级操作系统，它的 UI 是现代化的。在诞生之处，ChromeOS 目标是可以在廉价的设备上流畅运行，因此流畅度没有问题。最后，ChromeOS 内核基于 Linux，且系统相对封闭，在专用设备上稳定性应该有所保证（FydeOS for PC 这种无法面向一大类机器的发行版，无法保证稳定性）。</li>
<li>ChromeOS 系统层面，通过虚拟机技术，提供了具有独立内核的 Linux 子系统。</li>
<li>ChromeOS 就是对 Chrome 浏览器的操作系统化的产品，浏览器的体验毋庸置疑。</li>
<li>ChromeOS 可以在 Linux 子系统中安装 Linux GUI 程序，因此可以直接安装如 VSCode、Jetbrains 等 IDE，开发者软件生态丰富。</li>
<li>ChromeOS 系统层面，通过 Linux 容器化技术，提供了 Android 运行环境，可以安装和运行安卓 App，具有了 Android 应用生态。</li>
</ul>

<p>在国内，有一家厂商燧炻创新发行的 ChromeOS 的国内适配版: FydeOS。</p>

<p>在过去的几年中，也曾经尝试几次在一台闲置的 x86 设备上，安装过几次 FydeOS，但是总是有一些严重的问题，比如：wifi 连不上，安卓运行时初始化失败、Linux 子系统初始化失败等。</p>

<p>这两天，又一次尝试安装 FydeOS，发现这个版本，可以完美的支持这台设备。于是，探索了下 ChromeOS 提供的核心能力的技术原理，就有了这篇文章。</p>

<p>注：本文的的内容和结论，基于在一台古早的 x86 设备上安装的 FydeOS 系统上进行的探索和实验，并结合网络上 Chrome OS 的相关技术文档而得到。</p>

<h2 id="设备信息">设备信息</h2>

<p>这台是一个台 15.6 寸的笔记本电脑（<a href="https://www.haier.com/computers/bjb/20140321_92434.shtml">海尔 s500</a>），购置于 2015 年 9 月左右，当时京东购机价为 3599 元左右，基本参数如下：</p>

<ul>
<li>内存: 8G DDR3L 内存（原机 4G、后扩展到 8G）</li>
<li>CPU: 英特尔 酷睿i5 4代系列, <a href="https://www.intel.cn/content/www/cn/zh/products/sku/81012/intel-core-i54210m-processor-3m-cache-up-to-3-20-ghz/specifications.html">i5-4210M</a></li>
<li>磁盘: 1TB 5400 转 SATA 机械硬盘</li>
<li>WIFI: 802.11bgn (仅支持 2.4 Ghz)</li>
<li>蓝牙: 本机无，后购置了一个外置 USB 免驱蓝牙 (<a href="https://item.jd.com/100026235324.html">jd</a>)</li>
</ul>

<h2 id="系统版本和安装">系统版本和安装</h2>

<p>本次安装 ChromeOS 版本为，燧炻创新发布的 <a href="https://fydeos.com/download/pc/intel-hd">FydeOS for PC - Intel 酷睿系列第三代至第八代处理器及 Intel HD 系列核心显卡</a>，具体版本信息为:</p>

<ul>
<li>版本号: 14.2-SP1 (<a href="https://fydeos.com/release/14.2-SP1/amd64-fydeos">更新日志</a>)</li>
<li>发布日期: 2022-05-31</li>
<li>HASH(sha256): 8066c8e08129bd85838c00b5c96fb12a192b87e668657ca73634ecbf763ee8d2</li>
</ul>

<p>安装方式基本上是傻瓜式的，非常简单，参见：<a href="https://fydeos.com/help/knowledge-base/getting-started/fydeos-for-pc">首次运行 FydeOS for PC</a> 文档。</p>

<p>Chromium 版本为（进入系统后查看）： 96.0.4664.208</p>

<h2 id="开发环境搭建">开发环境搭建</h2>

<p>本小结，介绍如何使用 ChromeOS 的 Linux 子系统，搭建一个可用的软件开发环境。</p>

<p>首先，在设置 -&gt; 高级 -&gt; 开发者 -&gt; Linux 开发环境 中启用 Linux 开发环境。</p>

<h3 id="安装-ide-以-vscode-为例">安装 IDE (以 VSCode 为例)</h3>

<ul>
<li>打开 <a href="https://code.visualstudio.com/#alt-downloads">VSCode 下载页面</a>，下载 .deb 包。</li>
<li>打开 ChromeOS 自带额文件 App，我的文件 -&gt; 下载内容，右击 通过 Linux 安装。</li>
</ul>

<p>稍等片刻，即可安装完成。安装完成后可以直接在启动器 Linux 应用中看到图标。点击即可打开。</p>

<h3 id="配置中文和字体">配置中文和字体</h3>

<blockquote>
<p>参考：<a href="https://sspai.com/post/56234">安装 Android 应用、配置 Linux 环境：FydeOS 进阶使用教程</a></p>
</blockquote>

<p>首先是设置时区，在终端中输入：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo dpkg-reconfigure tzdata
<span style="color:#75715e"># 选择亚洲 (Asia), 上海 (Shanghai)</span></code></pre></div>
<p>安装中文字体：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt install fonts-wqy-microhei fonts-wqy-zenhei</code></pre></div>
<p>配置字符集为 <code>zh_CN.uft8</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo dpkg-reconfigure locales</code></pre></div>
<p>字符集配置完成完成后需重启生效：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo reboot</code></pre></div>
<h3 id="安装可视化包管理器">安装可视化包管理器</h3>

<blockquote>
<p>参考：<a href="https://chromeos.dev/en/linux/setup#visual-package-management">ChromeOS Linux setup</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt install -y gnome-software gnome-packagekit <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>sudo apt update</code></pre></div>
<h3 id="安装中文输入法">安装中文输入法</h3>

<p>Linux 子系统中是无法使用 ChromeOS 的输入法的，因此需要在 Linux 子系统中安装中文输入法。</p>

<p>具体步骤参见：<a href="https://fydeos.com/help/knowledge-base/linux-subsystem/chinese-ime-in-linux-subsystem">FydeOS 知识库</a> 。</p>

<p>注意：</p>

<ul>
<li>上文做 3.1 之前，需要先执行 fcitx 命令。</li>
<li>可以选择安装搜狗输入法：<a href="https://shurufa.sogou.com/linux">https://shurufa.sogou.com/linux</a> 下载下来，然后在 Linux 子系统中通过 <code>sudo dpkg -i xxx.deb</code> 来安装</li>
<li>可以选择安装 Google 拼音输入法，在 Linux 子系统中通过 <code>sudo apt install fcitx-googlepinyin</code> 来安装（需重启才能配置）。</li>
</ul>

<h3 id="vpn-和-网络代理">VPN 和 网络代理</h3>

<p>ChromeOS 和 Android 是深度集成的。ChromeOS 可以使用 Android 代理 App 提供的代理作为整个系统的全局 VPN。也就是说 ChromeOS 的浏览器、Android App、Linux 子系统都会走 Android App 的代理。</p>

<p>打开一个 Android 代理 App 后，在 ChromeOS 的网络设置里面，可以看到该 VPN 已经生效了。</p>

<h3 id="小提示">小提示</h3>

<p>至此，即可在 Linux 子系统中开发应用程序了，此外还有一些小提示：</p>

<ul>
<li>Linux 子系统的文件共享和 ChromeOS 的文件共享，参见下文：<a href="#linux-子系统分析">Linux 子系统分析</a></li>
<li>开发调试 Android App，参见 FydeOS 官方文档： <a href="https://fydeos.com/help/knowledge-base/linux-subsystem/android-development-guide-with-fydeos">在 FydeOS 下开发调试安卓程序指北</a></li>
</ul>

<h2 id="chrome-os-浅析">Chrome OS 浅析</h2>

<h3 id="crosh">crosh</h3>

<p>通过 ctrl + shift + t 可以进入 crosh (The Chromium OS shell) 终端，详细手册参见： <a href="https://chromium.googlesource.com/chromiumos/platform2/+/HEAD/crosh/README.md">Chromium crosh README</a>。</p>

<p>通过 <code>help</code> 和 <code>help_advanced</code> 命令可以查看 crosh 支持的命令列表（如 <code>free</code> 查看内存）。</p>

<p>这里通过 <code>shell</code> 命令，可以打开一个 bash 终端，这个终端运行在 ChromeOS 所在 Linux 内核中，用户为 chronos。</p>

<p>chronos 用户就是 ChromeOS 运行 Chrome 的用户。在 FydeOS 中，<a href="https://fydeos.com/help/faq#what-is-the-account-and-password-used-to-log-in-to-the-command-line">chronos 用户没有设置密码</a>，而且可拥有 sudo root 的权限（相当于 Android 系统 root 了），这给我们很大的自定义空间，后文将介绍这一点。</p>

<p>相关命令如下（下文命令均在该 shell 中执行）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># ctrl + shift + t</span>
shell
su su root</code></pre></div>
<h3 id="内核版本">内核版本</h3>

<p>执行 <code>uname -a</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Linux localhost 5.4.188-16917-g3358c5a3654f-dirty #2 SMP PREEMPT Thu May 26 15:47:47 CST 2022 x86_64 Intel(R) Core(TM) i5-4210M CPU @ 2.60GHz GenuineIntel GNU/Linux</pre></div>
<p>可以看出 Linux 内核版本为 5.4。</p>

<h3 id="init-进程选型">init 进程选型</h3>

<p>执行 <code>stat /proc/1/exe</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">  File: /proc/1/exe -&gt; /sbin/init
  Size: 0               Blocks: 0          IO Block: 1024   symbolic link
Device: 5h/5d   Inode: 783097      Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Context: u:r:cros_init:s0
Access: 2022-07-10 01:21:29.404407129 -0700
Modify: 2022-07-10 01:21:19.401406853 -0700
Change: 2022-07-10 01:21:19.401406853 -0700
 Birth: -</pre></div>
<p>执行 <code>/sbin/init</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Usage: init [OPTION]...
Process management daemon.

Options:
  -q, --quiet                 reduce output to errors only
  -v, --verbose               increase output to include informational messages
      --help                  display this help and exit
      --version               output version information and exit

This daemon is normally executed by the kernel and given process id 1 to denote its special status.  When executed by a user process, it
will actually run /sbin/telinit.

Report bugs to &lt;upstart-devel@lists.ubuntu.com&gt;</pre></div>
<p>可以看出 ChromeOS 的 init 进程为： <a href="https://en.wikipedia.org/wiki/Upstart">upstart</a>。经过搜索，可以找到：<a href="https://www.chromium.org/chromium-os/chromiumos-design-docs/boot-design/">ChromeOS 的 boot 设计文档</a>，该文档中确定了 ChromeOS 的 init 进程为 upstart。</p>

<p>知道了 init 是 upstart，我们就可以通过在 <code>/etc/init</code> 目录里面添加自定义的开启自启配置文件，即可做到在 ChromeOS 的所在的内核，开机自动运行一些进程/服务（下文有介绍）。</p>

<h3 id="libc-库选型">libc 库选型</h3>

<p>执行 <code>/lib64/libc.so.6</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">GNU C Library (Gentoo 2.32-r17 p8) stable release version 2.32.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 10.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
&lt;http://crbug.com/new&gt;.</pre></div>
<p>可以看出 ChromeOS 采用的 libc 为 Linux 标准的 glibc，版本为 2.32。这篇<a href="https://chromium.googlesource.com/chromiumos/third_party/glibc/+/chromeos-2.15/FAQ">文档</a>介绍了 ChromeOS 编译 glibc 的 FAQ，印证了 ChromeOS 使用 libc 版本为 glibc。</p>

<blockquote>
<p>不同的 libc 可能是相互不兼容，确认 ChromeOS 使用的是标准的 glibc，那么直接在 ChromeOS 所在内核运行程序的难度将会大大降低，因为 Linux 上主流的 C/C++ 编写的程序多数是首选兼容 glibc 的。</p>
</blockquote>

<h3 id="文件管理">文件管理</h3>

<p>在 ChromeOS 文件 APP 只能看到有限的一些目录，主要是我的文件目录。该目录 ChromeOS 所在内核文件系统的哪里呢？经过查找，发现位于 <code>/home/chronos/user/MyFiles</code> 路径下。</p>

<h3 id="linux-子系统分析">Linux 子系统分析</h3>

<blockquote>
<p>首先在 设置 -&gt; 高级 -&gt; 开发者 中开启 Linux 开发环境。</p>
</blockquote>

<p>经过检索，可以发现 ChromeOS 开发了一个半虚拟化的软件 <a href="https://chromium.googlesource.com/chromiumos/platform/crosvm/">crosvm</a> 来运行 Linux 子系统内核。</p>

<p>通过 <code>ps aux | grep crosvm</code> 可以看出运行了很多 crosvm 相关的进程。</p>

<p>下面介绍一下 Linux 子系统和 ChromeOS 文件共享的机制。</p>

<p><strong>将 Chrome OS 的文件共享到 Linux 子系统中</strong></p>

<ul>
<li>在 ChromeOS 的文件 APP 中，右击 我的文件 ，选择 <code>与 Linux 共享</code>。</li>
<li>此时即可在 Linux 子系统中，在 <code>/mnt/chromeos/MyFiles</code> 目录中看到，Downloads 等目录。该目录是有读写权限的。</li>
</ul>

<p><strong>读取 Linux 子系统 <code>$HOME</code> 目录</strong></p>

<ul>
<li>在 ChromeOS 的文件 APP 中，可以看到：<code>我的文件 &gt; Linux 文件</code>，通过这个目录可以访问 Linux 子系统中的 <code>$HOME</code> 目录。</li>

<li><p>在 ChromeOS 的 shell 中，如何访问该目录呢？</p>

<ul>
<li><p>通过 <code>mount | grep linux</code> 查看，输出如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">fuse:sshfs://rectcircle@penguin.termina.linux.test: on /media/fuse/crostini_ba1970ad840cb1d1a3344784b0d0cc65044919aa_termina_penguin type fuse.sshfs (rw,nosuid,nodev,noexec,relatime,nosymfollow,dirsync,user_id=1000,group_id=1001,default_permissions,allow_other)</pre></div></li>

<li><p>输出的第二行，可以该目录是通过 sshfs 的方式挂载到的了 ChromeOS 的 <code>/media/fuse/crostini_ba1970ad840cb1d1a3344784b0d0cc65044919aa_termina_penguin</code> 路径下。</p></li>

<li><p>该目录是有完整的读写权限的。也就是说，也可以通过该目录来实现共享。</p></li>
</ul></li>
</ul>

<h3 id="android-运行环境分析">Android 运行环境分析</h3>

<p>执行 <code>ps aux | grep android</code> 部分输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">656362   19537  0.0  1.3 3280440 106868 ?      Sl   Jul09   0:00 com.android.bluetooth
665415   19543  0.0  1.6 3306188 135056 ?      Sl   Jul09   0:00 com.android.systemui
665412   19811  0.0  2.2 3340244 180320 ?      S&lt;l  Jul09   0:01 com.android.documentsui
656428   19907  0.0  0.9 3254256 80592 ?       Sl   Jul09   0:00 com.android.se
665400   19978  0.0  1.0 3255944 85372 ?       Sl   Jul09   0:00 com.android.externalstorage
665370   19992  0.0  1.0 3265500 84240 ?       Sl   Jul09   0:00 com.android.printspooler
665366   20000  0.0  1.2 3263232 100612 ?      Sl   Jul09   0:00 com.google.android.deskclock
665394   20066  0.0  1.1 3255888 91116 ?       Sl   Jul09   0:00 android.process.media
665406   20143  0.0  1.1 3253692 92172 ?       Sl   Jul09   0:00 com.android.providers.calendar
665375   20319  0.0  1.5 1197284 124264 ?      Sl   Jul09   0:00 com.google.android.tts
665402   20507  0.0  0.9 3255064 79792 ?       Sl   Jul09   0:00 com.google.android.ext.services</pre></div>
<p>可以看出 Android 的进程在 ChromeOS 内核中，是可以直接列出的，因此基本可以确定 ChromeOS 的安卓运行时是通过 Linux 容器技术 来运行的（和 Docker 容器所依赖的 Linux 容器技术一致，即 Namespace 和 CGroup）。即：目前 FydeOS 使用的安卓运行时是基于 ARC++ 的 Android 9 版本。</p>

<p>ChromeOS 已经<a href="https://chromeos.dev/en/posts/making-android-more-secure-with-arcvm">转向的基于 VM 的 ARCVM 方案</a>，其安全性更高，Android 部分和 ChromeOS 的耦合将变低，但是性能会有损失，本文将不多介绍，关于 FydeOS 考虑参见：<a href="https://community.fydeos.com/t/topic/15374">社区</a>。</p>

<p>ARC 相关技术可以阅读博客：<a href="https://paul.pub/android-on-chrome-os/#id-arc-%E9%A1%B9%E7%9B%AE">Chrome OS上的Android系统</a></p>

<p>下面介绍安卓运行时和 ChromeOS 文件共享的机制。</p>

<ul>
<li>ChromeOS 文件 App，我的文件 -&gt; 安卓文件，在安卓系统看来，位于 <code>/storage/emulated/0/Android</code>。</li>
<li>Android 文件系统的  <code>/storage/emulated/0/Download</code> 实际上就是 ChromeOS 的 <code>/home/chronos/user/MyFiles/Downloads</code>。</li>
<li>通过 <code>mount | grep android</code> 可以看出 Android 文件系统根路径位于 ChromeOS 的 <code>/opt/google/containers/android/rootfs/root</code></li>
<li>通过 <code>mount | grep android</code> 可以看出 Android 文件系统的  <code>/storage/emulated/0</code>  位于 ChromeOS 的 <code>/run/arc/sdcard/default/emulated/0</code></li>
</ul>

<h3 id="总结">总结</h3>

<p>既封闭又开放。</p>

<ul>
<li>封闭指的是：虽然基于 Linux 内核，但是没有暴露 Linux 内核 API，也没有提供专用的系统级 API。而是 GUI 应用完全基于 Web API 仅能有限的使用操作系统资源。这保证流畅性和稳定性。</li>
<li>开放指的是：

<ul>
<li>通过 Android App 可以运行 Android App: Android 运行时和 ChromeOS 共用内核，通过 Linux 容器化技术（无虚拟化损失），可以兼顾性能和一定的安全性。</li>
<li>通过 Linux 子系统可以运行 Linux App（包括 GUI）: Linux 子系统是面向开发者的，采用虚拟化技术，损失的一点性能，满足了对开发者对灵活性的要求（拥有完整的内核权限），同时保证了 ChromeOS 的稳定性和安全性。</li>
</ul></li>
</ul>

<h2 id="在-chromeos-所在-linux-环境运行-linux-程序">在 ChromeOS 所在 Linux 环境运行 Linux 程序</h2>

<p>可以用下图来总结下上文分析的 ChromeOS 整体架构（逻辑上，仅供参考）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------+------------------------------------+-----------------------------------------------------+
|                   Android App                      |     Web Page, Extension, PWA       |               Linux Process (GUI App)               |
+------------------------------+---------------------+------------------------------------+-----------------------+-----------------------------+
|                              |                     |              Chrome                |                       |                             |
+  ARC++ (namespace &amp; cgroup)  |                     +------------------------------------+                       | crosvm (Guest Linux Kernel) |
|                              |                                 GUI FrameWork                                    |                             |
+------------------------------+----------------------------------------------------------------------------------+-----------------------------+
|                                                        Linux Kernel &amp; Hardware Driver                                                         |
+-----------------------------------------------------------------------------------------------------------------------------------------------+</pre></div>
<p>某些时候，我们可能需要直接在 ChromeOS 所在的 Linux 环境运行一些 Linux 程序（注意：不是 Linux 子系统）。</p>

<p>在本例中，我们会在 ChromeOS 中安装以一个罗技 k380 的程序，该程序会在该蓝牙键盘连接到设备后，恢复 F (F1~F12) 键为标准功能（ k380 这个蓝牙键盘，默认的 F 键是一些快捷键，比如 F12 是减小音量，这对于开发者非常难受）。</p>

<p>在 Windows 和 Mac 上，可以通过官方 Logi Option App 来配置。但是在 Linux 中，可以通过开源项目实现该效果，源码地址为： <a href="https://github.com/jergusg/k380-function-keys-conf">jergusg/k380-function-keys-conf</a>。既然该开源项目可以在常规的 Linux 中运行，那么应该也可以在 ChromeOS 所在的 Linux 环境中运行。</p>

<h3 id="编译">编译</h3>

<p>常规 Linux 开源项目编译环境一般都是主流的 Linux 发行版（Debian / Red Hat），因此对 <a href="https://github.com/jergusg/k380-function-keys-conf">jergusg/k380-function-keys-conf</a> 的编译，需要在 ChromeOS 的 Linux 子系统中编译。</p>

<p>进入 Linux 子系统，执行如下命令，clone 代码，安装编译工具，编译安装到 Linux 子系统：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/jergusg/k380-function-keys-conf.git
cd k380-function-keys-conf
<span style="color:#75715e"># 目前 Linux 子系统的版本为 Debian 11</span>
sudo apt install build-essential
sudo make install</code></pre></div>
<h3 id="安装">安装</h3>

<p>观察 Makefile（<code>cat Makefile</code>）其中安装部分的内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">CC = gcc
PREFIX = /usr/local
BINDIR = $(DESTDIR)$(PREFIX)/bin
UDEVDIR = $(DESTDIR)/etc/udev/rules.d

install: k380_conf
        install -d $(BINDIR)
        install k380_conf fn_on.sh $(BINDIR)
        install -d $(UDEVDIR)
        echo &#34;ACTION==\&#34;add\&#34;, KERNEL==\&#34;hidraw[0-9]*\&#34;, RUN+=\&#34;$(BINDIR)/fn_on.sh /dev/%k\&#34;&#34; &gt; $(UDEVDIR)/80-k380.rules</pre></div>
<p>可以看出，安装如下文件：</p>

<ul>
<li>/usr/local/bin/k380_conf</li>
<li>/usr/local/bin/fn_on.sh</li>
<li>/etc/udev/rules.d/80-k380.rules</li>
</ul>

<p>在 Linux 子系统的终端中，将这些文件复制到 Download 目录中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo mv /usr/local/bin/k380_conf /usr/local/bin/fn_on.sh /etc/udev/rules.d/80-k380.rules /mnt/chromeos/MyFiles/Downloads
<span style="color:#75715e"># 会输出 failed to preserve ownership 可以忽略</span></code></pre></div>
<p>ctrl + shift + t 打开 ChromeOS 的终端，输入 <code>shell</code>，将刚刚复制的，位于 <code>/home/chronos/user/MyFiles/Downloads</code> 目录下的文件，复制到 ChromeOS 的目录中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo mv /home/chronos/user/MyFiles/Downloads/k380_conf /usr/local/bin/k380_conf
sudo mv /home/chronos/user/MyFiles/Downloads/fn_on.sh /usr/local/bin/fn_on.sh
sudo mkdir -p /etc/udev/rules.d
sudo mv /home/chronos/user/MyFiles/Downloads/80-k380.rules /etc/udev/rules.d/80-k380.rules</code></pre></div>
<h3 id="运行">运行</h3>

<p>仍然在 ChromeOS 的终端中执行：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">fn_on.sh</code></pre></div>
<p>由于配置 <code>/etc/udev/rules.d/80-k380.rules</code>，k380 键盘通过连接后，将会自动执行 fn_on.sh 脚本，自动化的配置。</p>

<h3 id="开机自启动">开机自启动</h3>

<p>本例中，我们不需要配置开机自启，如果需要配置开机自启，只需要在：</p>

<ul>
<li>在 /etc/init 目录下添加配置文件。</li>
<li>使用 initctl 命令启动服务。</li>
</ul>

<p>更多参见：<a href="https://upstart.ubuntu.com/getting-started.html">upstart 官方文档</a>。</p>

<h3 id="总结-1">总结</h3>

<p>如果想在 ChromeOS 所在的 Linux 环境（注意：不是 Linux 子系统）中直接运行（或开机启动）一个程序，基本步骤如下：</p>

<ul>
<li>在 Linux 子系统 Clone 代码，并在 Linux 子系统中编译。</li>
<li>将 Linux 子系统的产物 Copy 到共享目录，如 <code>/mnt/chromeos/MyFiles/Downloads</code>。</li>
<li>在 ChromeOS Shell 中，将产物从共享目录（如：<code>/home/chronos/user/MyFiles/Downloads</code>）复制到指定目录中。</li>
<li>（可选）配置开机自启，在 ChromeOS Shell 中：

<ul>
<li>开机自启配置文件 <code>/etc/init/xxx.conf</code>。</li>
<li>通过 initctl 启动服务。</li>
</ul></li>
</ul>

<p>本部分介绍的是手动编译和安装软件到 ChromeOS 所在的 Linux 环境。但是开源届，有一个对 ChromeOS 所在的 Linux 环境进行包管理的命令行工具 <a href="https://chromebrew.github.io/">craw</a>。通过该工具可以直接在 ChromeOS 所在 Linux 环境安装各种软件包。</p>
]]></description></item><item><title>Docker 启动报错 trying to mount a directory onto a file</title><link>https://www.rectcircle.cn/posts/docker-mount-a-directory-onto-a-file-problem/</link><pubDate>Fri, 01 Jul 2022 16:01:15 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/docker-mount-a-directory-onto-a-file-problem/</guid><description type="html"><![CDATA[

<h2 id="问题日志">问题日志</h2>

<p>机器重启后，在调用 <code>docker start dd0e8a07a224</code> 将出现如下错误日志：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused &#34;process_linux.go:449: container init caused \&#34;rootfs_linux.go:58: mounting \\\&#34;/test/mount/abc\\\&#34; to rootfs \\\&#34;/data00/docker/overlay2/0709c78a56c450f17a533832db01f1a64deaecd0290bf905f9a171c89ddf1eea/merged\\\&#34; at \\\&#34;/data00/docker/overlay2/0709c78a56c450f17a533832db01f1a64deaecd0290bf905f9a171c89ddf1eea/merged/abc\\\&#34; caused \\\&#34;not a directory\\\&#34;\&#34;&#34;: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type
Error: failed to start containers: dd0e8a07a2243322f19ab55036f42295376927fffbb7b911cf4b8375a853adb1</pre></div>
<h2 id="问题复现">问题复现</h2>

<p>回忆该容器的创建命令（<code>docker run</code>），包含了 <code>-v /test/mount/abc:/abc</code>，挂载宿主机目录的参数。创建时 <code>/test/mount/abc</code> 是一个文件，出问题时 <code>/test/mount/abc</code> 变成了目录。</p>

<p>因此复现步骤如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建测试挂载文件并创建容器</span>
mkdir -p /test/mount
touch /test/mount/abc
docker run -d --name test-mount-error -v /test/mount/abc:/abc -it busybox sleep <span style="color:#ae81ff">100000000</span>

<span style="color:#75715e"># 观察输出挂载情况，发现文件一股在</span>
docker exec -it test-mount-error sh -c ls -al
<span style="color:#75715e"># abc   bin   dev   etc   home  proc  root  sys   tmp   usr   var</span>

<span style="color:#75715e"># 停止容器并删除宿主机的  /test/mount/abc 文件并创建 /test/mount/abc 目录</span>
docker stop test-mount-error
rm -rf /test/mount/abc <span style="color:#f92672">&amp;&amp;</span> mkdir /test/mount/abc

<span style="color:#75715e"># 重启启动已启动的容器</span>
docker start test-mount-error
<span style="color:#75715e"># Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused &#34;process_linux.go:449: container init caused \&#34;rootfs_linux.go:58: mounting \\\&#34;/test/mount/abc\\\&#34; to rootfs \\\&#34;/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/merged\\\&#34; at \\\&#34;/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/merged/abc\\\&#34; caused \\\&#34;not a directory\\\&#34;\&#34;&#34;: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type</span>
<span style="color:#75715e"># Error: failed to start containers: test-mount-error</span></code></pre></div>
<h2 id="问题分析">问题分析</h2>

<p>观察容器文件系统情况，执行 <code>docker inspect test-mount-error -f '{{.GraphDriver.Data}}'</code>，格式化输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">map[
    LowerDir:/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d-init/diff:/data00/docker/overlay2/b99572ac092c84df3fa29148b420387be46e950da68b0564b0e3bcf643fdf39c/diff 
    MergedDir:/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/merged 
    UpperDir:/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/diff 
    WorkDir:/data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/work]</pre></div>
<p>观察 <code>UpperDir</code> 的路径可以发现存在 <code>abc</code> 文件， <code>ls /data00/docker/overlay2/8e0704ed4c38d54469fc7ed205e538b8fabc7227b2d28e8f41ad2dc471f3062d/diff</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">-rwxr-xr-x <span style="color:#ae81ff">1</span> root root    <span style="color:#ae81ff">0</span> 7月   <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">16</span>:16 abc</code></pre></div>
<p>由此，结合 <a href="/posts/container-core-tech-3-namespace-mount/">mount namespace</a> 和 overlay2 文件系统相关知识，可以推测出 Docker 的 <code>-v</code> 和 <code>--mount</code> 到容器的指定目录的原理是：</p>

<ol>
<li>在容器文件系统 <code>diff</code> 目录中创建，待挂载的文件或目录对应的挂载点文件/目录（本例中为 <code>/test/mount/abc</code>），注意，<code>diff</code> 目录已经存在了对象的文件/目录且和宿主机路径对应的文件的类型不一致（本例中为  <code>&lt;容器文件系统存储&gt;/diff/abc</code>），则直接报错。原因是：

<ul>
<li>mount binding 系统调用的目标（挂载点），在当前文件系统中，必须存在。</li>
<li>mount binding 系统调用的源（宿主机路径）的文件类型必须和目标文件类型一致。</li>
</ul></li>
<li>容器启动过程中，在新的 mount namespace 中 mount binding 宿主机路径（本例中为 <code>/test/mount/abc</code>）到 路径作为根路径的对应路径（本例中为  <code>&lt;容器文件系统存储&gt;/diff/abc</code>）。</li>
<li>后面就是创建 overlay2 文件系统，然后调用 <code>pivot_root</code> 系统调用切换根文件系统。</li>
</ol>

<p>如上所示，原因就是 <code>/test/mount/abc</code> 和  <code>&lt;容器文件系统存储&gt;/diff/abc</code> 文件类型不一致。</p>

<h2 id="解决办法">解决办法</h2>

<p>网络上针对该问题的解决办法都要求删除该容器，这在某些场景是不可接受的，既然知道了上述原理，解决办法就很简单：手动删除 <code>&lt;容器文件系统存储&gt;/diff/</code> 中挂载点文件/目录，然后启动容器即可。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">rm -rf /data00/docker/overlay2/<span style="color:#66d9ef">$(</span>docker inspect test-mount-error -f <span style="color:#e6db74">&#39;{{.GraphDriver.Data.UpperDir}}&#39;</span><span style="color:#66d9ef">)</span>/abc
docker start test-mount-error
<span style="color:#75715e"># docker rm -f test-mount-error &amp;&amp; rm -rf /test # 实验完成后清理现场</span></code></pre></div>]]></description></item><item><title>Linux 网络虚拟化技术（五）隧道技术</title><link>https://www.rectcircle.cn/posts/linux-net-virual-05-tunnel/</link><pubDate>Sun, 19 Jun 2022 23:11:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-05-tunnel/</guid><description type="html"><![CDATA[

<h2 id="tun-tap-虚拟设备">tun/tap 虚拟设备</h2>

<h3 id="概述">概述</h3>

<p>TUN (network TUNnel) / TAP (network TAP) 是 Linux 提供的的两种虚拟网络设备（也是一个网络接口，因此需配置 ip 网段）。该设备一端连接内核网络网络协议栈，一端连接用户态进程的一个文件描述符。</p>

<p>在内核网络协议栈中处理该数据包时，根据目标 IP 和路由表判断，需要从该网络接口设备发出时，与该设备绑定的用户态进程的文件描述符将 read 系统调用将读到数据。</p>

<p>用户态进程调用 write 系统调用向与该设备绑定的文件描述符写数据时，该设备会将写入的数据流当做网络数据包发送到内核网络协议栈中。</p>

<p>一般情况下，用户态进程在读取到数据后，会通过一个 TCP/UDP socket 将数据发送到远端主机，远端主机接收到数据库在将数据转发到目标，这样在本机和远端主机之间通过 tun/tap 和 socket 建立了一个隧道。</p>

<p>下图是一个 VPN Client 侧的网络拓扑（来自： <a href="https://segmentfault.com/a/1190000009249039">Linux虚拟网络设备之tun/tap</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+
|                                                                |
|  +--------------------+      +--------------------+            |
|  | User Application A |      | User Application B |&lt;-----+     |
|  +--------------------+      +--------------------+      |     |
|               | 1                    | 5                 |     |
|...............|......................|...................|.....|
|               ↓                      ↓                   |     |
|         +----------+           +----------+              |     |
|         | socket A |           | socket B |              |     |
|         +----------+           +----------+              |     |
|                 | 2               | 6                    |     |
|.................|.................|......................|.....|
|                 ↓                 ↓                      |     |
|             +------------------------+                 4 |     |
|             | Newwork Protocol Stack |                   |     |
|             +------------------------+                   |     |
|                | 7                 | 3                   |     |
|................|...................|.....................|.....|
|                ↓                   ↓                     |     |
|        +----------------+    +----------------+          |     |
|        |      eth0      |    |      tun0      |          |     |
|        +----------------+    +----------------+          |     |
|    10.32.0.11  |                   |   192.168.3.11      |     |
|                | 8                 +---------------------+     |
|                |                                               |
+----------------|-----------------------------------------------+
                 ↓
         Physical Network</pre></div>
<p>简而言之，tun/tap 提供了一种在用户态进程，对数据包进行自定义处理的机制。一般用来实现 Tunnel/VPN。</p>

<p>tun 和 tap 两种设备的唯一区别在于数据包类型上：</p>

<ul>
<li>tun 处理的是 ip 数据包（三层）。</li>
<li>tap 处理的是 以太网数据包（二层）。</li>
</ul>

<h3 id="系统调用">系统调用</h3>

<blockquote>
<p><a href="https://man7.org/linux/man-pages/man2/ioctl.2.html">ioctl(2)</a> | <a href="https://man7.org/linux/man-pages/man7/netdevice.7.html">netdevice(7)</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;fcntl.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/ioctl.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;net/if.h&gt;     // #define TUNSETIFF     _IOW(&#39;T&#39;, 202, int) </span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> fd <span style="color:#f92672">=</span> open(<span style="color:#e6db74">&#34;/dev/net/tun&#34;</span>, O_RDWR);    <span style="color:#75715e">// ignore error
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">ioctl</span>(fd, SIOCSIFNETMASK, <span style="color:#66d9ef">struct</span> <span style="color:#f92672">*</span>ifreq ifr);
</code></pre></div>
<ul>
<li>使用 <code>open</code> 系统调用，打开 <code>&quot;/dev/net/tun&quot;</code> 文件，将会获取一个可以读写的文件描述符。</li>
<li>通过 <code>ioctl</code> 系统调用，将文件描述符和一个 tun/tap 设备进行关联。

<ul>
<li>如果 <code>struct *ifreq ifr</code> 指向的 tun/tap 设备已存在，则仅仅将文件描述符和虚拟网络设备关联，本进程退出后，该设备仍然存在。</li>
<li>如果 <code>struct *ifreq ifr</code> 指向的 tun/tap 设备不存在，则内核创建一个 tun/tap 设备（通过： ip addr show 可以看到），并将文件描述符和虚拟网络设备关联，本进程退出后，该设备将自动被删除。</li>
</ul></li>
</ul>

<h3 id="命令行创建">命令行创建</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip tuntap add dev tun-sample mode tun</code></pre></div>
<h3 id="go-语言-sdk">Go 语言 SDK</h3>

<p>github 有一个 star 为 <code>1.5k</code> 的 tun/tap 的第三方库 <a href="https://github.com/songgao/water"><code>songgao/water</code></a>。该库屏蔽了 tun/tap 在不同操作系统的差异。</p>

<p>上述库仅仅是 syscall 的简单封装，通过 Go 标准库的 syscall 包，同时参考上述项目源码，也可以很容易的 tun/tap。</p>

<h3 id="实验和说明">实验和说明</h3>

<blockquote>
<p>参考： <a href="https://segmentfault.com/a/1190000009249039">Linux虚拟网络设备之tun/tap</a> | <a href="https://www.kernel.org/doc/html/v5.8/networking/tuntap.html">Universal TUN/TAP device driver</a></p>
</blockquote>

<p>编写一个简单的实验程序。该程序，会创建一个 tun 设备，配置该 tun 设备网络信息为 <code>172.16.2.1/16</code>，并打印从该 tun 设备中读取到的 ip 数据包的大小。</p>

<p>这种读写 tun/tap 的程序，被称为 tun/tap 驱动程序。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc ./src/c/05-tun-tap/01-sample-tun.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e">// 修改自：https://segmentfault.com/a/1190000009249039
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;net/if.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/ioctl.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/stat.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;fcntl.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/if_tun.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;arpa/inet.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>tun_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tun-sample&#34;</span>;
<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>tun_ip <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;172.16.2.1&#34;</span>;
<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>tun_net_mask <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;255.255.0.0&#34;</span>;

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">set_tun_if</span>(<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>if_name)
{
    <span style="color:#75715e">// 简单起见，使用传统的 ioctl 系统调用，而非 netlink api。
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int</span> sockfd, err;
    <span style="color:#66d9ef">struct</span> ifreq ifr;
    <span style="color:#66d9ef">struct</span> sockaddr_in <span style="color:#f92672">*</span>addr;

    sockfd <span style="color:#f92672">=</span> socket(AF_INET, SOCK_DGRAM, <span style="color:#ae81ff">0</span>);
    <span style="color:#66d9ef">if</span> (sockfd <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;

    memset(<span style="color:#f92672">&amp;</span>ifr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span> ifr);
    strncpy(ifr.ifr_name, if_name, IFNAMSIZ);

    <span style="color:#75715e">// 设置 ip
</span><span style="color:#75715e"></span>    ifr.ifr_addr.sa_family <span style="color:#f92672">=</span> AF_INET;
    addr <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> sockaddr_in <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>ifr.ifr_addr;
    inet_pton(AF_INET, tun_ip, <span style="color:#f92672">&amp;</span>addr<span style="color:#f92672">-&gt;</span>sin_addr);
    <span style="color:#66d9ef">if</span> (err <span style="color:#f92672">=</span> ioctl(sockfd, SIOCSIFADDR, <span style="color:#f92672">&amp;</span>ifr) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        <span style="color:#66d9ef">return</span> err;
    <span style="color:#75715e">// 设置网络掩码
</span><span style="color:#75715e"></span>    ifr.ifr_netmask.sa_family <span style="color:#f92672">=</span> AF_INET;
    addr <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> sockaddr_in <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>ifr.ifr_netmask;
    inet_pton(AF_INET, tun_net_mask, <span style="color:#f92672">&amp;</span>addr<span style="color:#f92672">-&gt;</span>sin_addr);
    <span style="color:#66d9ef">if</span> (err <span style="color:#f92672">=</span> ioctl(sockfd, SIOCSIFNETMASK, <span style="color:#f92672">&amp;</span>ifr) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        <span style="color:#66d9ef">return</span> err;

    <span style="color:#75715e">// 启动接口
</span><span style="color:#75715e"></span>    ifr.ifr_flags <span style="color:#f92672">|=</span> IFF_UP;
    <span style="color:#66d9ef">if</span> (err <span style="color:#f92672">=</span> ioctl(sockfd, SIOCSIFFLAGS, <span style="color:#f92672">&amp;</span>ifr) <span style="color:#f92672">&lt;</span><span style="color:#ae81ff">0</span>)
        <span style="color:#66d9ef">return</span> err;
    close(sockfd);
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}


<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">tun_alloc</span>(<span style="color:#66d9ef">int</span> flags)
{

    <span style="color:#75715e">// 没有找到如何使用 netlink 创建 tun 设备的相关示例。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// https://man7.org/linux/man-pages/man7/netdevice.7.html
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> ifreq ifr;
    <span style="color:#66d9ef">int</span> fd, err;
    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>clonedev <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/dev/net/tun&#34;</span>;

    <span style="color:#75715e">// 打开 /dev/net/tun 文件，即创建一个用于收发 tun 虚拟网络设备的文件描述符
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 该文件一般是 o666 权限，因此不需要特殊权限。
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> ((fd <span style="color:#f92672">=</span> open(clonedev, O_RDWR)) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        <span style="color:#66d9ef">return</span> fd;
    }

    <span style="color:#75715e">// 设置 tun 虚拟网络设备
</span><span style="color:#75715e"></span>    memset(<span style="color:#f92672">&amp;</span>ifr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(ifr));
    ifr.ifr_flags <span style="color:#f92672">=</span> flags;  <span style="color:#75715e">// 设置设备标志
</span><span style="color:#75715e"></span>    strncpy(ifr.ifr_name, tun_name, IFNAMSIZ); <span style="color:#75715e">// 设置设备名
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">// 如果该 tun 设备不存在，内核创建一个 tun 设备（通过： ip addr show 可以看到），将文件描述符和虚拟网络设备关联。该进程退出后，该设备将自动被删除。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 如果该 tun 设备已经存在，则仅仅将文件描述符和虚拟网络设备关联。该进程退出后，设备仍然存在。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 该系统调用需要 CAP_NET_ADMIN 权限。
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> ((err <span style="color:#f92672">=</span> ioctl(fd, TUNSETIFF, (<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>ifr)) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        close(fd);
        <span style="color:#66d9ef">return</span> err;
    }

    printf(<span style="color:#e6db74">&#34;Open tun/tap device: %s for reading...</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ifr.ifr_name);

    <span style="color:#75715e">// 设置 ip、网络掩码 并 启动 tun 设备
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 等价于执行：
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   sudo ip addr add 172.16.2.1/8 dev tun-sample
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   sudo ip link set tun-sample up
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 会自动添加路由： 172.16.0.0/16 dev tun-sample proto kernel scope link src 172.16.2.1 （ip route show）
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> ((err <span style="color:#f92672">=</span> set_tun_if(ifr.ifr_name)) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        close(fd);
        <span style="color:#66d9ef">return</span> err;
    }
    <span style="color:#66d9ef">return</span> fd;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
    <span style="color:#66d9ef">int</span> tun_fd, nread;
    <span style="color:#66d9ef">char</span> buffer[<span style="color:#ae81ff">1500</span>];

    <span style="color:#75715e">/* Flags: IFF_TUN   - TUN device (no Ethernet headers) 即 IP 包
</span><span style="color:#75715e">     *        IFF_TAP   - TAP device 以太网包（包含 Ethernet headers）
</span><span style="color:#75715e">     *        IFF_NO_PI - Do not provide packet information，不包含额外的报信息，即传递到 tun_fd 中数据是纯粹的 ip 包。
</span><span style="color:#75715e">     *                    如果不设置该选项，传递到 tun_fd 中数据将包含 struct tun_pi { unsigned short flags; unsigned short proto; }
</span><span style="color:#75715e">     *                              flags - 设置 TUN_PKT_STRIP 选项时，表示用户缓冲区大小
</span><span style="color:#75715e">     *                              proto - 表示当前 IP 包的协议，https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
</span><span style="color:#75715e">     */</span>
    tun_fd <span style="color:#f92672">=</span> tun_alloc(IFF_TUN <span style="color:#f92672">|</span> IFF_NO_PI);

    <span style="color:#66d9ef">if</span> (tun_fd <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        perror(<span style="color:#e6db74">&#34;Allocating interface&#34;</span>);
        exit(<span style="color:#ae81ff">1</span>);
    }

    <span style="color:#75715e">// 该程序接收 tun 数据包后，仅打印收到的包长度，不做任何事情。
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">while</span> (<span style="color:#ae81ff">1</span>)
    {
        nread <span style="color:#f92672">=</span> read(tun_fd, buffer, <span style="color:#66d9ef">sizeof</span>(buffer));
        <span style="color:#66d9ef">if</span> (nread <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        {
            perror(<span style="color:#e6db74">&#34;Reading from interface&#34;</span>);
            close(tun_fd);
            exit(<span style="color:#ae81ff">1</span>);
        }

        printf(<span style="color:#e6db74">&#34;Read %d bytes from tun/tap device</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, nread);
    }
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<p>打开一个 shell A，编译并运行该程序 <code>gcc ./src/c/05-tun-tap/01-sample-tun.c &amp;&amp; sudo ./a.out</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Open tun/tap device: tun-sample <span style="color:#66d9ef">for</span> reading...
Read <span style="color:#ae81ff">76</span> bytes from tun/tap device
Read <span style="color:#ae81ff">48</span> bytes from tun/tap device
Read <span style="color:#ae81ff">76</span> bytes from tun/tap device
Read <span style="color:#ae81ff">76</span> bytes from tun/tap device
Read <span style="color:#ae81ff">76</span> bytes from tun/tap device
Read <span style="color:#ae81ff">48</span> bytes from tun/tap device
Read <span style="color:#ae81ff">48</span> bytes from tun/tap device
Read <span style="color:#ae81ff">48</span> bytes from tun/tap device</code></pre></div>
<p>注意：这些接收到数据可能是 arp 或 ICMPv6 (NDP) 相关协议的数据包。</p>

<p>打开一个 shell B。</p>

<p>观察路由表 <code>ip route show</code> ，内核自动为该 tun 设备添加了正确的路由（所有发往 <code>172.16.0.0/16</code> 网络的数据包，将会从 tun-sample 设备出）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">172.16.0.0/16 dev tun-sample proto kernel scope link src 172.16.2.1 </pre></div>
<p>此时，通过 <code>ping 172.16.0.1</code>，观察 shell A 会发现有 86 字节的数据包接收到的日志。可以看出，发往 <code>172.16.0.1</code> 数据包可以被的测试程序的和该设备关联的文件描述符读取到。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
...</pre></div>
<h3 id="实现简单的-vpn">实现简单的 vpn</h3>

<h4 id="vpn-简介">VPN 简介</h4>

<p>VPN (Virtual private network, 虚拟私有网络, 虚拟专用网络)，严谨定义参见： <a href="https://en.wikipedia.org/wiki/Virtual_private_network">wiki</a>。</p>

<p>从技术层面看，VPN 的落脚点是 Private Network，即私有网络。在 IP 协议中，对应的是 Private Network 地址：</p>

<ul>
<li>IPv4: <code>10.0.0.0/8</code>、<code>172.16.0.0/12</code>、<code>192.168.0.0/24</code>。</li>
<li>IPv6: <code>fd00::/8</code>。</li>
</ul>

<p>Virtual 要解决的是，在地理上，跨越地域，来搭建一个逻辑上 Private Network。比如某个组织，中国和美国有两个机房，我们希望这两个机房，可以通过私有网络地址可以相互访问，就像在同一个机房一样，此外，在任何一个地方的 PC 设备都可以安全的连入该私有网络，就像在这个机房一样的访问私有网络。</p>

<p>因为 VPN 解决的是跨地域的私有网络搭建。因此两个区域的流量需要通过一个或多个链路进行连通，这个链路被称为 Tunnel （隧道），这是 VPN 技术的核心之一。在现实中，这个 Tunnel 都是基于广域网（俗称公网/互联网）实现的。</p>

<p>由于 VPN 的跨地域流量是通过公网实现的，因此安全性是最重要的指标，而 VPN 协议主要就是来解决流量安全传输问题而诞生的，这部分参见下文：<a href="#常见的-vpn-协议">常见的 VPN 协议</a>。</p>

<h4 id="sampletun-流程分析">sampletun 流程分析</h4>

<p><a href="https://github.com/marywangran/simpletun">marywangran/simpletun</a> 是一个比较好的用来学习 tun 用法的开源项目。</p>

<p>该软件的用法为帮助信息如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Usage:
simpletun -i &lt;ifacename&gt; [-s|-c &lt;serverIP&gt;] [-p &lt;port&gt;] [-u|-a] [-d]
simpletun -h

-i &lt;ifacename&gt;: 绑定的网络接口名
-s: 以 server 模式运行
-c &lt;serverIP&gt;: 以 client 模式运行，并执行 server ip
-p &lt;port&gt;: 指定 server 绑定的端口默认是 55555
-u|-a: 使用 TUN (-u, 默认) 或 TAP (-a)
-d: 打印 debug 信息
-h: 打印这个帮助信息文本</pre></div>
<p>该软件同时包含 client 和 server，client 和 server 之间会建立一个 tcp 连接，用来进行数据包转发。</p>

<p>需要注意的是：该软件 server 端只接收一个 client 的连接，也就是说只能服务一个 client。也就是说，该项目提供的是一对一的 tunnel。</p>

<p>该软件的整体流程如下所示：</p>

<table>
<thead>
<tr>
<th>步骤</th>
<th>client</th>
<th>server</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>getopt</code></td>
<td>解析命令行参数</td>
<td>同 Client</td>
</tr>

<tr>
<td><code>tun_alloc</code></td>
<td>打开一个与 tun/tap 绑定的文件描述符 <code>tap_fd</code>，和<a href="#实验和说明">上文</a>一致</td>
<td>同 Client</td>
</tr>

<tr>
<td><code>socket</code></td>
<td>创建一个 TCP socket 对象 <code>sock_fd</code></td>
<td>同 Client</td>
</tr>

<tr>
<td>tunnel over tcp</td>
<td>通过 <code>connect</code> 连接到 server 的 TCP socket 中, 该文件描述符为 <code>net_fd</code></td>
<td><code>bind</code> 端口，<code>listen</code>，并 <code>accept</code> 等待 client 的 TCP 连接请求，该 client 的连接对应的文件描述符为 <code>net_fd</code></td>
</tr>

<tr>
<td>io copy</td>
<td><code>tap_fd</code> 和 <code>net_fd</code> 文件描述符数据拷贝（细节参见下文）</td>
<td>同 client</td>
</tr>
</tbody>
</table>

<p>在数据拷贝过程中，假设从 <code>tap_fd</code> 读取到的数据为 <code>data</code>，则发送到 <code>net_fd</code> 的数据将为 <code>len(data)</code> (1 字节) + <code>data</code>。</p>

<p>同理，从 <code>net_fd</code> 中读取数据发送到 <code>tap_fd</code>， 则先读取第一个字节的 <code>len(data)</code>，然后再读取剩余的 <code>data</code>。</p>

<h4 id="sampletun-简单体验">sampletun 简单体验</h4>

<p>实验规划和准备如下：</p>

<ul>
<li>虚拟机 1：外部 IP 地址为 <code>192.168.57.3</code>，作为 Server，分配的虚拟地址为 <code>172.16.1.1/24</code></li>
<li>虚拟机 2：外部 IP 地址为 <code>192.168.57.4</code>，作为 Client，分配的虚拟地址为 <code>172.16.1.2/24</code></li>
<li>虚拟机 1 准备参见：<a href="/posts/linux-net-virual-01-overview/#实验环境准备">Linux 网络虚拟化技术（一）概览 - 实验环境准备</a></li>

<li><p>虚拟机 2 从虚拟机 1 复制，并配置静态 IP。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># /etc/network/interfaces
auto enp0s8
iface enp0s8 inet static
address 192.168.56.4/24
gateway 192.168.56.1

auto enp0s9
iface enp0s9 inet static
address 192.168.57.4/24
gateway 192.168.57.1</pre></div></li>
</ul>

<p>执行如下命令准备测试</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 虚拟机 1 作为服务端</span>
sudo ip tuntap add dev tun-server mode tun
sudo ip addr add <span style="color:#ae81ff">172</span>.16.1.1/24 dev tun-server
sudo ip link set tun-server up
gcc ./src/c/05-tun-tap/simpletun.c <span style="color:#f92672">&amp;&amp;</span> sudo ./a.out -d -i tun-server -s


<span style="color:#75715e"># 虚拟机 2 作为客户端</span>
sudo ip tuntap add dev tun-client mode tun
sudo ip addr add <span style="color:#ae81ff">172</span>.16.1.2/24 dev tun-client
sudo ip link set tun-client up
gcc ./src/c/05-tun-tap/simpletun.c <span style="color:#f92672">&amp;&amp;</span> sudo ./a.out -d -i tun-client -c <span style="color:#ae81ff">192</span>.168.57.3</code></pre></div>
<p>在虚拟机 2 上执行 <code>ping 172.16.1.1</code> 可以正常输出响应。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># ping 172.16.1.1  # 虚拟机 2</span>
PING <span style="color:#ae81ff">172</span>.16.1.1 <span style="color:#f92672">(</span><span style="color:#ae81ff">172</span>.16.1.1<span style="color:#f92672">)</span> <span style="color:#ae81ff">56</span><span style="color:#f92672">(</span><span style="color:#ae81ff">84</span><span style="color:#f92672">)</span> bytes of data.
<span style="color:#ae81ff">64</span> bytes from <span style="color:#ae81ff">172</span>.16.1.1: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span> time<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>.19 ms
<span style="color:#ae81ff">64</span> bytes from <span style="color:#ae81ff">172</span>.16.1.1: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span> time<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>.01 ms
<span style="color:#ae81ff">64</span> bytes from <span style="color:#ae81ff">172</span>.16.1.1: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span> time<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>.86 ms
<span style="color:#ae81ff">64</span> bytes from <span style="color:#ae81ff">172</span>.16.1.1: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span> time<span style="color:#f92672">=</span><span style="color:#ae81ff">44</span>.8 ms
<span style="color:#ae81ff">64</span> bytes from <span style="color:#ae81ff">172</span>.16.1.1: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span> time<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>.49 ms
<span style="color:#75715e"># ...</span>

<span style="color:#75715e"># gcc ./src/c/05-tun-tap/simpletun.c &amp;&amp; sudo ./a.out -d -i tun-client -c 192.168.57.3  # 虚拟机 2</span>
<span style="color:#75715e"># ...</span>
TAP2NET <span style="color:#ae81ff">17</span>: Read <span style="color:#ae81ff">84</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">17</span>: Written <span style="color:#ae81ff">84</span> bytes to the network
NET2TAP <span style="color:#ae81ff">17</span>: Read <span style="color:#ae81ff">84</span> bytes from the network
NET2TAP <span style="color:#ae81ff">17</span>: Written <span style="color:#ae81ff">84</span> bytes to the tap interface
TAP2NET <span style="color:#ae81ff">18</span>: Read <span style="color:#ae81ff">84</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">18</span>: Written <span style="color:#ae81ff">84</span> bytes to the network
NET2TAP <span style="color:#ae81ff">18</span>: Read <span style="color:#ae81ff">84</span> bytes from the network
NET2TAP <span style="color:#ae81ff">18</span>: Written <span style="color:#ae81ff">84</span> bytes to the tap interface
NET2TAP <span style="color:#ae81ff">19</span>: Read <span style="color:#ae81ff">48</span> bytes from the network
NET2TAP <span style="color:#ae81ff">19</span>: Written <span style="color:#ae81ff">48</span> bytes to the tap interface
TAP2NET <span style="color:#ae81ff">19</span>: Read <span style="color:#ae81ff">48</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">19</span>: Written <span style="color:#ae81ff">48</span> bytes to the network
<span style="color:#75715e"># ...</span>

<span style="color:#75715e"># gcc ./src/c/05-tun-tap/simpletun.c &amp;&amp; sudo ./a.out -d -i tun-server -s  # 虚拟机 1</span>
<span style="color:#75715e"># ...</span>
NET2TAP <span style="color:#ae81ff">17</span>: Written <span style="color:#ae81ff">84</span> bytes to the tap interface
TAP2NET <span style="color:#ae81ff">17</span>: Read <span style="color:#ae81ff">84</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">17</span>: Written <span style="color:#ae81ff">84</span> bytes to the network
NET2TAP <span style="color:#ae81ff">18</span>: Read <span style="color:#ae81ff">84</span> bytes from the network
NET2TAP <span style="color:#ae81ff">18</span>: Written <span style="color:#ae81ff">84</span> bytes to the tap interface
TAP2NET <span style="color:#ae81ff">18</span>: Read <span style="color:#ae81ff">84</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">18</span>: Written <span style="color:#ae81ff">84</span> bytes to the network
TAP2NET <span style="color:#ae81ff">19</span>: Read <span style="color:#ae81ff">48</span> bytes from the tap interface
TAP2NET <span style="color:#ae81ff">19</span>: Written <span style="color:#ae81ff">48</span> bytes to the network
NET2TAP <span style="color:#ae81ff">19</span>: Read <span style="color:#ae81ff">48</span> bytes from the network
NET2TAP <span style="color:#ae81ff">19</span>: Written <span style="color:#ae81ff">48</span> bytes to the tap interface
<span style="color:#75715e"># ...</span></code></pre></div>
<p>最后恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 虚拟机 1</span>
sudo ip link delete tun-server
<span style="color:#75715e"># 虚拟机 2</span>
sudo ip link delete tun-client</code></pre></div>
<h4 id="基于路由表的-vpn-的简单规划">基于路由表的 VPN 的简单规划</h4>

<p>通过 tun 和配置路由表规划一个简单 VPN（网段为 <code>172.16.0.0/16</code>）网络，规划如下：</p>

<ul>
<li><code>172.16.0.0/24</code> 作为分配给 VPN Tunnel 的网段。</li>
<li><code>172.16.1.0/24</code> 分配给机房 A（位于北京），其中网关（路由器） <code>172.16.1.1</code> 拥有公网 IP <code>192.168.57.2</code> （仅做示例）。</li>
<li><code>172.16.2.0/24</code> 分配给机房 B（位于广州），其中网关（路由器） <code>172.16.2.1</code> 拥有公网 IP <code>192.168.57.3</code> （仅做示例）。</li>
</ul>

<p><img src="/image/sample-vpn-route-table.svg" alt="image" /></p>

<p>整体上来看，VPN Server 是一个由软件实现的路由器（3 层），因此 VPN Server 中也有一张特殊的路由表。该路由表的核心字段为：</p>

<ul>
<li>key: 目标 IP 网段</li>
<li>value: 对应的 VPN Server 的 公网 IP 和 UDP Port 以及可选的 UDP Connect。</li>
</ul>

<p>基于此方案，机房之间会形成一个网状的 UDP Tunnel。此外，上图没有画出的是，需要一个中心化存储来存储网段规划信息和 VPN Server IP Port 信息。</p>

<p>以上是一种简化的画法，其实 VPN Server 可以和网关分离，位于独立的设备中，只要配置好路由表，都可以正常工作。</p>

<h4 id="基于-iptables-的-vpn-的简单规划">基于 iptables 的 VPN 的简单规划</h4>

<p>假设只有一个机房，需求上也只有雇员 PC 单向访问该机房的需求，此时可以通过 iptables 来实现 VPN Server，相关假设如下：</p>

<ul>
<li>内网网段为 <code>172.16.1.0/24</code></li>
<li>采用本方案，VPN Client 和 VPN Server 的网段不能和内网网段重合，假设为 <code>192.168.60.0/24</code>。</li>
<li>VPN Server 拥有独立的公网 IP，假设为 <code>192.168.57.2</code>。</li>
</ul>

<p><img src="/image/sample-vpn-iptables.svg" alt="image" /></p>

<p>上图可以看出，该方式可以支持 Client 全部流量通过 VPN Server 转发。</p>

<h4 id="额外说明">额外说明</h4>

<p>以上是作者根据路由表 / iptables 等计算机网络相关知识做的推演。是否可能，未在实际生产环境测试过，请勿直接使用在生产环境。如需搭建 VPN，建议直接使用企业级或开源的 VPN 应用。</p>

<h3 id="tun-tap-其他应用场景">tun/tap 其他应用场景</h3>

<p>tun/tap 除了在 VPN 场景使用之外。也是 qemu-kvm 虚拟化技术中，网络虚拟化的基石。</p>

<p>基本原理是：虚拟机中进程对物理网卡的读写，在宿主机看来，是对 tap/tun 设备的读写。从而实现了虚拟机网络的虚拟化。更多参见：<a href="https://opengers.github.io/openstack/openstack-base-virtual-network-devices-tuntap-veth/">云计算底层技术-虚拟网络设备(tun/tap,veth)</a>。</p>

<h2 id="linux-tunnel">Linux Tunnel</h2>

<p>上文介绍的 tun/tap 是在应用层实现自定义 tunnel 的方式，除了这种方式外， Linux 原生支持一些标准的 Tunnel 实现（内核态实现，通过 <code>ip tunnel help</code> 可以查看支持的协议）。例如 ipip，参见：<a href="https://morven.life/posts/networking-3-ipip/">揭秘 IPIP 隧道</a>。</p>

<p>关于 Tunnel 和 VPN 的关系，可以说 VPN 是 Tunnel 的一个应用场景，或者说 VPN 是 基于 Tunnel 实现的，即： <code>VPN = 加密协议 + Tunnel</code>，参见：<a href="https://learningnetwork.cisco.com/s/question/0D53i00000Kt2skCAB/vpn-vs-tunneling">问答</a>。</p>

<h2 id="常见的-vpn-协议">常见的 VPN 协议</h2>

<p>从上文可以看到 VPN 协议主要解决的是 Tunnel 加密的问题，关于主流的 VPN 协议的可以阅读：</p>

<ul>
<li><a href="https://proprivacy.com/vpn/guides/vpn-encryption-the-complete-guide#preliminaries">OpenVPN vs IKEv2 vs PPTP vs L2TP/IPSec vs SSTP - Ultimate Guide to VPN Encryption</a></li>
<li><a href="https://www.cisco.com/c/zh_cn/support/docs/smb/routers/cisco-rv-series-small-business-routers/1399-tz-best-practices-vpn.html">思科业务VPN概述和最佳实践</a></li>
</ul>
]]></description></item><item><title>Linux 网络虚拟化技术（四）iptables</title><link>https://www.rectcircle.cn/posts/linux-net-virual-04-iptables/</link><pubDate>Mon, 13 Jun 2022 00:40:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-04-iptables/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/category/%e8%bf%90%e7%bb%b4%e7%9b%b8%e5%85%b3/iptables/">zsythink iptables 系列博客</a> | <a href="https://linux.die.net/man/8/iptables">iptables manual</a></p>
</blockquote>

<h2 id="iptables-简述">iptables 简述</h2>

<p>iptables 是一套针对 Linux 的，对 ipv4（ipv6 也存在对应的工具 ip6table） 数据包（流量）管理工具。在常见的 Linux 发行版均已预装，提供了如：包过滤、端口转发、NAT、流量审计等功能。</p>

<h2 id="准备">准备</h2>

<p>确保系统已经安装 iptables。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt install iptables</code></pre></div>
<p>确保测试虚拟机的网络接口配置为（<code>ip addr show</code>）：</p>

<ul>
<li>enp0s3 网卡1 - 选择 NAT，IP 地址为 <code>10.0.2.15/24</code>（不固定），用于访问公网。</li>
<li>enp0s8 网卡2 - 选择 仅主机网络，IP 地址为 <code>192.168.56.3/24</code>，用于 SSH 连接</li>
<li>enp0s9 网卡3 - 选择 仅主机网络，IP 地址为 <code>192.168.57.3/24</code>，用于测试 iptables 规则。</li>
</ul>

<p>配置方式参见：</p>

<ul>
<li><a href="/posts/container-core-tech-1-experiment-preparation-and-linux-base/#实验环境准备">容器核心技术（一） 实验环境准备 &amp; Linux 概述</a></li>
<li><a href="/posts/linux-net-virual-03-bridge/#实验准备">Linux 网络虚拟化技术（三）bridge 虚拟设备</a></li>
</ul>

<h2 id="测试程序">测试程序</h2>

<p>使用 C 语言。编写与一个简单的 TCP Server 测试程序，监听在 1234 端口。</p>

<p>该程序将，接收 TCP 请求，并响应一个字符串。该字符串包含如下信息：</p>

<ul>
<li>本次 TCP 请求的 Source IP、Source Port。</li>
<li>本次 TCP 请求的 Destination IP、Destination Port。</li>
<li>通过 <code>getsockopt</code> 配合 <code>SO_ORIGINAL_DST</code> 拿到的原始 Destination IP 和 Destination Port，如果报错将显示错误信息。</li>
</ul>

<p>响应完成后，将关闭该 TCP 连接。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// 必须安装 iptables 否则会报错：getsockopt error: Protocol not available
</span><span style="color:#75715e">// 运行： gcc ./src/c/03-iptables/test-iptables-server.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e">// 测试命令： nc localhost 1234
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;netinet/in.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;arpa/inet.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;errno.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#include</span><span style="color:#75715e">&lt;linux/netfilter_ipv4.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define BUFFER_SIZE 1024
</span><span style="color:#75715e">#define BACKLOG 5
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[])
{
    <span style="color:#66d9ef">int</span> sfd <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">int</span> cfd <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">int</span> n <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">int</span> port <span style="color:#f92672">=</span> <span style="color:#ae81ff">1234</span>;
    <span style="color:#66d9ef">struct</span> sockaddr_in server_addr;
    sfd <span style="color:#f92672">=</span> socket(AF_INET, SOCK_STREAM, <span style="color:#ae81ff">0</span>);
    <span style="color:#66d9ef">if</span> (sfd <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        perror(<span style="color:#e6db74">&#34;socket error&#34;</span>);
        exit(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>);
    }
    memset(<span style="color:#f92672">&amp;</span>server_addr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(server_addr));
    server_addr.sin_family <span style="color:#f92672">=</span> AF_INET;
    server_addr.sin_addr.s_addr <span style="color:#f92672">=</span> htonl(INADDR_ANY);
    server_addr.sin_port <span style="color:#f92672">=</span> htons(port);
    n <span style="color:#f92672">=</span> bind(sfd, (<span style="color:#66d9ef">struct</span> sockaddr <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>server_addr, <span style="color:#66d9ef">sizeof</span>(server_addr));
    <span style="color:#66d9ef">if</span> (n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        perror(<span style="color:#e6db74">&#34;bind error&#34;</span>);
        exit(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>);
    }
    n <span style="color:#f92672">=</span> listen(sfd, BACKLOG);
    <span style="color:#66d9ef">if</span> (n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        perror(<span style="color:#e6db74">&#34;listen error&#34;</span>);
        exit(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>);
    }
    <span style="color:#66d9ef">struct</span> sockaddr_in source_addr;
    memset(<span style="color:#f92672">&amp;</span>source_addr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(source_addr));
    socklen_t client_addr_len;
    client_addr_len <span style="color:#f92672">=</span> <span style="color:#66d9ef">sizeof</span>(source_addr);

    <span style="color:#66d9ef">struct</span> sockaddr_in dest_addr;
    memset(<span style="color:#f92672">&amp;</span>dest_addr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(dest_addr));
    socklen_t dest_addr_len;
    dest_addr_len <span style="color:#f92672">=</span> <span style="color:#66d9ef">sizeof</span>(dest_addr);

    <span style="color:#66d9ef">struct</span> sockaddr_in original_dest_addr;
    memset(<span style="color:#f92672">&amp;</span>original_dest_addr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(original_dest_addr));
    socklen_t original_dest_addr_len;
    original_dest_addr_len <span style="color:#f92672">=</span> <span style="color:#66d9ef">sizeof</span>(original_dest_addr);

    <span style="color:#66d9ef">while</span> (<span style="color:#ae81ff">1</span>)
    {
        cfd <span style="color:#f92672">=</span> accept(sfd, (<span style="color:#66d9ef">struct</span> sockaddr <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>source_addr, <span style="color:#f92672">&amp;</span>client_addr_len);
        <span style="color:#66d9ef">if</span> (cfd <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        {
            perror(<span style="color:#e6db74">&#34;accept error!&#34;</span>);
            exit(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>);
        }

        <span style="color:#75715e">// 获取到的是接收到的数据包中的 dest ip 和 port
</span><span style="color:#75715e"></span>        n <span style="color:#f92672">=</span> getsockname(cfd, (<span style="color:#66d9ef">struct</span> sockaddr <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>dest_addr, <span style="color:#f92672">&amp;</span>dest_addr_len);
        <span style="color:#66d9ef">if</span> (n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        {
            perror(<span style="color:#e6db74">&#34;getsockname error&#34;</span>);
            exit(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>);
        }

        <span style="color:#75715e">// 获取到的是 nat 之前的原始的 dest ip 和 port
</span><span style="color:#75715e"></span>        n <span style="color:#f92672">=</span> getsockopt(cfd, SOL_IP, SO_ORIGINAL_DST, <span style="color:#f92672">&amp;</span>original_dest_addr, <span style="color:#f92672">&amp;</span>original_dest_addr_len);
        <span style="color:#75715e">// 将信息发送给客户端
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">char</span> send_buff[BUFFER_SIZE];

        <span style="color:#75715e">// 不能一次性调用 sprintf ，原因是 inet_ntoa 共享一个 cache。
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// https://stackoverflow.com/questions/48799606/inet-ntoa-gives-the-same-result-when-called-with-two-different-addresses
</span><span style="color:#75715e"></span>        memset(send_buff, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(send_buff));
        sprintf(send_buff, <span style="color:#e6db74">&#34;source{ip: %s, port: %d}; &#34;</span>,
                inet_ntoa(source_addr.sin_addr), ntohs(source_addr.sin_port));
        send(cfd, send_buff, strlen(send_buff), <span style="color:#ae81ff">0</span>);
        memset(send_buff, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(send_buff));
        sprintf(send_buff, <span style="color:#e6db74">&#34;dest{ip: %s, port: %d}; &#34;</span>,
                inet_ntoa(dest_addr.sin_addr), ntohs(dest_addr.sin_port));
        send(cfd, send_buff, strlen(send_buff), <span style="color:#ae81ff">0</span>);
        memset(send_buff, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(send_buff));
        sprintf(send_buff, <span style="color:#e6db74">&#34;original dest{%s: %s, %s: %d}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>,
                n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;strerror&#34;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;ip&#34;</span>,
                n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">?</span> strerror(errno) <span style="color:#f92672">:</span> inet_ntoa(original_dest_addr.sin_addr), <span style="color:#75715e">// 如果上一步报错返回错误信息
</span><span style="color:#75715e"></span>                n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;errno&#34;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;port&#34;</span>,
                n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">?</span> errno : ntohs(original_dest_addr.sin_port));
        send(cfd, send_buff, strlen(send_buff), <span style="color:#ae81ff">0</span>);
        <span style="color:#75715e">// 关闭 TCP 连接
</span><span style="color:#75715e"></span>        close(cfd);
    }
}
</code></pre></div>
<p>通过 <code>gcc ./src/c/03-iptables/test-iptables-server.c &amp;&amp; sudo ./a.out</code> 命令编译运行。</p>

<p>通过 nc 命令访问该 server，<code>nc localhost 1234</code>，可以看到该测试程序的返回打印出来：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 127.0.0.1, port: 55958}; dest{ip: 127.0.0.1, port: 1234}; original dest{strerror: Protocol not available, errno: 92}</pre></div>
<p>此时是正常访问，可以看出：</p>

<ul>
<li>源 IP Port 为 <code>127.0.0.1:55958</code>。</li>
<li>目的 IP Port 为 <code>127.0.0.1:1234</code>。</li>
<li>由于没有进行 NAT 所以无法获取原始目标 IP Port，所以返回 <code>Protocol not available</code> 错误信息。</li>
</ul>

<h2 id="iptables-使用场景">iptables 使用场景</h2>

<ul>
<li>写一个简单 socket 程序用来测试。</li>
<li>重点画出，ip/tcp 包的内容变化。</li>
</ul>

<h3 id="查看规则">查看规则</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1493">iptables详解（2）：iptables实际操作之规则查询</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables --line-numbers -t filter -nvL INPUT</code></pre></div>
<ul>
<li><code>-t filter</code> 查看 <code>filter</code> 表（不填写时默认为 <code>filter</code>）。表的概念参见下文：<a href="#表">iptables 概念 - 表</a>。</li>
<li><code>--line-numbers</code> 展示规则在该链中的序号，可以简写为 <code>--line</code>。</li>
<li><code>-n</code> 不对 IP 地址进行名称反解，以提高性能。</li>
<li><code>-v</code> 展示更多详细信息。</li>
<li><code>-L</code> 列出规则。</li>
<li><code>INPUT</code> 展示某个链的规则列表（不填写展示全部的链）。</li>
</ul>

<p>输出如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Chain INPUT (policy ACCEPT 1352 packets, 252K bytes)
num   pkts bytes target     prot opt in     out     source               destination</pre></div>
<ul>
<li>第一行为下面表格的标题，展示当前列表是哪个链上的规则，括号里面的内容为该链的默认规则的流量细信息。

<ul>
<li><code>packets</code> 表示当前链（上例为 INPUT 链）默认策略匹配到的包的数量。</li>
<li><code>bytes</code> 表示当前链默认策略匹配到的所有包的大小总和（通过 <code>-x</code> 可以展示精确数字）。</li>
</ul></li>
<li>第二行为表格的表头，第三行开始为表格的内容。该表格的包含如下列：

<ul>
<li><code>num</code> 规则序号，从 1 开始，序号越小优先级越高。</li>
<li><code>pkts</code> 对应规则匹配到的报文的个数。</li>
<li><code>bytes</code> 对应匹配到的报文包的大小总和（通过 <code>-x</code> 可以展示精确数字）。</li>
<li><code>target</code> 规则对应的target，往往表示规则对应的动作，即规则匹配成功后需要采取的措施。</li>
<li><code>prot</code> 表示规则对应的协议，是否只针对某些协议应用此规则。</li>
<li><code>opt</code> 表示规则对应的选项。</li>
<li><code>in</code> 表示数据包由哪个接口(网卡)流入，即从哪个网卡来。</li>
<li><code>out</code> 表示数据包将由哪个接口(网卡)流出，即到哪个网卡去。</li>
<li><code>source</code> 表示规则对应的源头地址，可以是一个IP，也可以是一个网段。</li>
<li><code>destination</code> 表示规则对应的目标地址。可以是一个IP，也可以是一个网段。</li>
</ul></li>
</ul>

<h3 id="主机防火强">主机防火强</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1517">iptables详解（3）：iptables规则管理</a></p>
</blockquote>

<h4 id="描述">描述</h4>

<p>iptables 最核心的功能就是防火墙，防火墙的实现方式是按照配置的规则对 IP 数据包进行过滤，如果包符合规则，则允许通过，否则不允许通过。</p>

<p>主机防火箱指的是对该主机的出入流量的包过滤能力，在 iptables 中通过 <code>filter</code> 表实现，按照数据包的方向可以分为如下两类：</p>

<ul>
<li>INPUT - 入流量数据包过滤，一般在如下场景中使用：

<ul>
<li>屏蔽指定源 IP 的数据包（封禁 DDos 攻击 IP）。</li>
<li>仅开放某些 IP 的某些特殊端口的访问（如 22 号 ssh 端口），而屏蔽其他 IP 的访问。</li>
</ul></li>
<li>OUTPUT - 出流量数据包过滤，一般在如下场景中使用：

<ul>
<li>屏蔽某些特殊目的 IP 的访问（站点）。</li>
</ul></li>
</ul>

<h4 id="示例说明">示例说明</h4>

<p>添加一条屏蔽来自 192.168.57.1 的数据包的规则的命令如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t filter -I INPUT -s <span style="color:#ae81ff">192</span>.168.57.1 -j DROP</code></pre></div>
<ul>
<li><code>-t filter</code> 将规则写入 <code>filter</code> 表，表示该规则是一个包过滤类型的规则。表的概念参见下文：<a href="#表">iptables 概念 - 表</a>。</li>
<li><code>-I INPUIT</code> 将规则应用在 <code>INPUT</code> 链中，表示是一条入流量过滤规则，默认情况下将该规则添加到规则链的最上方（即优先级最高），如果想将该规则放在某个序号的位置该参数应该写为：<code>-I INPUT 1</code>（此处的 <code>1</code> 的取值范围为 <code>1 ~ MaxNumber+1</code>）。链的概念参见下文：<a href="#链">iptables 概念 - 链</a>。</li>
<li><code>-s 192.168.57.1</code> 表示该规则的匹配条件是：匹配源 IP 为 <code>192.168.57.1</code> 的数据包（在本实验中为宿主机）。其他可用的匹配条件，参见下文：<a href="#规则">iptables 概念 - 规则</a>。</li>
<li><code>-j DROP</code> 表示该规则匹配后的执行动作是：丢弃该数据包，发送者将会一直等待到超时。其他可用的执行动作，参见下文：<a href="#规则">iptables 概念 - 规则</a>。</li>
</ul>

<p>通过 <code>sudo iptables --line -nvL INPUT</code> 命令，可以看到刚刚配置的规则：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1        0     0 DROP       all  --  *      *       192.168.57.1         0.0.0.0/0</pre></div>
<p>此时，在宿主机执行 <code>nc 192.168.57.3 1234</code>，发现长时间获得不到输出，将卡住。</p>

<p>最后，通过如下命令可以删除规则：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 方式 1：通过规则的序号删除（上一步的 num 值）</span>
sudo iptables -t filter -D INPUT <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># 方式 2：通过规则的匹配条件和动作删除（即将添加规则的 -I 更改为 -D）</span>
sudo iptables -t filter -D INPUT -s <span style="color:#ae81ff">192</span>.168.57.1 -j DROP
<span style="color:#75715e"># 方式 3：清空某张表某条链上的全部规则</span>
sudo iptables -t filter -F INPUT
<span style="color:#75715e"># 方式 3：清空某张表的全部规则</span>
sudo iptables -t filter -F</code></pre></div>
<p>此时，再在宿主机执行 <code>nc 192.168.57.3 1234</code>，将获得如下输出：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 192.168.57.1, port: 50262}; dest{ip: 192.168.57.3, port: 1234}; original dest{strerror: Protocol not available, errno: 92}</pre></div>
<h4 id="默认动作">默认动作</h4>

<blockquote>
<p>参考： <a href="https://www.zsythink.net/archives/1604">iptables详解（9）：iptables的黑白名单机制</a></p>
</blockquote>

<p>通过 <code>sudo iptables -nvL INPUT</code> 输出的 <code>policy ACCEPT</code> 部分的 <code>ACCEPT</code> 表示该链的默认动作为 <code>ACCEPT</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination   </pre></div>
<p>默认动作的执行表示在没有匹配项的规则时执行的动作，因此，通过默认动作可以配置某一个链是白名单还是黑名单：</p>

<ul>
<li><code>policy ACCEPT</code> 默认放行，即链规则为黑名单制。</li>
<li><code>policy DROP</code> 默认丢弃，即链规则为白名单制。</li>
</ul>

<p>可以通过如下命令，修改某个链的默认动作：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t filter -P INPUT DROP</code></pre></div>
<h3 id="网络防火箱">网络防火箱</h3>

<blockquote>
<p>参考： <a href="https://www.zsythink.net/archives/1663">iptables详解（11）：iptables之网络防火墙</a></p>
</blockquote>

<p>和主机防火墙不同，网络防火墙指的是位于一个网络（多台主机）的入口位置（网关/路由器），对包转发进行包过滤的能力。在 iptables 中可以通过 filter 表的 <code>FORWARD</code> 链实现。</p>

<p>此外，网络防火强本事就是一个网关，因此需要开启 Linux 内核的 Forward 特性（<code>sysctl -w net.ipv4.ip_forward=1</code>），开启该特性后，该主机才会像路由器一样进行包转发。</p>

<p>一个配置示例如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 开启内核的 ip forward 特性</span>
cat /proc/sys/net/ipv4/ip_forward
sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>

<span style="color:#75715e"># 只允许网络内主机，访问网络外主机的 80 与 22 端口。</span>
iptables -A FORWARD -j REJECT
iptables -I FORWARD -s 网段 -p tcp --dport <span style="color:#ae81ff">80</span> -j ACCEPT
iptables -I FORWARD -d 网段 -p tcp --sport <span style="color:#ae81ff">80</span> -j ACCEPT
iptables -I FORWARD -s 网段 -p tcp --dport <span style="color:#ae81ff">22</span> -j ACCEPT
iptables -I FORWARD -d 网段 -p tcp --sport <span style="color:#ae81ff">22</span> -j ACCEPT</code></pre></div>
<h3 id="转发到本地某端口-redirect">转发到本地某端口（REDIRECT）</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1764">iptables详解（13）：iptables动作总结之二</a></p>
</blockquote>

<p>将端口转发到另一个端口，比如将 12345 端口转发到本机的 1234 端口。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -I PREROUTING -p tcp --dport <span style="color:#ae81ff">12345</span> -j REDIRECT --to-ports <span style="color:#ae81ff">1234</span>
sudo iptables -t nat -I OUTPUT -p tcp -o lo --dport <span style="color:#ae81ff">12345</span> -j REDIRECT --to-ports <span style="color:#ae81ff">1234</span></code></pre></div>
<ul>
<li>第一行实现的是目标端口为 12345 的 外部 TCP 入流量将转发到本地的 1234 端口。</li>
<li>第二行实现的是目标端口为 12345 的 loopback TCP 入流量将转发到本地的 1234 端口（参考：<a href="https://serverfault.com/questions/211536/iptables-port-redirect-not-working-for-localhost">iptables port redirect not working for localhost</a>）。</li>
</ul>

<p>在虚拟机上执行完成上述命令后：</p>

<ul>
<li><p>在虚拟机上执行 <code>nc localhost 12345</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 127.0.0.1, port: 46246}; dest{ip: 127.0.0.1, port: 1234}; original dest{ip: 127.0.0.1, port: 12345}</pre></div></li>

<li><p>在宿主机上执行 <code>nc 192.168.57.3 12345</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 192.168.57.1, port: 60332}; dest{ip: 192.168.57.3, port: 1234}; original dest{ip: 192.168.57.3, port: 12345}</pre></div></li>
</ul>

<p>可以看出，客户端向 12345 端口发送数据时，服务端看到的 dest port 是经过转发的 1234。通过 <code>getsockopt</code> 可以从内核中获取到原始的 dest port 是 12345。</p>

<p>以宿主机 192.168.57.1 向虚拟机 192.168.57.3:12345 发送请求为例，流量过程如下所示：</p>

<ul>
<li>请求

<ul>
<li>宿主机构造 TCP 数据包：<code>source{ip: 192.168.57.1, port: 60332}; dest{ip: 192.168.57.3, port: 12345}</code>。</li>
<li>虚拟机内核 iptables PREROUTING 链：

<ul>
<li>修改数据包为：<code>source{ip: 192.168.57.1, port: 60332}; dest{ip: 192.168.57.3, port: 1234}</code>。</li>
<li>更新 NAT 连接表（个人推测）：

<ul>
<li>key: <code>source{ip: 192.168.57.1, port: 60332}</code></li>
<li>value:

<ul>
<li><code>dest{ip: 192.168.57.3, port: 1234}</code></li>
<li><code>original dest{ip: 192.168.57.3, port: 12345}</code></li>
</ul></li>
</ul></li>
</ul></li>
<li>用户测试程序：

<ul>
<li><code>accept</code> 系统调用获取到： <code>source{ip: 192.168.57.1, port: 60332}</code>。</li>
<li><code>getsockname</code> 系统调用获取到： <code>dest{ip: 192.168.57.3, port: 1234}</code>。</li>
<li><code>getsockopt</code> 获取到 iptables 记录的原始目标地址：<code>original dest{ip: 192.168.57.3, port: 12345}</code>。</li>
</ul></li>
</ul></li>
<li>响应

<ul>
<li>用户测试程序，构造响应 TCP 数据包：<code>source{ip: 192.168.57.3, port: 1234}; dest{ip: 192.168.57.1, port: 60332}</code>。</li>
<li>虚拟机内核 iptables 处理（个人推测）：

<ul>
<li>根据 <code>dest{ip: 192.168.57.1, port: 60332}</code> 查找 NAT 连接表，获取原始目标地址。</li>
<li>修改数据包为：<code>source{ip: 192.168.57.3, port: 12345}; dest{ip: 192.168.57.1, port: 60332}</code>。</li>
</ul></li>
<li>宿主机接收到响应并打印输出。</li>
</ul></li>
</ul>

<p>上述流程如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          sip: 192.168.57.1, sport: 60332                          sip: 192.168.57.1, sport: 60332
                            +------------+                                           +------------+
          dip: 192.168.57.3,|dport: 12345|                         dip: 192.168.57.3,|dport: 1234 |
                            +------------+                                           +------------+
        ---------------------------------------&gt; NAT (REDIRECT)  ---------------------------------------&gt;
Client                                                                                                      Server
        &lt;--------------------------------------- NAT (REDIRECT)  &lt;---------------------------------------
                            +------------+                                           +------------+
          sip: 192.168.57.3,|sport: 12345|                         sip: 192.168.57.3,|sport: 1234 |
                            +------------+                                           +------------+
          dip: 192.168.57.1, dport: 60332                          dip: 192.168.57.1, dport: 60332</pre></div>
<p>从流量图可以看出，在一次请求/响应过程中（DNAT 也类似，后文将不再赘述）：
* 虽然 REDIRECT 只是在 <code>PREROUTING</code> 配置的动作，从而导致，在请求过程中修改了数据包的 dest port。
* 但是 iptables 会自动的在响应过程中将 source port 复原回来。</p>

<p>从上述说明可以看出 REDIRECT 和 自己在应用层实现一个端口转发服务效果看起来是类似的，但是 REDIRECT 有应用层端口无法提供的如下优点：</p>

<ul>
<li>性能更高：由于 iptables 在内核层实现，性能更高，整体上只需经过一次协议栈。而应用层实现服务需要至少经过两次协议栈。</li>
<li>对应用透明：由于 iptables 的 REDIRECT 是在协议栈层面实现的，因此对应用来说，感知到的源地址就是真实的源地址；而应用层实现感知到的源地址是端口转发服务的源地址，导致源地址信息丢失。</li>
</ul>

<p>最后，执行如下命令，删除规则，恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -D PREROUTING -p tcp --dport <span style="color:#ae81ff">12345</span> -j REDIRECT --to-ports <span style="color:#ae81ff">1234</span>
sudo iptables -t nat -D OUTPUT -p tcp -o lo --dport <span style="color:#ae81ff">12345</span> -j REDIRECT --to-ports <span style="color:#ae81ff">1234</span></code></pre></div>
<h3 id="出入流量劫持-redirect">出入流量劫持（REDIRECT）</h3>

<blockquote>
<p>参考：<a href="https://www.v2ray.com/chapter_02/protocols/dokodemo.html">v2ray - Dokodemo-door</a>。
* 对所有的出入站流量进行拦截，转到到本地的 proxy 中，从而实现 service mesh。参考： <a href="http://rui0.cn/archives/1619">Service Mesh中的 iptables 流量劫持</a> 、 <a href="https://jimmysong.io/blog/sidecar-injection-iptables-and-traffic-routing/#iptables-%E6%B5%81%E9%87%8F%E5%8A%AB%E6%8C%81%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3">Istio 中的 Sidecar 注入、透明流量劫持及流量路由过程详解</a> 、 <a href="https://www.ichenfu.com/2019/04/09/istio-inbond-interception-and-linux-transparent-proxy/">Istio的流量劫持和Linux下透明代理实现</a></p>
</blockquote>

<p>利用 iptables 的 REDIRECT 可以实现对符合某些规则的出入站流量进行拦截。因此可以实现：</p>

<ul>
<li>将所有出流量进行拦截，转发到本地的一个代理入口端口，该代理入口会解析目标 IP Port，将流量通过隧道从代理服务器侧发出，从而实现透明代理。参考：<a href="https://www.v2ray.com/chapter_02/protocols/dokodemo.html">v2ray - Dokodemo-door</a>。</li>
<li>对所有的出入站流量进行拦截，转到到本地的 proxy 中，从而实现 service mesh。参考： <a href="http://rui0.cn/archives/1619">Service Mesh中的 iptables 流量劫持</a> 、 <a href="https://jimmysong.io/blog/sidecar-injection-iptables-and-traffic-routing/#iptables-%E6%B5%81%E9%87%8F%E5%8A%AB%E6%8C%81%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3">Istio 中的 Sidecar 注入、透明流量劫持及流量路由过程详解</a> 、 <a href="https://www.ichenfu.com/2019/04/09/istio-inbond-interception-and-linux-transparent-proxy/">Istio的流量劫持和Linux下透明代理实现</a>。</li>
</ul>

<h3 id="转发到本地某端口-dnat">转发到本地某端口（DNAT）</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1764">iptables详解（13）：iptables动作总结之二</a></p>
</blockquote>

<p>将端口转发到另一个端口，比如将 12345 端口转发到本机的 1234 端口。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -I PREROUTING -p tcp --dport <span style="color:#ae81ff">12346</span> -j DNAT --to-destination <span style="color:#ae81ff">127</span>.0.0.1:1234
sudo sysctl -w net.ipv4.conf.enp0s9.route_localnet<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
sudo iptables -t nat -I OUTPUT -p tcp -o lo --dport <span style="color:#ae81ff">12346</span> -j DNAT --to-destination <span style="color:#ae81ff">127</span>.0.0.1:1234
<span style="color:#75715e"># sudo sysctl -w net.ipv4.ip_forward=1</span></code></pre></div>
<ul>
<li>第一行实现的是目标端口为 12346 的 外部 TCP 入流量将转发到本地的 1234 端口。</li>
<li>第二行：因为第一行会将请求到 12346 的数据包的 dest ip 修改为 127.0.0.1，而默认情况下 Linux 协议栈会丢弃所有不是从 lo 接口的接收到的目标 IP 是 127.0.0.1 的数据包。因此，此处通过 <code>net.ipv4.conf.enp0s9.route_localnet=1</code> 开启 <code>enp0s9</code> 可以接受目标 IP 是 127.0.0.1 的数据包（参考： <a href="https://unix.stackexchange.com/questions/570194/redirect-external-request-to-localhost-with-iptables">redirect external request to localhost with iptables</a>）。</li>
<li>第三行实现的是目标端口为 12346 的 loopback TCP 入流量将转发到本地的 1234 端口（参考：<a href="https://serverfault.com/questions/211536/iptables-port-redirect-not-working-for-localhost">iptables port redirect not working for localhost</a>）。</li>
<li>最后一行：如果 <code>--to-destination</code> 指向的是其他主机的 ip，则需要通过该命令开启 forward 特性。</li>
</ul>

<p>在虚拟机上执行完成上述命令后：</p>

<ul>
<li><p>在虚拟机上执行 <code>nc localhost 12346</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 127.0.0.1, port: 57626}; dest{ip: 127.0.0.1, port: 1234}; original dest{ip: 127.0.0.1, port: 12346}</pre></div></li>

<li><p>在宿主机上执行 <code>nc 192.168.57.3 12346</code> 输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">source{ip: 192.168.57.1, port: 58624}; dest{ip: 127.0.0.1, port: 1234}; original dest{ip: 192.168.57.3, port: 12346}</pre></div></li>
</ul>

<p><code>nc 192.168.57.3 12346</code> 的过程如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          sip: 192.168.57.1, sport: 60332                          sip: 192.168.57.1, sport: 60332
         +-------------------------------+                        +-------------------------------+
         |dip: 192.168.57.3, dport: 12346|                        |dip: 127.0.0.1,    dport: 1234 |
         +-------------------------------+                        +-------------------------------+
        ---------------------------------------&gt; NAT (DNAT)  ---------------------------------------&gt;
Client                                                                                                      Server
        &lt;--------------------------------------- NAT (DNAT)  &lt;---------------------------------------
         +-------------------------------+                        +-------------------------------+
         |sip: 192.168.57.3, sport: 12346|                        |sip: 127.0.0.1,    sport: 1234 |
         +-------------------------------+                        +-------------------------------+
          dip: 192.168.57.1, dport: 60332                          dip: 192.168.57.1, dport: 60332</pre></div>
<p>可以看出，通过 DNAT 可以实现和 REDIRECT 一样的效果，但是和 REDIRECT 相比：<strong>DNAT 除了修改了数据包的 port 还修改了 ip</strong>。</p>

<p>最后，执行如下命令，删除规则，恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -D PREROUTING -p tcp --dport <span style="color:#ae81ff">12346</span> -j DNAT --to-destination <span style="color:#ae81ff">127</span>.0.0.1:1234
sudo sysctl -w net.ipv4.conf.enp0s9.route_localnet<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
sudo iptables -t nat -D OUTPUT -p tcp -o lo --dport <span style="color:#ae81ff">12346</span> -j DNAT --to-destination <span style="color:#ae81ff">127</span>.0.0.1:1234</code></pre></div>
<p>注意：转发到本地某端口仅仅为了展示 DNAT 对数据包的修改情况，如果真的需要转发到本地某端口，应该直接使用：<a href="#转发到本地某端口redirect">转发到本地某端口（REDIRECT）</a>（DNAT 需要配置网卡 <code>route_localnet</code> 很不优雅）</p>

<h3 id="源网络地址转换-snat-masquerade">源网络地址转换（SNAT/MASQUERADE）</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1764">iptables详解（13）：iptables动作总结之二</a></p>
</blockquote>

<p>在从上文（<a href="#准备">准备</a>） 可以看到，实验用的虚拟机有三张网卡：</p>

<ul>
<li>enp0s3 网卡1 - 选择 NAT，IP 地址为 <code>10.0.2.15/24</code>（不固定），用于访问公网。</li>
<li>enp0s8 网卡2 - 选择 仅主机网络，IP 地址为 <code>192.168.56.3/24</code>，用于 SSH 连接</li>
<li>enp0s9 网卡3 - 选择 仅主机网络，IP 地址为 <code>192.168.57.3/24</code>，用于测试 iptables 规则。</li>
</ul>

<p>为了方便演示，现在通过如下命令为 <code>enp0s9</code> 网卡添加一个 E 类地址（保留为研究测试使用的 IP 地址） <code>240.0.0.3/24</code>（注意不直接使用 <code>192.168.57.3/24</code> 的原因是，VirtualBox 虚拟机的网关会自动对将私有网络做一次 SNAT，这样就没法实现下述的效果了）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip addr add <span style="color:#ae81ff">240</span>.0.0.3/24 dev enp0s9</code></pre></div>
<p>因此在虚拟机中，访问公网地址时，默认会通过 <code>enp0s3</code> 出去，源地址会被设置为 <code>10.0.2.15</code>（通过 <code>sudo tcpdump -e -n -i enp0s3</code> 观察）。比如执行 <code>curl qq.com</code> 将正常返回 html 文本。</p>

<p>此时手动指定源 IP 为 <code>240.0.0.3</code>， 比如执行 <code>curl --dns-interface enp0s3 --interface 240.0.0.3 qq.com</code> 将永远得不到返回。</p>

<p>此时，通过 iptables 的 SNAT 或者 MASQUERADE 动作可以实现，源 IP 为 <code>240.0.0.3</code>也可以访问公网。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -I POSTROUTING -p tcp -s <span style="color:#ae81ff">240</span>.0.0.3/24 ! -d <span style="color:#ae81ff">240</span>.0.0.3/24 -j SNAT --to-source <span style="color:#ae81ff">10</span>.0.2.15
<span style="color:#75715e"># sudo  iptables -t nat -I POSTROUTING -p tcp -s 240.0.0.3/24 ! -d 240.0.0.3/24 -o enp0s3 -j MASQUERADE</span>
<span style="color:#75715e"># sudo sysctl -w net.ipv4.ip_forward=1</span></code></pre></div>
<ul>
<li>第一行为：满足 IP 是 <code>240.0.0.3/24</code> 目标 IP 不是 <code>240.0.0.3/24</code> 的 TCP 数据包，将修改其源 IP 为 <code>10.0.2.15</code>。</li>
<li>第二行为：本例的另一种写法，满足 IP 是 <code>240.0.0.3/24</code> 目标 IP 不是 <code>240.0.0.3/24</code> 的 TCP 数据包，将修改其源 IP 为 enp0s3 绑定的 IP 地址（即 <code>10.0.2.15</code>）。</li>
<li>第三行为：本例中不需要，因为数据包来自本机。</li>
</ul>

<p>此时再执行 <code>curl --dns-interface enp0s3 --interface enp0s9 qq.com</code>，将正常返回 html 文本。</p>

<p>SNAT 整个流程（通过 <code>sudo tcpdump -e -n -i enp0s3</code> 可以观察到修改后的数据包）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">         +-------------------------------+                       +-------------------------------+
         |sip: 240.0.0.3,    sport: 54321|                       |sip: 10.0.2.15,    sport: 49254|
         +-------------------------------+                       +-------------------------------+
          dip: qq.com addr,  dport: 80                            dip: qq.com addr,  dport: 80 
        ---------------------------------------&gt; NAT (SNAT)  ---------------------------------------&gt;
Client                                                                                                      Server
        &lt;--------------------------------------- NAT (SNAT)  &lt;---------------------------------------
          sip: qq.com addr,  sport: 80                             sip: qq.com addr,  sport: 80 
         +-------------------------------+                        +-------------------------------+
         |dip: 240.0.0.3,    dport: 54321|                        |dip: 10.0.2.15,    dport: 49254|
         +-------------------------------+                        +-------------------------------+</pre></div>
<p>最后，执行如下命令恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo ip addr delete 240.0.0.3/24 dev enp0s9
sudo iptables -t nat -D POSTROUTING -p tcp -s 240.0.0.3/24 ! -d 240.0.0.3/24 -j SNAT --to-source 10.0.2.15
# sudo  iptables -t nat -D POSTROUTING -p tcp -s 240.0.0.3/24 ! -d 240.0.0.3/24 -o enp0s3 -j MASQUERADE
# sudo sysctl -w net.ipv4.ip_forward=0</pre></div>
<h3 id="转发到内网ip某端口-dnat">转发到内网IP某端口（DNAT）</h3>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1764">iptables详解（13）：iptables动作总结之二</a></p>
</blockquote>

<p>以该 <a href="https://www.zsythink.net/archives/1764#wznav_3">博客模型</a> 为例：</p>

<p>如果希望：主机 A 可以访问 主机 C 的 8080 端口，此时则需要在 主机 B 上执行如下命令：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
sudo iptables -t nat -I PREROUTING -p tcp --dport <span style="color:#ae81ff">80</span> -j DNAT --to-destination <span style="color:#ae81ff">10</span>.1.0.1:8080</code></pre></div>
<p>此时流量过程为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          sip: 192.168.1.147,sport: 54321                         sip: 192.168.1.147,sport: 54321
         +-------------------------------+                       +-------------------------------+
         |dip: 192.168.1.146,  dport: 80 |                       |dip: 10.1.0.1,     dport: 8080 |
         +-------------------------------+                       +-------------------------------+
        ---------------------------------------&gt; NAT (DNAT)  ---------------------------------------&gt;
A                                                     B                                                 C
        &lt;--------------------------------------- NAT (DNAT)  &lt;---------------------------------------
         +-------------------------------+                        +-------------------------------+
         |sip: 192.168.1.146,  sport: 80 |                        |sip: 10.1.0.1,     sport: 8080 |
         +-------------------------------+                        +-------------------------------+
          dip: 192.168.1.147,dport: 54321                          dip: 192.168.1.147,dport: 54321</pre></div>
<p><a href="https://www.zsythink.net/archives/1764#wznav_5">博客 DNAT 章节</a> 的说法不太正确，在此处只需要配置 DNAT 即可，不需要配置 SNAT（前提是 <code>10.1.0.1</code> 主机，有前往 <code>192.168.1.147</code> 的路由）。</p>

<p>最后，执行如下命令恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo sysctl -w net.ipv4.ip_forward=0
sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.1.0.1:8080</pre></div>
<p>测试是否可以实现公网 ip 的代理。</p>

<p>Docker 端口暴露原理：<a href="https://yeasy.gitbook.io/docker_practice/advanced_network/port_mapping">https://yeasy.gitbook.io/docker_practice/advanced_network/port_mapping</a></p>

<p>写个简单 tcp 测试程序，观察 source &amp; dest ip port。</p>

<h3 id="转发到公网ip某端口-dnat-snat">转发到公网IP某端口（DNAT&amp;SNAT）</h3>

<p>这里再描述一个复杂一点的例子，以我们实验的这个台虚拟机为例，从网络角度看：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">公网 &lt;---&gt; 10.0.2.15/24 (拥有公网出口的内网) &lt;---&gt; 192.168.57.3/24 (内网)
                     |                                |
                      --------------------------------
                                    |
                                    v
                                测试虚拟机</pre></div>
<p>此时如果想，192.168.57.<sup>3</sup>&frasl;<sub>24</sub> 网络上的机器，可以通过 <code>192.168.57.3:80</code> 访问 <code>qq.com:12347</code> （IP 从 <code>nslookup qq.com</code> 选取一个，本例中为 <code>183.3.226.35</code>）端口。</p>

<p>这次先分析下如何修改数据包才能实现该效果，假设我们只配置一个 DNAT：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">          sip: 192.168.57.1,sport: 54321                          sip: 192.168.1.147,sport: 54321 
         +-------------------------------+                       +-------------------------------+
         |dip: 192.168.57.3, dport:12347 |                       |dip: 183.3.226.35, dport: 80   |
         +-------------------------------+                       +-------------------------------+
        ---------------------------------------&gt; NAT (DNAT)  ---------------------------------------&gt;
宿主机                                               虚拟机                                                 qq.com
        &lt;--------------------------------------- NAT (DNAT)  &lt;---------------------------------------
                                                                  +-------------------------------+
                                                                  |sip: 183.3.226.35,   sport: 80 |
                                                                  +-------------------------------+
                                                                   dip:192.168.1.147, sport: 54321   # 回复消息的 dip 是个内网 IP 不可能，路由到我们的虚拟机中。</pre></div>
<p>因此，我们还需要配置一个 SNAT，最总整体包修改流程如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                                                                 +-------------------------------+                    +-------------------------------+
          sip: 192.168.57.1,sport: 54321                         |sip: 192.168.57.1, sport: 54321|                    |sip: 10.0.2.15,    sport: 43210|
         +-------------------------------+                       +-------------------------------+                    +-------------------------------+
         |dip: 192.168.57.3, dport:12347 |                       |dip: 183.3.226.35, dport: 80   |                     dip: 183.3.226.35, dport: 80
         +-------------------------------+                       +-------------------------------+                    
        ---------------------------------------&gt; NAT (DNAT)  ---------------------------------------&gt;  NAT (SNAT)  ---------------------------------------&gt;
宿主机                                               虚拟机                                                虚拟机                                                宿主机 &amp; 路由器 多级 SNAT  &lt;---&gt;   qq.com
        &lt;--------------------------------------- NAT (DNAT)  &lt;---------------------------------------  NAT (SNAT)  &lt;---------------------------------------
         +-------------------------------+                       +-------------------------------+
         |sip: 192.168.57.3,sport: 12347 |                       |sip: 183.3.226.35,   sport: 80 |                     sip: 183.3.226.35, sport: 80
         +-------------------------------+                       +-------------------------------+                    +-------------------------------+
          dip: 192.168.57.1, dport: 54321                        |dip: 192.168.57.1, dport: 54321|                    |dip: 10.0.2.15,    dport: 43210|
                                                                 +-------------------------------+</pre></div>
<p>命令如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo iptables -t nat -I PREROUTING -p tcp --dport 12347 -j DNAT --to-destination 183.3.226.35:80
sudo iptables -t nat -I POSTROUTING -p tcp -s 192.168.57.0/24 -d 183.3.226.35 -j SNAT --to-source 10.0.2.15
sudo sysctl -w net.ipv4.ip_forward=1</pre></div>
<p>最终访问在宿主机执行 <code>curl 192.168.57.3:12347 -H  'Host: qq.com'</code> 将正常输出 html 文本。（注意，本例仅用于理解 SNAT 和 DNAT，实际上这种做法，在虚拟机内部是无法访问通的）</p>

<p>最后，执行如下命令恢复现场：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -t nat -D PREROUTING -p tcp --dport <span style="color:#ae81ff">12347</span> -j DNAT --to-destination <span style="color:#ae81ff">183</span>.3.226.35:80
sudo iptables -t nat -D POSTROUTING -p tcp -s <span style="color:#ae81ff">192</span>.168.57.0/24 -d <span style="color:#ae81ff">183</span>.3.226.35 -j SNAT --to-source <span style="color:#ae81ff">10</span>.0.2.15
sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span></code></pre></div>
<h3 id="访问日志">访问日志</h3>

<p>参考：<a href="https://www.zsythink.net/archives/1684">iptables详解（12）：iptables动作总结之一</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables -I INPUT -p TCP --dport <span style="color:#ae81ff">22</span> -j LOG</code></pre></div>
<p>如上命令将会将 ssh 连接的数据包日志，记录到 /var/log/messages 文件中。（可通过 <code>etc/syslog.conf</code> 配置）。</p>

<p>注意和其他动作不同，LOG 行为不会终止后续规则的执行，也不会对数据包做任何修改。</p>

<h3 id="总结">总结</h3>

<p>本部分主要介绍了，包过滤、 NAT 和数据包日志三个 iptables 的特性。</p>

<p>为了更好的理解 NAT，需要定义连接的概念。</p>

<p>以 TCP/UDP 为例，连接表示两方 A、B 的相互通讯，一个连接由四元组标识：<code>ipA, portA, ipB, portB</code>。</p>

<p>每个连接有两种类型的数据包，分别为：</p>

<ul>
<li>A -&gt; B: sip = ipA, sport = portA, dip = ipB, dport = portB</li>
<li>B -&gt; A: sip = ipB, sport = portB, dip = ipA, dport = portA</li>
</ul>

<p>假设 A 发送第一个数据包，则称 A 为客户端。B 为服务端。</p>

<p>有了如上的定义，我们可以这么理解 SNAT 和 DNAT：</p>

<ul>
<li>SNAT 用于按照配置修改，客户端到服务端的数据包的 sip 和 sport。而接收到服务端到客户端的数据包时，自动的修正 dip 和 dport。</li>
<li>DNAT 用于按照配置修改，客户端到服务端的数据包的 dip 和 dport。而接收到服务端到客户端的数据包时，自动的修正 sip 和 sport。</li>
</ul>

<p>因此，可以看出 NAT 是面向连接的，在上面的例子中：</p>

<ul>
<li>SNAT 将 A &lt;-&gt; B 的通讯修改为：

<ul>
<li>在 A 看来：A &lt;-&gt; B</li>
<li>在 B 看来：A&rsquo; &lt;-&gt; B</li>
</ul></li>
<li>DNAT 将 A &lt;-&gt; B 的通讯修改为：

<ul>
<li>在 A 看来：A &lt;-&gt; B&rsquo;</li>
<li>在 B 看来：A &lt;-&gt; B</li>
</ul></li>
</ul>

<h2 id="iptables-原理">iptables 原理</h2>

<h3 id="netfilter-框架">netfilter 框架</h3>

<p>Linux 在内核层面实现 TCP/IP 协议栈，因此应用开发者只需要感知 Socket 网络编程模型即可实现常规的通过网络提供服务的程序。</p>

<p>但是，在有些场景，管理员需要，对 TCP/IP 数据包层面进行过滤修改，如上文提到的防火墙、NAT 等，在应用层是无法实现的。而需要在 TCP/IP 协议栈的流程中注入一些特殊的逻辑，才能实现。</p>

<p>因此 Linux 提供了 netfilter 框架，该框架定义了一套编程接口，允许实现该接口的程序（下文称为 Netfilter 程序）在 TCP/IP 协议栈的流程中注入自定义的逻辑（Hook）。</p>

<p>而 iptables 就是一套基于 netfilter 框架实现的程序，实现了管理员常用的防火墙和 NAT 等能力。（除了 iptables 之外，还有 <a href="http://www.linuxvirtualserver.org/">LVS</a> 负载均衡器等）</p>

<p>和其他领域一样，通用的标准/接口是为了服务与某些特定现实需求的，因此 netfilter 的诞生实际上就是为起初的 iptables 提供服务的。iptables 和 netfilter 是用一个项目组下的项目。</p>

<p>更多参见： <a href="https://www.netfilter.org/">netfilter.org</a>。</p>

<h3 id="iptables-架构">iptables 架构</h3>

<p>iptables 的由两个部分组成：</p>

<ul>
<li>用户态空间提供的 iptables 命令行程序。</li>
<li>注册在内核中 netfilter 钩子上的内核模块。</li>
</ul>

<p>可以看出 iptables 功能是在内核态实现的（原生的）。因此其性能优于逻辑实现在用户态的相关网络应用。</p>

<p>iptables 命令行工具有两种方式可以和内核通讯：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man8/xtables-legacy.8.html">xtables-legacy</a> 通过 <code>getsockopt/setsockopt</code> 系统调用和内核模块通讯。</li>
<li><a href="https://manpages.debian.org/testing/iptables/xtables-nft-multi.8.en.html">xtables-nft-multi</a> （又称 <a href="https://man7.org/linux/man-pages/man8/xtables-nft.8.html">xtables-nft</a>） 通过 <code>nftables</code> 内核 api 和内核模块通讯，并集成了 arptables、 ebtables 等一些了工具。</li>
</ul>

<h3 id="iptables-概念">iptables 概念</h3>

<blockquote>
<p>参考： <a href="https://www.zsythink.net/archives/1199">iptables概念</a></p>
</blockquote>

<p>netfilter 的是内核提供的通用编程接口，iptables 是基于通用编程接口实现的具有特定功能的工具。</p>

<p>iptables 工具对其要实现的功能进行了抽象，产生了如下一些概念。如果理解了这些概念，可以更好的使用 iptables。</p>

<h4 id="链">链</h4>

<p>上文提到了，netfilter 提供的是在 TCP/IP 协议栈的流程中注入自定义的逻辑的能力。netfilter 在整个 TCP/IP 协议栈的流程中，提供了多个注入点（Hook），在 iptables 中，这些注入点称为链（Chain）。iptables 提供了 5 种链，分别是：</p>

<ul>
<li>PREROUTING （Pre Routing 路由前）</li>
<li>INPUT （输入）</li>
<li>OUTPUT （输出）</li>
<li>FORWARD （转发）</li>
<li>POSTROUTING （Post Routing）</li>
</ul>

<p>数据包经过这些注入点的流程如下：</p>

<p><img src="/image/iptables-chain.svg" alt="iptables chain" /></p>

<p><strong>注意：</strong> localhost （127.0.0.1 / loopback） 的数据包只会经过 IPNUT 和 OUTPUT 链，不会经过 PREROUTING、FORWARD、POSTROUTING 链。</p>

<h4 id="规则">规则</h4>

<p>有了链（注入点）概念后，iptables 定义在这个注入上，每个 ip 数据包，满足什么样的条件后，做什么事情。此处的，满足什么样的条件后，做什么事情就是一条规则。</p>

<p>因此，一条规则有如下三个核心属性：</p>

<ul>
<li>链：该规则应用在哪个链上（哪个注入点）</li>
<li>匹配条件：该规则需要匹配那些数据包，条件是什么样的，如果存在多个条件，则所有都满足才算匹配（与关系）（参见：<a href="https://www.zsythink.net/archives/1544">iptables匹配条件总结之一</a>）。

<ul>
<li><code>-s</code> 源 ip 地址</li>
<li><code>-d</code> 目标 ip 地址</li>
<li><code>-p</code> 协议</li>
<li><code>-i</code> 源网络接口</li>
<li><code>-o</code> 目标网络接口</li>
<li>扩展匹配条件（参见：<a href="https://www.zsythink.net/archives/1564">常用扩展模块</a>）

<ul>
<li><code>-m tcp --sport</code> TCP 源端口</li>
<li><code>-m tcp --dport</code> TCP 目标端口</li>
<li><code>-m tcp -m multiport --sports 22,36,80,8000:8999</code> 多个 TCP 源端口之一的</li>
<li><code>-m tcp -m multiport --dports 22,36,80,8000:8999</code> 多个 TCP 目标端口之一的</li>
<li>以上的 udp 都存在</li>
<li><code>-m tcp --tcp-flags SYN,ACK,FIN,RST,URG,PSH SYN,ACK</code> 用于匹配报文的tcp头的标志位，更多参见：<a href="https://www.zsythink.net/archives/1578">iptables详解（6）：iptables扩展匹配条件之 &lsquo;tcp-flags&rsquo;</a></li>
<li><code>-m icmp --icmp-type 8/0</code> 匹配 icmp 报文 type = 8，code = 0 的报文，更多参见：<a href="https://www.zsythink.net/archives/1588">iptables详解（7）：iptables扩展之udp扩展与icmp扩展</a>。</li>
<li><code>-m iprange --src-range 192.168.1.127-192.168.1.146 --dst-range xxx</code> iprange 扩展模块，匹配一段范围 ip。</li>
<li><code>-m string --algo bm --string &quot;xxxx&quot;</code> string扩展模块，可以指定要匹配的字符串，如果报文中包含对应的字符串，则符合匹配条件。</li>
<li><code>-m time --timestart 09:00:00 --timestop 18:00:00</code> time扩展模块，根据时间段区匹配报文，如果报文到达的时间在指定的时间范围以内，则符合匹配条件。</li>
<li><code>-m connlimit --connlimit-above 2</code> 限制每个IP地址同时链接到server端的链接数量，如果不用指定IP，其默认就是针对每个客户端IP，即对单IP的并发连接数限制。</li>
<li><code>-m limit</code> limit模块，定义报文到达速率进行限制。</li>
<li><code>-m state --state RELATED,ESTABLISHED</code> 匹配所有已经建立了连接的数据包（表示只允许主机访问外部，不允许外部访问主机），更多参见：<a href="https://www.zsythink.net/archives/1597">iptables详解（8）：iptables扩展模块之state扩展</a>。</li>
</ul></li>
</ul></li>
<li>动作（Target）：满足该规则的数据包，需要对该数据包做那些事情。

<ul>
<li>基础动作（参见：<a href="https://www.zsythink.net/archives/1684">iptables动作总结之一</a> | <a href="https://www.zsythink.net/archives/1764">iptables动作总结之二</a>）

<ul>
<li>ACCEPT，接受数据包，进入后续流程，该规则后面的规则不会继续检测。</li>
<li>REJECT（可以使用 <code>--reject-with</code> 设置原因） 发送拒绝报文，该规则后面的规则不会继续检测。</li>
<li>LOG 记录日志，记录完成后，该规则后面的规则会继续检测。</li>
<li>DROP 丢弃该数据包，不会进入后续处理流程，发送者会一直等待到超时，该规则后面的规则不会继续检测。</li>
<li>RETURN 结束在目前规则链中的过滤程序。

<ul>
<li>如果是在自定义链中 return，则会继续匹配主链中的规则</li>
<li>如果是在主链（默认链）中 return，将使用当前链的默认行为。</li>
</ul></li>
<li>REDIRECT 本地重定向，参见上文。</li>
<li>MASQUERADE 网络地址转换，参见上文。</li>
<li>SNAT 参见上文。</li>
<li>DNAT 参见上文。</li>
<li>QUEUE、MIRROR、MARK 略</li>
</ul></li>
</ul></li>
</ul>

<h4 id="表">表</h4>

<p>有了链和规则概念 iptables 就可以支撑 iptables 的功能了，但是多数场景都需要多个链上的规则共同配合才能实现。因此 iptables 按照场景定义几张表。</p>

<ul>
<li><code>filter</code> 表： 实现过滤功能，防火墙。对应内核模块为 <code>iptables_filter</code>。</li>
<li><code>nat</code> 表：network address translation，网络地址转换。对应内核模块为 <code>iptable_nat</code>。</li>
<li><code>mangle</code> 表：拆解报文，做出修改，并重新封装。对应内核模块为 <code>iptable_mangle</code>。</li>
<li><code>raw</code> 表：关闭 nat 表上启用的连接追踪机制。对应的内核模块为 <code>iptable_raw</code>。</li>
</ul>

<p>关于表：</p>

<ul>
<li>每个表都对应一些具体的场景。</li>
<li>每个表可以为指定的几条链配置规则。</li>
<li>不同的表能配置的链是不同的，也就是说某些表无法配置某些链。</li>
<li>不通的表在同一个链上做的具体处理逻辑，也是不同的，对应的规则可能也是不同。</li>
</ul>

<p>表链关系如下所示：</p>

<table>
<thead>
<tr>
<th>表 \ 链</th>
<th>PREROUTING</th>
<th>INPUT</th>
<th>FORWARD</th>
<th>OUTPUT</th>
<th>POSTROUTING</th>
</tr>
</thead>

<tbody>
<tr>
<td>raw</td>
<td>✅</td>
<td></td>
<td></td>
<td>✅</td>
<td></td>
</tr>

<tr>
<td>mangle</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>nat</td>
<td>✅</td>
<td>✅</td>
<td></td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>filter</td>
<td></td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td></td>
</tr>
</tbody>
</table>

<p>在每个链上执行规则过程的优先为（从高到低）：raw -&gt; mangle -&gt; nat -&gt; filter。</p>

<h4 id="自定义链">自定义链</h4>

<blockquote>
<p>参考：<a href="https://www.zsythink.net/archives/1625">iptables自定义链</a></p>
</blockquote>

<p>很多时候，实现某个需求时，需要在某个链中配置配置多条规则，如果直接将规则添加到指定链中，会造成管理复杂的问题。</p>

<p>因此 iptables 提供了自定义链的能力，自定义链是一组规则的集合。通过自定义链可以一次性的启用/停用/删除这些规则。</p>

<p>自定义链如果想要工作，最终要和一个默认链关联（被引用），同样一个自定义链也可以和其他自定义链关联。</p>

<p>如此一来，在默认链看来，规则被组织成了一个树形结构，如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">INPUT
    规则 1
    规则 2
    自定义链 1
        规则 a
        规则 b
        自定义链 c
            规则 i
            规则 ii
    自定义链 2
    规则 3</pre></div>
<p>上文提到的 RETURN，将会跳出该自定义链的后续匹配规则，返回上一次层的匹配规则。</p>

<p>自定义链的常见操作示例如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 新增自定义链</span>
sudo iptables -t filter -N MY_CHAIN
<span style="color:#75715e"># 给自定义链添加规则</span>
sudo iptables -t filter -I MY_CHAIN -s <span style="color:#ae81ff">192</span>.168.57.1 -j DROP
<span style="color:#75715e"># 在某个链中引用自定义链（和添加规则类似）</span>
sudo iptables -t filter -I INPUT -j MY_CHAIN
<span style="color:#75715e"># 取消某个自定义链的引用</span>
sudo iptables -t filter -D INPUT -j MY_CHAIN
<span style="color:#75715e"># 删除自定义链（保证引用数为 0 并且不包含任何规则的）</span>
sudo iptables -X MY_CHAIN
<span style="color:#75715e"># 重命名自定义链</span>
iptables -E MY_CHAIN MY_CHAIN2</code></pre></div>
<h3 id="整体流程">整体流程</h3>

<p>简易版</p>

<p><img src="/image/iptables-process.svg" alt="iptables process" /></p>

<p>官方版</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg" alt="Netfilter-packet-flow.svg" /></p>

<h2 id="iptables-命令">iptables 命令</h2>

<blockquote>
<p>参见： <a href="https://man7.org/linux/man-pages/man8/iptables.8.html">iptables(8) — Linux manual page</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">### 新增规则 ###</span>
<span style="color:#75715e"># 方式 1：将规则添加到某个链的最上方（优先级最高、编号最小）</span>
sudo iptables -t 表名 -I 链名 规则的匹配条件 规则的动作
<span style="color:#75715e"># 方式 2：将规则添加到某个链的最下方（优先级最低、编号最大）</span>
sudo iptables -t 表名 -A 链名 规则的匹配条件 规则的动作
<span style="color:#75715e"># 方式 3：将规则添加到指定的编号位置（规则编号的取值范围为：1 ~ MaxNumber+1）</span>
sudo iptables -t 表名 -I INPUT 规则编号 规则的匹配条件 规则的动作

<span style="color:#75715e">### 查看规则 ###</span>
sudo iptables --line-numbers -t 表名 -nvL 链名

<span style="color:#75715e">### 删除规则 ###</span>
<span style="color:#75715e"># 方式 1：通过规则的序号删除</span>
sudo iptables -t 表名 -D 链名 规则编号
<span style="color:#75715e"># 方式 2：通过规则的匹配条件和动作删除（即将添加规则的 -I 更改为 -D）</span>
sudo iptables -t 表名 -D 链名 规则的匹配条件 规则的动作
<span style="color:#75715e"># 方式 3：清空某张表某条链上的全部规则</span>
sudo iptables -t 表名 -F 链名
<span style="color:#75715e"># 方式 3：清空某张表的全部规则</span>
sudo iptables -t 表名 -F

<span style="color:#75715e">### 修改规则（覆盖更新） ###</span>
sudo iptables -t 表名 -R 链名 规则编号 规则的匹配条件 规则的动作  <span style="color:#75715e"># 注意：规则的匹配条件和动作都不可省略</span>

<span style="color:#75715e">### 修改某个链的默认动作 ###</span> 
sudp iptables -t 表名 -P 链名 动作

<span style="color:#75715e">### 自定义链相关 ###</span>
<span style="color:#75715e"># 新增自定义链</span>
sudo iptables -t 表名 -N 自定义链名
<span style="color:#75715e"># 给自定义链添加规则</span>
sudo iptables -t 表名 -I 自定义链名 规则的匹配条件 规则的动作
<span style="color:#75715e"># 在某个链中引用自定义链</span>
sudo iptables -t 表名 -I 链名 -j 自定义链名
<span style="color:#75715e"># 取消某个自定义链的引用</span>
sudo iptables -t 表名 -D 链名 -j 自定义链名
<span style="color:#75715e"># 删除自定义链（保证引用数为 0 并且不包含任何规则的）</span>
sudo iptables -X 自定义链名
<span style="color:#75715e"># 重命名自定义链</span>
iptables -E 自定义链名 新自定义链名</code></pre></div>
<h2 id="系统启动自动加载规则">系统启动自动加载规则</h2>

<p>安装 iptables 开机自动加载服务 <code>iptables-persistent</code>：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt install iptables-persistent</code></pre></div>
<p>在安装过程中，会询问是否将当先的规则保存下来，可以选择是。</p>

<p>该服务（<code>sudo systemctl status iptables.service</code>）会在开机时自动加载如下 iptables 配置：</p>

<ul>
<li><code>/etc/iptables/rules.v4</code></li>
<li><code>/etc/iptables/rules.v6</code></li>
</ul>

<p>如果想将当前的规则配置到开机自动加载的文件，可以通过如下命令实现：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo sh -c <span style="color:#e6db74">&#39;iptables-save &gt; /etc/iptables/rules.v4&#39;</span>
sudo sh -c <span style="color:#e6db74">&#39;ip6tables-save &gt; /etc/iptables/rules.v6&#39;</span></code></pre></div>
<p>如果想手动从配置文件中加载配置，可以通过如下命令实现：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo iptables-restore &lt; /etc/iptables/rules.v4
sudo ip6tables-restore &lt; /etc/iptables/rules.v6</code></pre></div>
<h2 id="go-iptables-sdk">Go iptables SDK</h2>

<p>Go 没有看到比较稳定的 iptables 的 SDK，通过阅读 Docker 源码可以了解到，目前 Go 上如果想使用 iptables 的方式就是直接调用 iptables 命令，参见：<a href="https://github.com/moby/moby/blob/master/libnetwork/iptables/iptables.go">Docker 源码</a>。</p>

<h2 id="实例-docker-bridge-网络模拟实现">实例：docker bridge 网络模拟实现</h2>

<p>Docker 的默认网络是通过 bridge、veth、network namespace 和 iptables 技术实现的。</p>

<p>在本系列的前几篇文章，已经介绍了 veth 和 bridge 设备，本文介绍了 iptables 技术。</p>

<p>基于对这些技术，本节将模拟实现 docker 默认网络模型。（关于 network namespace 会在容器化技术详细讲解）。</p>

<h3 id="docker-默认网络模型分析">Docker 默认网络模型分析</h3>

<p>整个网络拓扑如下图所示：</p>

<p><img src="/image/docker-bridge-models.svg" alt="image" /></p>

<p>对照上图可以看出，Docker 默认网络提供了如下能力（和上图序号对应）：</p>

<ol>
<li>同一个网络下的每个容器都会分配一个同一网段的 IP 地址（定义为容器 IP）。</li>
<li>处于同一个网络的容器之间可以相互通过容器 IP 通信。</li>
<li>容器内进程可以访问宿主机 IP。</li>
<li>宿主机进程可以通过容器 IP 和任意容器通信。</li>
<li>容器内进程可以访问宿主机可以路由到的所有 IP。</li>
<li>宿主机外部的 IP （包括宿主机所在网段的其他 IP）<strong>无法访问</strong> 该宿主机上的容器 IP（未暴露的端口）。</li>
<li>宿主机外部的 IP 只能访问到容器显式声明暴露的 TCP/UDP 端口。</li>
</ol>

<h3 id="docker-网络行为模拟">Docker 网络行为模拟</h3>

<h4 id="内核参数配置">内核参数配置</h4>

<p>Docker 网络模型依赖 ip forward 特性，需通过如下命令开启（永久生效需修改 <code>/etc/sysctl.conf</code> 配置文件）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span></code></pre></div>
<p>注意：Docker 自身不会主动修改该配置。也就是说，如果 Docker 出现端口转发外部网路无法访问的情况，可以检查该配置项。</p>

<h4 id="docker-安装">Docker 安装</h4>

<p>在 Docker 安装阶段完成，Docker deamon 第一次启动后，对做如下事项：</p>

<ul>
<li>创建一个名为 docker0 的 bridge 设备，分配一个 ip，并启动该 bridge。</li>
<li>配置一个 MASQUERADE iptables 规则。</li>
<li>配置一个 在 nat 表的 PREROUTING 链上配置一个 return 规则。</li>
</ul>

<p>本例中，bridge 名为 demodocker0，并分配了 <code>172.16.0.1/16</code> 网段。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建 bridge、分配 ip 并 启动设备</span>
sudo ip link add demodocker0 type bridge
sudo ip addr add dev demodocker0 <span style="color:#ae81ff">172</span>.16.0.1/16
sudo ip link set demodocker0 up

<span style="color:#75715e"># 创建 MASQUERADE iptables 规则：source ip 是容器 IP 的且 output 的 interface 不是 bridge 其他容器的任意协议的数据包，将修改修改 Source IP 和 Source Port，已实现网络模型的 5（访问外部网络）。</span>
sudo iptables -t nat -A POSTROUTING -s <span style="color:#ae81ff">172</span>.16.0.1/16 ! -o demodocker0  -j MASQUERADE

<span style="color:#75715e"># 配置一个 在 nat 表的 PREROUTING 链上配置一个 return 规则：来自容器的数据包都不走后续的 DNAT。</span>
sudo iptables -t nat -A PREROUTING -i demodocker0 -j RETURN</code></pre></div>
<h4 id="容器启动准备">容器启动准备</h4>

<p>在执行 <code>docker run</code> 创建一个重启时，如果使用的是默认网路，Docker 会做如下关于网络相关的准备：</p>

<ul>
<li>创建 veth ，将一端的 veth 连接到 bridge 上，并启动这一端的 veth。</li>
<li>创建 network workspace ，并将另一端的 veth 加入到 network namespace 中。</li>
<li>在 network namespace 中分配 ip，配置默认路由，并启动这一端的 veth，同时启动 lo 设备。</li>
<li>添加暴露端口的 iptables DNAT 规则</li>
</ul>

<p>本例中，将假设 docker 创建了两个容器，即对应创建两对 veth、分配两个 ip、创建两个 network namespace。并且容器 0 暴露 8080 端口到宿主机的 18080，容器 1 暴露 8081 端口到宿主机的 18081。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建 veth ，将一端的 veth 连接到 bridge 上，并启动这一端的 veth。</span>
sudo ip link add veth0 type veth peer name veth0peer
sudo ip link add veth1 type veth peer name veth1peer
sudo ip link set dev veth0peer master demodocker0
sudo ip link set dev veth1peer master demodocker0
sudo ip link set veth0peer up
sudo ip link set veth1peer up

<span style="color:#75715e"># 创建 network workspace ，并将另一端的 veth 加入到 network namespace 中，分配 ip，并配置默认路由，并启动这一端的 veth。</span>
sudo ip netns add ns0
sudo ip netns add ns1
sudo ip link set veth0 netns ns0
sudo ip link set veth1 netns ns1
<span style="color:#75715e"># 注意</span> 
sudo ip netns exec ns0 ip addr add dev veth0 <span style="color:#ae81ff">172</span>.16.0.2/16
sudo ip netns exec ns1 ip addr add dev veth1 <span style="color:#ae81ff">172</span>.16.0.3/16
sudo ip netns exec ns0 ip link set veth0 up
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns0 ip route add default via <span style="color:#ae81ff">172</span>.16.0.1
sudo ip netns exec ns1 ip route add default via <span style="color:#ae81ff">172</span>.16.0.1
sudo ip netns exec ns0 ip link set lo up
sudo ip netns exec ns1 ip link set lo up


<span style="color:#75715e"># 添加暴露端口的 iptables DNAT 规则</span>
sudo iptables -t nat -A PREROUTING -p tcp --dport <span style="color:#ae81ff">18080</span> -j DNAT --to-destination <span style="color:#ae81ff">172</span>.16.0.2:8080
sudo iptables -t nat -A PREROUTING -p tcp --dport <span style="color:#ae81ff">18081</span> -j DNAT --to-destination <span style="color:#ae81ff">172</span>.16.0.3:8081</code></pre></div>
<p>真正的 Docker 还会为暴露的端口创建一些其他的 POSTROUTING，但是应该没有实际用处，参见： <a href="https://github.com/moby/moby/issues/12632">github docker issue</a>。</p>

<h3 id="测试网络">测试网络</h3>

<h4 id="0-测试准备">0. 测试准备</h4>

<p>在 ns0 network namespace 中，8080 端口启动一个 TCP Server。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装 nc</span>
sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt install -y netcat
<span style="color:#75715e"># nohup nc -lv 8080 &gt;/dev/null 2&gt;&amp;1 &amp;</span>
sudo ip netns exec ns0 nc -lvk <span style="color:#ae81ff">8080</span></code></pre></div>
<p>以下命令再另一个 shell 中执行。</p>

<h4 id="1-观察-ip-和路由情况">1. 观察 ip 和路由情况</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip netns exec ns1 ip addr show
sudo ip netns exec ns1 ip route show
<span style="color:#75715e"># ping 自己</span>
sudo ip netns exec ns1 ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">127</span>.0.0.1
sudo ip netns exec ns1 ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">172</span>.16.0.2</code></pre></div>
<h4 id="2-两个-network-namepspace-间互相通讯">2. 两个 network namepspace 间互相通讯</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip netns exec ns1 ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">172</span>.16.0.1</code></pre></div>
<h4 id="3-network-namepspace-访问宿主机-ip">3. network namepspace 访问宿主机 ip</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip netns exec ns1 ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">192</span>.168.57.3</code></pre></div>
<h4 id="4-宿主机访问-network-namepspace-ip">4. 宿主机访问 network namepspace ip</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">172</span>.16.0.2</code></pre></div>
<h4 id="5-network-namepspace-访问外部网络">5. network namepspace 访问外部网络</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo ip netns exec ns1 ping -c 4 qq.com</pre></div>
<h4 id="6-宿主机外部的-ip-无法访问未暴露的端口">6. 宿主机外部的 IP 无法访问未暴露的端口</h4>

<p>在宿主机（192.168.57.1）执行：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ping <span style="color:#ae81ff">172</span>.16.0.2</code></pre></div>
<h4 id="7-宿主机外部的-ip-访问暴露的端口">7. 宿主机外部的 IP 访问暴露的端口</h4>

<p>在宿主机（192.168.57.1）执行：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">echo hello | nc 192.168.57.3 18080</pre></div>
<p>观察：<a href="#0-测试准备">0、准备测试</a> 的 shell 将可以看到输出：<code>hello</code>。</p>

<h3 id="清理现场">清理现场</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 终止所有 network namespace 的进程</span>
<span style="color:#75715e"># 清理 iptables</span>
sudo iptables -t nat -F
<span style="color:#75715e"># 删除 bridge、veth、network namespace</span>
sudo ip netns delete ns0  <span style="color:#75715e"># veth 会一并删除</span>
sudo ip netns delete ns1  <span style="color:#75715e"># veth 会一并删除</span>
sudo ip link delete demodocker0
<span style="color:#75715e"># 恢复内核参数</span>
sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span></code></pre></div>
<h2 id="其他参考">其他参考</h2>

<ul>
<li><a href="https://zhuanlan.zhihu.com/p/223038075">手撕Linux网络——Linux虚拟网络设备与netfilter框架汇总</a></li>
<li><a href="https://opengers.github.io/openstack/openstack-base-netfilter-framework-overview/#bridge%E4%B8%8Enetfilter">云计算底层技术-netfilter框架研究</a></li>
<li><a href="https://man7.org/linux/man-pages/man8/ip-netns.8.html">ip-netns(8) — Linux manual page</a></li>
<li><a href="http://www.hyuuhit.com/2019/03/23/netns/">Linux network namespace 简单解读</a></li>
</ul>
]]></description></item><item><title>Kubernetes 包管理器 Helm</title><link>https://www.rectcircle.cn/posts/k8s-package-manager-helm/</link><pubDate>Fri, 20 May 2022 22:07:33 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/k8s-package-manager-helm/</guid><description type="html"><![CDATA[

<blockquote>
<p>版本: 3.8.2 | <a href="https://helm.sh/zh/docs/">官方文档</a></p>
</blockquote>

<h2 id="简述">简述</h2>

<p>Helm 是 Kubernetes 的包管理器，是 <a href="https://www.cncf.io/projects/">CNCF</a> 的毕业项目之一。</p>

<p>和 传统的包管理器类似（apt / npm / maven），Helm 提供了一个 CLI 客户端，定义并提供了 Helm Chart <a href="https://artifacthub.io/">包管理仓库</a> （Chart 指的是包）。</p>

<p>Helm 在用户侧看来，和 Kubernetes 的 Kubectl 类似，仅仅是一个 Client。因此 Helm 不需要 Kubernetes 做任何特殊了配置，即可在任何 Kubernetes 集群中使用。</p>

<p>但是需要注意的是，Helm 还是需要将一些元信息存储到 Kubernetes 中的。本文介绍的 helm 3.x 不再需要在 Kubernetes 中再部署一个 Server 端。而是 Helm 每次 Release，Client 会直接通过 Kubernetes API 将元数据记录到 Kubernetes 的 Secret 中，更多参见：<a href="https://helm.sh/docs/faq/changes_since_helm2/#removal-of-tiller">Changes Since Helm 2 - Removal of Tiller</a>。</p>

<p>Helm 的核心，定义了一套渲染 Kubernetes 声明式配置 的模板规范，并通过模板引擎实现了该规范。</p>

<p>作为一个运行在 Kubernetes 的应用开发者，只需要为该应用编写一个 Chart：Kubernetes 声明式配置模板、模板参数和默认值 和 外部依赖（如 MySQL、Redis），即可一键在 Kubernetes 集群中将该应用和依赖启动起来，并可以根据通过 Chart 参数灵活的配置应用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                +------------------------+
                |    Chart Repository    |
                +------------------------+
            +--&gt;|     Dependency A       |---+
            +--&gt;|     Dependency B       |---+
            |   +------------------------+   |
      (dependency)                           |
            |   +------------------------+   |
            |   |      Chart: App        |---+
            |   +------------------------+   |
            |   |      /templates        |   |
            |   |      values.yaml       |   |
            +---|      Chart.yaml        |   |
                +------------------------+   |
                                             |
    Deploy                                   +---(Go template engine)---&gt; Kubernetes Configurations ------&gt; Kubernetes Cluster Namespace (Release)
(helm install)                               |                                                                               |
                                             |                                                                               |
                +------------------------+   |                                      +-------------------------+              |
                |     Release Values     |---+                                      |Metadata Storage backends|    Record release metadata
                +------------------------+                                          +-------------------------+              |
                | --set-file values.yaml |                                          | On Released Kubernetes  |&lt;-------------+
                | --set foo=bar          |                                          |    Namespace Secret     |
                +------------------------+                                          +-------------------------+</pre></div>
<p>上图所示的是，对一个 Chart 进行部署的流程。</p>

<ul>
<li>Chart App 定义了一个 Chart，其依赖两个在 Chart Repository 中的两个 Chart。</li>
<li>用户通过 <code>helm install</code> 命令进行一次部署，并通过 <code>--set-file</code> 和 <code>--set</code> 覆盖 Chart App 中的参数。</li>
<li>Helm CLI 通过 Go 模板引擎将 Values 和 Templates 进行渲染，得到 Kubernetes 配置。</li>
<li>最后通过 Kubernetes API （类似于 kubectl apply） 将配置应用到 Kubernetes 集群中。</li>
<li>Chart 在 Kubernetes 集群中的对应物被称为一个 Release。</li>
<li>最后，将该 Release 的元信息记录到该 Release 所在 Namespace 的 Secret 对象中（也支持其他存储后端，参见：<a href="https://helm.sh/zh/docs/topics/advanced/#%E5%90%8E%E7%AB%AF%E5%AD%98%E5%82%A8">Helm 高级技术 - 存储后端</a>）。</li>
</ul>

<h2 id="helm-cli-安装">Helm CLI 安装</h2>

<blockquote>
<p>官方文档：<a href="https://helm.sh/zh/docs/intro/install/">安装 Helm</a></p>
</blockquote>

<p>安装指定版本和系统架构，参见：<a href="https://github.com/helm/helm/releases">github release</a>。</p>

<p>以 dockerfile 为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-dockerfile" data-lang="dockerfile"><span style="color:#66d9ef">RUN</span> wget https://get.helm.sh/helm-v3.8.1-linux-amd64.tar.gz <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    tar -zxvf helm-v3.8.1-linux-amd64.tar.gz <span style="color:#f92672">&amp;&amp;</span>  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    mv linux-amd64/helm /usr/local/bin/helm <span style="color:#f92672">&amp;&amp;</span>  <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    rm -rf linux-amd64 helm-v3.8.1-linux-amd64.tar.gz</code></pre></div>
<p>Mac OS 安装：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">brew install helm</code></pre></div>
<p>APT 安装：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo <span style="color:#e6db74">&#34;deb https://baltocdn.com/helm/stable/debian/ all main&#34;</span> | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm</code></pre></div>
<h2 id="ide-支持-vscode">IDE 支持 （VSCode）</h2>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Tim-Koehler.helm-intellisense">Helm Intellisense</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools">Kubernetes</a></li>
</ul>

<h2 id="示例仓库">示例仓库</h2>

<p>参见： <a href="https://github.com/rectcircle/helm-experiment">rectcircle/helm-experiment</a></p>

<h2 id="chart-开发指南">Chart 开发指南</h2>

<h3 id="helm-create-创建脚手架">helm create 创建脚手架</h3>

<p>执行 <code>helm create</code> 命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p deploy/charts
helm create deploy/charts/myapp</code></pre></div>
<h3 id="目录结构简述">目录结构简述</h3>

<p>通过 <code>tree deploy/charts/myapp</code> 观察 <code>helm create</code> 目录结构。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">deploy/charts/myapp
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml</pre></div>
<ul>
<li><code>Chart.yaml</code> 该 chart 的声明信息（类似于 npm 的 package.json、go 的 go.mod），包含命名、版本、依赖声明等信息。</li>
<li><code>charts/</code> 目录存放该 charts 的依赖（子 chart），有两种可能。

<ul>
<li>子 chart 目录，如 <code>mysql/</code>。</li>
<li>子 chart 压缩包如 <code>mysql-8.9.6.tgz</code>。</li>
</ul></li>
<li><code>templates/</code> 模板目录，包含如下内容：配置文件模板、模板目录。

<ul>
<li><code>test/</code> 测试用的 Kubernetes 声明式配置模板，一般会声明一个 Pod，来检测该 Chart 对应的 Release 正确性，通过 <code>helm test &lt;RELEASE_NAME&gt;</code> 触发，按照推荐可以放到 <code>test/</code> 目录下，但是这不是强制的。任意 <code>metadata.annotations</code> 包含 <code>helm.sh/hook: test</code> 的都会被视为一个测试模板。</li>
<li><code>*.yaml</code>  Kubernetes 声明式配置模板，会渲染成 Kubernetes 声明式配置。</li>
<li><code>NOTES.txt</code> 一个特殊的说明文件，会在 <code>helm install</code> 等之后输出该文件渲染后的内容，该文件内容不会被。</li>
<li><code>_helpers.tpl</code> 用来定义命名模板，可以被其他文件引用，该文件不会被渲染为 Kubernetes 声明式配置（原因是，helm 规定，以 <code>_</code> 开头的文件，都不会被渲染到 Kubernetes 声明式配置 中）。</li>
</ul></li>
<li><code>values.yaml</code> 模板变量的默认值，可以被 <code>templates/</code> 目录的文件引用。</li>
</ul>

<p>除了以上目录文件，还有可能包含如下文件：</p>

<ul>
<li><code>Chart.lock</code> 类似于 npm 的 package.lock，在运行 <code>helm dependency</code> 等命令后会出现。</li>
<li><code>values.schema.json</code> 可选: 一个使用 JSON Schema 文件，用来描述 values.yaml 文件的结构。</li>
<li><code>LICENSE</code> 可选: 包含 chart 许可证的纯文本文件</li>
<li><code>crds/</code> 可选：Kubernetes 自定义资源的定义文件，注意，不是模板，是配置文件本身。</li>
</ul>

<h3 id="go-template-详解">Go Template 详解</h3>

<blockquote>
<p>参考：<a href="https://pkg.go.dev/text/template">Go 标准库 <code>text/template</code></a></p>
</blockquote>

<p>上文提到 <code>templates/</code> 目录下的文件最终都会使用 Go 标准库的 <a href="https://pkg.go.dev/text/template"><code>text/template</code> 模板引擎</a>（即 Go Template）进行渲染。在此简单 Go Template 的语法。</p>

<p>在过去 Web 开发领域模板引擎非常常见，但经历过前后端分离后，模板引擎在 Web 开发领域逐渐淡出历史舞台。模板引擎可以理解 <code>模板 + 数据 = 输出</code>，即：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Render</span>(<span style="color:#a6e22e">tpl</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">values</span> <span style="color:#66d9ef">interface</span>{}) (<span style="color:#a6e22e">output</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>)</code></pre></div>
<p>一个模板都会定义一套模板语法。这套模板语法就是一种简单的编程语言。模板语法一般包含如下能力：</p>

<ul>
<li>引用数据（渲染数据）</li>
<li>流程控制（条件/循环渲染）</li>
<li>命名模板定义和使用（定义和引用定义的一个命名模板）</li>
<li>函数调用（调用注册在 Go 模板引擎对象中的 Go 函数）</li>
</ul>

<p>Go Template 比较简单，通过如下 demo 对照输出，应该可以快速理解 Go Template 的语法和特性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;text/template&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">Bar</span> <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">Baz</span> <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Foo</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;Foo{ Bar: %s, Baz: %d }&#34;</span>, <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Bar</span>, <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Baz</span>)
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Foo</span>) <span style="color:#a6e22e">MethodHasParam</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;a: %s, b: %s&#34;</span>, <span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>)
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Foo</span>) <span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">i</span> <span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">Foo</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Foo</span>{<span style="color:#a6e22e">Bar</span>: <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Bar</span>, <span style="color:#a6e22e">Baz</span>: <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Baz</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">i</span>}
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">data</span> = <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}{
	<span style="color:#e6db74">&#34;Foo&#34;</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Foo</span>{
		<span style="color:#a6e22e">Bar</span>: <span style="color:#e6db74">&#34;bar&#34;</span>,
		<span style="color:#a6e22e">Baz</span>: <span style="color:#ae81ff">42</span>,
	},
	<span style="color:#e6db74">&#34;Field&#34;</span>: <span style="color:#e6db74">&#34;string&#34;</span>,
	<span style="color:#e6db74">&#34;List&#34;</span>: []<span style="color:#66d9ef">string</span>{
		<span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#e6db74">&#34;b&#34;</span>, <span style="color:#e6db74">&#34;c&#34;</span>,
	},
	<span style="color:#e6db74">&#34;EmptyList&#34;</span>: []<span style="color:#66d9ef">string</span>{},
	<span style="color:#e6db74">&#34;Cond&#34;</span>:      <span style="color:#66d9ef">true</span>,
	<span style="color:#e6db74">&#34;Func&#34;</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">p</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;func called: &#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">p</span>
	},
	<span style="color:#e6db74">&#34;Map&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{
		<span style="color:#e6db74">&#34;a&#34;</span>: <span style="color:#e6db74">&#34;1&#34;</span>,
		<span style="color:#e6db74">&#34;b&#34;</span>: <span style="color:#e6db74">&#34;2&#34;</span>,
	},
	<span style="color:#e6db74">&#34;Nil&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]string(<span style="color:#66d9ef">nil</span>),
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">globalFuncs</span> = <span style="color:#a6e22e">template</span>.<span style="color:#a6e22e">FuncMap</span>{
	<span style="color:#e6db74">&#34;version&#34;</span>: <span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">string</span> { <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;v1.2.3&#34;</span> },
	<span style="color:#e6db74">&#34;add&#34;</span>:     <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span> { <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span> },
}

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">tpl</span> = <span style="color:#e6db74">`1. 不使用 </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;{{}}&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> 包裹的字符串会被原样输出：
</span><span style="color:#e6db74">	没有两个 { 开头和两个 } 结尾包裹的字符串被原样渲染。
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">2. 使用 </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;{{}}&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> 包裹，可以使用模板引擎提供的动态渲染的能力：
</span><span style="color:#e6db74">	</span><span style="color:#75715e">{{</span> <span style="color:#e6db74">&#34;模板语法&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">3. 前导和后续空白字符删除
</span><span style="color:#e6db74">	a. </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;{{- 模板语法 }}&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> ，会删除前导空白字符：
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		abc
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{-</span> <span style="color:#e6db74">&#34;前导空白字符会被删除&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		def
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;{{ 模板语法 -}}&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> ，会删除后续空白字符：
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		abc
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span> <span style="color:#e6db74">&#34;后续空白字符会被删除&#34;</span> <span style="color:#75715e">-}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		def
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	c. </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;{{- 模板语法 -}}&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> ，会删除前导和后续空白字符：
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		abc
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{-</span> <span style="color:#e6db74">&#34;前导和后续空白字符会被删除&#34;</span> <span style="color:#75715e">-}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		def
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">4. 注释语法 </span><span style="color:#75715e">{{</span> <span style="color:#e6db74">&#34;{{/* a comment */}}&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> 注释内容不会被渲染：
</span><span style="color:#e6db74">	abc </span><span style="color:#75715e">{{</span><span style="color:#75715e">/* a comment */</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> def
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">5. 字面量：
</span><span style="color:#e6db74">	a. bool </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">true</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. 字符串 </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;abc&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	c. 字符 </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#39;a&#39;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	d. int (位数取决于操作系统) </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">1</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	e. float </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">1</span><span style="color:#a6e22e">.1</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	f. 虚数 </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">1i</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	g. 复数 </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">1</span><span style="color:#960050;background-color:#1e0010">+</span><span style="color:#a6e22e">1i</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">6. 渲染数据 </span><span style="color:#75715e">{{</span> <span style="color:#e6db74">&#34;{{ .Field }}&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">：
</span><span style="color:#e6db74">	a. . 是一个特殊的变量，默认指向的是 Execute 函数 data 参数：</span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. 渲染 Field 字段：</span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Field</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	c. 渲染 Foo 结构体的 Bar 字段：</span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Foo.Bar</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	d. 渲染并调用无参数方法：</span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Foo.String</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	d. 调用并渲染有参数方法：</span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Foo.MethodHasParam</span> <span style="color:#e6db74">&#34;a&#34;</span> <span style="color:#e6db74">&#34;b&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	e. 渲染 data 的 List 切片的 0 号元素：</span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">index</span> <span style="color:#a6e22e">.List</span> <span style="color:#a6e22e">0</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	f. 调用渲染 data 的 函数类型变量 Func：</span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">call</span> <span style="color:#a6e22e">.Func</span> <span style="color:#e6db74">&#34;abc&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	g. 通过 key 渲染 map 的元素：</span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">index</span> <span style="color:#a6e22e">.Map</span> <span style="color:#e6db74">&#34;a&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> 或 </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Map.a</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">7. 临时改变 . 的指向: 
</span><span style="color:#e6db74">	with: </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">.Foo</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> Bar = </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Bar</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">, Baz = </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Baz</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	with-else: </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">.Nil</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">else</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> . 是 nil </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">8. 流程控制
</span><span style="color:#e6db74">	a. 条件渲染：
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.Cond</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> .Cond is true </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">if</span> <span style="color:#66d9ef">not</span> <span style="color:#a6e22e">.Cond</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> .Cond is false </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">else</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> .Cond is true </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> 
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">if</span> <span style="color:#66d9ef">lt</span> <span style="color:#a6e22e">.Foo.Baz</span> <span style="color:#a6e22e">10</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> .Foo.Baz &lt; 10 </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">lt</span> <span style="color:#a6e22e">.Foo.Baz</span> <span style="color:#a6e22e">100</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> 10 &lt;= .Foo.Baz &lt; 100 </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">else</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> .Foo.Baz &gt;= 100 </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> 
</span><span style="color:#e6db74">	b. 遍历（只支持  array, slice, map, or channel）：
</span><span style="color:#e6db74">		遍历并改变 . 指向：
</span><span style="color:#e6db74">			range-end: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">range</span> <span style="color:#a6e22e">.List</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">, </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">			range-else-end: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">range</span> <span style="color:#a6e22e">.EmptyList</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">, </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">else</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> is empty list </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">			range-if-break-continue: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">range</span> <span style="color:#a6e22e">.List</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">, </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">if</span> <span style="color:#66d9ef">eq</span> <span style="color:#a6e22e">.</span> <span style="color:#e6db74">&#34;a&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">continue</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">else</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">break</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">  </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		遍历不改变 . 指向，并获取索引：
</span><span style="color:#e6db74">			range-with-index: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">range</span> <span style="color:#a6e22e">$i</span><span style="color:#960050;background-color:#1e0010">,</span> <span style="color:#a6e22e">$e</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">.List</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">$i</span><span style="color:#75715e">}}</span><span style="color:#e6db74">: </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">$e</span><span style="color:#75715e">}}</span><span style="color:#e6db74">, </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		遍历 map：
</span><span style="color:#e6db74">			range-map: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">range</span> <span style="color:#a6e22e">$k</span><span style="color:#960050;background-color:#1e0010">,</span> <span style="color:#a6e22e">$v</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">.Map</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">$k</span><span style="color:#75715e">}}</span><span style="color:#e6db74">: </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">$v</span><span style="color:#75715e">}}</span><span style="color:#e6db74">, </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">9. Arguments 概念，如下几种语法都叫做 Arguments：
</span><span style="color:#e6db74">	a. 上文提到的字面量: </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">1</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. nil 关键字：</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;%v&#34;</span> <span style="color:#66d9ef">nil</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	c. 数据指向数据的引用 . 以及对数据中的字段的引用(map 或 结构体) .Field: </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.Field</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	d. $ 等对变量的引用，以及对变量的字段的引用（map 或结构体） $Xxx.Xxx，下文有阐述
</span><span style="color:#e6db74">	e. 数据或变量中，无参方法调用：</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">.Foo.String</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	f. 无参全局函数：</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">version</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	g. 上述之一的带括号的实例，用于分组。结果可以通过字段或映射键调用来访问。
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;.List[0]=%s&#34;</span> <span style="color:#f92672">(</span><span style="color:#66d9ef">index</span> <span style="color:#a6e22e">.List</span> <span style="color:#a6e22e">0</span><span style="color:#f92672">)</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span> <span style="color:#f92672">(</span><span style="color:#a6e22e">.Foo.Add</span> <span style="color:#a6e22e">1</span><span style="color:#f92672">)</span><span style="color:#a6e22e">.String</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">10. 全局函数调用
</span><span style="color:#e6db74">	语法为 </span><span style="color:#75715e">{{</span> <span style="color:#e6db74">&#34;{{函数名 Arguments1 Arguments2 ...}}&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">: </span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;%s&#34;</span> <span style="color:#e6db74">&#34;hello&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	内置全局函数(参见 https://pkg.go.dev/text/template#hdr-Functions ）调用：</span><span style="color:#75715e">{{</span> <span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;%s&#34;</span> <span style="color:#e6db74">&#34;hello&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	自定义全局函数，通过 func (*Template).Funcs(funcMap template.FuncMap) *template.Template 函数添加：</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">add</span> <span style="color:#a6e22e">1</span> <span style="color:#a6e22e">2</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">11. Pipelines 能力，和 shell 中的 Pipelines 类似，如下元素可以通过 | 管道符连接。
</span><span style="color:#e6db74">	a. 语法为: Argument | 函数或方法（可选） | 函数或方法（可选） | ...
</span><span style="color:#e6db74">		Argument 就是上文定义的
</span><span style="color:#e6db74">		函数或方法可能是：
</span><span style="color:#e6db74">			.XXx.XxxMethod [Argument...] 保证最后一个是有参数有返回值的方法
</span><span style="color:#e6db74">			$Xxx.XxxMethod [Argument...] 保证最后一个是有参数有返回值的方法
</span><span style="color:#e6db74">			有参数有返回值的全局函数 Func [Argument...]
</span><span style="color:#e6db74">		函数或方法的写法，不应书写最后一个参数，因为最后一个参数从 Pipeline 中来
</span><span style="color:#e6db74">	d. 一个例子： </span><span style="color:#75715e">{{</span><span style="color:#e6db74">&#34;Pipeline&#34;</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;hello %s&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">12. 变量
</span><span style="color:#e6db74">	a. 定义 $variable := pipeline: </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">$variable</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;value&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">$variable</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. 赋值 $variable = pipeline: </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">$variable</span> <span style="color:#960050;background-color:#1e0010">=</span> <span style="color:#e6db74">&#34;override&#34;</span> <span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span> <span style="color:#a6e22e">$variable</span> <span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">13. 嵌套模板
</span><span style="color:#e6db74">	a. 定义: </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">define</span> <span style="color:#e6db74">&#34;T1&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">ONE</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	b. 调用: </span><span style="color:#75715e">{{</span><span style="color:#66d9ef">template</span> <span style="color:#e6db74">&#34;T1&#34;</span> <span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	c. 不要求定义一定发生在调用之前：</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">template</span> <span style="color:#e6db74">&#34;T2&#34;</span> <span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">define</span> <span style="color:#e6db74">&#34;T2&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">TWO</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	d. 同一个 tpl 不允许重复定义，但是多个模板可以重复定义，后 Parse 的将覆盖之前的嵌套模板
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">template</span> <span style="color:#e6db74">&#34;T3&#34;</span> <span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74"> </span><span style="color:#75715e">{{</span><span style="color:#a6e22e">define</span> <span style="color:#e6db74">&#34;T3&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">THREE</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">	e. block 定义一个模板并立即调用，等价于 define 后立即 template 调用，用于实现给 template 提供一个默认值。
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">block</span> <span style="color:#e6db74">&#34;T4&#34;</span> <span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">T4 没有定义</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">		</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">block</span> <span style="color:#e6db74">&#34;T5&#34;</span> <span style="color:#a6e22e">.</span><span style="color:#75715e">}}</span><span style="color:#e6db74">T5 没有定义</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">14. Parse 多个模板时，Execute 函数，只会渲染最后一个包含实际内容的模板（不包含实际内容的模板指的是该模板只包含 define 没有其他内容）。
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">15. Action 和 函数
</span><span style="color:#e6db74">	在 Go 模板中 Action 和 全局函数，使用起来看似相同。但是 Action 不支持 Pipeline。
</span><span style="color:#e6db74">	除了 if 、else、range 这类流程控制的 Action 外，template 就是一个 Action，因此 template 不支持管道符。
</span><span style="color:#e6db74">`</span>

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">tpl2</span> = <span style="color:#e6db74">`</span><span style="color:#75715e">{{</span><span style="color:#a6e22e">define</span> <span style="color:#e6db74">&#34;T3&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">THREE 来自 tpl2</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}{{</span><span style="color:#a6e22e">define</span> <span style="color:#e6db74">&#34;T5&#34;</span><span style="color:#75715e">}}</span><span style="color:#e6db74">T5 有定义</span><span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span><span style="color:#e6db74">`</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">template</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;tpl&#34;</span>).<span style="color:#a6e22e">Funcs</span>(<span style="color:#a6e22e">globalFuncs</span>).<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">tpl</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">tpl2</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>, <span style="color:#a6e22e">data</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}</code></pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">1. 不使用 {{}} 包裹的字符串会被原样输出：
        没有两个 { 开头和两个 } 结尾包裹的字符串被原样渲染。

2. 使用 {{}} 包裹，可以使用模板引擎提供的动态渲染的能力：
        模板语法

3. 前导和后续空白字符删除
        a. {{- 模板语法 }} ，会删除前导空白字符：

                abc前导空白字符会被删除
                def

        b. {{ 模板语法 -}} ，会删除后续空白字符：

                abc
                后续空白字符会被删除def

        c. {{- 模板语法 -}} ，会删除前导和后续空白字符：

                abc前导和后续空白字符会被删除def

4. 注释语法 {{/* a comment */}} 注释内容不会被渲染：
        abc  def

5. 字面量：
        a. bool true
        b. 字符串 abc
        c. 字符 97
        d. int (位数取决于操作系统) 1
        e. float 1.1
        f. 虚数 (0+1i)
        g. 复数 (1+1i)


6. 渲染数据 {{ .Field }}：
        a. . 是一个特殊的变量，默认指向的是 Execute 函数 data 参数：map[Cond:true EmptyList:[] Field:string Foo:Foo{ Bar: bar, Baz: 42 } Func:0x10ee480 List:[a b c] Map:map[a:1 b:2] Nil:map[]]
        b. 渲染 Field 字段：string
        c. 渲染 Foo 结构体的 Bar 字段：bar
        d. 渲染并调用无参数方法：Foo{ Bar: bar, Baz: 42 }
        d. 调用并渲染有参数方法：a: a, b: b
        e. 渲染 data 的 List 切片的 0 号元素：a
        f. 调用渲染 data 的 函数类型变量 Func：func called: abc
        g. 通过 key 渲染 map 的元素：1 或 1

7. 临时改变 . 的指向: 
        with:  Bar = bar, Baz = 42 
        with-else:  . 是 nil 

8. 流程控制
        a. 条件渲染：
                 .Cond is true 
                 .Cond is true  
                 10 &lt;= .Foo.Baz &lt; 100  
        b. 遍历（只支持  array, slice, map, or channel）：
                遍历并改变 . 指向：
                        range-end:  a,  b,  c, 
                        range-else-end:  is empty list 
                        range-if-break-continue:  a,   b,  
                遍历不改变 . 指向，并获取索引：
                        range-with-index:  0: a,  1: b,  2: c, 
                遍历 map：
                        range-map:  a: 1,  b: 2, 

9. Arguments 概念，如下几种语法都叫做 Arguments：
        a. 上文提到的字面量: 1
        b. nil 关键字：&lt;nil&gt;
        c. 数据指向数据的引用 . 以及对数据中的字段的引用(map 或 结构体) .Field: string
        d. $ 等对变量的引用，以及对变量的字段的引用（map 或结构体） $Xxx.Xxx，下文有阐述
        e. 数据或变量中，无参方法调用：Foo{ Bar: bar, Baz: 42 }
        f. 无参全局函数：v1.2.3
        g. 上述之一的带括号的实例，用于分组。结果可以通过字段或映射键调用来访问。
                .List[0]=a
                Foo{ Bar: bar, Baz: 43 }

10. 全局函数调用
        语法为 {{函数名 Arguments1 Arguments2 ...}}: hello
        内置全局函数(参见 https://pkg.go.dev/text/template#hdr-Functions ）调用：hello
        自定义全局函数，通过 func (*Template).Funcs(funcMap template.FuncMap) *template.Template 函数添加：2

11. Pipelines 能力，和 shell 中的 Pipelines 类似，如下元素可以通过 | 管道符连接。
        a. 语法为: Argument | 函数或方法（可选） | 函数或方法（可选） | ...
                Argument 就是上文定义的
                函数或方法可能是：
                        .XXx.XxxMethod [Argument...] 保证最后一个是有参数有返回值的方法
                        $Xxx.XxxMethod [Argument...] 保证最后一个是有参数有返回值的方法
                        有参数有返回值的全局函数 Func [Argument...]
                函数或方法的写法，不应书写最后一个参数，因为最后一个参数从 Pipeline 中来
        d. 一个例子： hello Pipeline

12. 变量
        a. 定义 $variable := pipeline:  value
        b. 赋值 $variable = pipeline:  override

13. 嵌套模板
        a. 定义: 
        b. 调用: ONE
        c. 不要求定义一定发生在调用之前：TWO 
        d. 同一个 tpl 不允许重复定义，但是多个模板可以重复定义，后 Parse 的将覆盖之前的嵌套模板
                THREE 来自 tpl2 
        e. block 定义一个模板并立即调用，等价于 define 后立即 template 调用，用于实现给 template 提供一个默认值。
                T4 没有定义
                T5 有定义

14. Parse 多个模板时，Execute 函数，只会渲染最后一个包含实际内容的模板（不包含实际内容的模板指的是该模板只包含 define 没有其他内容）。

15. Action 和 函数
        在 Go 模板中 Action 和 全局函数，使用起来看似相同。但是 Action 不支持 Pipeline。
        除了 if 、else、range 这类流程控制的 Action 外，template 就是一个 Action，因此 template 不支持管道符。</pre></div>
<h3 id="helm-自定义模板函数">Helm 自定义模板函数</h3>

<p>Go Template 标准库提供了<a href="https://pkg.go.dev/text/template#hdr-Functions">全局模板函数</a>比较有限，Helm 再此基础上更多常用的全局模板函数。主要包含两部分：</p>

<ul>
<li>Sprig 库 v3 提供的。<a href="http://masterminds.github.io/sprig/">官方文档</a> 文档介绍的不全。全部可用参见：<a href="https://github.com/Masterminds/sprig/blob/3ac42c7bc5e4be6aa534e036fb19dde4a996da2e/functions.go#L97">源码</a>。</li>
<li>Helm 自定义的。参见：<a href="https://github.com/helm/helm/blob/a499b4b179307c267bdf3ec49b880e3dbd2a5591/pkg/engine/funcs.go#L44">源码</a>。</li>
</ul>

<p>官方有部分函数的介绍文档：</p>

<ul>
<li><a href="https://helm.sh/zh/docs/chart_template_guide/function_list/">模板函数列表</a>。</li>
<li><a href="https://helm.sh/zh/docs/chart_template_guide/named_templates/">include</a>。</li>
</ul>

<p>这里简单介绍下 include 函数。</p>

<p>在 Helm 中，利用 Go 模板引擎主要用来渲染 yaml 文件，因此处理缩进很重要。</p>

<p>但是，在 Go 模板引擎中的 <code>template</code> 是一个行为（即 Action 和 <code>if</code> 类似），不是一个函数。因此无法通过管道符来添加缩进（不能这么写 <code>{{ template &quot;mychart.app&quot; . | indent 4 }}</code>）</p>

<p>因此，Helm 自定义了一个函数 <code>include</code>，该函数的能力和 template 完全一致，因为其是一个函数，所以可以使用管道符（<code>{{ include &quot;mychart.app&quot; . | indent 4 }}</code>）。</p>

<h3 id="yaml-语法规范">yaml 语法规范</h3>

<p>参考官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/yaml_techniques/">附录： YAML技术</a></p>

<h3 id="helm-模板数据变量">Helm 模板数据变量</h3>

<blockquote>
<p>官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/builtin_objects/">内置对象</a></p>
</blockquote>

<p>在编写模板时，可以通过 {{ .Xxx.Xxx }} 来访问 Helm Chart 中的相关数据：</p>

<ul>
<li><code>.Release</code> 获取本次 Release 的相关信息，如 <code>{{ .Release.Name }}</code> release名称。</li>
<li><code>.Values</code> 从 <code>values.yaml</code> 文件和 <code>--set-file</code> 、<code>--set</code> 设置的变量，参见下文：<a href="#values-yaml-详解">values.yaml 详解</a>。</li>
<li><code>.Chart</code> Chart.yaml文件内容，参见下文：<a href="#chart-yaml-详解">Chart.yaml 详解</a>。</li>
<li><code>.Files</code> 访问 chart 内部的件的内容。（不能访问 <code>.helmignore</code> 忽略的文件 和 <code>/templates</code> 文件），更多参见：<a href="https://helm.sh/zh/docs/chart_template_guide/accessing_files/">在模板内部访问文件</a>。</li>
<li><code>.Capabilities</code> 提供关于Kubernetes集群支持功能的信息。</li>
<li><code>.Template</code> 包含当前被执行的当前模板信息。</li>
</ul>

<h3 id="调试模板">调试模板</h3>

<blockquote>
<p>参考官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/debugging/">调试模板</a></p>
</blockquote>

<p>有多种方式可以调试模板：</p>

<ul>
<li>场景 1：使用默认参数观察模板输出

<ul>
<li>观察全部整体渲染结果

<ul>
<li>命令行 <code>helm install release-name deploy/charts/myapp --dry-run --debug</code></li>
<li>VSCode 命令 <code>&gt;helm: preview template</code></li>
</ul></li>
<li>观察某个模板

<ul>
<li>命令行 <code>helm template hpa deploy/charts/myapp --show-only templates/deployment.yaml</code></li>
<li>VSCode 命令 <code>&gt;helm: preview template</code> （当前编辑器打开的）</li>
</ul></li>
</ul></li>
<li>场景 2：测试不同参数的不同行为

<ul>
<li>观察全部整体渲染结果

<ul>
<li>命令行 <code>helm install release-name deploy/charts/myapp --dry-run --debug --set xxx.xxx=xxx</code></li>
</ul></li>
<li>观察某个模板

<ul>
<li>命令行 <code>helm template hpa deploy/charts/myapp --show-only templates/deployment.yaml --set xxx.xxx=xxx</code></li>
</ul></li>
</ul></li>
<li>场景 3：观察已经安装到 k8s 集群中的 release 的情况：

<ul>
<li>命令行 <code>helm get manifest release-name</code></li>
</ul></li>
</ul>

<p>下文的介绍会多次使用这些类似的命令。</p>

<h3 id="chart-yaml-详解">Chart.yaml 详解</h3>

<blockquote>
<p>参见官方文档：<a href="https://helm.sh/zh/docs/topics/charts/#chartyaml-%E6%96%87%E4%BB%B6">Chart - Chart.yaml 文件</a></p>
</blockquote>

<p>通过 helm create 创建的 Chart.yaml 包含如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v2
name: myapp
description: A Helm chart for Kubernetes
type: application
version: <span style="color:#ae81ff">0.1.0</span>
appVersion: <span style="color:#e6db74">&#34;1.16.0&#34;</span></code></pre></div>
<p>所有字段和说明，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: chart API 版本 （必需）
name: chart名称 （必需）
version: 语义化2 版本（必需）
kubeVersion: 兼容Kubernetes版本的语义化版本（可选）
description: 一句话对这个项目的描述（可选）
type: chart类型 （可选）
keywords:
  - 关于项目的一组关键字（可选）
home: 项目home页面的URL （可选）
sources:
  - 项目源码的URL列表（可选）
dependencies: <span style="color:#75715e"># chart 必要条件列表 （可选）</span>
  - name: chart名称 (nginx)
    version: chart版本 (<span style="color:#e6db74">&#34;1.2.3&#34;</span>)
    repository: （可选）仓库URL (<span style="color:#e6db74">&#34;https://example.com/charts&#34;</span>) 或别名 (<span style="color:#e6db74">&#34;@repo-name&#34;</span>)
    condition: （可选） 解析为布尔值的yaml路径，用于启用/禁用chart (e.g. subchart1.enabled )
    tags: <span style="color:#75715e"># （可选）</span>
      - 用于一次启用/禁用 一组chart的tag
    import-values: <span style="color:#75715e"># （可选）</span>
      - ImportValue 保存源值到导入父键的映射。每项可以是字符串或者一对子/父列表项
    alias: （可选） chart中使用的别名。当你要多次添加相同的chart时会很有用
maintainers: <span style="color:#75715e"># （可选）</span>
  - name: 维护者名字 （每个维护者都需要）
    email: 维护者邮箱 （每个维护者可选）
    url: 维护者URL （每个维护者可选）
icon: 用做icon的SVG或PNG图片URL （可选）
appVersion: 包含的应用版本（可选）。不需要是语义化，建议使用引号
deprecated: 不被推荐的chart （可选，布尔值）
annotations:
  example: 按名称输入的批注列表 （可选）.</code></pre></div>
<p>在此介绍几个可能比较难以理解的字段：</p>

<ul>
<li>type 该 Chart 的类型，有两种选择：

<ul>
<li>application （默认）</li>
<li>library 表示该 Chart 是给其他 chart 依赖的，因此 libaray 类型的 chart 一般都是一些通过 <code>define</code> 定义的命名模板，如果 library 包含 Kubernetes 声明式配置，则会被忽略，该特性用处应该不大。更多参见：<a href="https://helm.sh/zh/docs/topics/library_charts/">库类型Chart</a>。</li>
</ul></li>
<li>dependencies[].condition 配置依赖启用/禁用的条件，假设某个应用，在 A 场景需要部署一个 MySQL 依赖，而在 B 场景不需要部署而是使用外部的 MySQL 实例，此时就可以通过该字段进行配置，配置方式如下：

<ul>
<li>Chart.yaml 添加： <code>dependencies[].condition: mysql.enabled</code>。</li>
<li>values.yaml 添加： <code>mysql.enabled: true</code> 默认开启。</li>
<li>helm install 时，通过 <code>--set mysql.enabled=true|false</code> 来决定是否启用 mysql。</li>
<li>关于该特性的官方说明，参见：<a href="https://helm.sh/zh/docs/topics/charts/#%E4%BE%9D%E8%B5%96%E4%B8%AD%E7%9A%84tag%E5%92%8C%E6%9D%A1%E4%BB%B6%E5%AD%97%E6%AE%B5">依赖中的tag和条件字段</a>。</li>
</ul></li>
<li>dependencies[].tags  用于实现一次性启用/禁用一组依赖的Chart，假设某应用可以分为前端部分和后端部分，每个部分都有多个依赖，此时希望一次性启用或禁用前端特性，就可以通过该字段，配置方式如下：

<ul>
<li>Chart.yaml 添加： <code>dependencies[].tags: [&quot;front-end&quot;]</code>。</li>
<li>values.yaml 添加： <code>tags.front-end: true</code> 默认开启。</li>
<li>helm install 时，通过 <code>--set tags.front-end=true|false</code> 来决定是否启用前端。</li>
<li>注意如果 <code>dependencies[].tags</code> 存在多个，只要有一个 tag 为 true 就会启用（或关系）。</li>
<li>关于该特性的官方说明，参见：<a href="https://helm.sh/zh/docs/topics/charts/#%E4%BE%9D%E8%B5%96%E4%B8%AD%E7%9A%84tag%E5%92%8C%E6%9D%A1%E4%BB%B6%E5%AD%97%E6%AE%B5">依赖中的tag和条件字段</a>。</li>
</ul></li>

<li><p>dependencies[].import-values 将子 chart 的值导入到 父 chart <code>values.yaml</code> 作为父 <code>values.yaml</code> 的默认值</p>

<ul>
<li><p>方式 1：import  子 chart 的 exports 下的值</p>

<ul>
<li><p>假设子 chart 的 values.yaml 包含</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">exports:
  data:
    myint: <span style="color:#ae81ff">99</span></code></pre></div></li>

<li><p>此时父 chart 的 Chart.yaml</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">dependencies:
  - name: subchart
    repository: http://localhost:<span style="color:#ae81ff">10191</span>
    version: <span style="color:#ae81ff">0.1.0</span>
    import-values:
      - data</code></pre></div></li>

<li><p>此时运行  <code>helm install ... --debug --dry-run</code> 观察计算出的 values 将包含</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">myint: <span style="color:#ae81ff">99</span></code></pre></div></li>
</ul></li>

<li><p>方式 2：import  子 chart 的非 exports 下的值</p>

<ul>
<li><p>假设子 chart 的 values.yaml 包含</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">default:
  data:
    myint: <span style="color:#ae81ff">999</span>
    mybool: <span style="color:#66d9ef">true</span></code></pre></div></li>

<li><p>假设父 chart 的 values.yaml</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">myimports:
  myint: <span style="color:#ae81ff">0</span>
  mybool: <span style="color:#66d9ef">false</span>
  mystring: <span style="color:#e6db74">&#34;helm rocks!&#34;</span></code></pre></div></li>

<li><p>此时父 chart 的 Chart.yaml</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">dependencies:
  - name: subchart
    repository: http://localhost:<span style="color:#ae81ff">10191</span>
    version: <span style="color:#ae81ff">0.1.0</span>
    import-values:
      - child: default.data
        parent: myimports</code></pre></div></li>

<li><p>此时运行  <code>helm install ... --debug --dry-run</code> 观察计算出的 values，子 chart 将覆盖父 chart，即：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">myimports:
  myint: <span style="color:#ae81ff">999</span>
  mybool: <span style="color:#66d9ef">true</span>
  mystring: <span style="color:#e6db74">&#34;helm rocks!&#34;</span></code></pre></div></li>

<li><p>此时运行  <code>helm install ... --set myimports.myint=1000  --debug --dry-run</code> 观察计算出的 values，可以发现 import 是单向的，手动改动父 chart 的 import 的值不会影响子 chart 的值：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">myimports:
  myint: <span style="color:#ae81ff">1000</span>
  mybool: <span style="color:#66d9ef">true</span>
  mystring: <span style="color:#e6db74">&#34;helm rocks!&#34;</span>
subchart:
  default:
    data:
      mybool: <span style="color:#66d9ef">true</span>
      myint: <span style="color:#ae81ff">999</span></code></pre></div></li>

<li><p>此时运行  <code>helm install ... --set subchart.default.myint=1001  --debug --dry-run</code> 观察计算出的 values，可以发现手动改动子 chart 的值会让 import 失效，父 chart 将保持其自身的默认值（行为很诡异，看起来像是 bug）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">myimports:
  mybool: <span style="color:#66d9ef">false</span>
  myint: <span style="color:#ae81ff">0</span>
  mystring: helm rocks!
subchart:
  default:
    data:
      mybool: <span style="color:#66d9ef">true</span>
      myint: <span style="color:#ae81ff">1001</span></code></pre></div></li>
</ul></li>

<li><p>可以看出 export import 仅仅是为了方便获取子 chart 的默认值，而覆盖的场景行为很诡异，<a href="https://helm.sh/zh/docs/topics/charts/#%E9%80%9A%E8%BF%87%E4%BE%9D%E8%B5%96%E5%AF%BC%E5%85%A5%E5%AD%90value">官方文档</a>也没有仔细覆盖场景的行为。</p></li>
</ul></li>

<li><p>appVersion，此字段仅供参考，对chart版本计算没有影响。比如 mysql 的 chart 该字段应该就是 mysql 的版本号。</p></li>
</ul>

<h3 id="chart-模板渲染结果-apply-顺序">Chart 模板渲染结果 apply 顺序</h3>

<p>Helm 对一个 包含依赖的 Chart 做一次 Release，比如 Chart A 依赖 Chart B。</p>

<p>这个 Chart 模板的渲染结果最终会 apply 到 Kubernetes 的某个 Namespace 中。直觉上理解可能是：</p>

<ul>
<li>先渲染 Chart B，然后 apply B 的结果。</li>
<li>等待 B apply 成功，再渲染 Chart A，然后 apply A 的结果。</li>
</ul>

<p>实际上，并非如此，Chart 底层会：</p>

<ul>
<li>将 Chart A 和 Chart B 进行渲染。</li>
<li>然后将渲染结果先按照 Kubernetes 资源类型进行排序（排序规则参见：<a href="https://github.com/helm/helm/blob/484d43913f97292648c867b56768775a55e4bba6/pkg/releaseutil/kind_sorter.go">源码</a>）。</li>
<li>在相同的资源类型 A 在前、B 在后。</li>
<li>然后一次性按照排序后的结果 apply 到 Kubernetes 中。</li>
</ul>

<p>因此，如果想控制服务粒度的启动顺序的前后依赖（比如 App 依赖 MySQL）。则还需要利用 kubernetes 的 init-containers 机制来实现。</p>

<h3 id="values-yaml-详解">values.yaml 详解</h3>

<blockquote>
<p>参见官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/values_files/">Values 文件</a></p>
</blockquote>

<p><code>values.yaml</code> 使用 yaml 语法定义了可以在模板中使用的变量。在模板中通过 <code>.Values</code> 来进行引用。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">replicaCount: <span style="color:#ae81ff">1</span>

image:
  repository: nginx
  pullPolicy: IfNotPresent
  <span style="color:#75715e"># Overrides the image tag whose default is the chart appVersion.</span>
  tag: <span style="color:#e6db74">&#34;&#34;</span></code></pre></div>
<p>比如 <code>replicaCount</code> 变量</p>

<ul>
<li>其默认值是 1，其在 <code>templates/deployment.yaml</code> 文件中被引用：<code>replicas: {{ .Values.replicaCount }}</code>。</li>
<li>如果想在部署时修改该变量，则可以通过 <code>--set-file xxx.yaml</code> 或者 <code>--set replicaCount=2</code> 来覆盖 <code>values.yaml</code> 中的默认值。</li>
</ul>

<p>values.yaml 文件中，存在几个特殊的 key， helm 会对其做特殊处理：</p>

<ul>
<li><code>exports</code> 作用参见上文： <a href="#chart-yaml-详解">Chart.yaml 详解</a>。</li>

<li><p><code>子chart名</code> 给依赖的子 chart 赋值。</p>

<ul>
<li><p>假设父 chart 依赖一个子 chart，在父 Chart 的 Chart.yaml 文件中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">dependencies:
  - name: subchart
    version: <span style="color:#e6db74">&#34;*.*.*&#34;</span></code></pre></div></li>

<li><p>子 chart 的 values.yaml 部分内容为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">replicaCount: <span style="color:#ae81ff">1</span></code></pre></div></li>

<li><p>此时，通过 <code>helm upgrade release-name deploy/charts/myapp --install --debug --dry-run</code> 观察输出将得到</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">replicaCount: <span style="color:#ae81ff">1</span> <span style="color:#75715e"># 这是父 chart 的 value</span>
subchart:
  replicaCount: <span style="color:#ae81ff">1</span> <span style="color:#75715e"># 这是子 chart 的 value</span></code></pre></div></li>

<li><p>因此如果想覆盖子 chart 的值，需要通过：</p>

<ul>
<li><p>在父 chart 的 <code>values.yaml</code> 添加配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">subchart:
  replicaCount: <span style="color:#ae81ff">2</span></code></pre></div></li>

<li><p><code>--set-file</code> 文件，结构和父 chart 的 <code>values.yaml</code> 一致</p></li>

<li><p><code>--set 子chart名.xxx=xxx</code> 即 <code>--set subchart.replicaCount=2</code></p></li>
</ul></li>

<li><p>更多参见官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/subcharts_and_globals/">子chart和全局值</a>：</p></li>
</ul></li>

<li><p><code>global</code> 全局值，上面说到，在父 chart 中如果想修改子 chart 的值则需要添加 <code>子chart名</code> 的前缀，因此通过 global key 可以声明一个全局的值，然后通过 <code>--set global.xxx</code> 即可同时修改父子的值。</p>

<ul>
<li><p>假设父 chart 依赖一个子 chart，在父 Chart 的 Chart.yaml 文件中：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">dependencies:
  - name: subchart
    version: <span style="color:#e6db74">&#34;*.*.*&#34;</span></code></pre></div></li>

<li><p>子 chart 和 父 chart 的 values.yaml 都包含：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># 子 chart（子 chart 可以不声明也可以，但是父 chart 必须声明，建议同时声明）</span>
global:
  aaa: child
<span style="color:#75715e"># 父 chart</span>
global:
  aaa: parent</code></pre></div></li>

<li><p>此时，通过 <code>helm upgrade release-name deploy/charts/myapp --install --debug --dry-run</code> 观察输出将得到：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">global:
  aaa: parent
subchart:
  global:
    aaa: parent</code></pre></div>
<p>可以看出，父 chart 的值会覆盖子 chart 的值。</p></li>

<li><p>此时，通过 <code>helm upgrade release-name deploy/charts/myapp --install --debug --dry-run --set global.aaa=override</code> 观察输出将得到：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">global:
  aaa: override
subchart:
  global:
    aaa: override</code></pre></div>
<p>可以看出，通过 global.aaa 同时改变了父子 chart 的值。</p></li>

<li><p>更多参见官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/subcharts_and_globals/">子chart和全局值</a>：</p></li>
</ul></li>
</ul>

<h3 id="templates-helpers-tpl-详解">templates/_helpers.tpl 详解</h3>

<blockquote>
<p>参见官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/named_templates/">命名模板</a></p>
</blockquote>

<p>首先来看 <code>templates/_helpers.tpl</code> 文件名。</p>

<ul>
<li><code>_</code> 开头是必须的，表示该文件不会被渲染为 Kubernetes 声明式配置。</li>
<li>文件名和后缀是不是强制的，是 helm 推荐的命名规则，也就是说，该文件名改为 <code>_xxx.abc</code> 不会有任何实际影响。</li>
</ul>

<p>再看该文件的内容，下面摘抄了部分内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{{/*
Expand the name of the chart.
*/}}
{{- define &#34;myapp.name&#34; -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix &#34;-&#34; }}
{{- end }}</pre></div>
<p>可以看出，该文件是一些命名模板的定义位置，以上面这个 <code>myapp.name</code> 命名模板为例，该命名模板会返回该 Chart 的名字，逻辑为：</p>

<ul>
<li>如果 value 中定义了 nameOverride，则使用 nameOverride；否则使用 Chart.yaml 中定义的 name。</li>
<li>获取到名字后，如果长度过长，则截断到 63 位。</li>
<li>删除可能的后缀一个或多个 <code>-</code>。</li>
</ul>

<h3 id="templates-yaml-详解">templates/*.yaml 详解</h3>

<p>templates/*.yaml 相关文件就是会被渲染成 Kubernetes 声明式配置 的模板文件。</p>

<p>其文件名可以是任何不以 <code>-</code> 开头的符合操作系统命名规则的字符串。</p>

<p>以 <code>templates/deployment.yaml</code> 文件，前面几行内容为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include <span style="color:#e6db74">&#34;myapp.fullname&#34;</span> . }}
  labels:
    {{- include <span style="color:#e6db74">&#34;myapp.labels&#34;</span> . | nindent <span style="color:#ae81ff">4</span> }}</code></pre></div>
<ul>
<li>前面 3 行都是会被原样输出的字符串</li>
<li>第 4 行调用了定义于 <code>templates/_helpers.tpl</code> 的 <code>myapp.fullname</code> 模板。</li>
<li>第 5 行调用了定义于 <code>templates/_helpers.tpl</code> 的 <code>myapp.labels</code> 模板，并使用 nindent 处理缩进。</li>
</ul>

<p>通过 <code>helm upgrade release-name deploy/charts/myapp --install --debug --dry-run</code> 或者 VSCode 的 <code>cmd + shfit + p</code> 输入： <code>&gt;helm: preview template</code> 即可观察最终渲染的结果：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-name-myapp
  labels:
    helm.sh/chart: myapp<span style="color:#ae81ff">-0.1.0</span>
    app.kubernetes.io/name: myapp
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: <span style="color:#e6db74">&#34;1.16.0&#34;</span>
    app.kubernetes.io/managed-by: Helm</code></pre></div>
<p>其他 templates/*.yaml 文件类似，在此不再赘述。</p>

<h3 id="templates-notes-txt-简述">templates/NOTES.txt 简述</h3>

<p>templates/NOTES.txt 的作用是，在执行完成 helm install 或 helm upgrade 后，输出的一些提示信息，比如告诉用户如何访问服务等。</p>

<p>更多参见官方文档：<a href="https://helm.sh/zh/docs/chart_template_guide/notes_files/">创建一个NOTES.txt文件</a></p>

<h3 id="helmignore-简述">.helmignore 简述</h3>

<p>在运行 helm package 时会忽略该文件声明的路径的文件。该文件的语法和 <code>.gitignore</code> 还是有所不同：</p>

<ul>
<li>不支持&rsquo;**&lsquo;语法。</li>
<li>globbing库是Go的 &lsquo;filepath.Match&rsquo;，不是fnmatch(3)</li>
<li>末尾空格总会被忽略(不支持转义序列)</li>
<li>不支持&rsquo;!&lsquo;作为特殊的引导序列</li>
</ul>

<h3 id="生命周期-和-hook">生命周期 和 Hook</h3>

<p>Helm Chart 在部署成一个 Release 的过程中的声明周期如下所示：</p>

<ul>
<li>用户执行 <code>helm install foo</code>。</li>
<li>Helm 库调用安装 API。</li>
<li>在一些验证之后，库会渲染 foo 模板。</li>
<li>库会加载模板渲染的结果，到 Kubernetes，并记录 Release 元信息到 Kubernetes 的 Secret 中。</li>
<li>库会返回发布对象（和其他数据）给客户端。</li>
<li>客户端退出。</li>
</ul>

<h3 id="chart-开发最佳实践">Chart 开发最佳实践</h3>

<p>参见：<a href="https://helm.sh/zh/docs/chart_best_practices/">Chart 最佳实践指南</a></p>

<h2 id="helm-命令详解">helm 命令详解</h2>

<h3 id="配置和环境">配置和环境</h3>

<blockquote>
<p>参考官方文档：<a href="https://helm.sh/zh/docs/helm/helm/">Helm</a> | <a href="https://helm.sh/zh/docs/helm/helm_env/">环境</a></p>
</blockquote>

<p>若想 helm 命令正常使用，至少需要配置一个 kubeconfig。和 kubectl 类似，默认位于：<code>~/.kube/config</code>。</p>

<p>helm 命令的自身的配置，可通过环境变量进行配置，列表参见：<a href="https://helm.sh/zh/docs/helm/helm/">官方文档</a>。</p>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_env/"><code>helm env [flags]</code></a> 命令可以查看当前的环境变量。</li>
</ul>

<p>最后 helm 也会在磁盘汇总存储一些信息，以 Linux 为例：</p>

<ul>
<li>从 Repository 下载的 Chart 的 缓存路径： <code>$HOME/.cache/helm</code>。</li>
<li>关于 Repository 的配置会存储到 helm 的配置文件目录中：<code>$HOME/.config/helm</code>。</li>
<li>数据目录：<code>$HOME/.local/share/helm</code>。</li>
</ul>

<h3 id="chart">Chart</h3>

<h4 id="创建-chart">创建 Chart</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_create/">helm create NAME [flags]</a>  使用给定名称创建新的 chart，如 <code>helm create deploy/charts/myapp</code>。</li>
</ul>

<h4 id="查看-chart-信息">查看 Chart 信息</h4>

<p><a href="https://helm.sh/zh/docs/helm/helm_show/"><code>helm show</code></a>，查看 chart 信息。</p>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_show_all/"><code>helm show all [CHART] [flags]</code></a> - 显示 chart 的所有信息。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_show_chart/"><code>helm show chart [CHART] [flags]</code></a> - 显示 chart 定义。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_show_crds/"><code>helm show crds [CHART] [flags]</code></a> - 显示 chart 的 CRD。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_show_readme/"><code>helm show readme [CHART] [flags]</code></a> - 显示 chart 的 README。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_show_values/"><code>helm show values [CHART] [flags]</code></a> - 显示 chart 的 values。</li>
</ul>

<h4 id="管理-chart-依赖">管理 Chart 依赖</h4>

<p><a href="https://helm.sh/zh/docs/helm/helm_dependency/"><code>helm dependency</code></a>，管理chart依赖。</p>

<p>在 helm 中， <code>helm install</code> 和 <code>helm upgrade</code> 都是都是基于本地文件的内容来操作的。</p>

<p>因此，在执行这些命令之前，需要通过 <code>helm dependency</code> 相关命令把 <code>Chart.yaml</code> 声明的依赖下载下来。保存到上文提到的，每个 chart 都有一个 <code>charts/</code> 目录中。</p>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_dependency_build/"><code>helm dependency build CHART [flags]</code></a> 基于 <code>Chart.lock</code> 文件构建 <code>charts/</code> 目录。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_dependency_list/"><code>helm dependency list CHART [flags]</code></a> 列出给定 Chart 的依赖。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_dependency_update/"><code>helm dependency update CHART [flags]</code></a> 基于 <code>Chart.yaml</code> 内容升级 <code>charts/</code>。</li>
</ul>

<h4 id="将-chart-进行打包">将 Chart 进行打包</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_package/"><code>helm package [CHART_PATH] [...] [flags]</code></a></li>
</ul>

<h4 id="调试-chart">调试 Chart</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_template/"><code>helm template [RELEASE_NAME] [CHART] [flags]</code></a> 本地渲染模板并显示输出。可以通过 <code>--show-only templates/deployment.yaml</code> 命令只渲染单个模板</li>
<li>通过 <code>--debug</code> 和 <code>--dry-run</code> 可以查看 <code>helm install</code> 执行的内容。</li>
</ul>

<h4 id="检查-chart">检查 Chart</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_lint/"><code>helm lint PATH [flags]</code></a> 验证 chart 是否存在问题。</li>
</ul>

<h3 id="repository">Repository</h3>

<h4 id="helm-repository-结构">Helm Repository 结构</h4>

<blockquote>
<p>参考官方文档：<a href="https://helm.sh/zh/docs/topics/chart_repository/">Chart仓库指南</a></p>
</blockquote>

<p>Helm 没有提供一个中心化的 Chart Repository，而是定义了一套 Repository 规范，该规范非常简单：</p>

<ul>
<li>一个 Repository 对应一个 http URL，该 URL 包含一个 index.yaml 文件以及多个 Chart 压缩包。</li>
<li>比如，<a href="https://example.com/charts">https://example.com/charts</a> Repository，存在

<ul>
<li><a href="https://example.com/charts/index.yaml">https://example.com/charts/index.yaml</a> 索引文件（必须）</li>
<li><a href="https://example.com/charts/name-x.y.z.tgz">https://example.com/charts/name-x.y.z.tgz</a> 索引文件对应的 Chart 的压缩包</li>
</ul></li>
</ul>

<p>这个 index.yaml 的一个 demo 为（注意，这个 index.yaml 不需要手动编写。可以通过 helm 命令自动生成）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">apiVersion: v1
entries:
  alpine:
    - created: <span style="color:#e6db74">2016-10-06T16:23:20.499814565</span><span style="color:#ae81ff">-06</span>:<span style="color:#ae81ff">00</span>
      description: Deploy a basic Alpine Linux pod
      digest: 99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd
      home: https://helm.sh/helm
      name: alpine
      sources:
      - https://github.com/helm/helm
      urls:
      - https://technosophos.github.io/tscharts/alpine<span style="color:#ae81ff">-0.2.0</span>.tgz
      version: <span style="color:#ae81ff">0.2.0</span>
    - created: <span style="color:#e6db74">2016-10-06T16:23:20.499543808</span><span style="color:#ae81ff">-06</span>:<span style="color:#ae81ff">00</span>
      description: Deploy a basic Alpine Linux pod
      digest: 515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727
      home: https://helm.sh/helm
      name: alpine
      sources:
      - https://github.com/helm/helm
      urls:
      - https://technosophos.github.io/tscharts/alpine<span style="color:#ae81ff">-0.1.0</span>.tgz
      version: <span style="color:#ae81ff">0.1.0</span>
  nginx:
    - created: <span style="color:#e6db74">2016-10-06T16:23:20.499543808</span><span style="color:#ae81ff">-06</span>:<span style="color:#ae81ff">00</span>
      description: Create a basic nginx HTTP server
      digest: aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff
      home: https://helm.sh/helm
      name: nginx
      sources:
      - https://github.com/helm/charts
      urls:
      - https://technosophos.github.io/tscharts/nginx<span style="color:#ae81ff">-1.1.0</span>.tgz
      version: <span style="color:#ae81ff">1.1.0</span>
generated: <span style="color:#e6db74">2016-10-06T16:23:20.499029981</span><span style="color:#ae81ff">-06</span>:<span style="color:#ae81ff">00</span></code></pre></div>
<p>因此如果想的搭建一个 Repository 是非常容易的，有如下几种方式：</p>

<ul>
<li>使用 Nigix / Apache 等，搭建一个静态站点服务器。</li>
<li>使用 gitHub 静态页面托管服务。</li>
<li>使用开源的 Helm Chart仓库服务器：<a href="https://chartmuseum.com/">ChartMuseum</a>。</li>
<li>其他云厂商提供的服务，</li>
</ul>

<p>如果想将这个仓库分享给其他人使用，有几种方式：</p>

<ul>
<li>公开给给社区搜索，前往 <a href="https://artifacthub.io，将这个站点公开你的仓库，这样其他用户就可以使用了。">https://artifacthub.io，将这个站点公开你的仓库，这样其他用户就可以使用了。</a></li>
<li>手动将仓库 URL 分享给相关人员，通过 helm repo 相关命令使用。</li>
</ul>

<h4 id="通过-oci-registry-分发">通过 OCI Registry 分发</h4>

<p>Helm 3.8.0 之后，已默认开启了对 OCI Registry 的支持。该方式的规范是基于如下两个 OCI 规范：</p>

<ul>
<li><a href="https://github.com/opencontainers/image-spec">Image Format</a></li>
<li><a href="https://github.com/opencontainers/distribution-spec">Distribution Specification</a></li>
</ul>

<p>相信这也是未来 Helm Chart 主流的发布方式。</p>

<p>更多参见官方文档：<a href="https://helm.sh/zh/docs/topics/registries/">注册中心</a>。</p>

<h4 id="向-repository-发布-chart">向 Repository 发布 Chart</h4>

<blockquote>
<p>参考官方文档：<a href="https://helm.sh/zh/docs/topics/chart_repository/">Chart仓库指南</a> | <a href="https://helm.sh/zh/docs/topics/provenance/">Helm来源和完整性</a> | <a href="https://helm.sh/zh/docs/topics/registries/">注册中心</a></p>
</blockquote>

<ul>
<li>参考 <a href="#chart-开发指南">Chart 开发指南</a> 开发一个 Chart。</li>
<li>将 chart 打一个包： <code>helm package charts/alpine</code>（可选的：对包进行签名，参见：<a href="https://helm.sh/zh/docs/topics/provenance/">Helm来源和完整性</a>）</li>
<li>（如果不存在）创建一个仓库目录： <code>mkdir repository/</code></li>
<li>场景 1：发布到采用 Helm Repository 结构的仓库：

<ul>
<li>将包移动到仓库目录中： <code>mv alpine-0.1.0.tgz repository/</code></li>
<li>根据仓库信息重新生成 index.yaml 文件： <code>helm repo index repository/ --url https://example.com/charts</code>，如果是第二次生成，可以添加 <code>--merge</code> 参数。</li>
<li>将 <code>repository/</code> 目录部署到 Web 服务中。</li>
</ul></li>
<li>场景 2：发布到 OCI Registry，更多参见，<a href="https://helm.sh/zh/docs/topics/registries/">注册中心</a>：

<ul>
<li>登录 OCI Registry <code>helm registry login $oci-host</code></li>
<li>发布到 OCI Registry <code>helm push mychart-0.1.0.tgz oci://$oci-host/helm-charts</code></li>
</ul></li>
</ul>

<h4 id="命令说明">命令说明</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo/"><code>helm repo</code></a> 可以用来管理（添加、列出、删除、更新和索引 chart 仓库）远程 Repository。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo_add/"><code>helm repo add [NAME] [URL] [flags]</code></a> 添加chart仓库。例如 <code>helm repo add bitnami https://charts.bitnami.com/bitnami</code>。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo_index/"><code>helm repo index [DIR] [flags]</code></a> 基于包含打包chart的目录，生成索引文件，具体参见上文。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo_list/"><code>helm repo list [flags]</code></a> 列举chart仓库。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo_remove/"><code>helm repo remove [REPO1 [REPO2 ...]] [flags]</code></a> 删除一个或多个仓库。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_repo_update/"><code>helm repo update [REPO1 [REPO2 ...]] [flags]</code></a> 从chart仓库中更新本地可用chart的信息。例如：<code>helm repo update bitnami</code>。</li>
</ul></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_pull/"><code>helm pull [chart URL | repo/chartname] [...] [flags]</code></a> 从仓库下载并（可选）在本地目录解压，如 <code>helm pull bitnami/redis --untar</code>（<code>--untar</code> 表示解压）。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_push/"><code>helm push [chart] [remote] [flags]</code></a> 上传 chart 到 OCI Registry。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_verify/"><code>helm verify PATH [flags]</code></a> 验证给定路径的 chart 已经被签名且有效。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_search/"><code>helm search</code></a> 搜索 Chart。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_search_hub/"><code>helm search hub [KEYWORD] [flags]</code></a> 搜索 <a href="https://artifacthub.io">Artifact Hub</a>，和从网页上搜索效果类似。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_search_repo/"><code>helm search repo [keyword] [flags]</code></a> 搜索本地通过 <code>helm repo add</code> 的 Repository。</li>
</ul></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_registry/"><code>helm registry</code></a> OCI Registry，参见上文。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_registry_login/"><code>helm registry login [host] [flags]</code></a> 对 OCI Registry 进行身份验证。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_registry_logout/"><code>helm registry logout [host] [flags]</code></a> 对 OCI Registry 移除认证信息。</li>
</ul></li>
</ul>

<h3 id="release">Release</h3>

<h4 id="安装">安装</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_install/">helm install [NAME] [CHART] [flags]</a> 安装一个 Chart 为一个 Release。</li>
</ul>

<h4 id="升级">升级</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_upgrade/"><code>helm upgrade [RELEASE] [CHART] [flags]</code></a> 升级一个 Chart 的版本。</li>
</ul>

<h4 id="测试">测试</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_test/"><code>helm test [RELEASE] [flags]</code></a> 对指定发布执行测试。</li>
</ul>

<h4 id="回滚">回滚</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_rollback/">helm rollback <RELEASE> [REVISION] [flags]</a> 回滚 Release 到上一个版本。</li>
</ul>

<h4 id="卸载">卸载</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_uninstall/"><code>helm uninstall RELEASE_NAME [...] [flags]</code></a> 卸载一个 Release。</li>
</ul>

<h4 id="查看">查看</h4>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_list/"><code>helm list [flags]</code></a> 返回当前 namespace（<code>kubectl config get-contexts</code>）下的所有 Release。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_history/"><code>helm history RELEASE_NAME [flags]</code></a> 查看 Release 的历史版本。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_status/"><code>helm status RELEASE_NAME [flags]</code></a> 查看 Release 状态包括：

<ul>
<li>最后部署时间</li>
<li>Release 版本所在的 k8s 命名空间</li>
<li>Release 状态（可选值： unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade 或 pending-rollback）</li>
<li>Release 版本 Revision</li>
<li>Release 版本描述（可以是完成信息或错误信息，需要用 <code>--show-desc</code> 启用）</li>
<li>列举 Release 包含的资源，按类型排序</li>
<li>最后一次测试套件运行的详细信息（如果使用）</li>
<li>Chart 中的 NOTE.txt 的渲染后的结果</li>
</ul></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_get/"><code>helm get</code></a> 获取 Release 的额外信息。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_get_all/"><code>helm get all RELEASE_NAME [flags]</code></a> 打印一个具有可读性的信息集合，包括注释，钩子，提供的values，以及给定版本生成的清单文件。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_get_hooks/"><code>helm get hooks RELEASE_NAME [flags]</code></a> 获取 Release 的钩子</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_get_manifest/"><code>helm get manifest RELEASE_NAME [flags]</code></a> 获取 Release 的 Kubernetes 声明式配置。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_get_notes/"><code>helm get notes RELEASE_NAME [flags]</code></a> 获取 Release 的 NOTE.txt 渲染结果。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_get_values/"><code>helm get values RELEASE_NAME [flags]</code></a> 获取 Release 的 values。</li>
</ul></li>
</ul>

<h3 id="其他命令">其他命令</h3>

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_plugin/"><code>helm plugin</code></a> 安装、列举或卸载Helm插件。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_plugin_install/"><code>helm plugin install [options] &lt;path|url&gt;... [flags]</code></a> 该命令允许您通过VCS仓库url或者本地路径安装插件。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_plugin_list/"><code>helm plugin list [flags]</code></a> 列举已安装的Helm插件</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_plugin_uninstall/"><code>helm plugin uninstall &lt;plugin&gt;... [flags]</code></a> 卸载一个或多个Helm插件</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_plugin_update/"><code>helm plugin update &lt;plugin&gt;... [flags]</code></a> 升级一个或多个Helm插件。</li>
</ul></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_version/"><code>helm version</code></a> 打印 Helm CLI 版本。</li>
<li><a href="https://helm.sh/zh/docs/helm/helm_completion/"><code>helm completion</code></a> 为各种 shell 生成自动补全脚本。

<ul>
<li><a href="https://helm.sh/zh/docs/helm/helm_completion_bash/">bash</a></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_completion_fish/">fish</a></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_completion_powershell/">powershell</a></li>
<li><a href="https://helm.sh/zh/docs/helm/helm_completion_zsh/">zsh</a></li>
</ul></li>
</ul>

<h2 id="场景">场景</h2>

<h3 id="动态依赖">动态依赖</h3>

<p>比如，不懂的部署场景，外部依赖不同，比如：</p>

<ul>
<li>部署环境 1：直接使用外部 MySQL，而不需要在 Kubernetes 中部署。</li>
<li>部署环境 2：需要在 Kubernetes 中部署 MySQL。</li>
</ul>

<p>这个场景的实现，参考：<a href="#chart-yaml-详解">Chart.yaml 详解 - dependencies 的 condition 和 tags 说明</a>。</p>

<h3 id="等待依赖就绪">等待依赖就绪</h3>

<p>如果想控制服务粒度的启动顺序的前后依赖（比如 App 依赖 MySQL）。</p>

<p>则需要利用 kubernetes 的 init-containers 机制来实现，参考：<a href="#chart-模板渲染结果-apply-顺序">Chart 模板渲染结果 apply 顺序</a></p>

<h3 id="数据库初始化和迁移">数据库初始化和迁移</h3>

<p>利用 Kubernetes 的 init-container 或者 <a href="#生命周期-和-hook">helm hook</a>，通过将 <code>.Chart.appVersion</code>、<code>.Release.IsUpgrade</code> 传递给 container 执行一些迁移/回滚命令。</p>
]]></description></item><item><title>Linux 网络虚拟化技术（三）bridge 虚拟设备</title><link>https://www.rectcircle.cn/posts/linux-net-virual-03-bridge/</link><pubDate>Sun, 15 May 2022 00:24:21 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-03-bridge/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考：<a href="https://wiki.archlinux.org/title/Network_bridge">Network bridge</a> | <a href="https://segmentfault.com/a/1190000009491002">Linux虚拟网络设备之bridge(桥)</a></p>
</blockquote>

<h2 id="简介">简介</h2>

<p>bridge 桥，是 Linux 中的一种虚拟网络设备，一般用于虚拟机或容器的网络流量管理。</p>

<p>bridge 具有现实中二层交换机的一切特性：判断包的类别（广播/单点），查找内部 MAC 端口映射表，定位目标端口号，将数据转发到目标端口或丢弃，自动更新内部 MAC 端口映射表以自我学习。</p>

<p>此外，bridge 设备自己有一个 Mac 地址，并可以绑定一个 IP 地址。换句话说 bridge 存在一个隐含的网络接口连接到本机的 Network Protocol Stack。</p>

<p>物理设备（如 eth）和虚拟设备（上篇提到的 veth 以及下文即将提到的 tap/tun）均可连接到 bridge 中，一个设备连接到 bridge 意味着：</p>

<ul>
<li>该设备配置其主（master）设备为该 bridge，该设备自身变成从设备 （slave）（brport）。</li>
<li>换句话说。该设备变成了一根网线，Mac 地址变得没有意义。

<ul>
<li>发送到该设备的数据，不做任何逻辑判断，直接到达 bridge。</li>
<li>bridge 发送该设备的数据，不做任何逻辑判断，直接到达该设备的另一端。</li>
</ul></li>

<li><p>也就是说，这个设备的另一端看到的是 bridge 的 Mac 地址。如下图所示：</p>

<ul>
<li>eth0 网线的另一端看到的是 br0 的 Mac 地址。</li>

<li><p>veth1 看到的是 br0 的 Mac 地址。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Network Protocol Stack             |       |
|       +------------------------------------------------+       |
|                         ↑                           ↑          |
|.........................|...........................|..........|
|                         ↓                           ↓          |
|        +------+     +--------+     +-------+    +-------+      |
|        |      |     |Mac Addr|     |       |    |       |      |
|        +------+     +--------+     +-------+    +-------+      |
|        | eth0 |&lt;---&gt;|   br0  |&lt;---&gt;| veth0 |    | veth1 |      |
|        +------+     +--------+     +-------+    +-------+      |
|            ↑                           ↑            ↑          |
|            |                           |            |          |
|            |                           +------------+          |
|            |                                                   |
+------------|---------------------------------------------------+
     ↓
Physical Network</pre></div></li>
</ul></li>
</ul>

<p>此外，当一个 bridge 连接了 1 个或多个从设备后，该 bridge 的 mac 地址将变成这些从设备的 mac 地址中的一个。</p>

<h2 id="常见操作和命令">常见操作和命令</h2>

<p>在 Linux 中，有很多命令行工具可以操作 bridge，如：</p>

<ul>
<li><code>iproute2</code> 包：<code>ip</code>、<code>bridge</code>。</li>
<li><code>bridge-utils</code> 包：<code>brctl</code>（已废弃）。</li>
<li><a href="https://wiki.archlinux.org/title/Bridge_with_netctl"><code>netctl</code> 包</a>。</li>
<li><a href="https://wiki.archlinux.org/title/Systemd-networkd#Bridge_interface">systemd-networkd 包</a>。</li>
</ul>

<p>本部分仅介绍 <code>iproute2</code> 包提供的命令。其他参见：<a href="https://wiki.archlinux.org/title/Network_bridge">Network bridge</a>。</p>

<p>下文描述的 bridge 名为 <code>br0</code>。</p>

<h3 id="创建并启动">创建并启动</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo ip link add name br0 type bridge
sudo ip link set br0 up</code></pre></div>
<h3 id="连接-断开其他设备">连接/断开其他设备</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 连接</span>
ip link set 设备名 master br0
<span style="color:#75715e"># 断开</span>
ip link set 设备名 nomaster</code></pre></div>
<h3 id="查看连接设备">查看连接设备</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo bridge link</code></pre></div>
<h3 id="分配-ip">分配 IP</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ip address add dev bridge_name <span style="color:#ae81ff">192</span>.168.66.66/24</code></pre></div>
<h3 id="删除">删除</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ip link delete bridge_name type bridge</code></pre></div>
<h2 id="go-api">Go API</h2>

<p>其 API 风格和 <code>ip</code> 命令类似，因此参考：<a href="https://pkg.go.dev/github.com/vishvananda/netlink">vishvananda/netlink docs</a> ，调用对应函数即可实现。</p>

<h2 id="实验和说明">实验和说明</h2>

<p>首先创建，名为 br0 的 bridge，名为 veth0/veth0peer、veth1/veth1peer 的两对 veth。</p>

<p>并将物理网卡 enp0s9、veth0、veth1 连接到 br0 中。</p>

<p>然后分配 IP：</p>

<ul>
<li>删除 enp0s9 的 IP。</li>
<li>veth0peer 配置 IP <code>192.168.57.4</code>。</li>
<li>veth1peer 配置 IP <code>192.168.57.5</code>。</li>
</ul>

<p>然后，然后观察 br0、veth0peer、veth1peer 的 Mac 地址，使用 ping 观察连通性。</p>

<p>拓扑如下图所示：</p>

<p>最后，为 br0 配置 IP 地址为 <code>192.168.57.3</code>。再次使用 ping 观察连通性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+-----------------------------------------------------------------------+
|                                                                       |
|       +------------------------------------------------+              |
|       |             Network Protocol Stack             |              |
|       +------------------------------------------------+              |
|                         ↑                           ↑                 |
|.........................|...........................|.................|
|                         ↓                           ↓                 |
|        +------+     +--------+     +-------+    +---------+           |
|        |      |     | .57.3  |     |       |    |.57.4    |           |
|        +------+     +--------+     +-------+    +---------+           |
|        |enp0s9|&lt;---&gt;|   br0  |&lt;---&gt;| veth0 |    |veth0peer|    veth.. |
|        +------+     +--------+     +-------+    +---------+           |
|            ↑                           ↑            ↑                 |
|            |                           |            |                 |
|            |                           +------------+                 |
|            |                                                          |
+------------|----------------------------------------------------------+
             ↓
     Physical Network</pre></div>
<p>注意：如上拓扑仅仅为了介绍 Bridge 的特性。该模型，无法在生产环境使用。</p>

<h3 id="实验准备">实验准备</h3>

<p>为待实验拟虚拟机（VirtualBox）添加一个新的网卡：</p>

<ul>
<li>新建一个网络界面：vboxnet1，地址 <code>192.168.57.1/24</code>。</li>
<li>网卡 3：仅主机网络，界面名称 vboxnet1，启用混杂模式。</li>

<li><p>启动虚拟机</p>

<ul>
<li><p>修改 <code>/etc/network/interfaces</code> 添加</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">auto enp0s9
iface enp0s9 inet dhcp</pre></div></li>

<li><p>应用网络配置 <code>sudo systemctl restart networking.service</code></p></li>
</ul></li>
</ul>

<p>此时，网络拓扑，如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Network Protocol Stack             |       |
|       +------------------------------------------------+       |
|            ↑                                                   |
|............|...................................................|
|            ↓                                                   |
|        +------+                                                |
|        |.57.3 |                                                |
|        +------+                                                |
|        |enp0s9|                                                |
|        +------+                                                |
|            ↑                                                   |
|            |                                                   |
|            |                                                   |
|            |                                                   |
+------------|---------------------------------------------------+
             ↓
     Physical Network</pre></div>
<p>执行 <code>ip addr show</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">...
4: enp0s9: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:fd:98:73 brd ff:ff:ff:ff:ff:ff
    inet 192.168.57.3/24 brd 192.168.57.255 scope global dynamic enp0s9
       valid_lft 469sec preferred_lft 469sec
    inet6 fe80::a00:27ff:fefd:9873/64 scope link 
       valid_lft forever preferred_lft forever</pre></div>
<h3 id="bridge-不配置-ip">bridge 不配置 IP</h3>

<p>操作过程如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建两对 veth</span>
sudo ip link add veth0 type veth peer name veth0peer
sudo ip link add veth1 type veth peer name veth1peer
<span style="color:#75715e"># 给这两对 veth 的对侧配置 ip 地址</span>
sudo ip addr add <span style="color:#ae81ff">192</span>.168.57.4/24 dev veth0peer
sudo ip addr add <span style="color:#ae81ff">192</span>.168.57.5/24 dev veth1peer
<span style="color:#75715e"># 启动这两对网卡</span>
sudo ip link set veth0 up
sudo ip link set veth1 up
sudo ip link set veth0peer up
sudo ip link set veth1peer up
<span style="color:#75715e"># 允许从非 lo 设备进来的数据包的源 IP 地址是本机地址</span>
sudo sysctl -w net.ipv4.conf.veth0.accept_local<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
sudo sysctl -w net.ipv4.conf.veth0peer.accept_local<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
sudo sysctl -w net.ipv4.conf.veth1.accept_local<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
sudo sysctl -w net.ipv4.conf.veth1peer.accept_local<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>

<span style="color:#75715e"># 删除物理网络接口 enp0s9 的 IP 地址</span>
sudo ip addr del <span style="color:#ae81ff">192</span>.168.57.3/24 dev enp0s9

<span style="color:#75715e"># 创建设置并启动 br0</span>
sudo ip link add name br0 type bridge
sudo ip link set dev veth0 master br0
sudo ip link set dev veth1 master br0
sudo ip link set dev enp0s9 master br0
sudo ip link set br0 up</code></pre></div>
<p>此时网络拓扑如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+-----------------------------------------------------------------------+
|                                                                       |
|       +------------------------------------------------+              |
|       |             Network Protocol Stack             |              |
|       +------------------------------------------------+              |
|                                                     ↑                 |
|.....................................................|.................|
|                                                     ↓                 |
|        +------+     +--------+     +-------+    +---------+           |
|        |      |     |        |     |       |    |.57.4    |   .57.5   |
|        +------+     +--------+     +-------+    +---------+           |
|        |enp0s9|&lt;---&gt;|   br0  |&lt;---&gt;| veth0 |    |veth0peer|    veth.. |
|        +------+     +--------+     +-------+    +---------+           |
|            ↑                           ↑            ↑                 |
|            |                           |            |                 |
|            |                           +------------+                 |
|            |                                                          |
+------------|----------------------------------------------------------+
             ↓
     Physical Network</pre></div>
<h4 id="通过-veth0-ping-veth0peer">通过 veth0 ping veth0peer</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ping -c 1 192.168.57.4 -I veth0</pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ping: Warning: source address might be selected on device other than: veth0
PING 192.168.57.4 (192.168.57.4) from 10.0.2.15 veth0: 56(84) bytes of data.

--- 192.168.57.4 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms</pre></div>
<p>可以看出，无法 ping 通。整个流量路径为：</p>

<ul>
<li>Network Protocol Stack 通过 veth0 发起 arp 请求（<code>sudo tcpdump -n -i veth0</code>）。</li>
<li>veth0peer 收到请求，发送 arp 回复（<code>sudo tcpdump -n -i veth0peer</code>）。</li>
<li>veth0 收到 arp 回复，直接<strong>发送给 br0</strong>（<code>sudo tcpdump -n -i veth0</code>）。</li>
<li>br0 没有配置 ip，不会将 arp 回复到 Network Protocol Stack（<code>sudo tcpdump -n -i br0</code>），所以直接丢弃。</li>
</ul>

<h4 id="通过-veth0peer-ping-veth1peer">通过 veth0peer ping veth1peer</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ping -c 2 192.168.57.5 -I veth0peer</pre></div>
<p>输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">PING 192.168.57.5 (192.168.57.5) from 192.168.57.4 veth0peer: 56(84) bytes of data.
64 bytes from 192.168.57.5: icmp_seq=1 ttl=64 time=0.070 ms

--- 192.168.57.5 ping statistics ---
2 packets transmitted, 1 received, 50% packet loss, time 1026ms
rtt min/avg/max/mdev = 0.070/0.070/0.070/0.000 ms</pre></div>
<p>可以看出第一次可以 ping 通，可以这么理解： 网络接口 veth0peer、veth1peer 和 二层交换机 br0 相连。</p>

<p>第二次无法 ping 通的原因，原因参见下文：<a href="#拓扑和流量分析">拓扑和流量分析</a>。</p>

<h4 id="在虚拟机外部-ping">在虚拟机外部 ping</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ping 192.168.57.5</pre></div>
<p>经测试（Mac VirtualBox v 6.1.34），时通时不通，原因参见下文：<a href="#拓扑和流量分析">拓扑和流量分析</a>。</p>

<h3 id="bridge-配置-ip">bridge 配置 IP</h3>

<p>在上文 <a href="#bridge-不配置-ip">bridge 不配置 IP</a> 的基础上，执行如下命令。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo ip addr add 192.168.57.3/24 dev br0</pre></div>
<p>此时网络拓扑如下图所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+-----------------------------------------------------------------------+
|                                                                       |
|       +------------------------------------------------+              |
|       |             Network Protocol Stack             |              |
|       +------------------------------------------------+              |
|                                                     ↑                 |
|.....................................................|.................|
|                                                     ↓                 |
|        +------+     +--------+     +-------+    +---------+           |
|        |      |     |.57.3   |     |       |    |.57.4    |   .57.5   |
|        +------+     +--------+     +-------+    +---------+           |
|        |enp0s9|&lt;---&gt;|   br0  |&lt;---&gt;| veth0 |    |veth0peer|    veth.. |
|        +------+     +--------+     +-------+    +---------+           |
|            ↑                           ↑            ↑                 |
|            |                           |            |                 |
|            |                           +------------+                 |
|            |                                                          |
+------------|----------------------------------------------------------+
             ↓
     Physical Network</pre></div>
<h4 id="通过-veth0-ping-veth0peer-1">通过 veth0 ping veth0peer</h4>

<p>和不配置 ip 效果一致。</p>

<h4 id="通过-veth0peer-ping-veth1peer-1">通过 veth0peer ping veth1peer</h4>

<p>和不配置 ip 效果一致。</p>

<h4 id="在虚拟机外部-ping-1">在虚拟机外部 ping</h4>

<p>和不配置 ip 效果一致。</p>

<p>ping br0 的 ip 地址仍然是，时通时不通，原因参见下文：<a href="#拓扑和流量分析">拓扑和流量分析</a>。</p>

<h3 id="恢复现场">恢复现场</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo ip link delete veth0
sudo ip link delete veth1
sudo ip link delete br0
sudo ip addr add 192.168.57.3/24 dev enp0s9</pre></div>
<h3 id="拓扑和流量分析">拓扑和流量分析</h3>

<p>我们知道，bridge 对应的是物理设备的二层交换机，我们将上述实验拓扑图可以简化为下图：</p>

<ul>
<li>br0 bridge 的就是二层交换机。</li>
<li>br0 是二层交换机自带的一个网络接口 ，和 Network Protocol Stack 相连。</li>
<li>enp0s9、veth0、veth1 是二层交换机的接口。</li>

<li><p>gateway （enp0s9 对应的物理网关）、veth0peer、veth1peer 是网络接口</p>

<ul>
<li>一端通过 link （物理或虚拟链路） 和二层交换机的接口相连。</li>

<li><p>另一端和一个 Network Protocol Stack 相连，有如下几种情况：</p>

<ul>
<li>和 br0 处于同一 Network Protocol Stack（本实验的 vethxpeer 是这种）。</li>
<li>和 br0 处于不同的 Network Protocol Stack（即不同的 network namespace，真正在生产环境常见的）。</li>

<li><p>其他物理设备的 Network Protocol Stack （如路由器/虚拟机所在宿主机）（本实验的 enp0s9是这种）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Network Protocol Stack -----------------+
|                             |
br0                            |        network interface (default)
|                             |
br0 bridge                        |
|                             |
+--------------------+                |
|       |            |                |
enp0s9   veth0        veth1              |        bridge interface
|       |            |                |        link
gateway  veth0peer    veth1peer          |        network interface (others)
|            |                |
+------------+----------------+</pre></div></li>
</ul></li>
</ul></li>
</ul>

<h4 id="为什么-enp0s9-需要开启混杂模式">为什么 enp0s9 需要开启混杂模式？</h4>

<p>按照如上实验，veth0peer、veth1peer、br0 网络接口处于同一个 Network Protocol Stack，具有独立的 Mac 地址。因此，当 gateway 需要访问，veth0、veth1 或 br0 时，在数据链路层目标 Mac 地址并非是 enp0s9 地址，而是 veth0、veth1 或 br0 的地址。</p>

<p>如果不开启混杂模式，enp0s9 将会直接丢弃这些二层数据包，导致无法访问。（因为在虚拟机中进行实验，所以需要在虚拟机配置页面配置开启混杂模式，而不是通过命令</p>

<h4 id="为什么上文实验时通时不通">为什么上文实验时通时不通</h4>

<p>通过执行如下命令，发现几个奇怪的点，但是并不确定这些是不是问题的原因。</p>

<ul>
<li>抓取 网络接口 数据包

<ul>
<li><code>sudo tcpdump -e -n -i br0</code></li>
<li><code>sudo tcpdump -e -n -i veth0peer</code></li>
<li><code>sudo tcpdump -e -n -i veth1peer</code></li>
<li><code>sudo tcpdump -e -n -i enp0s9</code></li>
</ul></li>
<li>查看 网络接口 地址 <code>ip addr show</code></li>
<li>参看 bridge 的 fdb （Mac 地址表） <code>sudo bridge fdb show | grep br0</code></li>
</ul>

<p><strong>奇怪点一</strong></p>

<p>通过多次执行 <code>sudo bridge fdb show | grep br0</code> 发现，ping 不通的时候，veth0peer、veth1peer 的 mac 地址对应的二层交换机的接口并不是 veth0 和 veth1，而变成了 enp0s9。通过抓包，确实可以看到，enp0s9 收到了 ping 请求。</p>

<p>这一点是可以确定是 ping 不通的表面原因。但是为什么会出现 Mac 地址表错误记录。并没有定位到，可能的方向是：</p>

<ul>
<li><code>ICMP6, router solicitation</code> 相关包导致的，经观察，当出现这些包时，Mac 地址表出现错误。</li>
<li>第一次 ping 基本上都能 ping 通，从第二次开始 ping 不通，经过多次抓包，发现 ARP 有些诡异。具体参见下文奇怪点二。</li>
</ul>

<p><strong>奇怪点二</strong></p>

<p>假设通过 veth0peer ping veth1peer，此时 Network Protocol Stack 会通过 veth0peer 发起一个 arp 请求。因为 arp 请求在数据链路层是广播，会到达 br0、gateway、veth0peer、veth1peer，按照直觉来看，只有 veth1peer 回复了 arp。</p>

<p>但是，Linux 的处理是：veth0peer、veth1peer 都会回复 arp，从 br0 抓包来看，即回复了 veth1peer 的 mac 地址也回复了 veth0peer 的 mac，而且收到了 4 个回复包。</p>

<p>这一点，<a href="https://lwn.net/Articles/45373/">Harping on ARP</a> 似乎有所讨论，但是不太理解。</p>

<h3 id="其他说明">其他说明</h3>

<ul>
<li>从上文可以看出， Bridge 和 连接到 Bridge 的 veth 网络接口（如 veth0peer、veth1peer）  处于同一 network namespace，则会出现 arp 问题。所以在生产环境：连接 bridge 的 veth 网络接口一般位于独立的 network namespace。</li>
<li>上文可以看出，外部网络通过 enp0s9 访问 veth0peer、veth1perr，mac 地址就不是物理网卡的 mac 地址，而是这两个网络接口的 mac 地址，因此如果 enp0s9 是一个 wifi 网络接口，则无法访问，因为 wifi 认证是绑定 mac 地址的，解决办法是（均为测试过）：

<ul>
<li>方式一：将 veth0peer、veth1peer 等网络接口的地址配置的和 enp0s9 一致。</li>
<li>方式二：参见 <a href="https://wiki.archlinux.org/title/Network_bridge#Wireless_interface_on_a_bridge">wiki</a>。</li>
</ul></li>
</ul>

<h2 id="bridge-生产环境实例">Bridge 生产环境实例</h2>

<h3 id="个人设备虚拟机">个人设备虚拟机</h3>

<ul>
<li>bridge 通过 tun/tap 和虚拟机网络接口相连。</li>
<li>bridge 物理网络接口 eth0 和外部网络相连。</li>

<li><p>bridge 不配置 ip 地址。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+-----------------------------------------+-----------------------------------------+
|                          Host                                  |              VirtualMachine1            |              VirtualMachine2            |
|                                                                |                                         |                                         |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|       |             Newwork Protocol Stack             |       |       |  Newwork Protocol Stack |       |       |  Newwork Protocol Stack |       |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|                          ↑                                     |                   ↑                     |                    ↑                    |
|..........................|.....................................|...................|.....................|....................|....................|
|                          ↓                                     |                   ↓                     |                    ↓                    |
|                     +--------+                                 |               +-------+                 |                +-------+                |
|                     | .3.101 |                                 |               | .3.102|                 |                | .3.103|                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|        | eth0 |&lt;---&gt;|   br0  |&lt;---&gt;|tun/tap|                   |               | eth0  |                 |                | eth0  |                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|            ↑             ↑             ↑                       |                   ↑                     |                    ↑                    |
|            |             |             +-------------------------------------------+                     |                    |                    |
|            |             ↓                                     |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |         |tun/tap|                                 |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |             ↑                                     |                                         |                    |                    |
|            |             +-------------------------------------------------------------------------------|--------------------+                    |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
+------------|---------------------------------------------------+-----------------------------------------+-----------------------------------------+
         ↓
 Physical Network  (192.168.3.0/24)</pre></div></li>
</ul>

<p>该拓扑来自博客：<a href="https://segmentfault.com/a/1190000009491002#item-6-1">Linux虚拟网络设备之bridge(桥)</a>，未经实验。</p>

<h3 id="docker-bridge-网络">Docker Bridge 网络</h3>

<ul>
<li>bridge 内置网络接口和连接到该 bridge 的 veth 的网络接口，位于一个私有网段。</li>
<li>bridge 内置网络接口 ip 是该私有网段的网关 ip。</li>
<li>bridge 通过 veth 和位于独立 network namespace 的网络接口相连。</li>

<li><p>流量先到达内置网络接口（网关），然后通过 NAT 通过物理网络接口 eth0 与外界网络连通。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+-----------------------------------------+-----------------------------------------+
|                          Host                                  |              Container 1                |              Container 2                |
|                                                                |                                         |                                         |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|       |             Newwork Protocol Stack             |       |       |  Newwork Protocol Stack |       |       |  Newwork Protocol Stack |       |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|            ↑             ↑                                     |                   ↑                     |                    ↑                    |
|............|.............|.....................................|...................|.....................|....................|....................|
|            ↓             ↓                                     |                   ↓                     |                    ↓                    |
|        +------+     +--------+                                 |               +-------+                 |                +-------+                |
|        |.3.101|     |  .9.1  |                                 |               |  .9.2 |                 |                |  .9.3 |                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|        | eth0 |     |   br0  |&lt;---&gt;|  veth |                   |               | eth0  |                 |                | eth0  |                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|            ↑             ↑             ↑                       |                   ↑                     |                    ↑                    |
|            |             |             +-------------------------------------------+                     |                    |                    |
|            |             ↓                                     |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |         |  veth |                                 |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |             ↑                                     |                                         |                    |                    |
|            |             +-------------------------------------------------------------------------------|--------------------+                    |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
+------------|---------------------------------------------------+-----------------------------------------+-----------------------------------------+
         ↓
 Physical Network  (192.168.3.0/24)</pre></div></li>
</ul>

<p>该拓扑来自博客：<a href="https://segmentfault.com/a/1190000009491002#item-6-1">Linux虚拟网络设备之bridge(桥)</a>，是 Docker 的默认网络以及自定义 bridge 网络的模型。</p>

<p>在下一篇介绍 netfilter 中，将介绍实战搭建该网络模型。</p>
]]></description></item><item><title>Linux 网络虚拟化技术（二）veth 虚拟设备</title><link>https://www.rectcircle.cn/posts/linux-net-virual-02-veth/</link><pubDate>Tue, 03 May 2022 23:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-02-veth/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考： <a href="https://segmentfault.com/a/1190000009251098">Linux虚拟网络设备之veth</a></p>
</blockquote>

<h2 id="功能特性">功能特性</h2>

<p>veth 即 virtual ethernet device，是对物理一台网卡的模拟。功能和物理以太网设备类似。此外有如下特点：</p>

<ul>
<li>veth 的一端连接着内核网络协议栈。</li>
<li>veth 设备是成对出现的，两个设备彼此相连（就位于两个主机的 eth 通过网线连接一样）。</li>
<li>一个设备收到协议栈的数据发送请求后，会将数据发送到另一个设备上去。</li>
</ul>

<p>如上特点图示如下（Network Protocol Stack 指 3 层协议栈）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Network Protocol Stack             |       |
|       +------------------------------------------------+       |
|              ↑               ↑               ↑                 |
|..............|...............|...............|.................|
|              ↓               ↓               ↓                 |
|        +----------+    +-----------+   +-----------+           |
|        |  enp0s3  |    |   veth0   |   | veth0peer |           |
|        +----------+    +-----------+   +-----------+           |
|10.0.2.15     ↑               ↑               ↑                 |
|              |               +---------------+                 |
|              |         192.168.4.2      192.168.4.3            |
+--------------|-------------------------------------------------+
               ↓
         Physical Network</pre></div>
<h2 id="实验设计">实验设计</h2>

<p>在一台虚拟机上实现上图所示的拓扑模型。并验证通过 <code>ping</code> 检查网络是否畅通。</p>

<h2 id="实验源码">实验源码</h2>

<h3 id="shell-描述">Shell 描述</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># 观察网卡情况</span>
echo <span style="color:#e6db74">&#39;===初始状态网络设备&#39;</span>
ip addr show
echo

echo <span style="color:#e6db74">&#39;===初始状态 arp 表&#39;</span>
cat /proc/net/arp
echo

echo <span style="color:#e6db74">&#39;===创建并配置veth&#39;</span>
<span style="color:#75715e"># 创建一对 veth</span>
sudo ip link add veth0 type veth peer name veth0peer
<span style="color:#75715e"># 给这一对 veth 配置 ip 地址</span>
sudo ip addr add <span style="color:#ae81ff">192</span>.168.4.2/24 dev veth0
sudo ip addr add <span style="color:#ae81ff">192</span>.168.4.3/24 dev veth0peer
<span style="color:#75715e"># 启动这两个网卡</span>
sudo ip link set veth0 up
sudo ip link set veth0peer up
<span style="color:#75715e"># 允许从非 lo 设备进来的数据包的源 IP 地址是本机地址</span>
sudo sysctl -w net.ipv4.conf.veth0.accept_local <span style="color:#ae81ff">1</span>
sudo sysctl -w net.ipv4.conf.veth0peer.accept_local <span style="color:#ae81ff">1</span>
echo <span style="color:#e6db74">&#39;完成创建并配置veth&#39;</span>
echo

<span style="color:#75715e"># 观察 arp</span>
echo <span style="color:#e6db74">&#39;===配置完 veth 后网络设备&#39;</span>
ip addr show
echo

<span style="color:#75715e"># 实验</span>
echo <span style="color:#e6db74">&#39;===尝试是否可以 ping 通&#39;</span>
ping -c <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">192</span>.168.4.3 -I veth0
echo

echo <span style="color:#e6db74">&#39;===ping 完成后 arp 表&#39;</span>
cat /proc/net/arp
echo

<span style="color:#75715e"># 恢复现场</span>
sudo ip link delete veth0</code></pre></div>
<h3 id="go-语言描述">Go 语言描述</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#75715e">// sudo go ./src/go/01-veth/
</span><span style="color:#75715e"></span>
<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;net&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>

	<span style="color:#a6e22e">sysctl</span> <span style="color:#e6db74">&#34;github.com/lorenzosaino/go-sysctl&#34;</span>
	<span style="color:#e6db74">&#34;github.com/vishvananda/netlink&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">beforeScript</span> = <span style="color:#e6db74">&#34;echo &#39;===初始状态网络设备&#39; &amp;&amp; ip addr show &amp;&amp; echo&#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34; &amp;&amp; echo &#39;===初始状态 arp 表&#39; &amp;&amp; cat /proc/net/arp &amp;&amp; echo&#34;</span>
	<span style="color:#a6e22e">afterScript</span> = <span style="color:#e6db74">&#34;echo &#39;===配置完 veth 后网络设备&#39; &amp;&amp; ip addr show &amp;&amp; echo&#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34; &amp;&amp; echo &#39;===尝试是否可以 ping 通&#39; &amp;&amp; ping -c 4 192.168.4.3 -I veth0 &amp;&amp; echo&#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34; &amp;&amp; echo &#39;===ping 完成后 arp 表&#39; &amp;&amp; cat /proc/net/arp &amp;&amp; echo &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34; &amp;&amp; sudo ip link delete veth0&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runtScript</span>(<span style="color:#a6e22e">script</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#75715e">// 输出初始化状态
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">runtScript</span>(<span style="color:#a6e22e">beforeScript</span>))

	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;===创建并配置veth&#34;</span>)
	<span style="color:#75715e">// 创建一对 veth
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAdd</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Veth</span>{
		<span style="color:#a6e22e">LinkAttrs</span>: <span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;veth0&#34;</span>,
		},
		<span style="color:#a6e22e">PeerName</span>: <span style="color:#e6db74">&#34;veth0peer&#34;</span>,
	}))
	<span style="color:#75715e">// 配置 ip 地址
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ip</span>, <span style="color:#a6e22e">ipNet</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">ParseCIDR</span>(<span style="color:#e6db74">&#34;192.168.4.2/24&#34;</span>)
	<span style="color:#a6e22e">ipNet</span>.<span style="color:#a6e22e">IP</span> = <span style="color:#a6e22e">ip</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">AddrAdd</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;veth0&#34;</span>}), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Addr</span>{<span style="color:#a6e22e">IPNet</span>: <span style="color:#a6e22e">ipNet</span>}))

	<span style="color:#a6e22e">ip</span>, <span style="color:#a6e22e">ipNet</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">ParseCIDR</span>(<span style="color:#e6db74">&#34;192.168.4.3/24&#34;</span>)
	<span style="color:#a6e22e">ipNet</span>.<span style="color:#a6e22e">IP</span> = <span style="color:#a6e22e">ip</span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">AddrAdd</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;veth0peer&#34;</span>}), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">Addr</span>{<span style="color:#a6e22e">IPNet</span>: <span style="color:#a6e22e">ipNet</span>}))

	<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetUp</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;veth0&#34;</span>}))
	<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkSetUp</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">NewLinkBond</span>(<span style="color:#a6e22e">netlink</span>.<span style="color:#a6e22e">LinkAttrs</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;veth0peer&#34;</span>}))

	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">sysctl</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;net.ipv4.conf.%s.accept_local&#34;</span>, <span style="color:#e6db74">&#34;veth0&#34;</span>), <span style="color:#e6db74">&#34;1&#34;</span>))
	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">sysctl</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;net.ipv4.conf.%s.accept_local&#34;</span>, <span style="color:#e6db74">&#34;veth0peer&#34;</span>), <span style="color:#e6db74">&#34;1&#34;</span>))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;完成创建并配置veth&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

	<span style="color:#75715e">// 实验
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">panicIfErr</span>(<span style="color:#a6e22e">runtScript</span>(<span style="color:#a6e22e">afterScript</span>))
}</code></pre></div>
<h2 id="实验输出和解释">实验输出和解释</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">===初始状态网络设备
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:7d:99:1d brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 72349sec preferred_lft 72349sec
    inet6 fe80::a00:27ff:fe7d:991d/64 scope link 
       valid_lft forever preferred_lft forever
3: enp0s8: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:06:ff:a6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.3/24 brd 192.168.56.255 scope global dynamic enp0s8
       valid_lft 466sec preferred_lft 466sec
    inet6 fe80::a00:27ff:fe06:ffa6/64 scope link 
       valid_lft forever preferred_lft forever

===初始状态 arp 表
IP address       HW type     Flags       HW address            Mask     Device
192.168.56.2     0x1         0x2         08:00:27:fa:d4:ec     *        enp0s8
169.254.169.254  0x1         0x0         00:00:00:00:00:00     *        enp0s3
10.0.2.2         0x1         0x2         52:54:00:12:35:02     *        enp0s3
192.168.56.1     0x1         0x2         0a:00:27:00:00:00     *        enp0s8

===创建并配置veth
net.ipv4.conf.veth0.accept_local = 1
net.ipv4.conf.veth0peer.accept_local = 1
完成创建并配置veth

===配置完 veth 后网络设备
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:7d:99:1d brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 72349sec preferred_lft 72349sec
    inet6 fe80::a00:27ff:fe7d:991d/64 scope link 
       valid_lft forever preferred_lft forever
3: enp0s8: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:06:ff:a6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.3/24 brd 192.168.56.255 scope global dynamic enp0s8
       valid_lft 466sec preferred_lft 466sec
    inet6 fe80::a00:27ff:fe06:ffa6/64 scope link 
       valid_lft forever preferred_lft forever
14: veth0peer@veth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ca:02:25:cf:ce:da brd ff:ff:ff:ff:ff:ff
    inet 192.168.4.3/24 scope global veth0peer
       valid_lft forever preferred_lft forever
    inet6 fe80::c802:25ff:fecf:ceda/64 scope link tentative 
       valid_lft forever preferred_lft forever
15: veth0@veth0peer: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 7a:2d:96:17:8d:bc brd ff:ff:ff:ff:ff:ff
    inet 192.168.4.2/24 scope global veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::782d:96ff:fe17:8dbc/64 scope link tentative 
       valid_lft forever preferred_lft forever

===尝试是否可以 ping 通
PING 192.168.4.3 (192.168.4.3) from 192.168.4.2 veth0: 56(84) bytes of data.
64 bytes from 192.168.4.3: icmp_seq=1 ttl=64 time=0.032 ms
64 bytes from 192.168.4.3: icmp_seq=2 ttl=64 time=0.084 ms
64 bytes from 192.168.4.3: icmp_seq=3 ttl=64 time=0.068 ms
64 bytes from 192.168.4.3: icmp_seq=4 ttl=64 time=0.086 ms

--- 192.168.4.3 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3068ms
rtt min/avg/max/mdev = 0.032/0.067/0.086/0.021 ms

===ping 完成后 arp 表
IP address       HW type     Flags       HW address            Mask     Device
192.168.56.2     0x1         0x2         08:00:27:fa:d4:ec     *        enp0s8
192.168.4.2      0x1         0x2         7a:2d:96:17:8d:bc     *        veth0peer
169.254.169.254  0x1         0x0         00:00:00:00:00:00     *        enp0s3
10.0.2.2         0x1         0x2         52:54:00:12:35:02     *        enp0s3
192.168.56.1     0x1         0x2         0a:00:27:00:00:00     *        enp0s8
192.168.4.3      0x1         0x2         ca:02:25:cf:ce:da     *        veth0</pre></div>
<ul>
<li>初始状态网络设备：在 VirtualBox 虚拟机上进行实验，有两张物理网卡分别是 <code>enp0s3</code> 和 <code>enp0s8</code>。</li>
<li>初始状态 arp 表：arp 表中只有关于这两种物理网卡的数据</li>
<li>创建并配置veth：

<ul>
<li>创建了一对 veth 分别命名为 <code>veth0</code> 和 <code>veth0peer</code>。</li>
<li>给这对 veth 分别分配两个 ip 地址： <code>192.168.4.2/24</code> 和 <code>192.168.4.3/24</code></li>
<li>启动这对 veth</li>
<li>打开这两个设备 <code>accept_local</code> 选项（允许从非 lo 设备进来的数据包的源 IP 地址是本机地址）。</li>
</ul></li>
<li>通过 ping 验证，其流量路径为：

<ul>
<li>请求：

<ul>
<li>ping 配置了出口设备为 <code>veth0</code>，所以程序发送 ICMP echo 数据包的配置源 IP 地址为 <code>veth0</code> 绑定的地址，即 <code>192.168.4.2</code>（不配置 <code>veth0</code> 则 源 IP 地址为 <code>192.168.4.3</code>），目标 IP 地址为 <code>192.168.4.3</code>。</li>
<li>由于配置了从 <code>veth0</code> 出口，因此需要 arp 流程，根据 local 路由表（<code>ip rule list</code>、<code>ip route list table local</code>），目标地址 <code>192.168.2.3</code> 和 <code>veth0</code> 地址处于同一网段，所以协议栈会先从 <code>veth0</code> 发送 ARP，询问 <code>192.168.2.3</code> 的 mac 地址。</li>
<li>内核协议栈将请求发送，将以太网数据包，发送到 <code>veth0peer</code>，<code>veth0peer</code> 将数据包转交到内核协议栈。</li>
<li>内核协议栈比对 目标 IP 地址和本地 IP 地址一致，构造 ICMP echo 数据包。</li>
</ul></li>
<li>响应

<ul>
<li>ICMP echo 数据包的目的地址是 <code>192.168.4.2</code>，是本地地址，所以会通过 lo 设备发送出去（<code>sudo tcpdump -n -i lo</code> 可以看到）（不需要 arp 流程）。</li>
<li>内核收到该数据包，传递到 ping 的 socket 中，ping 的相关函数解析并打印到标准输出中。</li>
</ul></li>
</ul></li>
</ul>

<p>ping local ip 的流量路径：</p>

<ul>
<li><code>ping 192.168.4.3</code>：<code>socket ---内核协议栈--&gt; lo ---内核协议栈回复---&gt; lo ---内核协议栈--&gt; socket</code></li>
<li><code>ping 192.168.4.3 -I 192.168.4.2</code>：<code>socket ---内核协议栈--&gt; lo ---内核协议栈回复---&gt; lo ---内核协议栈--&gt; socket</code>（猜测不会调用 socket.bind）</li>
<li><code>ping 192.168.4.3 -I veth0</code>：<code>socket ---内核协议栈--&gt; veth0 ---&gt; veth0peer ---内核协议栈回复---&gt; lo ---内核协议栈--&gt; socket</code>（猜测会调用 socket.bind）</li>
</ul>

<h2 id="c-语言描述-调用-netlink">C 语言描述（调用 netlink）</h2>

<p>正如 <a href="/posts/linux-net-virual-01-overview/#编程接口和工具">Linux 网络虚拟化技术（一）概览</a> 所讲的的，实验源码中使用到的 ip 命令（iproute2）和 go 的 netlink 库，底层都是基于 netlink socket。</p>

<p>本小结，尝试用 C 语言，通过 netlink socket 来实现创建一对 veth。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc ./src/c/01-veth/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e">// 忽略内存回收等问题，仅用来演示如何通过 netlink 创建一个 veth。
</span><span style="color:#75715e">// 实现代码参考：
</span><span style="color:#75715e">//   https://github.com/vishvananda/netlink/blob/main/link_linux.go
</span><span style="color:#75715e">//   https://github.com/vishvananda/netlink/blob/main/nl/nl_linux.go
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;errno.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;asm/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/netlink.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/rtnetlink.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/veth.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define MY_NETLINK_DEBUG 1
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> nextSeqNr <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;

<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">short</span> IFLA_ROOT <span style="color:#f92672">=</span> IFLA_MAX <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>;

<span style="color:#66d9ef">struct</span> rtattr_nest
{
    <span style="color:#66d9ef">struct</span> rtattr attr;
    size_t data_len;
    <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>data_ptr;

    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>first_child;
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>last_child;
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>next_sibling;

<span style="color:#75715e">#ifdef MY_NETLINK_DEBUG
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>debug_info;
<span style="color:#75715e">#endif
</span><span style="color:#75715e"></span>};

<span style="color:#75715e">#ifdef MY_NETLINK_DEBUG
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">nlmsghdr_print_debug</span>(<span style="color:#66d9ef">struct</span> nlmsghdr <span style="color:#f92672">*</span>nh)
{
    printf(<span style="color:#e6db74">&#34;nlmsghdr(len=%d, type=%d, flags=%d, seq=%d, pid=%d)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, nh<span style="color:#f92672">-&gt;</span>nlmsg_len, nh<span style="color:#f92672">-&gt;</span>nlmsg_type, nh<span style="color:#f92672">-&gt;</span>nlmsg_flags, nh<span style="color:#f92672">-&gt;</span>nlmsg_seq, nh<span style="color:#f92672">-&gt;</span>nlmsg_pid);
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">ifinfomsg_print_debug</span>(<span style="color:#66d9ef">struct</span> ifinfomsg <span style="color:#f92672">*</span>ifm)
{
    printf(<span style="color:#e6db74">&#34;ifinfomsg(family=%d, type=%d, index=%d, flags=%x, change=%x)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, ifm<span style="color:#f92672">-&gt;</span>ifi_family, ifm<span style="color:#f92672">-&gt;</span>ifi_type, ifm<span style="color:#f92672">-&gt;</span>ifi_index, ifm<span style="color:#f92672">-&gt;</span>ifi_flags, ifm<span style="color:#f92672">-&gt;</span>ifi_change);
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">rtattr_nest_print_debug</span>(<span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>attr, <span style="color:#66d9ef">int</span> level)
{
    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> level <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span>; i<span style="color:#f92672">++</span>) {
        printf(<span style="color:#e6db74">&#34; &#34;</span>);
    }
    <span style="color:#66d9ef">int</span> child_num <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>c <span style="color:#f92672">=</span> attr<span style="color:#f92672">-&gt;</span>first_child;
    <span style="color:#66d9ef">while</span> (c <span style="color:#f92672">!=</span> NULL)
    {

        child_num <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>;
        c <span style="color:#f92672">=</span> c<span style="color:#f92672">-&gt;</span>next_sibling;
    }
    printf(<span style="color:#e6db74">&#34;%s (is_root=%d, data_len=%d, child_num=%d)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, attr<span style="color:#f92672">-&gt;</span>debug_info, attr<span style="color:#f92672">-&gt;</span>attr.rta_type <span style="color:#f92672">==</span> IFLA_ROOT, attr<span style="color:#f92672">-&gt;</span>data_len, child_num);
    c <span style="color:#f92672">=</span> attr<span style="color:#f92672">-&gt;</span>first_child;
    <span style="color:#66d9ef">while</span> (c <span style="color:#f92672">!=</span> NULL)
    {
        rtattr_nest_print_debug(c, level <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>);
        c <span style="color:#f92672">=</span> c<span style="color:#f92672">-&gt;</span>next_sibling;
    }
}
<span style="color:#75715e">#endif
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span><span style="color:#a6e22e">new_rtattr_nest</span>(<span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">short</span> rta_type, <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>data_ptr, size_t data_len)
{
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>attr <span style="color:#f92672">=</span> malloc(<span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">struct</span> rtattr_nest));
    memset(attr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">struct</span> rtattr_nest));
    attr<span style="color:#f92672">-&gt;</span>attr.rta_type <span style="color:#f92672">=</span> rta_type;
    attr<span style="color:#f92672">-&gt;</span>data_len <span style="color:#f92672">=</span> data_len;
    attr<span style="color:#f92672">-&gt;</span>data_ptr <span style="color:#f92672">=</span> data_ptr;
    <span style="color:#66d9ef">return</span> attr;
}

<span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span><span style="color:#a6e22e">rtattr_nest_add</span>(<span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>attr, <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">short</span> rta_type, <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>data_ptr, size_t data_len)
{
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>new_attr <span style="color:#f92672">=</span> new_rtattr_nest(rta_type, data_ptr, data_len);
    <span style="color:#66d9ef">if</span> (attr<span style="color:#f92672">-&gt;</span>first_child <span style="color:#f92672">==</span> NULL) 
    {
        attr<span style="color:#f92672">-&gt;</span>first_child <span style="color:#f92672">=</span> new_attr;
        attr<span style="color:#f92672">-&gt;</span>last_child <span style="color:#f92672">=</span> new_attr;
    } 
    <span style="color:#66d9ef">else</span>
    {
        attr<span style="color:#f92672">-&gt;</span>last_child<span style="color:#f92672">-&gt;</span>next_sibling <span style="color:#f92672">=</span> new_attr;
        attr<span style="color:#f92672">-&gt;</span>last_child <span style="color:#f92672">=</span> new_attr;
    }
    <span style="color:#66d9ef">return</span> new_attr;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">rtattr_nest_serialize</span>(<span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>attr, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>buf, <span style="color:#66d9ef">int</span> offset)
{
    <span style="color:#66d9ef">int</span> next_offset <span style="color:#f92672">=</span> offset;
    <span style="color:#66d9ef">if</span> (attr<span style="color:#f92672">-&gt;</span>attr.rta_type <span style="color:#f92672">!=</span> IFLA_ROOT)
    {
        <span style="color:#75715e">// 先跳过 len
</span><span style="color:#75715e"></span>        next_offset <span style="color:#f92672">+=</span> <span style="color:#66d9ef">sizeof</span>(attr<span style="color:#f92672">-&gt;</span>attr.rta_len);
        <span style="color:#75715e">// 序列化 type
</span><span style="color:#75715e"></span>        memcpy(buf <span style="color:#f92672">+</span> next_offset, <span style="color:#f92672">&amp;</span>attr<span style="color:#f92672">-&gt;</span>attr.rta_type, <span style="color:#66d9ef">sizeof</span>(attr<span style="color:#f92672">-&gt;</span>attr.rta_type));
        next_offset <span style="color:#f92672">+=</span> <span style="color:#66d9ef">sizeof</span>(attr<span style="color:#f92672">-&gt;</span>attr.rta_type);
        <span style="color:#75715e">// 序列化 data
</span><span style="color:#75715e"></span>        memcpy(buf <span style="color:#f92672">+</span> next_offset, attr<span style="color:#f92672">-&gt;</span>data_ptr, attr<span style="color:#f92672">-&gt;</span>data_len);
        next_offset <span style="color:#f92672">+=</span> attr<span style="color:#f92672">-&gt;</span>data_len;
    }
    <span style="color:#75715e">// 序列化孩子，并记录孩子总长度
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int</span> children_len <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>c <span style="color:#f92672">=</span> attr<span style="color:#f92672">-&gt;</span>first_child;
    <span style="color:#66d9ef">while</span> (c <span style="color:#f92672">!=</span> NULL)
    {
        <span style="color:#66d9ef">int</span> l <span style="color:#f92672">=</span> NLMSG_ALIGN(rtattr_nest_serialize(c, buf, next_offset <span style="color:#f92672">+</span> children_len)); <span style="color:#75715e">// NLMSG_ALIGN表示，进行 4 字节对齐
</span><span style="color:#75715e"></span>        children_len <span style="color:#f92672">+=</span> l;
        c <span style="color:#f92672">=</span> c<span style="color:#f92672">-&gt;</span>next_sibling;
    }

    <span style="color:#66d9ef">if</span> (attr<span style="color:#f92672">-&gt;</span>attr.rta_type <span style="color:#f92672">==</span> IFLA_ROOT)
        <span style="color:#66d9ef">return</span> children_len;
    <span style="color:#75715e">// 计算当前节点总长度，并序列化
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">short</span> len <span style="color:#f92672">=</span> NLMSG_ALIGN(RTA_LENGTH(children_len <span style="color:#f92672">+</span> attr<span style="color:#f92672">-&gt;</span>data_len)); <span style="color:#75715e">// 最后一个可以不用 4 字节对齐？
</span><span style="color:#75715e"></span>    memcpy(buf <span style="color:#f92672">+</span> offset, <span style="color:#f92672">&amp;</span>len, <span style="color:#66d9ef">sizeof</span>(len));
    <span style="color:#75715e">// 返回长度
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> len;
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">recvfrom_kernal</span>(<span style="color:#66d9ef">int</span> rtnetlink_sock)
{
    <span style="color:#66d9ef">char</span> revice_buf[<span style="color:#ae81ff">65536</span>];
    <span style="color:#66d9ef">struct</span> nlmsghdr <span style="color:#f92672">*</span>rece_nh;
    <span style="color:#66d9ef">struct</span> sockaddr_nl from_addr;
    <span style="color:#66d9ef">int</span> sockaddr_nl_len <span style="color:#f92672">=</span> <span style="color:#66d9ef">sizeof</span>(from_addr);

    <span style="color:#66d9ef">while</span> (<span style="color:#ae81ff">1</span>)
    {

        <span style="color:#66d9ef">int</span> l <span style="color:#f92672">=</span> recvfrom(rtnetlink_sock, <span style="color:#f92672">&amp;</span>revice_buf, <span style="color:#ae81ff">65536</span>, <span style="color:#ae81ff">0</span>, (<span style="color:#66d9ef">struct</span> sockaddr <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>from_addr, <span style="color:#f92672">&amp;</span>sockaddr_nl_len);
        <span style="color:#66d9ef">if</span> (from_addr.nl_pid <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>)
        {
            fprintf(stderr, <span style="color:#e6db74">&#34;Wrong sender portid %d, expected %d&#34;</span>, from_addr.nl_pid, <span style="color:#ae81ff">0</span>);
            <span style="color:#66d9ef">return</span>;
        }
        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> offset <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; l <span style="color:#f92672">-</span> offset <span style="color:#f92672">&gt;=</span> NLMSG_HDRLEN; offset <span style="color:#f92672">+=</span> rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_len)
        {
            rece_nh <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> nlmsghdr <span style="color:#f92672">*</span>)(revice_buf <span style="color:#f92672">+</span> offset);
            <span style="color:#66d9ef">if</span> (rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_seq <span style="color:#f92672">!=</span> nextSeqNr)
            {
                <span style="color:#66d9ef">continue</span>;
            }
            <span style="color:#66d9ef">if</span> (rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_pid <span style="color:#f92672">!=</span> getpid()) {
                <span style="color:#66d9ef">continue</span>;
            }
            <span style="color:#66d9ef">if</span> (rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_type <span style="color:#f92672">==</span> NLMSG_DONE)
            {
                <span style="color:#75715e">// 成功
</span><span style="color:#75715e"></span>                <span style="color:#66d9ef">return</span>;
            }
            <span style="color:#66d9ef">if</span> (rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_type <span style="color:#f92672">==</span> NLMSG_ERROR)
            {
                errno <span style="color:#f92672">=</span> <span style="color:#f92672">-</span> <span style="color:#f92672">*</span>(<span style="color:#66d9ef">int</span> <span style="color:#f92672">*</span>)(revice_buf <span style="color:#f92672">+</span> offset <span style="color:#f92672">+</span> NLMSG_HDRLEN);
                <span style="color:#66d9ef">if</span> (errno <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
                    <span style="color:#75715e">// 成功
</span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">return</span>;
                }
                errExit(<span style="color:#e6db74">&#34;recvfrom_kernal&#34;</span>);
            }
            <span style="color:#66d9ef">if</span> (rece_nh<span style="color:#f92672">-&gt;</span>nlmsg_flags <span style="color:#f92672">&amp;</span> NLM_F_MULTI <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
            {
                <span style="color:#66d9ef">return</span>;
            }
        }
    }
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">link_add_veth</span>()
{
    <span style="color:#66d9ef">int</span> rtnetlink_sock <span style="color:#f92672">=</span> socket(AF_NETLINK, SOCK_RAW <span style="color:#f92672">|</span> SOCK_CLOEXEC, NETLINK_ROUTE);
    <span style="color:#66d9ef">struct</span>
    {
        <span style="color:#66d9ef">struct</span> nlmsghdr nh;
        <span style="color:#66d9ef">struct</span> ifinfomsg ifm;
        <span style="color:#66d9ef">char</span> attrbuf[<span style="color:#ae81ff">512</span>];
    } req; <span style="color:#75715e">//  route netlink socket 请求结构体
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">int</span> mtu <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>;

    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>veth <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;veth&#34;</span>;
    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>veth0 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;veth0&#34;</span>;
    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>veth0peer <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;veth0peer&#34;</span>;

    <span style="color:#75715e">// 结构体设置为 0
</span><span style="color:#75715e"></span>    memset(<span style="color:#f92672">&amp;</span>req, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(req));

    <span style="color:#75715e">// 设置 netlink message header
</span><span style="color:#75715e"></span>    req.nh.nlmsg_len <span style="color:#f92672">=</span> NLMSG_LENGTH(<span style="color:#66d9ef">sizeof</span>(req.ifm)); <span style="color:#75715e">//  len 字段，表示 req 结构体的总长度。
</span><span style="color:#75715e"></span>    printf(<span style="color:#e6db74">&#34;%d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, req.nh.nlmsg_len);
    req.nh.nlmsg_type <span style="color:#f92672">=</span> RTM_NEWLINK;                                            <span style="color:#75715e">//  新建一个 Link
</span><span style="color:#75715e"></span>    req.nh.nlmsg_flags <span style="color:#f92672">=</span> NLM_F_REQUEST <span style="color:#f92672">|</span> NLM_F_CREATE <span style="color:#f92672">|</span> NLM_F_EXCL <span style="color:#f92672">|</span> NLM_F_ACK; <span style="color:#75715e">// 该请求包含本操作的全部请求内容
</span><span style="color:#75715e"></span>    req.nh.nlmsg_seq <span style="color:#f92672">=</span> <span style="color:#f92672">++</span>nextSeqNr;

    <span style="color:#75715e">// 设置 interface infomartion messsage
</span><span style="color:#75715e"></span>    req.ifm.ifi_family <span style="color:#f92672">=</span> AF_UNSPEC;  <span style="color:#75715e">// 未指定的地质族（netlink 固定填写此字段）
</span><span style="color:#75715e"></span>    req.ifm.ifi_index <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;           <span style="color:#75715e">// interface （设备） index，创建时为 0
</span><span style="color:#75715e"></span>    req.ifm.ifi_flags <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;           <span style="color:#75715e">// interface flag
</span><span style="color:#75715e"></span>    req.ifm.ifi_change <span style="color:#f92672">=</span> <span style="color:#ae81ff">0xffffffff</span>; <span style="color:#75715e">// interface flag 掩码
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">// 设置 req.attrbuf
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">// 创建根属性
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>root <span style="color:#f92672">=</span> new_rtattr_nest(IFLA_ROOT, NULL, <span style="color:#ae81ff">0</span>);
    <span style="color:#75715e">// 设置名字
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>name <span style="color:#f92672">=</span> rtattr_nest_add(root, IFLA_IFNAME, veth0, strlen(veth0));
    <span style="color:#75715e">// 设置 link info
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>linkInfo <span style="color:#f92672">=</span> rtattr_nest_add(root, IFLA_LINKINFO, NULL, <span style="color:#ae81ff">0</span>);
    <span style="color:#75715e">// 设置 link 的类型
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>linkKind <span style="color:#f92672">=</span> rtattr_nest_add(linkInfo, IFLA_INFO_KIND, veth, strlen(veth));
    <span style="color:#75715e">// 设置 link 的数据
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>data <span style="color:#f92672">=</span> rtattr_nest_add(linkInfo, IFLA_INFO_DATA, NULL, <span style="color:#ae81ff">0</span>);
    <span style="color:#75715e">// 设置 link 的 peer
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">struct</span> ifinfomsg peer_ifm;
    memset(<span style="color:#f92672">&amp;</span>peer_ifm, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(peer_ifm));
    peer_ifm.ifi_family <span style="color:#f92672">=</span> AF_UNSPEC;
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>peer <span style="color:#f92672">=</span> rtattr_nest_add(data, VETH_INFO_PEER, <span style="color:#f92672">&amp;</span>peer_ifm, <span style="color:#66d9ef">sizeof</span>(peer_ifm));
    <span style="color:#66d9ef">struct</span> rtattr_nest <span style="color:#f92672">*</span>peerName <span style="color:#f92672">=</span> rtattr_nest_add(peer, IFLA_IFNAME, veth0peer, strlen(veth0peer));

    <span style="color:#75715e">// 序列化属性
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int</span> attrs_len <span style="color:#f92672">=</span> rtattr_nest_serialize(root, req.attrbuf, <span style="color:#ae81ff">0</span>);

    <span style="color:#75715e">// 更新总长度
</span><span style="color:#75715e"></span>    req.nh.nlmsg_len <span style="color:#f92672">=</span> NLMSG_ALIGN(attrs_len <span style="color:#f92672">+</span> req.nh.nlmsg_len);

<span style="color:#75715e">#ifdef MY_NETLINK_DEBUG
</span><span style="color:#75715e"></span>    root<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;attrs&#34;</span>;
    name<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IFLA_IFNAME=veth0&#34;</span>;
    linkInfo<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IFLA_LINKINFO&#34;</span>;
    linkKind<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IFLA_INFO_KIND=veth&#34;</span>;
    data<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IFLA_INFO_DATA&#34;</span>;
    peer<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;VETH_INFO_PEER data is `ifinfomsg`&#34;</span>;
    peerName<span style="color:#f92672">-&gt;</span>debug_info <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IFLA_IFNAME=veth0peer&#34;</span>;

    printf(<span style="color:#e6db74">&#34;===debug</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    nlmsghdr_print_debug(<span style="color:#f92672">&amp;</span>req.nh);
    ifinfomsg_print_debug(<span style="color:#f92672">&amp;</span>req.ifm);
    rtattr_nest_print_debug(root, <span style="color:#ae81ff">0</span>);
    printf(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
<span style="color:#75715e">#endif
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">// 发送消息
</span><span style="color:#75715e"></span>    send(rtnetlink_sock, <span style="color:#f92672">&amp;</span>req, req.nh.nlmsg_len, <span style="color:#ae81ff">0</span>);
    <span style="color:#75715e">// 等待响应
</span><span style="color:#75715e"></span>    recvfrom_kernal(rtnetlink_sock);
}

<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> before_scripts[] <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;/bin/sh&#34;</span>,
    <span style="color:#e6db74">&#34;-c&#34;</span>,
    <span style="color:#e6db74">&#34;echo &#39;===初始状态网络设备&#39; &amp;&amp; \
</span><span style="color:#e6db74">    ip addr show &amp;&amp; \
</span><span style="color:#e6db74">    echo &amp;&amp; \
</span><span style="color:#e6db74">    echo &#39;===初始状态 arp 表&#39; &amp;&amp; \
</span><span style="color:#e6db74">    cat /proc/net/arp &amp;&amp; \
</span><span style="color:#e6db74">    echo \
</span><span style="color:#e6db74">    &#34;</span>,
    NULL};
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> other_config_scripts[] <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;/bin/sh&#34;</span>,
    <span style="color:#e6db74">&#34;-c&#34;</span>,
    <span style="color:#e6db74">&#34;sudo ip addr add 192.168.4.2/24 dev veth0 &amp;&amp; \
</span><span style="color:#e6db74">    sudo ip addr add 192.168.4.3/24 dev veth0peer &amp;&amp; \
</span><span style="color:#e6db74">    sudo ip link set veth0 up &amp;&amp; \
</span><span style="color:#e6db74">    sudo ip link set veth0peer up &amp;&amp; \
</span><span style="color:#e6db74">    sudo sysctl -w net.ipv4.conf.veth0.accept_local=1 &amp;&amp; \
</span><span style="color:#e6db74">    sudo sysctl -w net.ipv4.conf.veth0peer.accept_local=1 &amp;&amp; \
</span><span style="color:#e6db74">    echo &#39;完成创建并配置veth&#39; &amp;&amp; \
</span><span style="color:#e6db74">    echo \
</span><span style="color:#e6db74">    &#34;</span>,
    NULL};
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> after_scripts[] <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;/bin/sh&#34;</span>,
    <span style="color:#e6db74">&#34;-c&#34;</span>,
    <span style="color:#e6db74">&#34;echo &#39;===配置完 veth 后网络设备&#39; &amp;&amp; \
</span><span style="color:#e6db74">    ip addr show &amp;&amp; \
</span><span style="color:#e6db74">    echo &amp;&amp; \
</span><span style="color:#e6db74">    echo &#39;===尝试是否可以 ping 通&#39; &amp;&amp; \
</span><span style="color:#e6db74">    ping -c 4 192.168.4.3 -I veth0 &amp;&amp; \
</span><span style="color:#e6db74">    echo &amp;&amp; \
</span><span style="color:#e6db74">    echo &#39;===ping 完成后 arp 表&#39; &amp;&amp; \
</span><span style="color:#e6db74">    cat /proc/net/arp &amp;&amp; \
</span><span style="color:#e6db74">    echo &amp;&amp; \
</span><span style="color:#e6db74">    sudo ip link delete veth0 \
</span><span style="color:#e6db74">    &#34;</span>,
    NULL};

pid_t <span style="color:#a6e22e">exec_shell</span>(<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> args[])
{
    pid_t p <span style="color:#f92672">=</span> fork();
    <span style="color:#66d9ef">if</span> (p <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
    {
        execv(args[<span style="color:#ae81ff">0</span>], args);
        perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
        exit(EXIT_FAILURE);
    }
    <span style="color:#66d9ef">return</span> p;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
    waitpid(exec_shell(before_scripts), NULL, <span style="color:#ae81ff">0</span>);
    sleep(<span style="color:#ae81ff">1</span>);
    printf(<span style="color:#e6db74">&#34;===创建并配置veth</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    link_add_veth();
    waitpid(exec_shell(other_config_scripts), NULL, <span style="color:#ae81ff">0</span>);
    waitpid(exec_shell(after_scripts), NULL, <span style="color:#ae81ff">0</span>);
}
</code></pre></div>
<h2 id="参考">参考</h2>

<ul>
<li><a href="https://man7.org/linux/man-pages/man8/ip-link.8.html">ip-link(8) — Linux manual page</a></li>
<li><a href="https://man7.org/linux/man-pages/man4/veth.4.html">veth(4) — Linux manual page</a></li>
</ul>
]]></description></item><item><title>通过和 IPv4 对比，学习 IPv6</title><link>https://www.rectcircle.cn/posts/learn-ipv6-by-ipv4-diff/</link><pubDate>Sat, 23 Apr 2022 20:01:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/learn-ipv6-by-ipv4-diff/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文面向的是开发者，旨在让开发者通过和 IPv4 对比的方式，了解 IPv6 的大概原理。专业网工或者想了解协议细节者，建议阅读 RFC 原文或设备厂商文档。</p>
</blockquote>

<h2 id="通信过程和几种表">通信过程和几种表</h2>

<blockquote>
<p>本部分主要来复习下，计算机网络的基本通讯流程。</p>
</blockquote>

<p>不管 IPv4 还是 IPv6，其单播通讯（关于单播参见下文）都依赖如下三种表：</p>

<table>
<thead>
<tr>
<th>类型</th>
<th>设备</th>
<th>层级</th>
<th>核心字段</th>
</tr>
</thead>

<tbody>
<tr>
<td>路由表</td>
<td>主机/路由器</td>
<td>三层</td>
<td>目标 IP (key)，下一跳 IP (value)，网口 (value)</td>
</tr>

<tr>
<td>ARP 表（IPv6 类似的叫 nd cache）</td>
<td>主机/路由器</td>
<td>二层到三层之间</td>
<td>IP 地址 (key) ，Mac 地址 (value)，网口 (value)</td>
</tr>

<tr>
<td>Mac 地址表</td>
<td>交换机</td>
<td>一层到二层之间</td>
<td>Mac 地址 (key)，网口 (value)</td>
</tr>
</tbody>
</table>

<p>这几种表在通讯过程中的用途需如下两种情况讨论：</p>

<ul>
<li>同网络通讯</li>
<li>跨网络通讯</li>
</ul>

<p>同网络通讯场景，假设网络拓扑如下（以 IPv4 为例），主机 A 发送 IP 报文给 Host B：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Host A  ------------ Switch 0 ------------ Host B
192.168.0.2/24                             192.168.0.3/24</pre></div>
<ul>
<li>Host A 构造消息

<ul>
<li>IP 层

<ul>
<li>目标地址需应用配置，为：<code>192.168.0.3</code>。</li>
<li>源地址若未配置按照如下逻辑确定，为 <code>192.168.0.2</code>。

<ul>
<li>用 目标地址 匹配每一个网卡的网段，如果在同一网段，则使用该网卡的 IP 地址作为源地址（本例符合该场景）。</li>
<li>否则，按照一定策略选择一个网卡作为源地址。</li>
<li>在这个场景，只有一个网卡，所以源地址为 <code>192.168.0.2</code>。</li>
</ul></li>
</ul></li>
<li>以太网层

<ul>
<li>目标地址按照如下逻辑确定，在这个场景中，Mac 地址就是 Host B 的 Mac 地址。

<ul>
<li>如果 IP 层的目标地址和源地址属于同一网段，则使用目标 IP 地址查询 <strong>ARP 表</strong>，获取目标的 Mac 地址（本例符合该场景）。</li>
<li>如果 IP 层的目标地址和源地址不属于同一网段，则查询<strong>路由表</strong>（主机的路由表是静态的下一跳固定位为网关地址），获取下一跳地址，则使用下一跳地址查询 <strong>ARP 表</strong>，获取目标的 Mac 地址。</li>
<li>如果 ARP 表中没有查到，将使用 ARP/NDP 协议解析 Mac 地址（参见下文：<a href="#mac-地址解析">Mac 地址解析</a>）</li>
</ul></li>
<li>源地址为选定网卡的 Mac 地址。</li>
</ul></li>
<li>物理层，直接按照 ARP 表选定的网口发送报文。</li>
</ul></li>
<li>Switch 0 转发消息

<ul>
<li>物理层，将信号还原为数据。</li>
<li>以太网层，查找 <strong>Mac 地址表</strong>，获取到目标 Mac 地址所属的网口（ARP/NDP 协议解析 会触发交换机的 Mac 地址表的自学习特性。所以一般情况下会存在，如果不存在就是属于过期的情况）。</li>
<li>物理层，如果 Mac 地址表找到了，直接将数据原封不动从该网口发送出去。如果找不到，则从所有网口发送出去。</li>
</ul></li>
<li>Host B 接收消息

<ul>
<li>物理层，将信号还原为数据。</li>
<li>以太网层，检查包的目标 Mac 地址和当前网卡的 Mac 地址是否一样，不一样将丢弃。</li>
<li>IP 层，检查包的目标 IP 地址和当前网卡的 Mac 地址是否一样，不一样将丢弃。</li>
</ul></li>
</ul>

<p>跨网络通讯场景，假设网络拓扑如下（以 IPv4 为例），主机 A 发送 IP 报文给 Host B：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Host A  ------------------------ Router 0 ------------------------ Host B
192.168.0.2/24     192.168.0.1/24        192.168.1.1/24            192.168.1.2/24</pre></div>
<ul>
<li>Host A 构造消息

<ul>
<li>IP 层

<ul>
<li>目标地址需应用配置，为：<code>192.168.1.2</code>。</li>
<li>源地址若未配置按照如下逻辑确定，为 <code>192.168.0.2</code>。

<ul>
<li>用 目标地址 匹配每一个网卡的网段，如果在同一网段，则使用该网卡的 IP 地址作为源地址（本例符合该场景）。</li>
<li>否则，按照一定策略选择一个网卡作为源地址。</li>
<li>在这个场景，只有一个网卡，所以源地址为 <code>192.168.0.2</code>。</li>
</ul></li>
</ul></li>
<li>以太网层

<ul>
<li>目标地址按照如下逻辑确定，在这个场景中，Mac 地址就是 Router0 左侧网口的 Mac 地址。

<ul>
<li>如果 IP 层的目标地址和源地址属于同一网段，则使用目标 IP 地址查询 <strong>ARP 表</strong>，获取目标的 Mac 地址。</li>
<li>如果 IP 层的目标地址和源地址不属于同一网段，则查询<strong>路由表</strong>（主机的路由表是静态的下一跳固定位为网关地址），获取下一跳地址，则使用下一跳地址查询 <strong>ARP 表</strong>，获取目标的 Mac 地址（本例符合该场景）。</li>
<li>如果 ARP 表中没有查到，将使用 ARP/NDP 协议解析 Mac 地址（参见下文：<a href="#mac-地址解析">Mac 地址解析</a>）</li>
</ul></li>
<li>源地址为选定网卡的 Mac 地址。</li>
</ul></li>
<li>物理层，直接按照 ARP 表选定的网口发送报文。</li>
</ul></li>
<li>Router 0 路由消息

<ul>
<li>物理层接收解析，将信号还原为数据。</li>
<li>以太网层接收解析，检查包的目标 Mac 地址和当前设备的 Mac 地址是否一样，不一样将丢弃。</li>
<li>IP 层接收解析，更新 IP 头上的跳数。</li>
<li>以太网层构造消息

<ul>
<li>目标 Mac 地址，根据包的目标 IP 地址，查询<strong>路由表</strong>，获取到下一跳地址（本例中为：<code>192.168.1.2</code>），使用下一跳地址查询 <strong>ARP 表</strong>，获取目标的 Mac 地址。</li>
<li>源 Mac 地址为当前设备的 Mac 地址。</li>
</ul></li>
<li>物理层，直接按照 ARP 表选定的网口发送报文。</li>
</ul></li>
<li>Host B  接收消息

<ul>
<li>物理层，将信号还原为数据。</li>
<li>以太网层，检查包的目标 Mac 地址和当前网卡的 Mac 地址是否一样，不一样将丢弃。</li>
<li>IP 层，检查包的目标 IP 地址和当前网卡的 Mac 地址是否一样，不一样将丢弃。</li>
</ul></li>
</ul>

<p>值得一提的是：</p>

<ul>
<li>交换机只会用到 <strong>Mac 地址表</strong>，而主机和路由器的行为非常类似，都会用到 <strong>ARP 表</strong>和 <strong>路由表</strong>。</li>
<li>数据包经过交换器，数据包内容基本不变。</li>
<li>数据包经过路由器：

<ul>
<li>IP 层源和目的 IP 地址不变，只有 跳数 加一。</li>
<li>以太网层的源和目的地址都变了。</li>
</ul></li>
</ul>

<p>参考：</p>

<ul>
<li><a href="https://www.bilibili.com/read/cv12782091">20张图深度详解MAC地址表、ARP表、路由表</a></li>
</ul>

<h2 id="报文格式">报文格式</h2>

<blockquote>
<p>参考：<a href="https://ccie.lol/knowledge-base/ipv4-and-ipv6-packet-header/">IPv4 和 IPv6 报头格式说明</a></p>
</blockquote>

<p>先来观察 IPv4 Packet Header 格式，如下所示（<a href="https://datatracker.ietf.org/doc/html/rfc791">RFC 791: INTERNET PROTOCOL IP 协议</a> ，关于 IPv4 更多参见： <a href="/posts/learn-net-proto-stack-by-linux-api-2-ip/">通过 Linux API 学习网络协议栈（二）IP 协议</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre></div>
<p>先来观察 IPv6 Packet Header 格式，如下所示（<a href="https://datatracker.ietf.org/doc/html/rfc8200">RFC 8200: Internet Protocol, Version 6 (IPv6) Specification</a>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| Traffic Class |           Flow Label                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Payload Length        |  Next Header  |   Hop Limit   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                         Source Address                        +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                      Destination Address                      +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre></div>
<ul>
<li>IPv6 最重要的一个目标就是解决 IPv4 地址空间太小的问题，因此 IPv6 的地址长度是 128 位，是 IPv4 的 4 倍。</li>

<li><p>虽然 IPv6 地址长度是 IPv4 的 4 倍，但是报头长度只是 IPv4 的 2 倍。从上面对比可以看出，IPv6 的报文格式似乎比 IPv4 的报文格式更简单，字段更少。IPv6将报头分文了两个部分：</p>

<ul>
<li>固定 40 个字节的基本报头（上图表述的就是基本报头），包含了路由过程所需要的数据；</li>
<li>0 或多个 扩展报头，提供了更好的扩展性（而 IPv4 报文头中的选项字段最多只有40字节）。</li>
</ul></li>

<li><p>IPv6 报头为定长的 40 bytes，IPv4 报头为不定长。IPv4 首部的选项字段允许 IP 首部被扩展，由此导致数据报首部长度可变，故不能预先确定数据字段从何开始，同时也使路由器处理一个 IP 数据报所需时间差异很大（有的要处理选项，有的不需要）。基于此，IPv6 采用固定 40 字节长度的报头长度（称基本报头）。然后 IPv6 通过扩展报头的选项字段实现类似于 IPv4 的扩展功能，并由 IPv6 基本报头的 <code>Next Header</code> 字段指向扩展报头（如果有的话）。路由器不处理扩展报头，提升了路由器转发效率。同时，IPv6 报头字段 64 bit 对齐，能够直接对内存进行存取。</p></li>

<li><p>字段：IPv4 报头有 14 个字段（带选项和填充字段），基本的 IPv4 报头有 12 个字段；IPv6 报头只有 8 个字段。IPv4 中的 header length(4)、Identifier(16)、Flags(3)、Framented offset(13)、Options(Length variable、used for test)、Padding 这些项在 IPv6 中都没有了。</p></li>

<li><p>首部检查和：每个路由器上 IPv4 首部检查和都需要重新计算，是一项耗时操作。加之数据链路层和传输层协议已经执行了检验操作，网络传输可靠性提升，所以 IPv6 不进行首部检查和，从而更快速处理 IP 分组。</p></li>

<li><p>选项和填充：选项由扩展报头处理，填充字段也去掉。</p></li>
</ul>

<h2 id="地址表示法">地址表示法</h2>

<blockquote>
<p>在计算机看来，网络地址就是一个二进制串，但是人类无法直接记忆和书写二进制串，因此需要定义一套标准的人类可读的地址表示法。</p>
</blockquote>

<p>在 IPv4 中，网络地址长度只有 32 位，通常使用点分十进制格式来表示一个 IP 地址，如 <code>192.168.1.1</code>，对于该地址所属网段的表示方式，在 IPv4 中有两种方式表示：</p>

<ul>
<li>前缀长度 <code>192.168.1.1/24</code> 即 <code>/24</code>。</li>
<li>子网掩码 <code>255.255.255.0</code>。</li>
</ul>

<p>在 IPv6 中，网络地址长度变成了 128 位，如果仍然使用点分十进制表示，那个就太长了，且不易与 IPv4 的地址区分。因此：</p>

<ul>
<li>IPv6地址被表示为以冒号（:）分隔的一连串16比特的十六进制数，每个IPv6地址被分为8组，每组的16比特用4个十六进制数来表示，组和组之间用冒号隔开，比如：<code>2001:0000:130F:0000:0000:09C0:876A:130B</code>。</li>
<li>为了简化IPv6地址的表示，对于IPv6地址中的 <code>0</code> 可以有下面的处理方式：

<ul>
<li>每组中的前导 <code>0</code> 可以省略，即上述地址可写为 <code>2001:0:130F:0:0:9C0:876A:130B</code>。</li>
<li>如果地址中包含连续两个或多个均为 <code>0</code> 的组，则可以用双冒号 <code>::</code> 来代替，即上述地址可写为 <code>2001:0:130F::9C0:876A:130B</code>。</li>
</ul></li>
<li>注意在一个IPv6地址中只能使用一次双冒号 <code>::</code>，否则当设备将 <code>::</code> 转变为 <code>0</code> 以恢复 128 位地址时，将无法确定 <code>::</code> 所代表的0的个数。</li>
</ul>

<p>IPv6 中不再使用子网掩码来表示一个子网，而是使用前缀长度表示：<code>IPv6地址/前缀长度</code>，如：<code>2001:0:130F::9C0:876A:130B/64</code>。</p>

<h2 id="地址分类">地址分类</h2>

<blockquote>
<p>相关概念：网络/子网/网段。不管 IPv4 还是 IPv6，都有该概念，子网是通过 IP 地址划分的，比如 <code>192.168.0.1/24</code> 就定义了一个子网，这个子网的地址范围是 <code>192.168.0.1 ~ 192.168.0.255</code>，其中，<code>192.168.0.1</code> 分配给路由器，即网关，<code>192.168.0.2~192.168.0.254</code> 分配给主机，<code>192.168.0.255</code> 就是广播地址。因此可以说一个网络/子网，有一个路由器（网关）（换句话说：路由器分割了网络），有 0 到多个主机（设备），有 1 个广播地址（广播域）。</p>
</blockquote>

<p>在 IPv4 中，地址的分类，除了组播和保留地址外，会按照分配规则（A、B、C 类地址以及 CIDR）进行划分（决定一个组织可以获得多少个 IP 地址，这些 IP 地址组成个网络），然后再分为广播地址和单播地址：</p>

<ul>
<li>A 类地址（<code>0.0.0.0/8 ~ 127.255.255.255/8</code>）、B 类地址（<code>128.0.0.0/16 ~ 191.255.255.255/16</code>）、C 类地址（<code>192.0.0.0/24 ~ 223.255.255.255/24</code>）。

<ul>
<li>无分类编址 CIDR，即在 A、B、C 类地址上再次划分，比如：<code>192.168.0.0/24</code> 可以划分为 <code>192.168.0.0/25</code> 和 <code>192.168.0.128/25</code> 两个子网。

<ul>
<li>单播地址 <code>192.168.0.0/25</code> 的单播地址为 <code>192.168.0.0 ~ 192.168.0.126</code></li>
<li>广播地址 <code>192.168.0.0/25</code> 的广播地址为 <code>192.168.0.127</code></li>
</ul></li>
</ul></li>
<li>D 类地址（组播）：前缀为 <code>1110</code>，地址范围为 <code>224.0.0.0 ~ 239.255.255.255</code>（用法参见下文）</li>
<li>E 类地址（保留）：前缀为 <code>1111</code>，地址范围为 <code>240.0.0.0 ~ 255.255.255.255</code></li>
</ul>

<p>由于 IPv6 地址空间巨大，因此 IPv6 没有强制的分配规则，所以 IPv6 的地址可以分为如下三类：</p>

<ul>
<li>单播地址：除了组播地址外的所有地址。</li>
<li>组播地址：<code>ff00::/8</code>。</li>
<li>任播地址：从单播地址空间中进行分配，使用单播地址的格式。</li>
</ul>

<p>可以看出，IPv6 和 IPv4 相比，添加了任播地址，删除了广播地址。</p>

<p>单播（unicast）就是点对点的通讯，开发者接触的 99.9% 的场景都是单播。</p>

<p>关于：广播（broadcast）、组播（multicast，又叫多播）、任播（anycast），参见下文专门的章节介绍。</p>

<h2 id="常见的保留单播地址">常见的保留单播地址</h2>

<blockquote>
<p>保留单播地址指的是：非公网单播 IP 地址。</p>
</blockquote>

<p>在 IPv4 中，常见保留的单播地址，可以分为如下几类：</p>

<ul>
<li>未指定地址：<code>0.0.0.0/32</code></li>
<li>本地回环地址 <code>127.0.0.1/8</code></li>
<li>链路本地地址：<code>169.254.0.0/16</code></li>
<li>私有地址空间（专用网络，私有 IP）：

<ul>
<li><code>10.0.0.0/8</code> （<code>10.0.0.0 ~ 10.255.255.255</code>）</li>
<li><code>172.16.0.0/12</code> （<code>172.16.0.0 ~ 172.31.255.255</code>）</li>
<li><code>192.168.0.0/16</code> （<code>192.168.0.0 ~ 192.168.255.255</code>）</li>
</ul></li>
</ul>

<p>在 IPv6 中，上述地址，也有对应物：</p>

<ul>
<li>未指定地址：<code>::/32</code>。</li>
<li>本地回环地址 <code>::1/128</code>。</li>
<li>链路本地地址：<code>fe80::/10</code>。</li>
<li>私有地址空间（在 IPv6 中叫做 Unique local address）：<code>fc00::/7</code>，其中 <code>fd00::/8</code> 可使用，<code>fc00::/8</code> 未定义。</li>
</ul>

<h2 id="广播">广播</h2>

<p>广播（broadcast），只在 IPv4 中存在，在 IPv6 中被组播（all-nodes multicast address，所有节点的组播地址 <code>FF02::1</code>）代替了，向当前网络内的所有主机发送报文（注意，广播不会跨网络/路由器/广播域，又称为本地广播）。在开发中，基本上不会直接接触 IPv4 的广播能力，而是通过上层 UDP 协议来实现广播。</p>

<p>IPv4 想要实现广播协议，其基于的数据链路层必须也要支持广播。因此 IPv4 的广播的原理就是以太网广播。</p>

<ul>
<li>假设发送方想向 <code>192.168.0.1/24</code> 网络内的所有主机发送一个广播消息，发送过程如下：

<ul>
<li>在 UDP 层，目标地址设置为 <code>255.255.255.255</code>，端口设置为指定端口。</li>
<li>在 IPv4 层，目标地址设置为 <code>192.168.0.255</code>。</li>
<li>以太网层，目标地址 Mac 地址设置为 <code>FF:FF:FF:FF:FF:FF</code>。</li>
</ul></li>
<li>交换机接收到这个消息，将这个消息发送给所有的物理端口（主机）</li>
<li>所有收到这个消息的主机将逐层解包

<ul>
<li>以太网层，发现该消息的目标是指是广播消息，交由上层。</li>
<li>IPv4 层，交由上层处理。</li>
<li>在 UDP 层，检查端口是否有进程绑定，没有则丢弃，否则交由进程处理。</li>
</ul></li>
</ul>

<p>从上文可以看出，在 IPv6 中废弃了 IPv4 的这种广播，原因是：</p>

<ul>
<li>广播对网络和主机都产生了很大的压力，不需要接收该消息的主机也会收到了这些消息。因此，推荐使用组播代替。</li>
<li>广播只能在当前网络中进行，即本地广播，不支持跨网络广播（除非特殊支持）。</li>
</ul>

<h2 id="组播">组播</h2>

<p>组播（multicast，又叫多播）在 IPv4 和 IPv6 中都存在，并且其在 IPv6 中占有更重要的地位，取代了 ARP 协议，为 Mac 地址解析提供底层支持。</p>

<p>组播的应用场景比较多，比如 NTP 协议， mDNS 协议，IPTV，视频会议等。在应用层基本也是通过 UDP 协议使用。</p>

<p>在 IPv4 中，组播可以实现：发送方，向一组（1个或多个）主机，发送报文，只需要指定一个组播地址，即可将消息发送到这个组内的所有主机。</p>

<p>从这个需求，可以看出，要实现这个效果，则需要实现组播 IP 和多个主机的映射。因此，需要一个协议，可以将一个主机加入到一个组中，同样也需要将一个主机从一个组中移除。在 IPv4 中，这个协议就是 IGMP（组成员关系管理）。</p>

<p>此外组播和广播不同，可以实现跨网段的组网，因此需要一个协议，来进行跨网段路由，这个协议就是 PIM（域内组播路由协议），MBGP， MSDP（域间组播路由协议）。</p>

<p>在此，只讨论一下在同一个网段下，发送一个组播消息的大概流程：</p>

<ul>
<li>前置条件，需要接收消息的主机，通过 IGMP 协议，加入该组，该组的 IP 为 <code>225.1.1.1</code>，同时配置网卡的组播 Mac 地址（参见下文）。</li>
<li>假设发送方想向 <code>225.1.1.1</code> 组发送消息，发送过程如下：

<ul>
<li>在 IPv4 层，目标地址设置为 <code>239.1.1.1</code>（固定前缀为 4 位，所以地址空间为 28 位）。</li>
<li>以太网层，目标地址 Mac 地址设置为，组播 Mac 地址：前缀为 <code>01:00:5E:0</code>（25位），后缀为广播地址的后 23 位，注意这个地址是一个逻辑地址，并不是一个真实的地址（无法一一对应，可能有冲突，但是不要紧，概率很小）。</li>
</ul></li>
<li>交换机接收到这个消息，会根据 <code>IGMP</code> 协议构建的组播表，定向的将消息发送给指定端口（主机）。</li>
<li>所有收到这个消息的主机将逐层解包

<ul>
<li>以太网层，校验目标 Mac 地址是否和组播 Mac 一致，如果一致交由上层处理。</li>
<li>IPv4 层，校验目标 IP 地址是否和组播 IP 一致，如果一致交由上层处理。</li>
<li>在 UDP 层，检查端口是否有进程绑定，没有则丢弃，否则交由进程处理。</li>
</ul></li>
</ul>

<p>更多参见：<a href="https://support.huawei.com/enterprise/zh/doc/EDOC1100105907">华为 - 什么是组播</a> 和 <a href="http://www.h3c.com/cn/d_200803/336048_30003_0.htm">新华三 - 组播技术白皮书</a>。</p>

<p>在 IPv6 中，组播的原理上和 IPv4 类似，详细可以看：<a href="http://www.h3c.com/cn/d_200803/336046_30003_0.htm">新华三 - IPv6组播技术白皮书</a>。</p>

<p>下文将，着重介绍将组播中的一种特殊情况，其给 IPv6 取代 ARP 协议提供支持。</p>

<h2 id="mac-地址解析">Mac 地址解析</h2>

<p>在 IPv4 中，为了解决 IP 地址和 Mac 地址（以太网地址）的映射问题，使用的是 ARP 协议，ARP 协议基于的是以太网广播机制，这造成了难以避免的 ARP 攻击 和 ARP 风暴的问题。ARP 协议原理参见：<a href="https://www.cnblogs.com/juankai/p/10315957.html">博客</a>。</p>

<p>在 IPv6 中，Mac 地址解析不再使用 ARP 协议，而是使用基于一种点对点组播的 ICMPv6 协议。是 NDP （邻居发现协议）的一部分。</p>

<ul>
<li>每个 IPv6 单播地址的主机都会自动加入一个称为，被请求节点组播地址 的组播组。其组播地址为 <code>FF02::1:FFxx:xxxx</code>，其中 <code>xx:xxxx</code> 为该主机单播 IPv6 地址的最后 32 位。并将组播地址通过 MLD 协议（对应的是 IPv4 的 IGMP）进行上报。</li>
<li>此时，主机 A，如果需要查询一个 IPv6 单播地址的 Mac 地址，发送者将：

<ul>
<li>ICMPv6 层，消息 <code>Type = NS</code></li>
<li>IPv6 层，目标地址为单播 IPv6 地址的组播地址 <code>FF02::1:FFxx:xxxx</code>。</li>
<li>以太网层，目标地址 Mac 地址设置为组播地址：前缀为 <code>33:33</code>（16位），后缀为组播地址的后 32 位，即 <code>FF:xx:xx:xx</code>。</li>
</ul></li>
<li>交换机接收到这个消息，会根据 <code>MLD</code> 协议构建的组播表，定向的将消息发送给指定端口（主机）。与此同时，向交换表加一条记录。</li>
<li>只有主机 B 能收到该消息，将逐层解包：

<ul>
<li>以太网层，校验目标 Mac 地址是否和组播 Mac 一致，如果一致交由上层处理。</li>
<li>IPv6 层，校验目标 IP 地址是否和组播 IP 一致，如果一致交由上层处理。</li>
<li>ICMPv6 处理，并发送回复消息，消息 <code>Type = NA</code></li>
</ul></li>
<li>主机 B 回复消息

<ul>
<li>ICMPv6 层，消息 <code>Type = NA</code></li>
<li>IPv6 层，目标地址为单播 IPv6 地址，即上一步接收到消息的 source IP，源地址设置为当前主机地址。</li>
<li>以太网层，目标地址为上一步接收到消息的 source Mac 地址，源地址为当前主机地址。</li>
</ul></li>
<li>交换器接收该消息，根据交换表可以直接发送给主机 A</li>
<li>主机 A 将收到该消息，将逐层解包：

<ul>
<li>以太网层，校验目标 Mac 地址是否和 Mac 一致，如果一致交由上层处理。</li>
<li>IPv6 层，校验目标 IP 地址是否和 IP 一致，如果一致交由上层处理。</li>
<li>ICMPv6 处理，构建 nd cache 表（IPv4 中的 arp 表）。</li>
</ul></li>
</ul>

<p>IPv6 的 Mac 地址解析机制的总结：</p>

<ul>
<li>利用点对点组播，解决了 ARP 协议的风暴问题（由 ARP 广播查询，更改为了组播上报，单点查询），提高了效率。</li>
<li>如果想要安全，可以使用 SEND 协议。</li>
<li>通过 ICMPv6 统一实现，避免每一种数据链路都需要重复实现类似 ARP 类型的功能。</li>
</ul>

<p>关于 NDP 协议的其他内容，参见下文的：<a href="#链路本地地址">链路本地地址</a></p>

<p>参考：</p>

<ul>
<li><a href="https://zhuanlan.zhihu.com/p/451627391">知乎 - IPV6深入-NDP邻居发现协议</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/79633456">知乎 - IPv6 原理机制</a></li>
<li><a href="https://blog.51cto.com/u_7658423/1337745">IPv6的组播地址（掌握IPv6通信原理的关键知识点）</a></li>
<li><a href="https://www.cnblogs.com/mysky007/p/11261559.html">IPV6 组播学习理解</a></li>
<li><a href="https://blog.csdn.net/Johan_Joe_King/article/details/105566111">IPv4/IPv6组播地址和组播MAC地址的转换</a></li>
<li><a href="https://blog.csdn.net/jy15569597/article/details/7992127">对被请求-节点多播地址(solicited-node multicast address) 的理解</a></li>
</ul>

<h2 id="任播">任播</h2>

<p>IPv4 作为实验特性，IPv6 正式纳入标准。简单的来说，当一个单播地址被分配到多于一个的接口上时，这些接口组成了一个任播组，这个 IP 可以称为任播 IP。</p>

<p>任播在使用时，可以同时支持 TCP/UDP 协议的使用。</p>

<p>当请求该任播 IP，路由协议会按照一定的规则（如最短路径，超时时间等等）选择一个接口进行通讯。</p>

<p>可以看出，任播本质就是在网络层，实现负载均衡（参考：<a href="https://blog.cloudflare.com/cloudflares-architecture-eliminating-single-p/">cloudflare blog</a>）和就近服务的能力。</p>

<p>任播的应用场景有：</p>

<ul>
<li>根 DNS 服务器，采用任播提供服务。</li>
<li>CDN 实现就近接入，参考：<a href="https://www.cloudflare.com/zh-cn/learning/cdn/glossary/anycast-network/">cloudflare</a>。</li>
<li>缓解 DDoS 攻击，参考：<a href="https://www.cloudflare.com/zh-cn/learning/cdn/glossary/anycast-network/">cloudflare</a>。</li>
</ul>

<p>更多参见：<a href="https://en.wikipedia.org/wiki/Anycast">Anycast</a>。</p>

<h2 id="链路本地地址">链路本地地址</h2>

<p>在 IPv4 中，链路本地地址为 <code>169.254.0.0/16</code>，是可选项，所以，只有在 Mac 连 WIFI 抽风时，才可能可以看到这个地址（DHCP 失败时，一些操作系统可能会自动的分配一个链路本地地址）。链路本地地址的特点是：</p>

<ul>
<li>路由器不会将该地址转发到其他网络中，因此只能在网络内部使用。</li>
<li>该类地址的分配是完全自动化的，主机随机分配，然后利用 ARP 协议检查是否和网络上的其他主机冲突。</li>
</ul>

<p>在 IPv6 中，链路本地地址为 <code>fe80::/10</code>，每个接口必须分配一个，也就是说，IPv6 的每个网卡，最少要有两个单播地址：</p>

<ul>
<li>链路本地地址 <code>fe80::/10</code> （scope link）。</li>
<li>全局单播地址 (scope global)。</li>
</ul>

<p>在 IPv4 中，IP 地址的分配主要有两种方式：手动配置、DHCP（有状态地址自动分配）。</p>

<p>在 IPv6 中，IP 地址的分配主要有三种方式：手动配置、DHCPv6（无状态地址自动分配）、无状态地址分配。</p>

<p>在这里，重点介绍无状态地址分配。无状态地址分配原理是：基于 IPv6 的链路本地地址，可以自动的分配一个全局单播地址。流程基本如下：</p>

<ul>
<li>主机启动，自动生成一个链路本地地址。

<ul>
<li>一般按照 <code>EUI-48</code> 或 <code>EUI-64</code> 算法，自动分配一个链路本地地址（可以是任何算法，由实现者决定）。</li>
<li>通过上文提到 <a href="#mac-地址解析">Mac 地址解析</a>（ICMPv6） 类似的方式，网络中是否已经存在了该链路本地地址（存在是，将能收到响应 <code>NA 消息</code>），称为 DAD （Duplicate address Detection，重复地址检测）。</li>
</ul></li>
<li>主机通过，路由器发现协议，获取当前网络的网络信息（全局单播地址的前缀）

<ul>
<li>主机发送一个 <code>Type = RS</code> 的 ICMPv6 消息（Router Solicitation 路由器请求），其目标地址为 <code>all-router multicast address</code>（即 <code>ff02::2</code> 一个路由器会加入的组播地址），源地址为链路本地地址。</li>
<li>路由器收到该消息后，会回复一个 <code>Type = RA</code> 的 ICMPv6 消息（Router Advertisement，路由器公告），其目的地址为主机的链路本地地址，源地址为路由器的链路本地地址。RA 消息包含了当前网络的一些配置情况：

<ul>
<li>是否使用 DHCPv6 分配地址？</li>
<li>网络前缀是什么？</li>
</ul></li>
<li>此外：路由器会定时给所有节点发送 <code>Type = RA</code>，以告知节点网络变更情况，其目标地址是 <code>all-nodes multicast address</code>（即 <code>ff02::1</code> 一个所有节点会加入的组播地址）</li>
</ul></li>
<li>主机接收到 <code>RA</code> 消息后，根据网络前缀，自动生成一个全局单播地址。流程和第一步的自动生成一个链路本地地址类似。</li>
</ul>

<p>可以看出，无状态地址分配有如下好处：</p>

<ul>
<li>真正的即插即用。节点连接到没有DHCP服务器的网络时，无需手动配置地址等参数便可访问网络。</li>
<li>网络迁移方便。当一个站点的网络前缀发生变化，主机能够方便地进行重新编址而不影响网络连接。</li>
<li>地址配置方式选择灵活。系统管理员可根据情况决定使用何种配置方式——有状态，无状态还是两者兼容。</li>
</ul>

<p>由于，链路本地地址是一个 IPv6 中的一个整个网段，如果一个设备有多个网卡，连入了多个网络，他们的链路本地地址可能是一样，因此，需要区分从网卡。因此在应用层使用链路本地地址时，需要通过 <code>%网卡名</code> 指定网卡。 如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ping -6 fe80::xxxxx%eth0</code></pre></div>
<p>参考：</p>

<ul>
<li><a href="https://blog.csdn.net/Gina_wj/article/details/106708770">IPv6知识概述 - ND协议</a></li>
<li><a href="https://blog.51cto.com/u_11529070/3609608">IPV6的链路本地地址和站点本地地址的不同</a></li>
<li><a href="https://blog.csdn.net/Johan_Joe_King/article/details/105564841">IPv4和IPv6的链路本地地址的自动分配</a></li>
<li><a href="https://network.51cto.com/article/610565.html">IPv6邻居发现，地址重复检测，及路由器发现机制，一分钟了解下</a></li>
<li><a href="https://cshihong.github.io/2018/01/29/IPv6%E9%82%BB%E5%B1%85%E5%8F%91%E7%8E%B0%E5%8D%8F%E8%AE%AE/">IPv6邻居发现协议</a></li>
<li><a href="https://zh.wikipedia.org/wiki/%E9%93%BE%E8%B7%AF%E6%9C%AC%E5%9C%B0%E5%9C%B0%E5%9D%80">链路本地地址</a> | <a href="https://en.wikipedia.org/wiki/Link-local_address">Link-local address</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/349733311">IPv6地址的%是啥意思</a></li>
</ul>

<h2 id="ipv6-网口地址">IPv6 网口地址</h2>

<p>一旦节点启用 IPv6，那么网口（接口）就会自动绑定下列地址（也就是说通过这些地址可以将消息发送到该网口）：</p>

<ul>
<li>单播地址

<ul>
<li>回环地址 <code>::1</code></li>
<li>本地链路地址 <code>fe80::xxxxxx</code></li>
<li>全局单播地址</li>
</ul></li>
<li>组播地址

<ul>
<li>本地链路地址对应的，被请求节点组播地址 <code>FF02::1:FFxx:xxxx</code></li>
<li>全局单播地址对应的，被请求节点组播地址 <code>FF02::1:FFxx:xxxx</code></li>
<li>所有节点的组播地址 <code>FF02::1</code></li>
<li>如果是路由器，还会有所有路由器组播地址 <code>FF02::2</code></li>
</ul></li>
</ul>

<h2 id="ipv6-和-nat">IPv6 和 NAT</h2>

<p>IPv6 最重要的职责就是解决 IPv4 地址空间太小的问题。因此在 IPv6 中推荐不使用 NAT。也就是说，建议组织内网也采用公网单播地址。</p>

<p>但是，在企业内部中，多数还是沿袭 IPv4 的习惯，仍然只给内网分配 私有 IPv6 地址，使用 IPv6NAT。主要原因是如果给内网分配公网单播地址。之前基于 NAT 的安全策略可能都要重新设计了，成本比较高。</p>

<h2 id="linux-常用命令">Linux 常用命令</h2>

<p>观察网卡绑定的 IP 地址，如果存在 IPv6 将返回包含 inet6 的行。如果是本地链路地址，则显示 <code>scope link</code>，如果是全局单播地址则显示 <code>scope global</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ip addr show 
ip addr show eth0</code></pre></div>
<h2 id="socket-api">Socket API</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 创建 socket https://man7.org/linux/man-pages/man2/socket.2.html
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">socket</span>(<span style="color:#66d9ef">int</span> domain, <span style="color:#66d9ef">int</span> type, <span style="color:#66d9ef">int</span> protocol);

<span style="color:#66d9ef">int</span> tcp6_socket <span style="color:#f92672">=</span> socket(AF_INET6...)
<span style="color:#75715e">// 是否支持 IPv4/IPv6 双栈，默认值为 `/proc/sys/net/ipv6/bindv6only`
</span><span style="color:#75715e">// 可以通过 setsockopt 系统调用手动强制配置
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> ipv6_only_flag <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
setsockopt(tcp6_socket, IPPROTO_IPV6, IPV6_V6ONLY, (<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>ipv6_only_flag, <span style="color:#66d9ef">sizeof</span>(ipv6_only_flag));
</code></pre></div>
<p>更多参见：<a href="/posts/learn-net-proto-stack-by-linux-api-1-overview/">通过 Linux API 学习网络协议栈（一）概览</a></p>

<h2 id="其他参考">其他参考</h2>

<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8200">RFC 8200: Internet Protocol, Version 6 (IPv6) Specification</a></li>
<li><a href="https://en.wikipedia.org/wiki/IPv6">Wiki IPv6</a> | <a href="https://zh.wikipedia.org/wiki/IPv6">Wiki IPv6 zh</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/71684181">IPv4与IPv6的区别是什么？</a></li>
<li><a href="https://www.ibm.com/docs/zh/i/7.2?topic=6-comparison-ipv4-ipv6">IBM IPv4 与 IPv6 的比较</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/64598841">IPv6系列-入门指南</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/67843942">IPv6系列-初学者的10个常见困扰</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/79633456">IPv6 原理机制</a></li>
</ul>
]]></description></item><item><title>Linux 网络虚拟化技术（一）概览</title><link>https://www.rectcircle.cn/posts/linux-net-virual-01-overview/</link><pubDate>Thu, 21 Apr 2022 00:24:21 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-net-virual-01-overview/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>通过对 Linux 内核提供的关于网络虚拟化相关 API 的学习和实验代码的编写实验，从底层理解基于 Linux 的物理网络设备（路由器/交换机）、数据中心网络、云计算虚拟机网络、容器网络（Docker/K8S）的原理。</p>

<h2 id="实验环境准备">实验环境准备</h2>

<blockquote>
<p>参考：<a href="/posts/container-core-tech-1-experiment-preparation-and-linux-base/#实验环境准备">容器核心技术（一） 实验环境准备 &amp; Linux 概述</a>。</p>
</blockquote>

<p>安装相关外部命令。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo apt update
sudo apt install -y iproute2 tcpdump</pre></div>
<h2 id="实验代码库">实验代码库</h2>

<p>本系列实验代码库位于：<a href="https://github.com/rectcircle/linux-network-virtualization-experiment">rectcircle/linux-network-virtualization-experiment</a>。</p>

<h2 id="编程接口和工具">编程接口和工具</h2>

<p>众所周知周知，在 Linux 上，编写普通的网络应用程序（tcp 客户端/服务端）依赖的编程接口是最早来自于 BSD 的 Socket 模型。</p>

<p>而对于内核网络的管理，在 Linux 的发展中，有两个阶段：</p>

<ul>
<li>net-tools 阶段，通过 <code>/proc</code> 文件系统和 <code>ioctl</code> 系统调用来实现对内核网络的管理。</li>
<li>iproute2 阶段，在通过一种称为 <code>netlink</code> 的特殊类型的 <code>socket</code> 对象来实现对内核网络的管理。</li>
</ul>

<p><code>net-tools</code> 工具箱提供了 <code>netstat</code>、<code>route</code>、<code>ifconfig</code> 等命令（<code>dpkg -L net-tools | grep &quot;[s]*bin&quot;</code>），2001 起就停止维护了。</p>

<p>目前主流的 Linux 发行版，推荐使用 <code>iproute2</code> 工具箱来实现对内核网络的进行管理。该工具箱提供了 <code>ip</code> 等命令（<code>dpkg -L iproute2 | grep &quot;[s]*bin&quot;</code>）。</p>

<p>关于 <code>net-tools</code> 和 <code>iproute2</code> 区别和对比，参考：<a href="http://www.jiatcool.com/?p=762">网络管理工具变迁 - 从 net-tools 到 iproute2</a>。</p>

<p>本系列实验，将使用使用如下编程接口和命令行工具：</p>

<ul>
<li>Shell 描述：使用 <a href="https://github.com/shemminger/iproute2">iproute2 工具集</a>，基于 netlink socket 实现。</li>
<li>Go 语言描述：使用 <a href="https://github.com/vishvananda/netlink">vishvananda/netlink 库</a> （<a href="https://github.com/opencontainers/runc/blob/main/go.mod#L21">runc 同款依赖</a>），基于 netlink socket 实现。</li>
</ul>

<p>因此 netlink socket 是关键 (参见：<a href="https://man7.org/linux/man-pages/man7/netlink.7.html">netlink(7) 手册</a>) ，netlink socket 有很多中类型。本章节主要介绍的是 NETLINK_ROUTE 类型（参见：<a href="https://man7.org/linux/man-pages/man7/rtnetlink.7.html">rtnetlink(7)</a> 和 <a href="https://man7.org/linux/man-pages/man3/rtnetlink.3.html">rtnetlink(3)</a>） 。</p>

<p>下面简要介绍 rtnetlink 的请求消息结构示例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">第一部分: netlink message header 长度 16 字节。
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
|                       nlmsghdr.nlmsg_len                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      nlmsghdr.nlmsg_type      |     nlmsghdr.nlmsg_flags      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     nlmsghdr.nlmsg_seq                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     nlmsghdr.nlmsg_pid                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第二部分: Messages ，其类型由 nlmsghdr.nlmsg_type 决定。
如下展示 nlmsghdr.nlmsg_type 为 RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK 的请求体。
即 ifinfomsg interface information message (ifinfomsg, 下面简写为 ifim)。
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
|ifim.ifi_family|ifim.__ifi_pad |         ifim.ifi_type         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        ifim.ifi_index                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        ifim.ifi_flags                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        ifim.ifi_change                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第三部分：Routing attributes 列表，其包含的内容，由 ifim.ifi_type 决定。
每个 attribute 的组成为：
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
|         rtattr.rta_len        |        rtattr.rta_type        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  data (len = rtattr.rta_len - 4)  ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

attribute 支持任意级别的嵌套，比如一个请求包含属性 a1, b1。a1 内部包含 a2 a3。
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
|         a1.rta_len = 20       |          a1.rta_type          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         a2.rta_len = 6        |          a2.rta_type          | --
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |
|         a2.data               |              对齐              | --
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |---- a1 的 data
|         a3.rta_len = 8        |          a3.rta_type          | --
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |
|                           a3.data                             | --
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         b1.rta_len = 12       |          b1.rta_type          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         b2.data                                               |
|                               |              对齐              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre></div>
<p>注意：</p>

<ul>
<li>以上有三种类型的结构体，每个结构体和结构体之间按 4 字节对齐。</li>
<li>字节序取决于本机字节序。</li>
</ul>

<p>直接操纵 netlink socket 的方式，过于原始，开发成本过高，本系列仅提供一个 C 语言编写的基于 netlink socket 的示例。参见： <a href="/posts/linux-net-virual-02-veth/#c-语言描述调用-netlink">veth 虚拟设备 - 实验 - C 语言描述</a>。其他章节部分仅提供 Golang 和 Shell 示例。</p>

<p>如果想使用 C 语言编写 Linux 网络虚拟化相关程序，可以直接使用 <a href="https://www.infradead.org/~tgr/libnl/">libnl 库</a>，而不是直接使用 netlink socket 这么底层的 API。</p>

<p>参考：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man8/ip.8.html">ip(8) — Linux manual page</a></li>
<li><a href="https://www.hyuuhit.com/2018/04/05/network-device-list/#rtnetlink">获取网卡列表的几种方式</a></li>
<li><a href="http://www.hyuuhit.com/2018/08/22/netlink/">Linux netlink socket 内核通信</a></li>
</ul>

<h2 id="网络设备概述">网络设备概述</h2>

<p>Linux 网络设备可以分为物理网络设备和虚拟网络设备，这些网络设备可以通过：<code>ip addr show</code> 可以查看。</p>

<p>最常见的物理网络设备为：</p>

<ul>
<li><code>eth</code> 物理以太网设备（又称物理网卡），名字一般以 <code>eth</code> 开头，如 <code>eth0</code>（在 VirtualBox 虚拟机中以 <code>enp</code> 开头）。</li>
</ul>

<p>本系列介绍的虚拟网络设备为：</p>

<ul>
<li><code>bridge</code> 桥设备，即一个软件实现的交换机或者路由器，名字一般以 <code>br</code> 开头，如 <code>br0</code>。</li>
<li><code>veth</code> 虚拟以太网设备。</li>
<li><code>tun/tap</code> （network TUNnel / network TAP）。</li>
<li><code>ipvlan/macvlan</code> 虚拟设备。</li>
<li><code>vxlan</code> 虚拟设备。</li>
</ul>

<h2 id="术语和绘图说明">术语和绘图说明</h2>

<p>在学习 Linux 网络虚拟化相关技术时。经常会遇到一些术语，在此解释下这些术语的含义：</p>

<ul>
<li>Network Protocol Stack 网络协议栈，指的是位于内核的处理互联网协议的相关功能的统称，。</li>
<li>Network interface 网络接口。网络接口指的是一个接收或发送网络数据的对象，一定会配置一个 Mac 地址，可选的配置一个 IP 地址，是逻辑上的抽象。网络接口会和 网络协议栈 相连。在中文语境下，网卡一般指的就是网络接口（有时指的是网卡设备）。</li>
<li>Network device 网络设备。网络设备是指具有特定功能的对象，当使用网络设备术语时，强调的是该设备的特定功能，一个网络设备会拥有一个网络接口。</li>
</ul>

<p>在绘制网络拓扑图时，为了简化：</p>

<ul>
<li>Network Protocol Stack 一般指网际层协议处理程序（IP 层）。</li>
<li>一般不会区分网络接口和网络设备，而是统一看成一个拥有网络接口的网络设备。</li>
</ul>

<!-- ## 路由表、arp 表和 fdb

TODO 添加，查看这几个表对应查看命令以及简述

https://yuerblog.cc/2019/11/18/%E7%AE%80%E8%BF%B0linux%E8%B7%AF%E7%94%B1%E8%A1%A8/

* 这几张表如何维护的
* 这个几个表在 IP 通讯过程中的过程作用 
* 添加相关 api 的权限说明（创建和管理必须有 CAP_NET_ADMIN 权限，docker 容器默认权限是不可以创建/修改网络设备）

-->

<h2 id="ip-地址和网络接口关系">IP 地址和网络接口关系</h2>

<p>通过 <code>ip addr show</code> 查看到的 ip，看起来 ip 和 网络接口绑定在了一起。</p>

<ul>
<li>接收数据时，直觉上看，似乎是到达该网络设备的流量的目标 IP 必须和该接口上的 IP 匹配上，数据包才会正常处理。实际上并非如此，在处理 IP 数据包时，Linux 内核并不管该数据包是从那个网络设备进入的，只要数据包目标 IP 地址和任一 <code>ip addr show</code> 显示的 IP 地址一致时，即可进行处理。因此在接收数据时，ip 地址是一个全局属性。（参见：<a href="https://lwn.net/Articles/45373/">Harping on ARP</a>）</li>
<li>在发送数据时，Linux 会根据路由表来选择出网的网络接口，而网络接口的确认是根据网关地址自动选择的。因此在发送数据时，ip 地址是网络接口的一个属性。</li>
</ul>

<p>路由表主要用来配置出流量从哪个网络接口出去，工作原理是根据目标 ip 匹配路由表中的网段，如果匹配，则从该路由表项目对应的网络接口出去，此外：</p>

<ul>
<li>如果 socket 没有 bind 一个 ip，会使用路由表的 src 字段作为源 ip，参见：<a href="https://serverfault.com/questions/451601/ip-route-show-src-field">ip-route-show-src-field</a>。</li>
<li>路由表的操作，参见：<a href="http://linux-ip.net/html/tools-ip-route.html">ip route 文档页</a>。</li>
</ul>
]]></description></item><item><title>Docker 开启 IPv6</title><link>https://www.rectcircle.cn/posts/docker-ipv6/</link><pubDate>Thu, 21 Apr 2022 00:22:33 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/docker-ipv6/</guid><description type="html"><![CDATA[

<h2 id="背景知识">背景知识</h2>

<h3 id="ipv6">IPv6</h3>

<p>参考：<a href="/posts/learn-ipv6-by-ipv4-diff/">通过和 IPv4 对比，学习 IPv6</a></p>

<h3 id="docker-网络">Docker 网络</h3>

<p>在 Docker 中，网络是一个重要抽象。一个 Docker 可以有多个网络，每个容器可以连接到一个或多个中。</p>

<p>docker 安装完成后，会自动创建三个网络，分别是 bridge、host 和 none。通过 <code>docker network ls</code>  命令可以查看：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">NETWORK ID          NAME                        DRIVER              SCOPE
11da7fc827b4        bridge                      bridge              local
4cd2eae9c4cd        host                        host                local
12730ca5beca        none                        null                local</pre></div>
<p>其中名字为 bridge 的 <a href="https://docs.docker.com/network/bridge/">bridge 类型网络</a>，就是 docker 的默认网络（<code>docker run</code> 默认使用的网络）。</p>

<p>默认网络的实现是在宿主机环境创建一个名为 docker0 的 bridge 设备，并为其配置一个私有网段的网关 IP 地址。通过 <code>ip addr show docker0</code> 可以查看更该设备信息。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">3: docker0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default
    link/ether 02:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::xxxx:xx:xx:xxxx/64 scope link
       valid_lft forever preferred_lft forever</pre></div>
<p>docker <a href="https://docs.docker.com/network/bridge/">bridge 网络</a>，在 IPv4 场景下拓扑如下所示（来自于：<a href="https://www.cnblogs.com/jmilkfan-fanguiju/p/10589727.html">KVM + LinuxBridge 的网络虚拟化解决方案实践</a>）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">+----------------------------------------------------------------+-----------------------------------------+-----------------------------------------+
|                          Host                                  |              Container 1                |              Container 2                |
|                                                                |                                         |                                         |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|       |             Newwork Protocol Stack             |       |       |  Newwork Protocol Stack |       |       |  Newwork Protocol Stack |       |
|       +------------------------------------------------+       |       +-------------------------+       |       +-------------------------+       |
|            ↑             ↑                                     |                   ↑                     |                    ↑                    |
|............|.............|.....................................|...................|.....................|....................|....................|
|            ↓             ↓                                     |                   ↓                     |                    ↓                    |
|        +------+     +--------+                                 |               +-------+                 |                +-------+                |
|        |.3.101|     |  .9.1  |                                 |               |  .9.2 |                 |                |  .9.3 |                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|        | eth0 |     |   br0  |&lt;---&gt;|  veth |                   |               | eth0  |                 |                | eth0  |                |
|        +------+     +--------+     +-------+                   |               +-------+                 |                +-------+                |
|            ↑             ↑             ↑                       |                   ↑                     |                    ↑                    |
|            |             |             +-------------------------------------------+                     |                    |                    |
|            |             ↓                                     |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |         |  veth |                                 |                                         |                    |                    |
|            |         +-------+                                 |                                         |                    |                    |
|            |             ↑                                     |                                         |                    |                    |
|            |             +-------------------------------------------------------------------------------|--------------------+                    |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
|            |                                                   |                                         |                                         |
+------------|---------------------------------------------------+-----------------------------------------+-----------------------------------------+
             ↓
     Physical Network  (192.168.3.0/24)</pre></div>
<p>通过 <code>docker network inspect bridge</code> 可以查看某该默认网络配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[
    {
        &#34;Name&#34;: &#34;bridge&#34;,
        &#34;Id&#34;: &#34;11da7fc827b4dxxx&#34;,
        &#34;Created&#34;: &#34;2021-11-22T12:04:03.408536176+08:00&#34;,
        &#34;Scope&#34;: &#34;local&#34;,
        &#34;Driver&#34;: &#34;bridge&#34;,
        &#34;EnableIPv6&#34;: false,
        &#34;IPAM&#34;: {
            &#34;Driver&#34;: &#34;default&#34;,
            &#34;Options&#34;: null,
            &#34;Config&#34;: [
                {
                    &#34;Subnet&#34;: &#34;172.17.0.0/16&#34;,
                    &#34;Gateway&#34;: &#34;172.17.0.1&#34;
                }
            ]
        },
        &#34;Internal&#34;: false,
        &#34;Attachable&#34;: false,
        &#34;Ingress&#34;: false,
        &#34;ConfigFrom&#34;: {
            &#34;Network&#34;: &#34;&#34;
        },
        &#34;ConfigOnly&#34;: false,
        &#34;Containers&#34;: {
            &#34;0d744147030829f0247xx&#34;: {
                &#34;Name&#34;: &#34;container1&#34;,
                &#34;EndpointID&#34;: &#34;6f539a054ae35cbxx&#34;,
                &#34;MacAddress&#34;: &#34;02:42:xx:xx:xx:xx&#34;,
                &#34;IPv4Address&#34;: &#34;172.17.0.14/16&#34;,
                &#34;IPv6Address&#34;: &#34;&#34;
            },
        },
        &#34;Options&#34;: {
            &#34;com.docker.network.bridge.default_bridge&#34;: &#34;true&#34;,
            &#34;com.docker.network.bridge.enable_icc&#34;: &#34;true&#34;,
            &#34;com.docker.network.bridge.enable_ip_masquerade&#34;: &#34;true&#34;,
            &#34;com.docker.network.bridge.host_binding_ipv4&#34;: &#34;0.0.0.0&#34;,
            &#34;com.docker.network.bridge.name&#34;: &#34;docker0&#34;,
            &#34;com.docker.network.driver.mtu&#34;: &#34;1500&#34;
        },
        &#34;Labels&#34;: {}
    }
]</pre></div>
<p>可以通过 <a href="https://docs.docker.com/engine/reference/commandline/network_create/">docker network create 命令</a>，创建一个自定义 bridge 网络。关于，默认网络和自定义 bridge，有如下不同：</p>

<ul>
<li>自定义 bridge 网络会使用 docker 内嵌的 <a href="https://docs.docker.com/config/containers/container-networking/#dns-services">dns server 服务</a>，配置地址为 <code>127.0.0.11</code>，通过 iptables 转发到 <code>43747</code> 端口。因此可以直接通过 container name 访问同一个自定义网络下的其他容器网络。而默认网络则不支持。</li>
<li>自定义 bridge 有更好的隔离性。</li>
<li>一个容器可以在运行时动态的连接/断开一个自定义 bridge，默认网络只能重新创建。</li>
<li>自定义 bridge 可以在创建的时候配置 Linux bridge，如果要修改默认网络的 bridge 则需要重启 docker daemon。</li>
</ul>

<p>因此，官方更推荐在生产环境使用自定义 bridge 而非默认网络。</p>

<h2 id="默认网络支持-ipv6">默认网络支持 IPv6</h2>

<blockquote>
<p>本章节介绍的是如何配置默认的 bridge 网络支持 ipv6。（未经过测试，仅供参考）</p>
</blockquote>

<p>前置条件：确保自己的设备被分配了一个 IPv6。通过 <code>ip addr show</code> 查看当前设备的 IPv6。其输出的物理网卡存在包含 <code>inet6</code> 和 <code>scope global</code> 的行时，表示该网卡支持 IPv6。需要注意的是：其 IPv6 地址的前缀不能是 <code>/128</code>，如果是 <code>/128</code>，参见：<a href="#通过-ipv6nat-方式支持-ipv6">通过 IPv6NAT 方式支持 IPv6</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether fa:16:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 10.227.8.141/22 brd 10.227.11.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2xxx:xxxx::xxxx/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link
       valid_lft forever preferred_lft forever</pre></div>
<p>修改 <code>/etc/docker/daemon.json</code>，其中 <code>fixed-cidr-v6</code> 是上一步获取到的 IPv6 网段的子网（配置默认网络，前缀长度最大为 <code>/80</code>）（参考：<a href="https://docs.docker.com/config/daemon/ipv6/">官方文档</a>）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
  &#34;ipv6&#34;: true,
  &#34;fixed-cidr-v6&#34;: &#34;2xxx:xxxx::/80&#34;
}</pre></div>
<p>reload 配置，docker daemon 将会使用 IPv6 网络。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">sudo systemctl reload docker</pre></div>
<p>通过 <code>docker network inspect bridge</code> 命令检查是否生效。若生效，则 <code>EnableIPv6</code> 值为 <code>true</code>，<code>IPAM.Config[1].Subnet</code> 是上一步配置的 <code>fixed-cidr-v6</code>。</p>

<p>注意经测试，如下场景可能不会生效：</p>

<ul>
<li><code>/etc/docker/daemon.json</code> 存在 <code>&quot;live-restore&quot;: true</code> 字段。</li>
<li>reload 时有容器仍然存在。</li>
</ul>

<p>根据众多<a href="https://medium.com/@skleeschulte/how-to-enable-ipv6-for-docker-containers-on-ubuntu-18-04-c68394a219a2">博客</a>的说法，还需如下两步（docker 官方文档没有提及）（网络拓扑参见：<a href="https://dker.ru/docs/docker-engine/user-guide/network-configuration/default-bridge-network/ipv6-with-docker/">IPv6 with Docker</a>）：</p>

<ul>
<li><p><code>/etc/sysctl.conf</code> 添加，并执行 <code>sysctl -f</code>，配置宿主机和 docker0 网卡支持 NDP proxy。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"># docker0 是 docker 默认的网桥 (bridge)
net.ipv6.conf.docker0.proxy_ndp=1
# eth0 表示物理网卡，注意替换为物理网卡
net.ipv6.conf.eth0.proxy_ndp=1</pre></div></li>

<li><p>默认的 ndp 邻居发现配置仅允许单个 IP 配置。需要安装 ndppd 服务来转发邻居发现消息（这一步还有一个替代方案：手动为每一个容器配置如：<code>ip -6 neigh add proxy 2xxx:xxxx::1 dev ens3</code>，其中，<code>2xxx:xxxx::1</code> 为容器的分配的 IPv6，ens3 为宿主机绑定 IPv6 的网卡）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">apt-get update -y
apt-get install -y ndppd
cp /usr/share/doc/ndppd/ndppd.conf-dist /etc/ndppd.conf</pre></div>
<ul>
<li>更改 <code>proxy eth0 {</code> 行到宿主机绑定 IPv6 的网卡，如： <code>proxy ens3 {</code>。</li>
<li>更改 <code>rule 1111:: {</code> 行为需要暴露的网段 <code>2xxx:xxxx::/80 {</code>。</li>
</ul>

<p>最后执行 <code>systemctl restart ndppd</code></p></li>
</ul>

<p><strong>注意：</strong></p>

<ul>
<li>本方法仅针对新装 Docker 场景，如果是将 IPv6 升级到 IPv4，参见：<a href="#自定义网络支持-ipv6">自定义网络支持 IPv6</a>。</li>
<li><a href="#默认网络支持-ipv6">本章节</a> 和 <a href="#自定义网络支持-ipv6">自定义网络支持 IPv6</a> 配置的 IPv6 和 docker 默认 IPv4 是不同的。

<ul>
<li>容器的 IPv6 用的不是私有网段，而是宿主机网络或者是宿主机网络的一个子网。因此，宿主机所在的网络的所有实例可以直接通过 IPv6 的地址。也就是说：<strong>容器的所有端口对于 IPv6 来说都是公开的，而无需 public</strong>。</li>
<li>而容器的 IPv4 分配的是私有网段，因此，容器网段和宿主机网段是通过 NAT 转发数据的，因此宿主机所在网络的其他实例是无法直接访问容器。也就是说：<strong>容器的所有端口对于 IPv4 来说都是私有的，需 public 到 host 网络才能被外部访问到</strong>。</li>
</ul></li>
</ul>

<h2 id="自定义网络支持-ipv6">自定义网络支持 IPv6</h2>

<blockquote>
<p>本章节介绍的是如何创建一个支持 IPv6 的 bridge 网络。（未经过测试，仅供参考）</p>
</blockquote>

<ul>
<li>前置条件：确保自己的设备被分配了一个 IPv6，参见：<a href="#默认网络支持-ipv6">默认网络支持 IPv6</a>。</li>

<li><p>创建一个支持 IPv6 的 bridge 网络。其中 <code>--subnet</code> 参数为上一步获取到的 IPv6 网段的子网（自定义 bridge 网络，前缀长度不限制，可以大于于 80）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker network create my-net-ipv6 --ipv6 --subnet<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2xxx:xxxx::/80&#34;</span></code></pre></div></li>

<li><p>通过 <code>docker network inspect my-net-ipv6</code> 命令检查是否生效。若生效，则 <code>EnableIPv6</code> 值为 <code>true</code>，<code>IPAM.Config[1].Subnet</code> 是上一步配置的 <code>fixed-cidr-v6</code>。</p></li>

<li><p>创建容器时，通过 <code>--network my-net-ipv6</code> 参数，给容器开启 IPv6 网络，如 <code>docker run --network my-net-ipv6 -it busybox ip addr show</code>，可以看到，网卡被分配了 IPv6 地址。</p></li>

<li><p>配置网卡支持 NDP proxy 和 转发邻居发现消息。参见：<a href="#默认网络支持-ipv6">默认网络支持 IPv6</a>。</p></li>
</ul>

<p><strong>注意事项：</strong>参见<a href="#默认网络支持-ipv6">默认网络支持 IPv6</a> 第二条。</p>

<h2 id="通过-ipv6nat-方式支持-ipv6">通过 IPv6NAT 方式支持 IPv6</h2>

<blockquote>
<p>测试可行，推荐使用该方式。</p>
</blockquote>

<p>上文也提到，上文展示的方案，容器获得的 IPv6 IP 并不是私有网络 IP，是和外部网络直接连通，而不会经过 NAT。在如下场景下，以上方式可能不能满足要求：</p>

<ul>
<li>安全性，要求容器的网络是私有的，需要容器的网络行为和 Docker IPv4 的行为一致，只有特定端口才能访问。</li>
<li>宿主机处于一个很小范围的网段（前缀大于 <code>/80</code>），如 <code>xxx::xx/128</code>，没有多余的 IPv6 可以分给容器。</li>
</ul>

<p>此时就需要，给容器配置一个私有 IPv6 网段，并启用 NAT。</p>

<p>但是 Docker 官方并没有内置 IPv6 的 NAT，如果想要使用 IPv6 NAT，需要安装外挂的 IPv6 启动，参见：<a href="https://github.com/robbertkl/docker-ipv6nat">robbertkl/docker-ipv6nat</a>。</p>

<p>有这这些准备后，实施步骤如下所示：</p>

<ul>
<li><p>使用如下命令，后台启动 IPv6 NAT（通过 <code>--restart always</code> 配置了开机自启）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">docker run -d --name ipv6nat --privileged --network host --restart always -v /var/run/docker.sock:/var/run/docker.sock:ro -v /lib/modules:/lib/modules:ro robbertkl/ipv6nat</pre></div></li>

<li><p>和 <a href="#自定义网络支持-ipv6">自定义网络支持 IPv6</a> 类似，创建一个支持 IPv6 的 bridge 网络。其中 <code>--subnet</code> 参数为 <code>fe80::/10</code> 的一个子网。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker network create my-net-ipv6 --ipv6 --subnet<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;fd00:1::1/80&#34;</span> --gateway<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;fd00:1::1&#34;</span></code></pre></div></li>

<li><p>通过 <code>docker network inspect my-net-ipv6</code> 命令检查是否生效。若生效，则 <code>EnableIPv6</code> 值为 <code>true</code>，<code>IPAM.Config[1].Subnet</code> 是上一步配置的 <code>fixed-cidr-v6</code>。</p></li>

<li><p>创建容器时，通过 <code>--network my-net-ipv6</code> 参数，给容器开启 IPv6 网络，如 <code>docker run --network my-net-ipv6 -it busybox sh</code>：</p>

<ul>
<li><code>ip addr show</code> ，可以看到，网卡被分配了 IPv6 地址。</li>
<li><code>wget https://ipv6.icanhazip.com  -O /dev/stdout 2&gt;/dev/null</code> 可以看到出网 IPv6 地址。</li>
</ul></li>
</ul>

<h2 id="参考">参考</h2>

<ul>
<li><a href="https://medium.com/@skleeschulte/how-to-enable-ipv6-for-docker-containers-on-ubuntu-18-04-c68394a219a2">How to enable IPv6 for Docker containers on Ubuntu 18.04</a></li>
<li><a href="https://github.com/robbertkl/docker-ipv6nat">robbertkl/docker-ipv6nat</a></li>
<li><a href="https://docs.docker.com/network/bridge/">Docker Docs: Use bridge networks</a></li>
<li><a href="https://www.cnblogs.com/jmilkfan-fanguiju/p/10589727.html">KVM + LinuxBridge 的网络虚拟化解决方案实践</a></li>
<li><a href="https://docs.docker.com/engine/reference/commandline/network_create/">Docker Docs: docker network create 命令</a></li>
<li><a href="https://docs.docker.com/config/containers/container-networking/#dns-services">Docker Docs: dns server 服务</a></li>
<li><a href="https://docs.docker.com/config/daemon/ipv6/">Docker Docs: Enable IPv6 support</a></li>
<li><a href="https://dker.ru/docs/docker-engine/user-guide/network-configuration/default-bridge-network/ipv6-with-docker/">IPv6 with Docker（网络拓扑视角）</a></li>
<li>本文未提及的 IPvlan 模式 <a href="https://dev.to/joeneville_/build-a-docker-ipv6-network-dfj">Build a Docker IPv6 Network</a> | <a href="https://docs.docker.com/network/ipvlan/">Docker Docs: Use IPvlan networks</a></li>
</ul>
]]></description></item><item><title>探索终端的历史渊源</title><link>https://www.rectcircle.cn/posts/terminal-history/</link><pubDate>Sat, 09 Apr 2022 10:26:07 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/terminal-history/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>电子计算机作为第三次工业革命的代表，其发明和迭代不是一蹴而就的，都是在历史技术的基础上一步一步演进而来的。</p>

<p>计算机技术发源于欧美，欧美上一代从业人员是见证了计算技术的诞生和早期发展阶段的，欧美当代从业人员可以从童年接触的老物件以及上一代从业人员，获取到关于计算机技术演进过程的历史线索。</p>

<p>中国没有参与过第一和第二次工业革命。而针对计算机技术，早期（五、六十年代）也是同步发展，但是中间由于特殊历史时期，中断了。当代计算机技术，是近 30 年，重新引入西方已经成熟的技术，才逐步跟上。</p>

<p>而对于国内从业人员来说，当代从业者面对的是当代引进的成熟技术，缺乏历史线索。因此，很多时候，我们面对的一些技术，我们虽然可以很清楚知道用法，知道实现原理，但是很少知道为什么是这样的。这对构建完整的知识体系，非常不利。而 终端（Terminal） 技术就是这样一个例子。</p>

<p>阅读本文可以得到：</p>

<ul>
<li>从历史角度理解技术背后动因的思路。</li>
<li>解释各个操作系统都存在终端长现在这个样子的历史原因。</li>
<li>了解 <code>ed</code>、<code>vi</code> 诞生的技术支撑。</li>
<li>了解 ssh 远程登录，终端相关的原理。</li>
</ul>

<h2 id="背景">背景</h2>

<p>最近，在学习 <a href="https://github.com/krallin/tini">tini</a> （容器的进程管理器）源码时，发现很多之前似懂非懂的感念，如：信号、会话、终端、控制终端、前台进程组、后台进程组。于是，翻开保留的唯一一本本科教材 <a href="http://www.apuebook.com/">《Unix 环境高级编程 第三版》（APUE）</a>，上述概念在书中都有比较详细的描述，但是关于终端部分，始终不太理解。而这些概念都是相互关联的，缺少这一环，对于 <a href="https://github.com/krallin/tini">tini</a> 源码的理解就无法达到融汇贯通的层次，简直如鲠在喉。</p>

<p>于是，经过一番检索，在了解了 Terminal 的历史发展过程，再结合书中的概念，终于豁然开朗。</p>

<h2 id="理解终端的意义">理解终端的意义</h2>

<p>终端（命令行交互页面）是人机交互发展史非常重要的一环。虽然目前普通用户使用的都是图形化的人机交互界面，而对于计算机从业人员而言：</p>

<ul>
<li>对于研发人员而言，命令行交互界面仍然是研发最常用的交互手段，理解了终端可以帮助研发人员更好的开发命令行交互的程序。</li>
<li>对于产品/设计人员而言，可以从中看到交互设计和硬件演变相互促进的过程，对图形化人机交互设计，以及面向未来的新硬件的人机交互（VR/AR），应该有所启发。</li>
</ul>

<h2 id="历史">历史</h2>

<blockquote>
<p>本小结的阶段指的是计算机终端发展史。
主要参考：<a href="https://www.howtogeek.com/727213/what-are-teletypes-and-why-were-they-used-with-computers/">What Are Teletypes, and Why Were They Used with Computers?</a> 和 <a href="https://en.wikipedia.org/wiki/Computer_terminal">Computer terminal Wiki</a></p>
</blockquote>

<h3 id="史前阶段">史前阶段</h3>

<blockquote>
<p>参考：<a href="https://en.wikipedia.org/wiki/Teleprinter">Teleprinter wiki</a></p>
</blockquote>

<p>先聊一聊计算机诞生之前，应用于电报的一种设备，电传打字机 Teleprinter。</p>

<p>电报是一种点对点的文本信息系统。在 1840 年代被发明出来，并不断演进，如今已被移动通讯系统逐步取代。</p>

<p>由于，中国没有参与第二次工业革命，我们印象中的电报指的就是，使用滴答声音和<a href="https://en.wikipedia.org/wiki/Morse_code">莫尔斯码</a>的电报（使用 <a href="https://en.wikipedia.org/wiki/Telegraph_key">Telegraph key</a> 和 <a href="https://en.wikipedia.org/wiki/Telegraph_sounder">Telegraph sounder</a>，作为输入和输出）。</p>

<p>上述的方式输入和输入成本都非常高，原因是需要人来进行翻译：输入，需要将文本翻译为莫尔斯码；输出，需要将莫尔斯码翻译为文本。</p>

<p>因此，在欧美国家，电报获得了更一步的发展最终通过电传打字机和路由自动化技术，最终诞生了电报互联网 <a href="https://en.wikipedia.org/wiki/Telex">Telex</a>。</p>

<p>电传打字机，这种设备可能很少出现在中国的家庭中，但是在欧美国家应该比较常见。</p>

<p>电传打字机就是一个负责输入和输出的设备，包含两个部分，键盘和打字机，通过该设备，人们可以无需学习任何电报编码表，即可收发电报。其核心能力就两个：</p>

<ul>
<li>将接收到的电信号转化为机械运动驱动打印机将字母打印到纸上。</li>
<li>将在键盘上的输入信号编码转换为电信号发送出去</li>
</ul>

<p>这里有几个来自 Youtube 的关于电传打字机的老视频（更多可以在 Youtube 搜索：<a href="https://www.youtube.com/results?search_query=Teleprinter">Teleprinter</a>）。</p>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/n-eFFd5BmpU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/-2gXC-ZPKCM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<h3 id="早期阶段">早期阶段</h3>

<p>在计算机刚刚发明的时期，计算机面临这人机交互的问题。而电传打字机是一个很好的选择。起初，电传打字机会将输入转换为<a href="https://en.wikipedia.org/wiki/Punched_card">穿孔卡片</a>再输入计算机，计算机输出到穿孔卡片连接到电传打印到纸上。后面随着技术发展，电传打字机可以通过线缆直接连接到计算机上。</p>

<p>最著名的电传打字机的型号是 <a href="https://en.wikipedia.org/wiki/Teletype_Model_33">Teletype Model 33</a>，是最早使用 ASCII 码的产品之一。</p>

<p><a href="https://en.wikipedia.org/wiki/Unix">Unix</a> 在 1970 年左右被开发出来，并运行在 PDP-11 上，使用了 <a href="https://en.wikipedia.org/wiki/Teletype_Model_33">Teletype Model 33</a> 作为其核心用户界面，这极大的影响了后面计算机终端的发展。</p>

<blockquote>
<p>在计算机领域，由于 Teletype 这家公司太过出名，Teletype 就是电传打字机的代名词，因此其直接被翻译为了电传打字机，在类 Unix 系统中，tty 就是此单词的缩写。其中最著名的是 Teletype Model 33 型，其历史影响参见：<a href="https://en.wikipedia.org/wiki/Teletype_Model_33#Historical_impact">wiki</a> （这里提一下许多场景推荐的最大行长度限制 72，猜测主要自于 Teletype Model 33 的每行打印字母数）</p>
</blockquote>

<p>下面有一些视频，可以看到电传打字机和计算机连接的场景。</p>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/E4IztV7M3jI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/ObgXrIYKQjc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/_eShaxVcLo8?start=99" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<iframe  width="760" height="500"  src="https://www.youtube.com/embed/39ZCb65plIQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<p>受限于电传打字机的物理特性，影响到了 Unix 的用户交互界面的设计以及一些其他特性：</p>

<ul>
<li>逐行依次输出文本，因为电传打字机是无法修改已经打印到纸上的字符的。</li>
<li>各种 shell 都提供命令提示符，用来区分需要区分用户的输入和机器的输出，因为电传打字机是黑白，只能通过命令提示符来区分。</li>
<li>回车和换行符是两个，因为电传打字机切换行时是需要先将打字头回到开头，然后纸筒滚动一行。</li>
<li>由于电传打字机的键盘是计算机的唯一的输入端，因此通过 ctrl 加字符打印出一些特殊字符，以给正在运行程序发送信号。</li>
<li>多数库函数的打印日志均采用 <code>print</code> 单词，原因在于，在当时看，就是物理意义上打印到纸上。</li>
</ul>

<p>时至今日，目前的类 Unix 系统仍然可以使用电传打字机作为用户界面使用，下面有一个使用 1930 年的电传打字机操作 Linux 的案例。</p>

<iframe width="760" height="500"  src="//player.bilibili.com/player.html?aid=582863800&bvid=BV1U64y1T7xR&cid=178411950&page=1&t=660.5" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

<p>在最初的 Unix 中，附带了几个著名的程序，均可以很好的在电传打字机交互界面中使用：</p>

<ul>
<li><a href="https://en.wikipedia.org/wiki/Ed_(text_editor)">ed 文本编辑器</a></li>
<li><a href="https://en.wikipedia.org/wiki/Thompson_shell">Thompson shell 以及之后衍生的众多 shell</a></li>
</ul>

<p>在计算机领域内 <a href="https://en.wikipedia.org/wiki/Computer_terminal">终端（Terminal） 概念</a>就是对电传打字机这类负责计算机和人类交互的输入输出设备的统称。</p>

<p>在早期，可以说 Terminal 就是 Teletype，但是随着计算机的发展 Terminal 有了其他的形态，但是电传打字机奠定目前仍在使用的终端的基本形式和基本架构。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">用户 &lt;------&gt; 电传打字机/终端硬件 &lt;------&gt; 终端设备驱动 &lt;-------&gt; 终端行规程（内核） &lt;------&gt; 进程的标准 IO</pre></div>
<ul>
<li>终端设备驱动，维护输入和输出队列，负责开关回显功能</li>
<li><a href="https://en.wikipedia.org/wiki/Line_discipline">终端行规程</a>（参见：<a href="https://www.php.cn/linux-382655.html">博客</a>），在输入时，它处理特殊字符，例如中断字符（通常是 Control-C）和擦除和终止字符（通常分别是退格或删除和 Control-U），在输出时，它用一个替换所有 LF 字符CR/LF 序列等等。</li>
<li>进程的标准 IO，抽象成标准 IO，让程序可以使用标准的 IO 接口读写终端。</li>
</ul>

<h3 id="中期阶段">中期阶段</h3>

<p>随着显示技术（CRT 技术）的发展，视频终端的成本逐年下降，电传打字机逐渐退出了历史舞台。视频技术带来了电传打字机难以实现的效果：</p>

<ul>
<li>更多的字符集支持。</li>
<li>光标可寻址，即可以修改整个屏幕任意光标位置的字符

<ul>
<li>vi/vim（取代了 ed 命令） 更加直观。</li>
<li>基于此诞生了最早的由文本绘制的图形交互界面。</li>
</ul></li>
<li><a href="https://man7.org/linux/man-pages/man4/console_codes.4.html">字符颜色</a>/字重/字样的支持。</li>
</ul>

<p>在 1980 年代，<a href="https://en.wikipedia.org/wiki/VT100">DEC VT-100</a> 是最具代表性的产品。该产品是第一个遵循 <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI escape codes 标准</a> 的产品，事实上推动了该标准的落地。该设备带来了如下影响。</p>

<ul>
<li>目前 <a href="https://man7.org/linux/man-pages/man7/term.7.html">Linux 终端</a>仍然支持 vt100。</li>
<li>24 行 80 字符宽，许多场景推荐的最大行长度限制 <sup>79</sup>&frasl;<sub>80</sub> 个字符（也可能来源于 IBM 穿孔卡片 或者 <a href="https://www.dongwm.com/post/pep8-max-line-length/">A4 纸打印等宽字符</a>）。</li>
</ul>

<iframe width="760" height="500" src="https://www.youtube.com/embed/6zBvYs5Zej0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<p>此后推出的 <a href="https://en.wikipedia.org/wiki/VT220">DEC VT220</a> 的键盘布局极大的影响了 <a href="https://en.wikipedia.org/wiki/Model_M_keyboard">IBM Model M 键盘</a> 布局，从而影响到当代全键盘的样式。</p>

<p>到了这个阶段，我们当代从业人员使用的终端的标准已经确定了下来。也就是说，当代计算机行业从业人员，穿越到使用视频终端和 <code>vi</code> 的年代，我们也可以无成本的从事老本行。</p>

<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Computer_terminal#%22Intelligent%22_terminals">“智能终端”(&ldquo;Intelligent&rdquo; terminals)</a>和<a href="https://zh.wikipedia.org/wiki/%E5%93%91%E7%BB%88%E7%AB%AF">哑终端 (dumb terminal)</a>：上文提到的 VT100 等设备因为能正确识别并处理光标移动等控制字符处理在当时被称为智能终端，在当时，此类设备价格昂贵。因此，很多相对廉价的视频终端的能力和传统的电传打字机一样，无法支持光标移动，此类终端称为哑终端。</p>
</blockquote>

<h3 id="当代阶段">当代阶段</h3>

<p>随着视频设备的和个人 PC 的发展，专门的终端硬件设备已经退出了历史舞台，终端的硬件被独立的显示器和键盘取代。而终端能力在当代的图形化交互界面中，被抽象为一类叫做虚拟终端的软件，如：</p>

<ul>
<li><a href="https://invisible-island.net/xterm/">类 Unix 系统的 xterm</a></li>
<li><a href="https://iterm2.com/">MacOS 的 iterm2</a></li>
<li><a href="https://github.com/microsoft/terminal">Windows terminal</a></li>
<li><a href="https://xtermjs.org/">浏览器的 xterm.js</a></li>
</ul>

<p>在此阶段，通过伪终端和编程的方式，实现对终端设备的模拟，此时终端模型变为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">用户 &lt;------&gt; 电传打字机/终端硬件设备产生的信号 &lt;-----------&gt; 终端设备驱动 &lt;-------------------------------&gt; 终端行规程（内核） &lt;------&gt; 进程的标准 IO
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
用户 &lt;------&gt; 任意可以转换为 IO 流的东西（如网络IO） &lt;------&gt; 伪终端主设备（进程 A）&lt;---&gt; 伪终端从设备 &lt;------&gt; 终端行规程（内核） &lt;------&gt; 进程的标准 IO（进程 B）</pre></div>
<p>具体而言，SSH 远程登录 shell 的模型如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">  -------------客户端-------------                                 -----------------------------服务端-----------------------------
 |                               |                               |                                                               |
 |                               |                               |                                                               |
用户 &lt;---&gt; 客户端虚拟终端 &lt;---&gt; ssh 客户端进程 &lt;-----(TCP)----&gt; ssh 服务端进程（伪终端主设备） &lt;---&gt; 伪终端从设备 &lt;---&gt; 终端行规程（内核） &lt;---&gt; shell 进程（标准 IO）</pre></div>
<ul>
<li>注意，ssh 客户端进程需关闭终端行规程的所有默认处理（即 raw mode 参见 <a href="https://api.libssh.org/master/libssh_tutor_shell.html#write_data">libssh</a>），所有行为交由服务端的终端行规程处理。</li>
</ul>
]]></description></item><item><title>进程管理器（三）通过 Go 语言实现 tini</title><link>https://www.rectcircle.cn/posts/process-manager-03-single-process-tini-go-impl/</link><pubDate>Tue, 05 Apr 2022 18:13:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/process-manager-03-single-process-tini-go-impl/</guid><description type="html"><![CDATA[

<blockquote>
<p>参考的 tini 版本： <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c">v0.19.0</a></p>
</blockquote>

<h2 id="概述">概述</h2>

<p>本文将介绍如何使用 Go 语言，实现功能上和 C 语言编写的 tini 完全相同的一个命令行程序。</p>

<p>Go 语言和 C 语言是两种完全不兼容的，不同的语言，虽然 Go 存在 cgo 可以调用动态链接库，但是这样做比较繁琐，且依赖比较大。</p>

<p>因此，本文不以 cgo，而是以纯 Go 语言的方式来实现 tini。下文，着重于介绍，在实现过程，遇到的一些核心问题。</p>

<p>理解这些问题，足以了解如何将一个与操作系统紧密联系的 C 语言程序转写成一个纯 Go 语言的程序，以及如何将用 C 语言描述的 <a href="https://man7.org/">Linux Manuel</a> 应用到 Go 语言中。对理解众多，用 Go 语言开发的开源的云原生基础设施的源码很有帮助，如 docker、k8s。</p>

<h2 id="go-解析命令行参数">Go 解析命令行参数</h2>

<h3 id="posix-标准库函数-getopt">POSIX 标准库函数 <code>getopt</code></h3>

<blockquote>
<p><a href="https://pubs.opengroup.org/onlinepubs/009696799/functions/getopt.html">POSIX getopt</a> | <a href="https://en.wikipedia.org/wiki/Getopt">getopt wiki</a> | <a href="https://www.gnu.org/software/gnuprologjava/api/gnu/getopt/Getopt.html">gun getopt</a> | <a href="https://man7.org/linux/man-pages/man3/getopt_long.3.html">getopt(3) — Linux manual page</a></p>
</blockquote>

<p><code>getopt</code> 参数风格又称：C 风格命令行参数解析器，Unix 风格命令参数。</p>

<p><code>int getopt(int argc, char *const argv[], const char *optstring)</code> 是 POSIX 标准库中的一个函数，用于解析命令行参数中的短选项（短选项使用 <code>-</code> 开头，选项表示为单个字符，可选的传递参数。如 <code>-h</code>、<code>-a 1</code>）。</p>

<ul>
<li>getopt 第一个参数为命令行参数的长度，第二个参数为命令行参数数组。</li>
<li>getopt 第三个参数，传递一个模式字符串，如 <code>&quot;ha:c::&quot;</code>，其中：

<ul>
<li><code>h</code> 表示声明了一个无参数选项</li>
<li><code>a:</code> 表示声明了一个有参数的选项。</li>
<li><code>c::</code> 表示声明了一个可选参数的选项</li>
<li><code>W;</code> （GUN 扩展，描述的很模糊），参见：<a href="https://stackoverflow.com/questions/14338223/what-is-the-w-option-of-gnu-getopt-used-for">stackoverflow</a></li>
<li><code>+</code> 开头 （GUN 扩展，或者设置了 <code>POSIXLY_CORRECT</code> 环境变量），表示不对 argv 进行重新排列，即不支持 <code>ls / -al</code>，参见下文（这个 POSIX 标准一致）。</li>
<li><code>-</code> 开头（GUN 扩展），表示非选项参数也作为选项处理，如 <code>ps aux</code> 等价于 <code>ps -aux</code>。</li>
<li><code>:</code> 开头（或者 <code>:</code> 在 <code>+</code>、<code>-</code> 的后面），此时如果遇到选项缺少参数的错误，将返回 <code>':'</code> 而不是 <code>?</code>，</li>
</ul></li>
<li>getopt 返回值是解析到的选项的字符，如 <code>'h'</code>。

<ul>
<li>如果是 <code>'?'</code>，表示有两种类型的错误

<ul>
<li>未定义的选项，此时 <code>optopt</code> 表示不存在的选项的字符。</li>
<li>选项缺少参数，此时 <code>optopt</code> 表示缺少参数的选项的字符（<code>optstring</code> 不以 <code>:</code> 开头）。</li>
</ul></li>
<li>如果是 <code>':'</code>，选项缺少参数，此时 <code>optopt</code> 表示缺少参数的选项的字符（<code>optstring</code> 以 <code>:</code> 开头）。</li>
<li>如果返回 <code>-1</code>，表示接下来不是一个选项，解析结束，

<ul>
<li>如 <code>cmd -h -a 1 subcmd</code>。检查到 <code>subcmd</code> 不是一个选项，将返回 -1，此时 <code>optind</code> 为 4。</li>
<li>如 <code>cmd -h -a 1 -- subcmd</code>。检查到 <code>--</code> 是一个特殊标志（强制结束扫描），将返回 -1，此时 <code>optind</code> 为 5。</li>
</ul></li>
</ul></li>
<li>getopt 还导出了几个全局变量

<ul>
<li><code>extern char *optarg</code> 如果选项包含参数，则参数值将被设置到给参数中。当传递 <code>-a 1</code> 时，getopt 返回 <code>'a'</code> 时，为 <code>&quot;1&quot;</code></li>
<li><code>extern int optind</code> 为下一个需要解析的命令行参数的下标，初始化为 1，如果需要重新解析，需要将该值重设为 1。</li>
<li><code>extern int opterr</code> 如果设置该全局变量为 0 （默认不为 0） 且 <code>optstring</code> 第一个字符为 <code>:</code>，<code>getopt()</code> 将不打印错误消息到 stderr，此时调用者可以通过 <code>'?'</code> 手动打印错误消息。</li>
<li><code>extern int optopt</code> 出现错误时，错误的选项字符。</li>
</ul></li>
<li>getopt 在 GUN 实现中，有一个扩展：支持非选项参数放置在选项的前面。如 <code>ls / -a</code>。这种写法在其他 Unix 系统就会报错，如 MacOS。针对非选项参数放置在选项的前面的情况，getopt 会对命令行参数进行重新排列将非选项参数放到后面，即 <code>ls / -a</code> 变成 <code>ls -a /</code>，然后再解析。如果设置 <code>POSIXLY_CORRECT</code> 环境变量或者 <code>optstring</code> 参数以 <code>+</code> 开头，则禁用该特性。</li>
<li>getopt 支持如下方式传递选项和选项的参数

<ul>
<li><code>-a -b -c 1</code> 分别传递</li>
<li><code>-abc 1</code> 合并传递，其中 <code>a</code> 和 <code>b</code> 不能包含参数， 最后一个 <code>c</code> 可以包含参数。</li>
<li><code>-ffile</code> 如果 <code>f</code> 包含参数，等价于 <code>-f file</code>。</li>
<li><code>-vvv</code> 一个选项可以出现多次，均可以识别到。</li>
</ul></li>
</ul>

<p>一般情况下，getopt 放在一个 while 循环中被调用。一般形式为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>

<span style="color:#66d9ef">int</span>
<span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[ ])
{
    <span style="color:#66d9ef">int</span> c;
    <span style="color:#66d9ef">int</span> bflg, aflg, errflg;
    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>ifile;
    <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>ofile;
    <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>optarg;
    <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">int</span> optind, optopt;
    . . .
    <span style="color:#66d9ef">while</span> ((c <span style="color:#f92672">=</span> getopt(argc, argv, <span style="color:#e6db74">&#34;:abf:o:&#34;</span>)) <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
        <span style="color:#66d9ef">switch</span>(c) {
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;a&#39;</span><span style="color:#f92672">:</span>
            <span style="color:#66d9ef">if</span> (bflg)
                errflg<span style="color:#f92672">++</span>;
            <span style="color:#66d9ef">else</span>
                aflg<span style="color:#f92672">++</span>;
            <span style="color:#66d9ef">break</span>;
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;b&#39;</span><span style="color:#f92672">:</span>
            <span style="color:#66d9ef">if</span> (aflg)
                errflg<span style="color:#f92672">++</span>;
            <span style="color:#66d9ef">else</span> {
                bflg<span style="color:#f92672">++</span>;
                bproc();
            }
            <span style="color:#66d9ef">break</span>;
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;f&#39;</span><span style="color:#f92672">:</span>
            ifile <span style="color:#f92672">=</span> optarg;
            <span style="color:#66d9ef">break</span>;
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;o&#39;</span><span style="color:#f92672">:</span>
            ofile <span style="color:#f92672">=</span> optarg;
            <span style="color:#66d9ef">break</span>;
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;:&#39;</span><span style="color:#f92672">:</span>       <span style="color:#75715e">/* -f or -o without operand */</span>
                fprintf(stderr,
                        <span style="color:#e6db74">&#34;Option -%c requires an operand</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, optopt);
                errflg<span style="color:#f92672">++</span>;
                <span style="color:#66d9ef">break</span>;
        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;?&#39;</span><span style="color:#f92672">:</span>
                    fprintf(stderr,
                            <span style="color:#e6db74">&#34;Unrecognized option: -%c</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, optopt);
            errflg<span style="color:#f92672">++</span>;
        }
    }
    <span style="color:#66d9ef">if</span> (errflg) {
        fprintf(stderr, <span style="color:#e6db74">&#34;usage: . . . &#34;</span>);
        exit(<span style="color:#ae81ff">2</span>);
    }
    <span style="color:#66d9ef">for</span> ( ; optind <span style="color:#f92672">&lt;</span> argc; optind<span style="color:#f92672">++</span>) {
        <span style="color:#66d9ef">if</span> (access(argv[optind], R_OK)) {
    . . .
}
</code></pre></div>
<p>我们常用的 ls、ps 等实际上使用的是 <code>getopt</code> 的超集 <code>getopt_long</code>，该函数虽然不是 POSIX 标注，但是类 Unix 平台也都支持。</p>

<p>关于 <code>getopt_long</code> 参见：<a href="https://man7.org/linux/man-pages/man3/getopt_long.3.html">getopt(3)</a>。在此就不多赘述了。</p>

<h3 id="go-语言版本的-getopt">Go 语言版本的 <code>getopt</code></h3>

<p>在 Go 语言中，标准库中的 <code>flag</code> 包也可以实现 <code>getopt</code> 类似的效果，但是其并不遵循 POSIX 标准，比如，其同时支持 <code>--</code> 和 <code>-</code> 没有区别。</p>

<p>经过搜寻，找到一个第三方的开源的符合 <code>POSIX/GUN</code> 标准的选项解析器： <a href="https://github.com/pborman/getopt">pborman/getopt</a>。</p>

<p>在本次实现中，就使用了该库，源码参见：<a href="https://github.com/rectcircle/tini-go/blob/e0f3f7ac3cff8406c87472fef392bc5123b60933/main_linux.go#L122">parseArgs 函数</a>。</p>

<h2 id="go-使用-linux-系统调用和库函数">Go 使用 Linux 系统调用和库函数</h2>

<p>现代高级编程语言的跨平台特性，是非常重要的。因此在高级编程语言的标准库中，提供了对主流操作系统能力的通用化封装，这意味着，某些特定操作系统的能力在标准库中并不存在。Go 语言也是如此。</p>

<p>Go 提供了针对操作系统平台的条件编译的能力。针对 Linux 平台，对于操作系统的底层能力，Go 将其封装到了 <code>syscall</code> 包中。</p>

<p>直接通过 <code>syscall</code> 包，调用 Linux 的系统调用太过原始。Go 将 unix 相关的系统调用封装到了官方的第三方模块 <a href="https://pkg.go.dev/golang.org/x/sys/unix"><code>golang.org/x/sys/unix</code></a> 中，glibc 中的函数多数函数均可在这个模块中找到。</p>

<h2 id="go-语言信号处理">Go 语言信号处理</h2>

<p>和无法在 Go 中创建操作系统级别的线程，而只能创建协程类似。Go 语言把信号处理相关内容封装到了运行时中，这就意味着，我们无法直接调用相关操作系统的系统调用来设置信号屏蔽字等原始的信号操作。而只能使用 <code>os/signal</code> 包提供的信号操作。</p>

<p>首先，需要了解 <code>os/signal</code> 包的使用，以及 Go 运行时对于信号的实现。才能设法通过 Go 语言实现和 tini 类似效果的。</p>

<h3 id="go-os-signal-包">Go <code>os/signal</code> 包</h3>

<blockquote>
<p><a href="https://pkg.go.dev/os/signal">os/signal</a></p>
</blockquote>

<p>在 Go 语言中，信号的默认行为和 Linux(POSIX) 大致相同，但是存在如下区别：</p>

<ul>
<li><code>SIGBUS</code>（总线错误）, <code>SIGFPE</code>（算术错误）, <code>SIGSEGV</code>（段错误）称为同步信号，它们在程序执行错误时触发，而不是通过 <code>os.Process.Kill</code> 之类的触发。在 Linux 中是产生 core 文件的，在 Go 中是产生 panic 的。</li>
<li><code>SIGPROF</code>，在 Linux 中默认行为为终止， Go 运行时使用该信号实现 <code>runtime.CPUProfile</code>。</li>
<li><code>SIGPIPE</code>，默认行为和 Linux 不同：

<ul>
<li>写入文件描述符 1 或 2 上的损坏管道（标准输出或标准错误），将导致程序触发 SIGPIPE 信号，此时其默认行为为退出。</li>
<li>其他场景（如往一个关闭的 socket 或 pipe 写数据时），传统 Linux 程序会触发  <code>SIGPIPE</code> 信号，而 Go 只会返回 <code>EPIPE</code> 错误，也会出触发 SIGPIPE 信号，但此时其默认行为为什么都不做。</li>
</ul></li>
</ul>

<p>在 Go 中，信号接收被抽象成了 Go 语言的 channel 特性。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/signal&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#75715e">// 构造一个 channel，用于接收信号
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 如果在发送信号时我们还没有准备好接收，我们必须使用缓冲通道，否则可能会丢失信号。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Interrupt</span>)

    <span style="color:#75715e">// 阻塞等待，直到收到信号。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">c</span>
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Got signal:&#34;</span>, <span style="color:#a6e22e">s</span>)
}</code></pre></div>
<p>其他函数为：</p>

<ul>
<li><code>signal.Ignore</code> 和 Linux 不同，在 Go 语言中忽略一个信号通过该专门的函实现的</li>
<li><code>signal.Ignored</code> 检查一个信号是否被忽略。</li>
<li><code>signal.Reset</code> 将信号处理函数恢复为默认行为（这里的默认行为是 Go 定义的默认行为）。</li>
<li><code>signal.Stop</code> 不再将转发信号到信号中，但是不会将信号恢复为默认行为：</li>
</ul>

<h3 id="信号在-go-runtime-的实现">信号在 Go runtime 的实现</h3>

<blockquote>
<p><a href="https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/signal/">Go 语言原本</a> | <a href="https://github.com/golang/go/blob/f229e7031a6efb2f23241b5da000c3b3203081d6/src/runtime/sigtab_linux_generic.go#L9">源码：Linux 下对信号的配置</a> | <a href="https://github.com/golang/go/blob/9839668b5619f45e293dd40339bf0ac614ea6bee/src/runtime/signal_unix.go#L608">源码：</a></p>
</blockquote>

<p>Go 存在一个运行时，在 Runtime 启动之处的主线程 M0，会对信号进程初始化（只会在 M0 初始化 1 次），大致步骤为：</p>

<ul>
<li>记录启动之初的信号屏蔽字，原因是，后续步骤会对信号屏蔽字进行更改，需要保存下来，以再创建新的进程时（<code>os/exec</code>）进行恢复。</li>
<li>初始化信号栈，原因是， Go 的协程栈的特殊性，Go 的信号处理函数需要在单独的栈中运行。</li>
<li>初始化信号屏蔽字，原因是，在 Go Runtime 启动时从父进程继承的信号屏蔽字可能屏蔽了一些信号，Go 为了一致性和 Go runtime 的正确性，需要将其恢复。（比如 Go 承诺 <code>ctrl + c</code> 可以终止进程，如果不恢复，<code>ctrl + c</code> 就失效了）</li>
<li>for 循环，为大多数信号（从父进程继承下来的 Ignore 情况等除外）注册信号处理函数。注意，会使用 <code>SA_ONSTACK</code> 标志，理由和第二点一致。这个处理函数实现了 <code>os/signal</code> 描述的默认行为，以及将信号发送给 <code>os/signal</code> 的 <code>Notify</code> 注册的 Channel 中。</li>
</ul>

<h3 id="一个-bug-或者说-特性">一个 bug 或者说 特性</h3>

<p>从源码来看 <code>signal.Reset</code> 只能将 <code>signal.Notify</code> 的对应的信号的行为恢复为默认，对于 <code>signal.Igonre</code> 的信号则不会生效。</p>

<h3 id="tini-信号相关的设计">tini 信号相关的设计</h3>

<p>在 tini 中，对信号信号处理的安排如下：</p>

<ul>
<li>主进程:

<ul>
<li>SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS 保持默认行为</li>
<li>SIGTTIN, SIGTTOU 行为设置为忽略</li>
<li>其他信号转发给工作进程</li>
</ul></li>
<li>子进程:

<ul>
<li>和主进程从父进程继承下来的配置一致</li>
</ul></li>
</ul>

<h3 id="go-语言实现思路">Go 语言实现思路</h3>

<p>在 Go 中，虽然不能实现通过信号屏蔽字实现如上效果，但是可以通过 Go 提供的 <code>os/signal</code> 实现类似的效果：</p>

<ul>
<li>主进程

<ul>
<li>使用 <code>os/signal</code> 的 <code>Notify</code> 接收除了 SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU 之外的信号，并启动一个协程转发信号。</li>
<li>使用 <code>os/signal</code> 的 <code>Ignored</code> 记录 SIGTTIN, SIGTTOU 信号的初始状态。</li>
<li>使用 <code>os/signal</code> 的 <code>Ignore</code> 忽略 SIGTTIN, SIGTTOU 信号。</li>
<li>fork 子进程，将 SIGTTIN, SIGTTOU 的初始状态通过命令行参数传递给子进程（参见下文）。</li>
</ul></li>
<li>业务进程（子进程）

<ul>
<li>在引导阶段，恢复信号

<ul>
<li>如果 SIGTTIN, SIGTTOU 的默认状态为 Ignore，什么都不做，因为从父进程继承了 Ignore 的状态。</li>
<li>否则，使用 <code>os/signal</code> 的 <code>Notify</code> 以及 <code>Reset</code> 将 SIGTTIN（先 <code>Notify</code> 再 <code>Reset</code> 的原因参见上文所述 bug）, SIGTTOU 恢复为默认行为。</li>
</ul></li>
</ul></li>
</ul>

<h2 id="go-语言程序实现子进程的引导阶段">Go 语言程序实现子进程的引导阶段</h2>

<p>上文我们看到。我们需要在子进程中的引导阶段（fork 和 exec 调用之间）插入一段逻辑，来恢复信号。</p>

<h3 id="linux-c-的-fork-exec">Linux C 的 fork-exec</h3>

<p>在 Linux 创建一个进程并执行一个程序，分为两步，fork 和 exec。在 fork 后，子进程进行一些初始化操作后（引导阶段），调用 exec 执行新的进程。</p>

<h3 id="go-os-exec-包及其原理">Go <code>os/exec</code> 包及其原理</h3>

<p>Go 语言的启动一个新的进程，被封装到了 <code>os/exec</code> 下，看起来没有 Linux 中的 fork exec 两步。实际上和 Linux C 类似，也有 fork exec 两个阶段，源码位于：<a href="https://github.com/golang/go/blob/a2baae6851a157d662dff7cc508659f66249698a/src/syscall/exec_unix.go#L141">syscall.forkExec</a>。</p>

<h3 id="方案一-syscall-sys-fork-错误">方案一：syscall.SYS_FORK（错误）</h3>

<p>该方案为：不使用使用 Go 标准库的 <code>os/exec</code> 而是直接使用 <code>syscall.SYS_FORK</code> 来 fork，然后加入一部分逻辑，然后再执行 <code>syscall.Exec</code> 启动业务进程。</p>

<p>但是这样做是错误的，原因在于：<code>fork</code> 在多线程场景的局限性。即：不管当前进程有多少个线程，fork 后创建的子进程，也只会有一个线程，其他线程都将不会被复制下来。</p>

<p>因此在 Go 语言中，经测试 fork 系统调用后， <code>os/signal</code> 将会失效，原因猜测是，fork 后，由于线程的丢失，在 runtime 中，与信号处理相关的逻辑将失效。</p>

<h3 id="方案二-通过一个特殊参数启动当前程序">方案二：通过一个特殊参数启动当前程序</h3>

<p>fork 子进程仍然通过 <code>os/exec</code> 方式启动，但是启动的程序就是主进程的程序。子进程执行完引导逻辑后，调用 <code>syscall.Exec</code> 执行其他程序（注意，第三个参数为 <code>os.Environ()</code> 以继承环境变量）。</p>

<p>在 Linux 中，大致实现如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 父进程
</span><span style="color:#75715e"></span><span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Cmd</span>{
    <span style="color:#a6e22e">Path</span>:   <span style="color:#e6db74">&#34;/proc/self/exe&#34;</span>, <span style="color:#75715e">// 先只支持 Linux
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">Args</span>:   []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;tini-go-bootstrap&#34;</span>, <span style="color:#f92672">...</span>},
    <span style="color:#a6e22e">Stdin</span>:  <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>,
    <span style="color:#a6e22e">Stdout</span>: <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>,
    <span style="color:#a6e22e">Stderr</span>: <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>,
}
<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>()

<span style="color:#75715e">// 子进程
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;tini-go-bootstrap&#34;</span> {
        <span style="color:#75715e">// 引导逻辑
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>        <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Exec</span>(<span style="color:#a6e22e">childPath</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>:], <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Environ</span>())
        <span style="color:#66d9ef">return</span> 
    }
}</code></pre></div>
<h2 id="源码">源码</h2>

<p>除如上核心问题外，其他部分和 tini C 语言版本差别不大。</p>

<p>Go 语言版本的 tini 已经开源在了 Github 上： <a href="https://github.com/rectcircle/tini-go">rectcircle/tini-go</a>。</p>
]]></description></item><item><title>进程管理器（二）单进程管理器 tini 源码分析</title><link>https://www.rectcircle.cn/posts/process-manager-02-single-process-tini-source/</link><pubDate>Tue, 05 Apr 2022 18:11:53 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/process-manager-02-single-process-tini-source/</guid><description type="html"><![CDATA[

<h2 id="tini-简介">tini 简介</h2>

<p><a href="https://github.com/krallin/tini">tini</a> 是一个超轻量级的 <code>init</code> （进程管理器），被设计作为容器的 1 号进程。</p>

<p>tini 只会做如下事情：</p>

<ul>
<li>生成一个进程（tini 旨在在容器中运行），并一直等待它退出。</li>
<li>收割僵尸。</li>
<li>执行信号转发。</li>
</ul>

<p>tini 并不是一个像 systemd 一样的全功能进程管理器，而是一个服务于容器的单进程管理器，只能管理一个进程。一般情况下，服务容器化要求一个容器尽量只做一件事情，即只有一个或一组进程，因此 tini 在容器化场景足够使用了。</p>

<p>tini 编译产物只有一个可执行文件，其静态编译版本，没有任何依赖（如 glibc），可以在任何 Linux 发行版中使用。</p>

<h2 id="tini-使用">tini 使用</h2>

<blockquote>
<p><a href="https://github.com/krallin/tini#using-tini">README#using-tini</a></p>
</blockquote>

<p>tini 预装到了 docker ce 发行版中了，在 <code>docker run</code> 命令中，可以通过 <a href="https://docs.docker.com/engine/reference/commandline/run/"><code>--init</code></a> 参数即可无感的使用 tini。虽然此方式，无法使用 <code>tini</code> 的一些选项，但在绝大多数场景够用。</p>

<p>也可以吧 tini 直接打包到镜像中。然后配置 <code>ENTRYPOINT</code> 为： <code>[&quot;/path/to/tini&quot;, &quot;--&quot;]</code>。（可以添加 <code>tini</code> 的一些选项，但是需要在 <code>--</code> 的前面，如打印更详细的日志： <code>[&quot;/path/to/tini&quot;, &quot;-vvv&quot;, &quot;--&quot;]</code> ）。</p>

<p>更多关于 tini 的选项，参见下文：<a href="#解析参数">解析参数</a>。</p>

<h2 id="tini-优势">tini 优势</h2>

<p>通过 tini 可以避免业务进程重复编写本该由 1 号进程该做的事情，可以帮传统的应用可以无感迁移到容器化部署。</p>

<ol>
<li>收割意外的产生僵尸进程（如果业务进程作为容器的 1 号进程，且没有 wait 子进程退出，则可能产生僵尸进程）。</li>
<li>接收并转发信号，以实现优雅退出（如果业务进程作为容器的 1 号进程，且没有配置信号处理程序，因为 1 号进程的信号的默认行为为：什么都不做，这导致 docker stop 时，发送给该进程的 SIGTERM 信号无法让进程退出）。</li>
<li>从不使用 tini，切换到使用 tini，是透明的，只需要 docker run 时添加 <code>--init</code> 选项，即：

<ul>
<li>不需要改变镜像</li>
<li>不需要 entrypoint 和 command</li>
</ul></li>
</ol>

<blockquote>
<p>shell 也可以做到如上第 1 点，但是无法做到第 2 点。shell 默认的信号处理行为是默认，在 1 号进程中就是忽略，并不会将信号转发给其子进程，因此无法实现 <code>TERM</code> 信号优雅退出。更多参见：<a href="https://github.com/krallin/tini/issues/8">What is advantage of Tini?</a></p>
</blockquote>

<h2 id="tini-源码分析">tini 源码分析</h2>

<blockquote>
<p>版本： <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c">v0.19.0</a></p>
</blockquote>

<h3 id="项目结构">项目结构</h3>

<p>tini 是一个 cmake 项目。代码非常简短，只有一个不到 700 行的 <code>.c</code> 源代码文件（<a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c">tini.c</a>）。</p>

<p>一些功能可以通过一些宏控制是否编译到产物中，本文默认所有的宏均生效，即开启全部特性。</p>

<h3 id="流程概述">流程概述</h3>

<p>tini 在运行时一共有两个进程：主进程和业务进程，由主进程启动业务进程。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">              主进程                                         业务进程
=====================================================================
初始化:       解析参数
                │
                │
                ↓ 
             配置信号
                │
                │
                ↓
    配置父进程退出时子进程触发的信号
                │
                │
                ↓
      将当前进程注册为僵尸收割者
                │
                │
                ↓
       检查当前进程是否是进程收割者
                │
                │
                ↓
           fork 业务进程
                │
                │-----------------------------→   引导阶段: 隔离业务进程
                ↓                                             │
循环流程:   等待并转发信号 -------------------------              │
             ↑    │                             |             ↓
             │    │                             |          恢复信号处理
             │    ↓                             |             │
           收割僵尸进程 ←─--------                |             │
                │               |               |             ↓
                │               |                --------→ 业务程序执行
                ↓               |                             │
               结束              |                             │
                                |                             ↓
                                 --------------------------- 退出</pre></div>
<h3 id="主进程初始化流程">主进程初始化流程</h3>

<h4 id="解析参数">解析参数</h4>

<p>主要解析，命令行参数和环境变量，在源代码中对应的函数分别是 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L308"><code>parse_args</code></a> 和 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L395"><code>parse_env</code></a>。</p>

<p>命令行参数的解析使用 <a href="https://man7.org/linux/man-pages/man3/getopt.3.html"><code>getopt</code> 库函数</a>进行解析。</p>

<ul>
<li><code>--version</code>：只有一个 <code>--version</code> 参数时，打印版本信息。</li>
<li><code>-v</code>、 <code>-vv</code>、 <code>-vvv</code> 影响 tini 打印日志的多少，即日志级别，这些日志到标准输出和标准出错里面。v 越多，打印的约详细。

<ul>
<li>环境变量 <code>TINI_VERBOSITY=0</code>： <code>FATAL</code> 级别。</li>
<li>默认： <code>WARNING</code> 级别。</li>
<li><code>-v</code>： <code>INFO</code> 级别。</li>
<li><code>-vv</code>： <code>DEBUG</code> 级别。</li>
<li><code>-vvv</code>： <code>TRACE</code> 级别。</li>
</ul></li>
<li><code>-h</code> 打印 usage。</li>
<li><code>-s</code> 开启子进程收割者，当前进程作为非 1 号进程时，开启了该特性后，该进程的子孙进程变为孤儿进程时，其父进程将变为当前主进程，而不是 1 号进程。</li>
<li><code>-p SIGNAL</code> 配置父进程结束后，要求内核发送给该进程。</li>
<li><code>-w</code> 是否打印收割非业务进程的日志。</li>
<li><code>-g</code> 将信号转发给业务进程组额不是只是业务进程。</li>
<li><code>-e EXIT_CODE</code> 配置当该业务进程的退出码为指定值时，tini 进程正常退出（退出码为 0），支持配置多个。</li>
<li><code>-l</code> 打印许可证</li>
<li>未知选项：打印 usage</li>
</ul>

<p>环境变量的解析比较简单，通过 <a href="https://man7.org/linux/man-pages/man3/getenv.3.html"><code>getenv</code> 库函数</a>进行解析，环境变量会覆盖命令参数。</p>

<ul>
<li><code>TINI_SUBREAPER</code> 等价于 <code>-s</code>，值任意。</li>
<li><code>TINI_KILL_PROCESS_GROUP</code> 等价于 <code>-g</code>，值任意。</li>
<li><code>VERBOSITY_ENV_VAR</code> 等价于 <code>-v</code> (<code>2</code>)，<code>-vv</code> (<code>3</code>)，<code>-vvv</code> (<code>&gt;=4</code>)，值为整数，<code>1</code> 是默认值，<code>&lt;=0</code> 表示日志级别设置为 <code>FATAL</code>。</li>
</ul>

<h4 id="配置信号">配置信号</h4>

<p>配置信号，在源码中对应的函数是 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L456"><code>configure_signals</code></a>。</p>

<ul>
<li>通过 <a href="https://linux.die.net/man/3/sigfillset"><code>sigfillset</code> 库函数</a> 和 <a href="https://man7.org/linux/man-pages/man3/sigdelset.3p.html"><code>sigdelset</code> 库函数</a>，设置一个信号集。这个信号集包含除了 <code>SIGFPE</code>, <code>SIGILL</code>, <code>SIGSEGV</code>, <code>SIGBUS</code>, <code>SIGABRT</code>, <code>SIGTRAP</code>, <code>SIGSYS</code>, <code>SIGTTIN</code>, <code>SIGTTOU</code> 之外的所有信号。</li>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/sigprocmask.2.html"><code>sigprocmask</code> 系统调用</a>，将主进程的信号屏蔽字设置上一步设置的信号集（这些被屏蔽的信号会在<a href="#主进程循环流程">主进程循环流程</a>，以同步的方式处理），并保存旧的屏蔽字（在<a href="#恢复信号处理">恢复信号处理</a>步骤会用到）。</li>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/sigaction.2.html"><code>sigaction</code> 系统调用</a>，特殊处理 <code>SIGTTIN</code> 和 <code>SIGTTOU</code> 这两个信号，将这两个信号处理函数设置为忽略，并保存旧的行为（在<a href="#恢复信号处理">恢复信号处理</a>步骤会用到）。原因在于：

<ul>
<li>主进程进程不在前台进程组，且主进程会打印一些日志到标准输出中，如果主进程所在的终端配置了 <code>TOSTOP</code>，且不禁用 <code>SIGTTOU</code> 的话将导致进程停止，这不是期望的行为。</li>
<li>在业务进程中调用 <a href="https://man7.org/linux/man-pages/man3/tcgetpgrp.3.html"><code>tcgetpgrp</code> 库函数</a>让业务进程组设置为前台进程组（下文将解释），此时如果 <code>SIGTTOU</code> 没被忽略，则业务进程会被停止。而在父进程中，<code>SIGTTOU</code> 被忽略被继承到业务进程中，从而不会出现这个问题。</li>
<li>关于 <code>SIGTTIN</code> 的忽略，没有具体原因。可能是 <code>SIGTTIN</code> 和 <code>SIGTTOU</code> 这两个信号一般都是一起处理的。</li>
</ul></li>
</ul>

<h4 id="配置父进程退出时子进程触发的信号">配置父进程退出时子进程触发的信号</h4>

<p>当命令行参数包含 <code>-p SIGNAL</code> 时，使用 <code>-p</code> 指定的信号，配置父进程结束后，要求内核发送给主进程信号。源码位于 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L643">643 行</a>。</p>

<p>该特性，通过 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl</code> 系统调用</a> 和 <code>PR_SET_PDEATHSIG</code> 选项实现。</p>

<h4 id="将当前进程注册为僵尸收割者">将当前进程注册为僵尸收割者</h4>

<p>命令行参数包含 <code>-s</code> 是，则配置主进程称为，子进程收割者。即当，当前进程作为非 1 号进程时，开启了该特性后，该进程的子孙进程变为孤儿进程时，其父进程将变为当前主进程，而不是 1 号进程。在源码中对应的函数是 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L416"><code>register_subreaper</code></a>。</p>

<p>该特性，通过 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl</code> 系统调用</a> 和 <code>PR_SET_CHILD_SUBREAPER</code> 选项实现。</p>

<h4 id="检查当前进程是否是进程收割者">检查当前进程是否是进程收割者</h4>

<p>检查主进程是否是子进程收割者。如果检查不通过，只会打印警告信息，流程继续，而不会失败退出。如下两种情况检查通过：</p>

<ul>
<li>主进程为 1 号进程，通过 <a href="https://man7.org/linux/man-pages/man2/getpid.2.html"><code>getpid</code> 系统调用</a>获取。</li>
<li><a href="#将当前进程注册为僵尸收割者">将当前进程注册为僵尸收割者</a>配置成功.</li>
</ul>

<p>通过 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl</code> 系统调用</a> 和 <code>PR_GET_CHILD_SUBREAPER</code> 选项可以进行检查。</p>

<h4 id="fork-业务进程">fork 业务进程</h4>

<p>经过上述准备，主进程可以 fork 业务进程了。在源码中对应的函数是： <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L181"><code>spawn</code></a>，通过 <a href="https://man7.org/linux/man-pages/man2/fork.2.html">fork 系统调用</a>实现创建子进程，子进程启动后进入<a href="#业务进程引导阶段流程">引导阶段</a>，主进程进入<a href="#主进程循环流程">循环流程</a>。</p>

<h3 id="业务进程引导阶段流程">业务进程引导阶段流程</h3>

<h4 id="隔离业务进程">隔离业务进程</h4>

<p>为了更好的管理业务进程，需要将业务进程和主进程进行隔离。在源码中对应的函数是： <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L150">isolate_child</a>。主要做了两件事：</p>

<ul>
<li>为业务进程创建一个进程组，当前业务进程为进程组组长。通过 <a href="https://www.man7.org/linux/man-pages/man2/setpgid.2.html">setpgid 系统调用</a>实现。</li>
<li>将当前进程组设置为前台进程组。通过 <a href="https://man7.org/linux/man-pages/man3/tcgetpgrp.3.html"><code>tcsetpgrp</code> 库函数</a> 和 <a href="https://man7.org/linux/man-pages/man3/getpgrp.3p.html"><code>getpgrp</code> 库函数</a> 实现。值得注意的是，当当前会话没有 tty 时，仅仅打印 Debug 日志，而不是报错退出（比如 <code>docker run</code> 没有 <code>-t</code> 参数场景）。</li>
</ul>

<h4 id="恢复信号处理">恢复信号处理</h4>

<p>由于主进程对信号进行了操作，因此需要在执行业务程序之前进行恢复。在源码中对应的函数是：<a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L131">restore_signals</a>。</p>

<ul>
<li>恢复信号屏蔽字。通过 <a href="https://man7.org/linux/man-pages/man2/sigprocmask.2.html"><code>sigprocmask</code> 系统调用</a>实现。</li>
<li>恢复 <code>SIGTTIN</code> 和 <code>SIGTTOU</code> 的处理函数为之前的行为。通过 <a href="https://man7.org/linux/man-pages/man2/sigaction.2.html"><code>sigaction</code> 系统调用</a>实现。</li>
</ul>

<h3 id="业务进程执行阶段">业务进程执行阶段</h3>

<p>通过 <a href="https://man7.org/linux/man-pages/man3/exec.3.html"><code>execvp</code> 库函数</a> 启动业务程序，进入执行阶段。</p>

<h3 id="主进程循环流程">主进程循环流程</h3>

<p>主进程进入一个死循环，主要做如下两件事情：</p>

<h4 id="等待并转发信号">等待并转发信号</h4>

<p>等待并转发其他进程发送的信号（如 docker stop 发送 <code>SIGTERM</code> 信号，如业务进程退出信号 <code>SIGCHLD</code>），在源码中对应的函数是 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L501"><code>wait_and_forward_signal</code></a>。</p>

<p>首先，通过 <a href="https://man7.org/linux/man-pages/man2/sigwaitinfo.2.html"><code>sigtimedwait</code> 系统调用</a> 系统调用，非阻塞的递送未决状态的信号（超时 1 秒钟）。如果在此期间如果没有收到信号，则返回。否则：</p>

<ul>
<li>如果收到的是 <code>SIGCHLD</code> 信号，则啥也不做，返回。</li>
<li>如果收到了其他信号，则将信号发送给业务进程组/进程，具体发送给进程组还是进程，由是否传递了命令行参数 <code>-g</code> 决定。返回。</li>
</ul>

<h4 id="收割僵尸进程">收割僵尸进程</h4>

<p>收割僵尸进程，在源码中对应的函数是 <a href="https://github.com/krallin/tini/blob/v0.19.0/src/tini.c#L541"><code>reap_zombies</code></a>)。</p>

<p>该函数，在一个死循环中。在该死循环中：</p>

<ol>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/wait.2.html"><code>waitpid</code> 系统调用</a>配合 <code>WNOHANG</code> 标志，非阻塞的收割僵尸进程。</li>
<li>如果，主进程没有子进程，此时说明业务进程已经退出了，因此<strong>子进程退出码</strong>指针被设置了，结束循环，返回。</li>
<li>如果，没有收割到僵尸进程，打印日志，结束循环并返回。</li>
<li>如果，当收割到僵尸进程时：

<ol>
<li>当收割到的进程不是业务进程时，打印日志，继续死循环，跳转到步骤 1。</li>
<li>当收割到的进程是当前业务进程，指向完如下操作后，继续死循环，跳转到步骤 1：

<ol>
<li>通过 <code>WIFEXITED</code> 宏获取到子进程是否是自己退出的，如果是，则设置<strong>子进程退出码指针</strong>指向的的值为业务进程的退出吗（通过 <code>WEXITSTATUS</code> 宏获取）。</li>
<li>通过 <code>WIFSIGNALED</code> 宏获取到当前进程是否是因为默认行为为终止的信号而退出，如果是，设置<strong>子进程退出码指针</strong>指向的值设置为 <code>(128 + 触发信号) % 256</code>（触发的信号通过 <code>WTERMSIG</code> 宏获取）。如果用户命令行参数配置的 <code>-e EXIT_CODE</code> 和<strong>子进程退出码指针</strong>指向的值相同，则将 <strong>子进程退出码指针</strong>指向的值设置为 0。</li>
<li>其他情况，异常退出。</li>
</ol></li>
</ol></li>
</ol>

<h4 id="主进程流程结束">主进程流程结束</h4>

<p>收割僵尸进程函数存在一个传出参数 <strong>子进程退出码指针</strong> 如果被设置了，则流程结束，退出码为 <strong>子进程退出码指针</strong> 指向的值。</p>
]]></description></item><item><title>进程管理器（一） Linux 背景知识</title><link>https://www.rectcircle.cn/posts/process-manager-01-linux-background-knowledge/</link><pubDate>Tue, 05 Apr 2022 18:09:52 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/process-manager-01-linux-background-knowledge/</guid><description type="html"><![CDATA[

<h2 id="系列综述">系列综述</h2>

<p>本系列将从 Linux 基础知识起步，理解 tini 源码，并实现一个 Golang 版本的单进程管理器。该进程管理器将作为容器的 entrypoint 进程，即容器的 1 号进程，来管理工作进程。</p>

<p>20221113 updated: 新增 <a href="/posts/process-manager-04-go-supervisord/">《进程管理器（四） Go Supervisord》</a>。</p>

<h2 id="本节概述">本节概述</h2>

<p>在 Linux 中实现一个可用于生产环境的 1 号进程并不容易，主要需要考虑如下问题：</p>

<ul>
<li>信号处理和转发</li>
<li>收割子进程</li>
<li>终端设备（tty）、会话和进程组管理</li>
<li>正确的设置工作进程的权限</li>
</ul>

<p>本章节主要参考：</p>

<ul>
<li>书籍 <a href="http://www.apuebook.com/">APUE</a> （即《Unix 环境高级编程》） 第三版</li>
<li><a href="https://man7.org/">Linux Manual 站点</a></li>
</ul>

<p>注意：本文主要以 Linux 为例阐述，不保证其他兼容 POSIX.1 标准的操作系统（Linux、MacOS 均是、Windows 不是）有同样的能力。</p>

<h2 id="进程">进程</h2>

<h3 id="进程和进程树">进程和进程树</h3>

<ul>
<li>在 Linux 中，每个用户态进程都有一个父进程（1 号进程除外，1 号进程的父进程是 内核进程即 0 号进程），这样就构成了一颗根节点为 1 号进程的树。</li>
<li>当进程 a 创建了进程 b，此时进程 a 是进程 b 的父进程，进程 b 是进程 a 的子进程。</li>
<li>每一个进程都有一个唯一标识 ID （即 PID），该 ID 在进程退出之前永远不变。</li>
</ul>

<h3 id="进程的两个阶段">进程的两个阶段</h3>

<p>进程的创建是以 <code>fork</code> 系统调用返回 0 作为起始的，而程序的执行是以 <code>exec</code> 系统调用载入于一个程序开始。在本系列，我们将定义：</p>

<ul>
<li><code>fork</code> 到当前进程的最后一次 <code>exec</code> 之间称为：引导阶段。</li>
<li>当前进程的最后一次 <code>exec</code> 之后称为：执行阶段。</li>
</ul>

<p>图示如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">fork ---&gt; exec ---&gt; exec ... ---&gt; exec ---&gt;  exit
 |                                |  |        |
  --------------------------------    ————————
                  |                       |
                  v                       v
               引导阶段                  执行阶段</pre></div>
<p>编写普通程序时，一般是 fork 之后 立即 exec，因此，引导阶段什么都不做。</p>

<p>但是在编写一个进程管理器场景，区分这两个阶段非常重要。因为，进程管理器需要在引导阶段对当前进程进行一些配置工作。</p>

<h3 id="进程和权限">进程和权限</h3>

<p>在 Linux 操作系统中，多数发行版的进程管理器是 systemd，作为进程管理器，其一般以 root 权限运行。当我们使用安装一个 mysql server 后，其是以 mysql 用户运行的，systemd 是如何实现的呢？</p>

<p>Linux 提供了一个 <code>setuid/setgid</code> 的系统调用，当 root 权限（<code>CAP_SYS_ADMIN</code> 权限）的进程调用时，将会将该进程的 uid 和 gid 设置为指定的用户和组。因此就实现了权限降级，以实现最小化权限的要求。</p>

<p><strong>扩展知识（和本系列无关，仅做分享）：权限升级</strong></p>

<p>上文提到了权限通过 <code>setuid/setgid</code> 即可实现权限降级，但是如何实现权限升级呢？比如一个没有 root 的进程如何以 root 的身份执行一个程序（比如 sudo、su 命令可以创建一个拥有 root 权限的 shell 进程）。</p>

<p>Linux 在文件系统层面，为可执行文件提供了一种称为 设置用户/组 id 位的 flag 属性，当一个文件的属性中启用了 设置用户/组 id 位。那么一个进程使用 <code>exec</code> 系统调用执行该程序时，该程序的权限将变为这个可知执行文件的所属用户和所属组。</p>

<p>因此，一个具有 设置用户/组 id 位 的程序需要自行实现对用户的身份验证，以保证系统安全。</p>

<p>关于此，更多参见： 《APUE》第 4.4、8.11 章节</p>

<h3 id="进程组">进程组</h3>

<p>进程组是一个或多个进程的集合。对于一个进程组：</p>

<ul>
<li>有一个进程组 ID（<code>pgid</code>），其值是是该进程组组长进程的进程 ID。</li>
</ul>

<p>关于进程组的系统调用主要有：</p>

<ul>
<li><code>pid_t getpgrp(void)</code> 系统调用获取当前进程所属的进程组 ID。</li>
<li><code>pid_t getpgid(pid_t pid)</code> 系统调用可以获取指定进程的进程组 ID <code>getpgid(0)</code> 等价于 <code>getpgrp()</code>。</li>
<li><code>int setpgid(pid_t pid, pid_t pgid)</code> 可以将一个进程加入一个现有的进程组，或者创建一个新的进程组且当前调用进程作为进程组组长（<code>setpgid(0, 0)</code>）。<code>pid</code> 参数只能是 0 （当前进程）、当前进程 id、子进程 id（孙子进程不行）。</li>
</ul>

<p>进程组主要有如下几个作用：</p>

<ul>
<li>通过 <code>kill</code> 系统调用，给一个进程组下的所有进程发送信号。</li>
<li>通过 <code>waitpid</code> 系统调用，等待该进程组中的孩子进程改变状态。</li>
<li>在一个会话中：

<ul>
<li>前台进程组中的所有进程都可以读写终端（通过标准 IO）。</li>
<li>终端产生的信号会发送到前台进程组中的所有进程。</li>
</ul></li>
</ul>

<p>进程管理器需要为其管理的进程，在其引导阶段，创建一个进程组，这样做的目的是：通过进程组可以将信号转发给该进程组中的所有信号。</p>

<h3 id="会话">会话</h3>

<p>会话是一个或多个进程组的集合。对于一个会话：</p>

<ul>
<li>有一个会话首进程，该进程是的进程 id 为 <code>sid</code>，属于为会话首进程 id，或会话 ID。</li>
<li>有一个前台进程组（关联终端后才有此概念）</li>
<li>有 0 个或多个后台进程组（关联终端后才有此概念）</li>
</ul>

<p>关于会话的系统调用主要有：</p>

<ul>
<li><code>pid_t setsid(void)</code> 系统调用 创建一个新的会话。调用的进程必须<strong>不</strong>是一个进程组的组长。调用后，将发生如下事情：

<ul>
<li>创建一个新的会话，该进程是该会话中的第一个进程即会话首进程，其进程 id 为该会话的会话 id （<code>sid</code>）。</li>
<li>同步为该进程创建一个进程组，该进程为该进程组的组长，该进程的进程 id 为该进程组的进程组 id。</li>
<li>该进程和调用之前的终端的联系将被切断（即标准 IO 不再指向终端设备）。</li>
</ul></li>
<li><code>pid_t getsid(pid_t pid)</code> 获取当前进程的 sid。

<ul>
<li>pid 为 0 时，获取当前进程的 sid。</li>
<li>pid 为非 0 时，只有 pid 在当前进程所在的 sid 中时才会返回正确结果。</li>
</ul></li>
</ul>

<p>会话主要最终要的作用是：和一个终端关联，即标准 IO 关联的终端是哪一个，这个与会话关联的终端称为该会话的控制终端。当然一个会话也可以不和终端关联，这种场景比较少见，一般传统的 Unix Daemon 程序 才会这么做。</p>

<p>进程管理器可以考虑为其管理的每个进程创建一个会话，并分配一个终端（伪终端），以更好的管理这些进程的日志（标准输出），这设计终端相关内容，参见下文终端小节。</p>

<h3 id="扩展知识-孤儿进程组">扩展知识：孤儿进程组</h3>

<p>定义为：某进程组存在一个进程的其进程的父进程不是改进程组的成员，也不是该会话的其他进程组的成员。</p>

<p>造成存在一个进程组变为孤儿进程组的原因一般是：进程组组长退出，而孩子还活着。</p>

<p>面对新晋孤儿进程组，内核会向该进程组的每一个处于停止状态的进程发送 SIGHUP 信号，然后再发送 SIGCONT 信号。</p>

<h2 id="终端">终端</h2>

<p>终端 (tty, Teletype, Teletypewriter, Teleprinter) 是对一套输入输出设备的抽象，在 Linux 中，终端有字符终端和图形终端。在小节讨论的是字符终端，不会涉及图形终端相关内容。</p>

<p>关于终端历史演进，推荐阅读：<a href="/posts/terminal-history/">探索终端的历史渊源</a></p>

<p>我们通过 xterm、iterm2 以及 ssh 连接到 server 中，获取的一个 shell 时，这个 shell 进程就关联了一个终端，shell 运行的进程也可以在该终端中获取输入打印输出。因此，每一个终端是被多个进程共享的资源，因此为了让各个进程合理的使用终端，产生了会话、控制终端、会话首进程、前台进程组、后台进程组等概念。</p>

<ul>
<li>在应用程序中，我们一般不会感知到终端的原因，Linux 已经把终端抽象成，我们熟知的标准 IO 了。</li>
<li>当我们使用 shell 执行命令时，此时 shell 和正在运行的命令所在进程都和这个终端关联，也就是说，一个终端可以关联多个进程。与同一个终端关联的进程组被称为个：<strong>会话</strong>。而这个和会话关联终端被称为会话的<strong>控制终端</strong>，创建了该会话的进程被称为<strong>会话首进程</strong>。</li>
<li>当我们通过快捷键发送特殊字符时，正在运行的进程可以收到相关信号，此外只有我们输入回车，正在运行的程序才能通过标准 IO 读取到输入的数据。这说明在用户输入内容并不是直接传递到进程中的标准 IO 中的，而是在用户输入流和进程标准 IO 之间存在一套处理逻辑，来实现上述内容，这个程序被称为：<strong>终端行规程</strong>，能接收这些信号进程组称为：<strong>前台进程组</strong>。</li>
<li>当我们通过 <code>命令 &amp;</code> 方式可以创建一个进程，这个进程就会在后台运行，从而不能获取到标准输入，不会接收到信号，但是仍然会向终端输出内容。这种进程所在的进程组称为：<strong>后台进程组</strong>。</li>
</ul>

<p>终端、会话、前后台进程组、会话首进程关系如下图所示（来自 APUE）。</p>

<p><img src="/image/pg-session-ctty.png" alt="image" /></p>

<p>在当代终端已经没有专门的物理设备了，一般通过伪终端（pty）相关系统调用通过编程创建一个虚拟终端。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">用户 &lt;------&gt; 电传打字机/终端硬件设备产生的信号 &lt;-----------&gt; 终端设备驱动 &lt;-------------------------------&gt; 终端行规程（内核） &lt;------&gt; 进程的标准 IO
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
 ‖                        ‖                                   ‖                                          ‖                       ‖
用户 &lt;------&gt; 任意可以转换为 IO 流的东西（如网络IO） &lt;------&gt; 伪终端主设备（进程 A）&lt;---&gt; 伪终端从设备 &lt;------&gt; 终端行规程（内核） &lt;------&gt; 进程的标准 IO（进程 B）</pre></div>
<p>上述模型进程 A 和进程 B 的核心系统调用为：</p>

<ul>
<li>进程 A

<ul>
<li><code>int posix_openpt(int oflag)</code> 创建一个伪终端主设备，返回伪终端主设备文件描述符。</li>
<li><code>int grantpt(int fd)</code> 更改从设备的权限（fd 参数为伪终端主设备文件描述符）</li>
<li><code>int unlockpt(int fd)</code> 允许打开伪终端从设备（fd 参数为伪终端主设备文件描述符）</li>
<li><code>char *ptsname(int fd)</code> 返回该伪终端从设备的文件名（fd 参数为伪终端主设备文件描述符）</li>
<li><code>fork</code> 子进程 B</li>
</ul></li>
<li>子进程 B

<ul>
<li><code>int setsid()</code> 设置新的会话</li>
<li><code>int open(char *name, int flag)</code> 使用普通的 IO 函数打开伪终端从设备</li>
<li>可选的 <code>tcsetattr</code> 设置终端属性</li>
<li>可选的 <code>ioctl</code> 设置终端窗口尺寸</li>
<li><code>dup2</code> 将伪终端分配给标准输入、标准输出、标准出错文件描述符</li>
<li><code>exec</code> 执行程序</li>
</ul></li>
</ul>

<p>关于终端和前后台进程组、会话首进程的系统调用主要有：</p>

<ul>
<li><code>pid_t tcgetpgrp(int fd)</code> 返回 fd 指向终端的关联的前台进程组 id （fd 一般为 0 标准输入）。</li>
<li><code>int tcsetpgrp(int fd, pid_t pgrpid)</code> 更改 fd 指向终端的关联的前台进程组 id 为 <code>pgrpid</code> （fd 一般为 0 标准输入，pgrpid 一般为 <code>getpgrp</code> 返回值，即当前进程所在进程组 id）。</li>
<li><code>pid_t tcgetsid(int fd)</code> 返回 fd 指向终端的会话首进程。</li>
</ul>

<h2 id="信号">信号</h2>

<blockquote>
<p>Linux 手册： <a href="https://man7.org/linux/man-pages/man7/signal.7.html">signal(7) 文档</a></p>
</blockquote>

<h3 id="信号概念">信号概念</h3>

<p>信号是软件中断。当一个进程收到一个信号时，可以配置如下几种处理方式：</p>

<ul>
<li>默认行为，不同的信号有不同的默认行为，默认行为有如下几种：

<ul>
<li>TERM 终止进程（进程直接结束）。</li>
<li>Ign 忽略信号。</li>
<li>Core 终止进程，并触发 core dump。</li>
<li>Stop 停止进程（进程调度状态变更为 Stop，进程不会结束）。</li>
<li>Cont 如果它目前已停止，继续进程。</li>
</ul></li>
<li>忽略信号</li>
<li>自定义处理函数</li>
<li>屏蔽信号</li>
</ul>

<h3 id="信号异步处理">信号异步处理</h3>

<p>信号异步处理指的是，给某个信号设置了自定义处理函数，此时就是信号异步处理函数。</p>

<p>（异步信号处理和多线程一样，存在并发问题，因此不推荐，具体参见下文：中断系统调用和库函数 和 可重入函数）</p>

<h4 id="配置自定义处理函数的方式">配置自定义处理函数的方式</h4>

<ul>
<li><p><a href="https://man7.org/linux/man-pages/man2/signal.2.html">signal(2) 系统调用</a>（不推荐），该函数的语义，对信号的处理可能是不可靠的。原因是早期，当 signal 注册的自定义函数是一次性的，也就是说当函数被调用后，信号处理方式就会恢复为默认行为，这样可能会造成信号丢失。所以一般的如果需要一直生效的写法就是：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">sig_int</span>() {
    <span style="color:#75715e">// 这个时间段，信号处理方式恢复为默认行为，导致信号丢失
</span><span style="color:#75715e"></span>    signal(SIGINT, sig_int);
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}
</code></pre></div>
<p>但是现代 Linux 中，signal 是通过  sigaction 实现的，并不要上述写法，所以也是可靠的。</p></li>

<li><p><a href="https://man7.org/linux/man-pages/man2/sigaction.2.html">sigaction(2) 系统调用</a>，细粒度的控制一个信号的行为。可以用来是实现 <code>signal(2) 系统调用</code> 。可以实现如下效果</p>

<ul>
<li>一旦对一个信号设置了一个动作，那么在再次显示的调用 sigaction 改变之前，将一直生效。</li>
<li>信号处理函数处理过程中屏蔽一些信号递送，通过 <code>sigaction.sa_mask</code> 设置在信号处理过程中，如果触发了其他信号，将这些信号屏蔽住，直到当前信号处理函数返回后，将自动解除这些信号屏蔽，进行递送。关于信号屏蔽参见同步信号处理。</li>
</ul></li>
</ul>

<h4 id="自定义处理函数调用流程">自定义处理函数调用流程</h4>

<p>当针对一个进程配置一个自定义处理函数后，当信号被触发时，对这个自定义处理函数的调用，并不是启动一个新的线程进行处理。而是：</p>

<ol>
<li>将当前进程的主线程暂停，将栈指针和寄存器信息保存下来。</li>
<li>在当前进程的主线程中执行自定义信号处理函数。</li>
<li>恢复主进程的上下文信息，继续执行。</li>
</ol>

<h4 id="中断系统调用和库函数">中断系统调用和库函数</h4>

<p>假设在信号被触发时，当前进程正在调用阻塞性的系统调用时（如：read、write），这些系统调用可能被中断，此时这些函数有如下两种可选的行为：</p>

<ul>
<li>自动重启，当使用 <code>signal</code> 注册自定义处理函数，或者 <code>sigaction</code> 设置了 <code>SA_RESTART</code> 标志时。</li>
<li>返回 <code>EINTR</code> 失败，<code>sigaction</code> 未设置 <code>SA_RESTART</code> 标志时。</li>
</ul>

<p>更多参见： <a href="https://man7.org/linux/man-pages/man7/signal.7.html">Interruption of system calls and library functions by signal handlers</a></p>

<h4 id="可重入函数">可重入函数</h4>

<p>在信号处理函数的编写和普通函数的编写有额外的要求，即不可调用不可重入函数。</p>

<p>可重入函数，即异步信号安全函数。和多线程的线程安全概念类似，更多参见：<a href="https://man7.org/linux/man-pages/man7/signal-safety.7.html">signal-safety(7)</a></p>

<h3 id="信号同步处理">信号同步处理</h3>

<h4 id="信号未决和信号集-信号屏蔽">信号未决和信号集、信号屏蔽</h4>

<p>在信号产生（generation）到信号递送（delivery），之前有一个状态，被称为信号未决（pending）。</p>

<p>在信号异步处理过程中吗，信号未决基本无感，在信号同步处理过程中，可以实现让某信号长时间的处于信号未决的状态，然后通过调用某些函数让这些信号处于已递送的状态。实现这种效果的行为被称为信号屏蔽。换句话说：</p>

<ul>
<li>当一个信号被屏蔽之后，不管之前是该信号配置了处理函数还是忽略还是默认行为，该信号将不会触发任何行为。</li>
<li>当一个信号被取消屏蔽后，如果存在一个处于未决状态的信号，则这个信号将会立即触发配置的行为（可能是忽略、默认行为、自定义处理函数）。</li>
</ul>

<p>Linux 支持屏蔽一批信号，标识这一批信号的的概念是<a href="https://linux.die.net/man/3/sigfillset">信号集</a>（具体实现上是一个位图）， 相关系统调用如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigemptyset</span>(sigset_t <span style="color:#f92672">*</span>set); <span style="color:#75715e">// 将参数修改为空的信号集
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigfillset</span>(sigset_t <span style="color:#f92672">*</span>set);  <span style="color:#75715e">// 赋值为将系统使用的所有信号机
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigaddset</span>(sigset_t <span style="color:#f92672">*</span>set, <span style="color:#66d9ef">int</span> signum); <span style="color:#75715e">// 添加一个信号
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigdelset</span>(sigset_t <span style="color:#f92672">*</span>set, <span style="color:#66d9ef">int</span> signum); <span style="color:#75715e">// 删除一个信号
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigismember</span>(<span style="color:#66d9ef">const</span> sigset_t <span style="color:#f92672">*</span>set, <span style="color:#66d9ef">int</span> signum); <span style="color:#75715e">// 判断一个信号是否在该信号集中
</span></code></pre></div>
<p>屏蔽一批信号的系统调用为 <a href="https://man7.org/linux/man-pages/man2/sigprocmask.2.html">sigprocmask(2) 系统调用</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">sigprocmask</span>(<span style="color:#66d9ef">int</span> how, <span style="color:#66d9ef">const</span> sigset_t <span style="color:#f92672">*</span><span style="color:#66d9ef">restrict</span> set,
                sigset_t <span style="color:#f92672">*</span><span style="color:#66d9ef">restrict</span> oldset);
</code></pre></div>
<ul>
<li>how 为如何修改

<ul>
<li>SIG_BLOCK 添加</li>
<li>SIG_UNBLOCK 删除</li>
<li>SIG_SETMASK 覆盖</li>
</ul></li>
<li>set 带设置的信号机，如果为 NULL 则不会更改</li>
<li>oset 返回修改前的被屏蔽的信号集</li>
</ul>

<p>该系统调用的原理是读取和更改内核中当前进程的信号屏蔽字。</p>

<h4 id="标准信号和实时信号">标准信号和实时信号</h4>

<p>在 Linux 中，信号分为如下两类</p>

<ul>
<li>标准信号，即 POSIX.1 定义的信号。针对每一个标准信号，每个进程只会存在一个未决的信号，也就是说：

<ul>
<li>当某个标准信号被屏蔽后，且收到一个该信号，此时该信号状态是未决的。</li>
<li>此后，在解除屏蔽之前，又收到了该信号多次（这些信号会被丢弃）。</li>
<li>在解除屏蔽后，该信号只会递送一次。</li>
</ul></li>
<li>实时信号，<code>SIGRTMIN</code> 到 <code>SIGRTMAX</code> 之间的信号。针对每一个标准信号，每个进程可以存在多个未决的信号，也就是说：

<ul>
<li>当某个标准信号被屏蔽后，且收到一个该信号，此时该信号状态是未决的。</li>
<li>此后，在解除屏蔽之前，又收到了该信号多次（进行排队）。</li>
<li>在解除屏蔽后，该信号会递送多次次。</li>
</ul></li>
</ul>

<h4 id="递送信号">递送信号</h4>

<p>除了上文描述的，解除一个信号的屏蔽，来让未决的信号递送外，还可以通过如下系统调用和库函数，将信号设置为已递送（这些函数调用后，相当于消费掉了这个信号，解除屏蔽后，不会再重复递送了，即不会触发任何行为）。</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/sigwaitinfo.2.html">sigwaitinfo(2) 和 sigtimedwait(2) 系统调用</a></li>
<li><a href="https://man7.org/linux/man-pages/man3/sigwait.3.html">sigwait(3) 库函数</a></li>
</ul>

<p>注意：<a href="https://man7.org/linux/man-pages/man2/sigpending.2.html">sigpending(2) 系统调用</a> 可以获取到所有处于未决状态的信号，但是不会将这些信号设置为已递送。</p>

<h3 id="信号继承">信号继承</h3>

<p>在 Linux 中，一个进程的 引导阶段 (fork) 和 执行阶段 (exec) 对上面信号的配置的继承是不一样的</p>

<ul>
<li>引导阶段 (fork)，当前进程和父进程的信号处理器完全一样，信号屏蔽字完全一样。</li>
<li>执行阶段 (exec)，和 fork 阶段对比：

<ul>
<li>相同的是：

<ul>
<li>信号处理器为 ignore 和 默认的信号。</li>
<li>信号屏蔽字。</li>
</ul></li>
<li>不同的是：

<ul>
<li>信号处理器为 自定义函数 的信号其信号处理器将恢复为默认。</li>
</ul></li>
</ul></li>
</ul>

<h3 id="作业控制信号">作业控制信号</h3>

<p>POSIX.1 定义了 6 个作业控制相关的信号。</p>

<ul>
<li>SIGCHLD 子进程已停止或者终止，父进程将接收到该信号，默认行为为忽略。</li>
<li>SIGCONT 如果进程已停止，则使其继续运行，默认行为为继续该进程。</li>
<li>SIGSTOP 停止信号（不能被捕捉、忽略或屏蔽），默认行为为停止该进程。</li>
<li>SIGTSTP （Ctrl + Z）交互式停止信号，默认行为为停止该进程。</li>
<li>SIGTTIN 后台进程组成员读控制终端，默认行为停止。</li>
<li>SIGTTOU 后台进程组成员写控制终端，默认行为停止（仅当 tty 被设置为 TOSTOP 时才会发生，即停止后台进程组的输出（在 shell 中可以通过 <code>stty tostop</code> 命令关闭））。</li>
</ul>

<p>注意</p>

<ul>
<li>当一个进程产生四种为停止信号时（SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU），处于未决状态的 SIGCONT 的将被丢弃。</li>
<li>当一个进程产生 SIGCONT 时，处于未决状态的四种停止信号时（SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU）将被丢弃。</li>
<li>SIGCONT 默认行为为继续，但是如果当前进程本身就继续，则该信号不会做任何事情。</li>
</ul>

<h2 id="shell-原理简述">Shell 原理简述</h2>

<ul>
<li>shell 进程会不会创建会话，会话的创建取决于创建 shell 的进程（sshd、xterm 等）是否在其引导阶段分配，一般会分配一个。</li>
<li>创建一个后台进程组：执行 <code>cmd1 &amp;</code> ，通过 <code>setpgid</code> 系统调用创建。</li>
<li>将一个后台进程组切换到前台：<code>fg &lt;jobid&gt;</code> 命令，通过 <code>tcsetpgrp</code> 系统调用将某后台进程组切换到前台。</li>
<li>将一个前台进程组的进程切换到后台，并继续运行：

<ul>
<li>输入 <code>ctrl + z</code> 发送 <code>SIGTSTP</code> 让该进程组的进程停止。</li>
<li><code>shell</code> 进程接收到 <code>SIGCHLD</code> 信号，了解到进程子进程的状态变化，通过 <code>tcsetpgrp</code> 系统调用将 <code>shell</code> 所在进程组设置到前台，并打印提示输出和命令提示符。</li>
<li>通过 <code>bg</code> 命令，向后台进程组发送 <code>SIGCONT</code> 信号，让其继续运行。</li>
</ul></li>
<li>管道符连接命令：<code>ps -o pid,ppid,pgid,sid,tpgid,comm | cat | cat</code>

<ul>
<li><code>bourne shell</code> 即 <code>sh</code> （debian 中的 <code>sh</code> 实际上是 <code>dash</code>，并不是 <code>bourne shell</code>）流程如下所示：

<ul>
<li>shell 主进程 fork 一个 c 进程, c 进程准备两个管道： <code>ab</code>, <code>bc</code> 。</li>
<li>c 进程 fork a 进程， a 进程 <code>dup2</code> 其标准输出为 <code>ab</code> 的输入端； <code>exec ps</code>。</li>
<li>c 进程 fork b 进程， b 进程 <code>dup2</code> 其标准输入为 <code>ab</code> 的输出端，<code>dup2</code> 其标准输出为 <code>bc</code> 的输入端； <code>exec cat</code> 。</li>
<li>c 进程， <code>dup2</code> 其标准输入为 <code>bc</code> 的输出端， <code>exec cat</code>。</li>
<li>a，b，c 进程依次退出，c 进程退出时，shell 主进程将收到 c 的 SIGCHLD 信号，且 waitpid 返回。</li>
<li>shell 记录 c 进程的退出吗。</li>
</ul></li>
<li><code>bourne-agent shell</code> 即 <code>bash</code> （debian 中的 <code>sh</code> 实际上是 <code>dash</code>，并不是 <code>bourne shell</code>）流程如下所示：

<ul>
<li>shell 主进程，准备两个管道（<code>pipe</code>）： <code>ab</code>, <code>bc</code></li>
<li>shell 主进程 fork a 进程，主进程和 a 同时调用 <code>setpgid</code> 为 a 进程创建一个新的进程组（同时调用目的是防止出现时序问题）。</li>
<li>shell 主进程 fork b, c 进程，随后 b, c 进程加入进程 a 所在的进程组。</li>
<li>a 进程 <code>dup2</code> 其标准输出为 <code>ab</code> 的输入端； <code>exec ps</code>。</li>
<li>b 进程 <code>dup2</code> 其标准输入为 <code>ab</code> 的输出端，<code>dup2</code> 其标准输出为 <code>bc</code> 的输入端； <code>exec cat</code> 。</li>
<li>c 进程， <code>dup2</code> 其标准输入为 <code>bc</code> 的输出端， <code>exec cat</code>。</li>
<li>a，b，c 进程依次退出，shell 主进程将收到这些进程的 SIGCHLD 信号，且 waitpid 返回。</li>
<li>shell 记录 c 进程的退出吗。</li>
</ul></li>
</ul></li>
</ul>

<p>其他更多参见：APUE 第 9.8 章、9.9 章。</p>

<h2 id="1-号进程">1 号进程</h2>

<p>1 号进程在 Linux 中的职责是系统的进程管理器，因此和普通进程相比，其有如下不同点：</p>

<ul>
<li>当一个普通进程的父进程先退出的，那么该进程的父进程将变为 1 号进程。因此 1 号进程必须实现对僵尸进程的收割（调用 <code>waitpid</code>）。</li>
<li>1 号进程的所有信号的默认处理行为是忽略（除了 SIGKILL、SIGSTOP 外）。因此必须显式的注册信号处理函数或者通过信号同步处理的方式对信号进行合适的处理。</li>
</ul>
]]></description></item><item><title>通过 Linux API 学习网络协议栈（二）IP 协议</title><link>https://www.rectcircle.cn/posts/learn-net-proto-stack-by-linux-api-2-ip/</link><pubDate>Sun, 27 Mar 2022 22:48:15 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/learn-net-proto-stack-by-linux-api-2-ip/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>本部分将从 协议 和 Socket 编程模型两方面学习/复习 IP 协议。</p>

<p>最后，通过手动实现 ICMP 协议来对本部分内容进行实战。</p>

<h2 id="ip-协议">IP 协议</h2>

<blockquote>
<p>在关于 IP 协议的标准文档，参见如下链接：</p>

<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc791">RFC 791: INTERNET PROTOCOL IP 协议</a> | <a href="https://python.iitter.com/other/229489.html">中文</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc2460">RFC 2460: Internet Protocol, Version 6 (IPv6) Specification IPv6 协议</a></li>
<li><a href="https://en.wikipedia.org/wiki/IPv4">Wiki: IPv4</a> | <a href="https://en.wikipedia.org/wiki/IPv6">Wiki: IPv6</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc1122">RFC 1122: Requirements for Internet Hosts &ndash; Communication Layers Internet 主机的要求——通信层</a></li>
</ul>
</blockquote>

<p>IP 协议是互联网的基石，可以说 IP 协议定义互联网的基本结构。</p>

<p>IP 协议的核心目标是：实现超大规模的互联网中的任意两台主机之间可以相互通讯。</p>

<p>本部分仅介绍 IPv4 协议 的 Packet Header 格式，他请阅读 RFC 文档。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre></div>
<ul>
<li>采用大端字节序 (Big-endian)(<a href="https://en.wikipedia.org/wiki/Endianness">wiki</a>|<a href="https://www.ruanyifeng.com/blog/2016/11/byte-order.html">博客</a>)</li>
<li>Version：版本为 4</li>
<li>IHL：协议头长度，以32位（四字节）为单位，指定用户数据的开始位置，协议头最小的长度为5，也就是20字节。</li>

<li><p>Type of Service: 服务类型，用于控制该 Packet 的优先级，该字段组成如下所示</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    0     1     2     3     4     5     6     7
+-----+-----+-----+-----+-----+-----+-----+-----+
|                 |     |     |     |     |     |
|   PRECEDENCE    |  D  |  T  |  R  |  0  |  0  |
|                 |     |     |     |     |     |
+-----+-----+-----+-----+-----+-----+-----+-----+</pre></div>
<ul>
<li>PRECEDENCE 优先级

<ul>
<li><code>000</code> 普通 (Routine)</li>
<li><code>001</code> 优先的 (Priority)</li>
<li><code>010</code> 立即的发送 (Immediate)</li>
<li><code>011</code> 闪电式的 (Flash)</li>
<li><code>100</code> 比闪电还闪电式的 (Flash Override)</li>
<li><code>101</code> CRITIC/ECP</li>
<li><code>110</code> 网间控制 (Internetwork Control)</li>
<li><code>111</code> 网络控制 (Network Control)</li>
</ul></li>
<li>D 时延: 0 - 普通；1 - 延迟尽量小</li>
<li>T 吞吐量: 0 - 普通；1 - 流量尽量大</li>
<li>R 可靠性: 0 - 普通；1 - 可靠性尽量大</li>
<li>00 最后2位被保留，恒定为0</li>
</ul></li>

<li><p>Total Length：总长度包括报文头和数据部分，以字节为单位，这个字段允许报文最大长度为 65535 个字节（64k）。在工程中，IP 协议要求所有主机必须支持 576（512 + 64） 个字节长度的 Packet。</p></li>

<li><p>Identification：报文发送方可以为每个报文设置一个数字，方便后续分段和组装报文。</p></li>

<li><p>Flags: 多用途控制标志。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    0   1   2
+---+---+---+
|   | D | M |
| 0 | F | F |
+---+---+---+</pre></div>
<ul>
<li>0: 保留，必须为零</li>
<li>DF: 0 - 可以分段；1 - 不分段。</li>
<li>MF: 0 - 最后一个分段，1 - 后续还有更多分段。</li>
</ul></li>

<li><p>Fragment Offset: 表示这个分段在报文中的位置。偏移量是以8个字节为单位，第一个分段的偏移量为 0。</p></li>

<li><p>Time to Live: 这个字段表明在网络中报文的最大生命周期。如果这个字段的值为0 ，这个报文必须被删除。这个字段在头部处理过程中被修改。时间单位为秒，每个处理报文的模块最少减去一个秒，即使他处理的时间要少于一秒，TTL用来表明报文被删除的剩余时间。这个字段的目的就是删除网络上不能分发的报文。</p></li>

<li><p>Protocol: 这个字段说明数据部分使用的协议，具体的协议列表在 <a href="https://datatracker.ietf.org/doc/html/rfc790">RFC 790 (ASSIGNED INTERNET PROTOCOL NUMBERS)</a> 中有介绍。</p></li>

<li><p>Header Checksum: 只对头部进行校验和运算。因为头部会变化（比如time to live），所以每个处理节点都需要重新计算校验和。校验和算法与TCP的校验和算法是一样的，这是一个简单的计算过程，但是经过验证这是可以使用的，这只是一个暂时的方案，未来版本可能会用CRC取代。算法如下：</p>

<ul>
<li>把 Header Checksum 字段以全 0 填充；</li>
<li>对每 16 位（2 Byte）进行二进制反码求和（有进位则需要加到最低位）。</li>
</ul></li>

<li><p>Source Address: 源 IP 地址</p></li>

<li><p>Destination Address: 目标 IP 地址</p></li>

<li><p>Options: 可选的选项，长度可变。</p>

<ul>
<li>该字段有两种情况：

<ul>
<li>只有一个 8 位长度的选项类型</li>
<li>一个 8 位长度的选项类型，一个 8 位表示长度（这个 Option 字段的长度），其余表示内容。</li>
</ul></li>

<li><p>选型类型说明如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| C | CLASS | NUMBER            |
| O |       |                   |
| P |       |                   |
| I |       |                   |
| E |       |                   |
| D |       |                   |
+---+---+---+---+---+---+---+---+

copied flag 表示这个选项会被复制到所有的数据报分段中

0 = not copied
1 = copied

class 字段

0 = control 控制类
1 = reserved for future use 留作将来使用
2 = debugging and measurement 调试和测量
3 = reserved for future use 留作将来使用

已经定义的选项参见下表

CLASS NUMBER LENGTH DESCRIPTION
----- ------ ------ -----------
0     0      -    选项列表结尾（End of Option List），只占一个字节，没有长度值。
0     1      -    没有指定操作（No Operation），只占一个字节，没有长度值。
0     2     11    安全（Security），用来表示安全，隔离，用户组（TCC），处理与DOD（https://www.oreilly.com/library/view/ccent-cisco-certified/9781118435250/chap02-sec001.html）模型的限制码兼容要求。
0     3     var.  源地址松散路由（Loose Source Routing）。基于数据报的源地址进行路由（不必严格根据发送端提供的信息进行路径选择）。
0     9     var.  源地址严格路由（Strict Source Routing），基于数据报的源地址进行路由（必须严格根据发送端提供的信息进行路径选择）。
0     7     var.  记录路径（Record Route）。记录报文通过的路径。
0     8      4    流id（Stream ID）.  标记流id.
2     4     var.  网络时间戳（Internet Timestamp）.

更多参见 RFC 791 Page 16: https://datatracker.ietf.org/doc/html/rfc791#page-16</pre></div></li>
</ul></li>
</ul>

<h2 id="icmp-协议">ICMP 协议</h2>

<blockquote>
<p>在关于 ICMP 协议的标准文档，参见如下链接：</p>

<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc792">RFC 792</a></li>
</ul>
</blockquote>

<p>在使用 ping 命令来测试网络连通性时所使用的协议就是 ICMP。该协议就建立在 IP 协议之上。</p>

<p>因为该协议相对简单，因此后文，将以此协议的实现，来介绍如何使用 Raw Socket 编程接口进行 IP 层网络编程。</p>

<p>本节，简单介绍一下 ICMP 协议的内容，ICMP 协议的报文存放在 IP 协议 Packet 的 Data 部分。</p>

<p>为了清晰，我们将 IP Packet Header 也列出来，因此一个 ICMP 报文构成如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   IP Packet Header
|Version|  IHL  |Type of Service|          Total Length         |
|4      |  5    | 0             |          *                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
|         *                     |0b010|      0                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
|  0            |    1          |         *                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
|                       *                                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
|                    *                                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   IP Packet Data: ICMP Message (ICMP Header: first 64 bits)
|     Type      |     Code      |          Checksum             |   
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   由 Type 和 Code 决定（长度为 32 位）           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  由 Type 和 Code 决定（长度不确定）   ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre></div>
<p>关于 ICMP 报文的 IP Packet Header 部分：</p>

<ul>
<li>Protocol 参见：<a href="https://datatracker.ietf.org/doc/html/rfc790">RFC 790 (ASSIGNED INTERNET PROTOCOL NUMBERS)</a> ，ICMP 协议为 1</li>
<li>忽略了 Option 字段</li>
<li><code>*</code> 表示不确定，由运行时决定</li>
</ul>

<p>下面主要介绍 ICMP Echo / Echo Reply 部分</p>

<table>
<thead>
<tr>
<th>说明</th>
<th>方向</th>
<th>Type (0~7 位)</th>
<th>Code (8~15 位)</th>
<th>Checksum (16~31 位)</th>
<th>32~63 位</th>
<th>64~&hellip;</th>
</tr>
</thead>

<tbody>
<tr>
<td>发送一个 Echo 消息</td>
<td>Request</td>
<td>8</td>
<td>0</td>
<td>ICMP Header 的校验和（前 64 位），和 IP 校验和计算方式一致</td>
<td>前 16 位为 Identifier ，后 16 位为 Sequence Number</td>
<td>任意数据，echo 的内容</td>
</tr>

<tr>
<td>回复一个 Echo 消息</td>
<td>Reply</td>
<td>0</td>
<td>0</td>
<td>重新按照如上算法计算</td>
<td>内容和待回复的 Echo 消息一致</td>
<td>内容和待回复的 Echo 消息一致</td>
</tr>
</tbody>
</table>

<ul>
<li>Identifier，标识符，用来标示此发送的报文，类似与 TCP 的端口号，用于区分会话，用来实现同一个主机上运行多个 Ping 程序。</li>
<li>Sequence Number 序列号，发送端发送的报文的顺序号。每发送一次顺序号就加1。</li>
<li>Code 是错误码，用来标识错误的具体类型，在 Type 为 0 和 8 时都为 0，其他 Type 有具体定义，再次不赘述了。</li>
</ul>

<p>扩展： Traceroute 利用的是 ICMP 的 IP 的 TTL （Time to Live） 参数和 ICMP 的 Destination Unreachable 类型消息（Type = 3）来实现路由追踪的。因为按照 IP 协议栈规定所有的主机都需要实现 ICMP 协议，并且任意的 IP 包无法送达下一跳而被丢弃时，都需要给源 IP 回复 ICMP  Destination Unreachable 消息，因此只需从 1 开始递增的设置 IP 协议的TTL 给目标 IP 发送 IP 报文，即可收到各个节点回复的 Destination Unreachable 消息，从这些消息的的 Source IP 即可获得路由信息，更多参见<a href="https://zhuanlan.zhihu.com/p/101810847">知乎文章</a>。</p>

<h2 id="raw-socket">Raw socket</h2>

<p>在 Linux 中。通过编程直接操作 IP 协议，参见如下链接：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man7/ip.7.html">ip(7) 手册</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/ipv6.7.html">ip6(7) 手册</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/raw.7.html">raw(7) 手册</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/packet.7.html">packet(7) 手册</a></li>
<li><a href="https://zh.wikipedia.org/wiki/%E5%8E%9F%E5%A7%8B%E5%A5%97%E6%8E%A5%E5%AD%97">Wiki 原始套接字</a></li>
</ul>

<h3 id="手册">手册</h3>

<p>创建一个协议为 <a href="https://datatracker.ietf.org/doc/html/rfc790"><code>protocol</code></a> 的 IPv4 原始套接字。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;netinet/in.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>raw_socket <span style="color:#f92672">=</span> socket(AF_INET, SOCK_RAW, <span style="color:#66d9ef">int</span> protocol);
</code></pre></div>
<ul>
<li>创建 raw socket 的进程必须拥有 <code>CAP_NET_RAW</code> 权限。</li>
<li>内核会将接收的 IP Packet 复制一份发送给 <code>protocol</code> 参数匹配的 raw socket，但是注意，内核的默认行为不会发生改变，如果需要禁用内核的默认行为，参考：<a href="https://serverfault.com/questions/387263/disable-kernel-processing-of-tcp-packets-for-raw-socket">serverfault</a>。如果想 bind 的指定的地址，使用 <a href="https://man7.org/linux/man-pages/man2/bind.2.html"><code>bind(2) 系统调用</code></a>。</li>
<li><a href="https://man7.org/linux/man-pages/man2/sendto.2.html"><code>sendto(2) 系统调用</code></a> 发送消息时

<ul>
<li>默认情况下，不需要提供 IP Packet Header，内核自动生成，此时如果想设置 IP Packet Header 的 Option，则可以通过 <a href="https://man7.org/linux/man-pages/man2/setsockopt.2.html"><code>setsockopt(2) 系统调用</code></a>  设置，更多参见 <a href="https://man7.org/linux/man-pages/man7/ip.7.html"><code>ip(7) 文档</code></a>。</li>
<li>如果该 raw socket 通过 <a href="https://man7.org/linux/man-pages/man2/setsockopt.2.html"><code>setsockopt(2) 系统调用</code></a> 设置了 <code>IP_HDRINCL</code> 则发送的消息必须包含 IP Packet Header。</li>
</ul></li>
<li><code>protocol</code> 参数说明

<ul>
<li>列表参见： <a href="https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml">iana 站点</a></li>
<li>如果 <code>protocol</code> 为  <code>IPPROTO_RAW</code> 且 通过 <a href="https://man7.org/linux/man-pages/man2/setsockopt.2.html"><code>setsockopt(2) 系统调用</code></a> 设置了 <code>IP_HDRINCL</code> 。

<ul>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/sendto.2.html"><code>sendto(2) 系统调用</code></a> 发送消息时可以指定任意协议。</li>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/recvfrom.2.html"><code>recvfrom(2) 系统调用</code></a> 接收不到任何消息，如果想接收任意协议的 IP Packet，需使用：<a href="https://man7.org/linux/man-pages/man7/packet.7.html">packet(7)</a> socket 并设置 <code>ETH_P_IP</code>（注意 Raw socket 会自动根据 MTU 分片，而 packet socket 不会）。</li>
</ul></li>
</ul></li>
</ul>

<h3 id="示例">示例</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc ./src/c/01-icmp/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;            // for perror(3), printf(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;           // for exit(3), EXIT_FAILURE</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;           // for strcmp(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;           // for close(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;        // for u_int16_t</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;       // for socket(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;arpa/inet.h&gt;        // for inet_addr(3), inet_ntoa(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;netinet/ip_icmp.h&gt;  // for icmphdr</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 按照 16 位为单位进行反码求和，进位需加回最低位。
</span><span style="color:#75715e"></span>u_int16_t <span style="color:#a6e22e">checksum</span>(<span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">short</span> <span style="color:#f92672">*</span>buf, <span style="color:#66d9ef">int</span> size)
{
    <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">long</span> sum <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    <span style="color:#66d9ef">while</span> (size <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">1</span>)
    {
        sum <span style="color:#f92672">+=</span> <span style="color:#f92672">*</span>buf;
        buf<span style="color:#f92672">++</span>;
        size <span style="color:#f92672">-=</span> <span style="color:#ae81ff">2</span>;
    }
    <span style="color:#66d9ef">if</span> (size <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>)
        sum <span style="color:#f92672">+=</span> <span style="color:#f92672">*</span>(<span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>)buf;
    sum <span style="color:#f92672">=</span> (sum <span style="color:#f92672">&amp;</span> <span style="color:#ae81ff">0xffff</span>) <span style="color:#f92672">+</span> (sum <span style="color:#f92672">&gt;&gt;</span> <span style="color:#ae81ff">16</span>);
    sum <span style="color:#f92672">=</span> (sum <span style="color:#f92672">&amp;</span> <span style="color:#ae81ff">0xffff</span>) <span style="color:#f92672">+</span> (sum <span style="color:#f92672">&gt;&gt;</span> <span style="color:#ae81ff">16</span>);
    <span style="color:#66d9ef">return</span> <span style="color:#f92672">~</span>sum;
}

<span style="color:#75715e">//  创建 protocol 的 raw socket
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">make_raw_socket</span>(<span style="color:#66d9ef">int</span> protocol)
{
    <span style="color:#66d9ef">int</span> s <span style="color:#f92672">=</span> socket(AF_INET, SOCK_RAW, protocol);
    <span style="color:#66d9ef">if</span> (s <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
    {
        perror(<span style="color:#e6db74">&#34;socket&#34;</span>);
        exit(EXIT_FAILURE);
    }
    <span style="color:#66d9ef">return</span> s;
}

<span style="color:#75715e">//  构造 ICMP echo 消息的 Header
</span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setup_icmp_echo_hdr</span>(u_int16_t id, u_int16_t seq, <span style="color:#66d9ef">struct</span> icmphdr <span style="color:#f92672">*</span>icmphdr)
{
    memset(icmphdr, <span style="color:#ae81ff">0</span>, <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">struct</span> icmphdr));
    icmphdr<span style="color:#f92672">-&gt;</span>type <span style="color:#f92672">=</span> ICMP_ECHO;
    icmphdr<span style="color:#f92672">-&gt;</span>code <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    icmphdr<span style="color:#f92672">-&gt;</span>checksum <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
    icmphdr<span style="color:#f92672">-&gt;</span>un.echo.id <span style="color:#f92672">=</span> id;
    icmphdr<span style="color:#f92672">-&gt;</span>un.echo.sequence <span style="color:#f92672">=</span> seq;
    icmphdr<span style="color:#f92672">-&gt;</span>checksum <span style="color:#f92672">=</span> checksum((<span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">short</span> <span style="color:#f92672">*</span>)icmphdr, <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">struct</span> icmphdr));
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">**</span>argv)
{
    <span style="color:#66d9ef">int</span> n, s;
    <span style="color:#66d9ef">char</span> buf[<span style="color:#ae81ff">1500</span>];
    <span style="color:#66d9ef">struct</span> sockaddr_in target_addr;
    <span style="color:#66d9ef">struct</span> in_addr recv_source_addr;
    <span style="color:#66d9ef">struct</span> icmphdr icmphdr;
    <span style="color:#66d9ef">struct</span> iphdr <span style="color:#f92672">*</span>recv_iphdr;
    <span style="color:#66d9ef">struct</span> icmphdr <span style="color:#f92672">*</span>recv_icmphdr;
    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>target_addr_str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>;

    target_addr.sin_family <span style="color:#f92672">=</span> AF_INET;
    target_addr.sin_addr.s_addr <span style="color:#f92672">=</span> inet_addr(target_addr_str);
    <span style="color:#75715e">// 创建一个 ICMP 协议的 Raw Socket
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 可以直接向该 socket 发送消息，发送的消息体只需要给 IP Data 部分的内容
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 从该 socket 接收消息所有发给该主机的 IP 消息的一份拷贝，接收消息内容是整个 IP packet （包括 IP Header）的内容
</span><span style="color:#75715e"></span>    s <span style="color:#f92672">=</span> make_raw_socket(IPPROTO_ICMP);
    setup_icmp_echo_hdr(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>, <span style="color:#f92672">&amp;</span>icmphdr);

    <span style="color:#75715e">// 发送 ICMP echo 消息到 target_addr
</span><span style="color:#75715e"></span>    n <span style="color:#f92672">=</span> sendto(s, (<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>icmphdr, <span style="color:#66d9ef">sizeof</span>(icmphdr), <span style="color:#ae81ff">0</span>, (<span style="color:#66d9ef">struct</span> sockaddr <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>target_addr, <span style="color:#66d9ef">sizeof</span>(target_addr));
    <span style="color:#66d9ef">if</span> (n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">1</span>)
    {
        perror(<span style="color:#e6db74">&#34;sendto&#34;</span>);
        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>;
    }

    <span style="color:#75715e">// 接收 ICMP 消息，因为上面代码发送到了 127.0.0.1 所以：
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 第 1 个消息是上面代码发送的 echo 消息
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 第 2 个消息是内核回复的 echo reply 消息
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 如果 target 是其他主机，则只会收到第 2 个消息
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">2</span>; i<span style="color:#f92672">++</span>) {
        <span style="color:#75715e">// 整个 IP packet 将填充到 buf 里
</span><span style="color:#75715e"></span>        n <span style="color:#f92672">=</span> recv(s, buf, <span style="color:#66d9ef">sizeof</span>(buf), <span style="color:#ae81ff">0</span>);
        <span style="color:#66d9ef">if</span> (n <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">1</span>)
        {
            perror(<span style="color:#e6db74">&#34;recv&#34;</span>);
            <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>;
        }
        <span style="color:#75715e">// 转换为 IP Header 类型
</span><span style="color:#75715e"></span>        recv_iphdr <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> iphdr <span style="color:#f92672">*</span>)buf;
        <span style="color:#75715e">// 根据 ihl 协议头长度获取 IP Data，即 ICMP Header
</span><span style="color:#75715e"></span>        recv_icmphdr <span style="color:#f92672">=</span> (<span style="color:#66d9ef">struct</span> icmphdr <span style="color:#f92672">*</span>)(buf <span style="color:#f92672">+</span> (recv_iphdr<span style="color:#f92672">-&gt;</span>ihl <span style="color:#f92672">&lt;&lt;</span> <span style="color:#ae81ff">2</span>));
        recv_source_addr.s_addr <span style="color:#f92672">=</span> recv_iphdr<span style="color:#f92672">-&gt;</span>saddr;
        <span style="color:#75715e">// 检查回复的消息的 Source IP 和 发送消息的 Target IP 是否一样 且 消息类型需要是 ICMP Echo Reply
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>strcmp(target_addr_str, inet_ntoa(recv_source_addr)) <span style="color:#f92672">&amp;&amp;</span> recv_icmphdr<span style="color:#f92672">-&gt;</span>type <span style="color:#f92672">==</span> ICMP_ECHOREPLY)
            printf(<span style="color:#e6db74">&#34;icmp echo reply from %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, target_addr_str);
    }
    close(s);
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<p>输出为： <code>icmp echo reply from 127.0.0.1</code></p>
]]></description></item><item><title>Go HTTP Client 和 http_proxy 环境变量</title><link>https://www.rectcircle.cn/posts/go-http-client-and-http-proxy-env/</link><pubDate>Sat, 26 Mar 2022 19:09:01 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-http-client-and-http-proxy-env/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>在某个场景中，一个 Go 程序在运行时可能会存在 http_proxy、https_proxy、no_proxy 环境变量。而我们并不希望该 Go 程序中的 http client 走这些环境变量配置的代理。</p>

<h2 id="实现">实现</h2>

<h3 id="方式一-unset-环境变量">方式一：unset 环境变量</h3>

<p>HTTP Client 位于 Go 标准库 http 包中，http 包导出了如下关于 http client 相关的默认变量：</p>

<ul>
<li><code>http.DefaultClient *http.Client</code>，从 <code>http.Client.transport</code> 的实现可以看出 <code>http.DefaultClient</code> 会调用 <code>http.DefaultTransport</code>。</li>
<li><code>http.DefaultTransport *http.Transport</code> 的 Proxy 字段会被设置为 <code>ProxyFromEnvironment</code>，ProxyFromEnvironment 会读取 <code>http_proxy</code> 等相关环境变量。
<br /></li>
</ul>

<p>可看出，这些环境变量会在第一次发起 http client 请求时被读取，然后就不会变了。</p>

<p>此外，我们需要让 unset 的调用在其他所有 init 之前执行（防止如果其他 init 函数调用了 http client 就会导致 <code>ProxyFromEnvironment</code> 得到调用，从而导致再 unset 也不生效）。</p>

<p>站在 package 的视角看，go 的初始化顺序为：</p>

<p><img src="/image/go-init-seq.png" alt="image" /></p>

<p>同一个包如果有多个文件，可以理解为，需要按照如下规则将拼装成单文件：</p>

<ol>
<li>将所有文件按照文件名按照 ascii 顺序排列</li>
<li>抽取每个文件的 import 块按照第一步的顺序合并</li>
<li>每个文件的剩下的部分按照第一步的顺序合并</li>
</ol>

<p>因此我们需要按照如下方式操作，才能确保 unsetenv 在其他 init 函数之前执行：</p>

<ol>
<li><p>创建一个 <code>unsetenv</code> 包，并在 init 函数中执行  unsetenv 函数，<code>unsetenv/unsetenv.go</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">unsetenv</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">init</span>() {
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;HTTP_PROXY&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;http_proxy&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;HTTPS_PROXY&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;https_proxy&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;NO_PROXY&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;no_proxy&#34;</span>)
    <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Unsetenv</span>(<span style="color:#e6db74">&#34;REQUEST_METHOD&#34;</span>)
}</code></pre></div></li>

<li><p>在 <code>main</code> 包，创建 <code>0.go</code> 文件。并导入 <code>unsetenv</code> 包</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
    <span style="color:#a6e22e">_</span> <span style="color:#e6db74">&#34;xxx/xxx/unsetenv&#34;</span>
)</code></pre></div></li>
</ol>

<p>说明：</p>

<ol>
<li>文件名必须是 <code>0.go</code>，<code>0</code> 在 ascii 码中排序小，才能保证其在 main 包中的所有的其他文件之前执行。</li>

<li><p>如下所示的方法，直接在包含 main 函数的文件中，直接导入 <code>unsetenv</code> 是不可以的，因为 gofmt 会强制对 import 进行排序，无法保证 <code>unsetenv</code> 在其他包的 init 函数之前执行。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">_</span> <span style="color:#e6db74">&#34;xxx/xxx/unsetenv&#34;</span>
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div></li>
</ol>

<h3 id="方式二-手动配置-http-client">方式二：手动配置 http.Client</h3>

<p>配置所有的 http client 的 <code>*http.Transport.Proxy</code> 为 nil</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 默认的 DefaultTransport 的 Proxy 设置为 nil
</span><span style="color:#75715e"></span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultTransport</span>.(<span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Transport</span>).<span style="color:#a6e22e">Proxy</span> = <span style="color:#66d9ef">nil</span>
<span style="color:#f92672">//</span> <span style="color:#a6e22e">其他手动创建的</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Transport</span> <span style="color:#960050;background-color:#1e0010">（</span><span style="color:#a6e22e">注意包含间接引用的地方</span><span style="color:#960050;background-color:#1e0010">）</span><span style="color:#a6e22e">的</span> <span style="color:#a6e22e">Proxy</span> <span style="color:#a6e22e">设置为</span> <span style="color:#66d9ef">nil</span></code></pre></div>
<h2 id="结语">结语</h2>

<p>从这个场景可以看出，依赖全局变量以及 <code>init()</code> 函数是一个非常不好的设计，因为开发者很难控制这些全局变量和 <code>init()</code> 的初始化顺序。因此我们在工程中，要尽量避免使用全局变量以及 <code>init()</code> 函数。</p>

<p>比如对 http client 的使用，需要避免使用 <code>http.DefaultClient</code> 和 <code>http.DefaultTransport</code>。</p>

<p>这样，我们就不需要通过如上方式一，如此不优雅的方式来实现该场景了。</p>
]]></description></item><item><title>通过 Linux API 学习网络协议栈（一）概览</title><link>https://www.rectcircle.cn/posts/learn-net-proto-stack-by-linux-api-1-overview/</link><pubDate>Fri, 25 Mar 2022 22:50:14 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/learn-net-proto-stack-by-linux-api-1-overview/</guid><description type="html"><![CDATA[

<h2 id="系列说明">系列说明</h2>

<p>本系列文章面对的读者是互联网应用后端开发者，互联网应用后端一般具有如下特点：</p>

<ul>
<li>可以感知到的网络协议栈的最底层的一般是 IP 协议；</li>
<li>互联网应用后端程序一般运行在 Linux 中。</li>
</ul>

<p>此外网络协议栈是分层的，每一层都是基于上一层之上的实现的。</p>

<p>因此，本系列将：</p>

<ul>
<li>从 IP 协议开始学习/复习网络协议栈；</li>
<li>通过某协议的下层协议（对应的 Linux API），编写代码，实现某协议的方式实战化化的学习/复习网络协议栈；</li>
<li>介绍 Linux 平台上某协议的标准 API（系统调用/命令行工具/设备等），以及常见的应用场景和操作。</li>
</ul>

<h2 id="实验代码">实验代码</h2>

<p>github: <a href="https://github.com/rectcircle/linux-internet-proto-learn">rectcircle/linux-internet-proto-learn</a></p>

<h2 id="linux-socket-api-概述">Linux Socket API 概述</h2>

<blockquote>
<p>手册： <a href="https://man7.org/linux/man-pages/man2/socket.2.html">socket(2)</a></p>
</blockquote>

<p>在计算机网络方面，有多套模型：</p>

<ul>
<li>实践上，Socket 编程模型；</li>
<li>理论上，TCP/IP 四层网络模型 和 OSI 七层网络模型。</li>
</ul>

<p>本部分先介绍在 Linux 中 Socket 编程模型，最后再介绍，编程模型和网络模型的对应关系。</p>

<p>Linux 网络 API 采用的是 BSD 的 Socket 模型。Socket 模型支持 TCP/IP 模型的网络访问层（Mac 数据帧）、网际层（IP 数据包）、传输层（TCP / UDP）。</p>

<h3 id="创建-socket">创建 Socket</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 创建 socket https://man7.org/linux/man-pages/man2/socket.2.html
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">socket</span>(<span style="color:#66d9ef">int</span> domain, <span style="color:#66d9ef">int</span> type, <span style="color:#66d9ef">int</span> protocol);
</code></pre></div>
<p>参数说明</p>

<ul>
<li>domain，即 communication domain 通讯域，表示使用的协议族。从网络模型角度看，该参数用于选择网际层协议，即该 Socket 是通过 IPv4 还是 IPv6 等来进行通讯的。常见的可选值为：

<ul>
<li><code>AF_UNIX</code>，Unix Domain Socket；</li>
<li><code>AF_INET</code>，IPv4 Only；</li>
<li><code>AF_INET6</code>，IPv6 Only 或 IPv4 / IPv6 双栈（默认值为 <code>/proc/sys/net/ipv6/bindv6only</code>，通过 <code>setsockopt</code> 手动修改）；</li>
<li><code>AF_PACKET</code>，直接面向数据链路层 Packet 原始数据。（本系列可能不会涉及）</li>
</ul></li>
<li>type，即 Socket 的类型，表示收发数据的特点，会影响操作 Socket 时的系统调用，该参数有如下可能性：

<ul>
<li>选择该 Socket 最终使用的网络模型的传输层协议：

<ul>
<li><code>SOCK_STREAM</code> 流式，表示可靠的连接，对应的 protocol 为 TCP；</li>
<li><code>SOCK_DGRAM</code> 数据报，表示不可靠消息，对应的 protocol 为 UDP。</li>
</ul></li>
<li>该参数也可以用于指定该 Socket 收发底层数据：

<ul>
<li><code>SOCK_RAW</code> 提供原始网络协议访问；</li>
<li><del><code>SOCK_PACKET</code> 已过时，需使用 type = <code>AF_PACKET</code> 替代</del>。</li>
</ul></li>
<li>该参数可以 <code>|</code> 原酸用来配置文件描述符的一些特性；

<ul>
<li><code>SOCK_NONBLOCK</code> 设置该文件描述符是非阻塞 IO。</li>
<li><code>SOCK_CLOEXEC</code> 表示该文件描述符在 fork-exec 后关闭</li>
</ul></li>
</ul></li>
<li>protocol，即 Socket 的具体网络协议，一般情况下设置为 0，表示根据 type 选择一个默认协议。具体可选值说明参见：<a href="https://man7.org/linux/man-pages/man5/protocols.5.html">protocols(5)</a></li>
</ul>

<p>最后，该函数将返回一个 socket 文件描述符。</p>

<h3 id="socket-选项">Socket 选项</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 设置/获取 socket 的一些选项 https://man7.org/linux/man-pages/man2/setsockopt.2.html
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">getsockopt</span>(<span style="color:#66d9ef">int</span> sockfd, <span style="color:#66d9ef">int</span> level, <span style="color:#66d9ef">int</span> optname,
                <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">restrict</span> optval, socklen_t <span style="color:#f92672">*</span><span style="color:#66d9ef">restrict</span> optlen);
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">setsockopt</span>(<span style="color:#66d9ef">int</span> sockfd, <span style="color:#66d9ef">int</span> level, <span style="color:#66d9ef">int</span> optname,
                <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>optval, socklen_t optlen);
</code></pre></div>
<p>比如手动指定一个 IPv6 TCP Socket 同时支持 IPv4：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/socket.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> tcp6_socket <span style="color:#f92672">=</span> socket(AF_INET6, SOCK_STREAM, <span style="color:#ae81ff">0</span>);
<span style="color:#66d9ef">int</span> ipv6_only_flag <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
setsockopt(tcp6_socket, IPPROTO_IPV6, IPV6_V6ONLY, (<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>)<span style="color:#f92672">&amp;</span>ipv6_only_flag, <span style="color:#66d9ef">sizeof</span>(ipv6_only_flag));
</code></pre></div>
<h3 id="操作-socket">操作 Socket</h3>

<p>socket 创建出来后，我们就可以通过想过 API 将该 Socket 连接/绑定到一个地址，然后就可以收发数据了。</p>

<p>不同 type 的 socket 模型中，对应的操作 API 是不同的。</p>

<ul>
<li><code>SOCK_STREAM</code> 即 TCP 协议 的 Socket，可以使用的常见的系统调用为：

<ul>
<li><code>connect(2)</code> 连接到 server</li>
<li><code>bind(2)</code> 绑定地址</li>
<li><code>accept(2)</code> 等待连接</li>
<li><code>write(2)</code>、<code>send(2)</code> 发送数据</li>
<li><code>read(2)</code>、<code>recv(2)</code> 接收数据</li>
</ul></li>
<li><code>SOCK_DGRAM</code> 即 UDP 协议 的 Socket 和 <code>SOCK_RAW</code> 即 原始 Socket，可以使用的常见的系统调用为：

<ul>
<li><code>sendto(2)</code></li>
<li><code>recvfrom(2)</code></li>
</ul></li>
<li><code>SOCK_PACKET</code> 即 数据链路层 Socket，可用的系统调用参见： <a href="https://man7.org/linux/man-pages/man7/packet.7.html">packet(7)</a></li>
</ul>

<h3 id="对应表">对应表</h3>

<table>
<thead>
<tr>
<th>socket 模型</th>
<th>操作 socket 的系统调用</th>
<th>协议</th>
<th>TCP/IP 四层模型</th>
<th>OSI 七层模型</th>
</tr>
</thead>

<tbody>
<tr>
<td>无</td>
<td>无</td>
<td>HTTP</td>
<td>应用层 (4)</td>
<td>应用层 (7)</td>
</tr>

<tr>
<td>无</td>
<td>无</td>
<td>Telent</td>
<td>应用层 (4)</td>
<td>表示层 (6)</td>
</tr>

<tr>
<td>无</td>
<td>无</td>
<td>DNS</td>
<td>应用层 (4)</td>
<td>会话层 (5)</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man7/tcp.7.html"><code>socket(AF_INET, SOCK_STREAM, 0)</code></a></td>
<td><code>connect(2)</code>、<code>bind(2)</code>、<code>accept(2)</code>、<code>write(2)</code>、<code>send(2)</code> 、<code>read(2)</code>、<code>recv(2)</code></td>
<td>TCP</td>
<td>传输层 (3)</td>
<td>传输层 (4)</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man7/udp.7.html"><code>socket(AF_INET, SOCK_DGRAM, 0)</code></a></td>
<td><code>sendto(2)</code>、  <code>recvfrom(2)</code></td>
<td>UDP</td>
<td>传输层 (3)</td>
<td>传输层 (4)</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man7/raw.7.html"><code>socket(AF_INET, SOCK_RAW, int protocol)</code></a></td>
<td><code>sendto(2)</code>、  <code>recvfrom(2)</code></td>
<td>IP</td>
<td>网际层 (2)</td>
<td>网络层 (3)</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man7/packet.7.html"><code>socket(AF_PACKET, int socket_type, int protocol)</code></a></td>
<td>略</td>
<td></td>
<td>数据链路层 (1)</td>
<td>数据链路层 (2)</td>
</tr>

<tr>
<td>无</td>
<td>无</td>
<td></td>
<td>数据链路层 (1)</td>
<td>物理层 (1)</td>
</tr>
</tbody>
</table>
]]></description></item><item><title>Go 1.18 新特性</title><link>https://www.rectcircle.cn/posts/go-1-18-features/</link><pubDate>Thu, 17 Mar 2022 23:40:42 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-1-18-features/</guid><description type="html"><![CDATA[

<blockquote>
<p>官方文档：<a href="https://go.dev/doc/go1.18">Go 1.18 Release Notes</a></p>
</blockquote>

<h2 id="介绍">介绍</h2>

<p>2022 年 3 月 15 日，Go 1.18 正式发布，Go 1.18 是一个非常重要的更新。Go 1.18 虽然发布了泛型、Fuzzing 测试和工作空间等重磅特性。但是仍提供了完全的 <a href="https://go.dev/doc/go1compat">Go 1 兼容性</a>，也就是说，符合 <a href="https://go.dev/doc/go1compat">Go 1 兼容性</a> 要求的代码均可直接使用 Go 1.18 编译。</p>

<p>本文整体参考如下官方文档，介绍与 Go 1.17 相比，Go 1.18 的新特性。</p>

<ul>
<li><a href="https://go.dev/doc/go1.18">Go 1.18 Release Notes</a></li>
<li>泛型相关

<ul>
<li><a href="https://go.dev/doc/tutorial/generics">Tutorial: Getting started with generics</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md">Type Parameters Proposal</a></li>
<li><a href="https://go.dev/ref/spec#Type_parameter_declarations">Spec - Type parameter declarations</a></li>
<li><a href="https://go.dev/ref/spec#Type_parameter_declarations">Spec - Type constraints</a></li>
<li><a href="https://go.dev/ref/spec#General_interfaces">Spec - Interface types - General interfaces</a></li>
</ul></li>
<li>Fuzzing 单元测试

<ul>
<li><a href="https://go.dev/doc/tutorial/fuzz">Tutorial: Getting started with fuzzing</a></li>
<li><a href="https://go.dev/doc/fuzz/">Go Fuzzing</a></li>
<li><a href="https://github.com/golang/go/issues/44551">testing: add fuzz test support #44551</a></li>
</ul></li>
<li>工作空间

<ul>
<li><a href="https://go.dev/doc/tutorial/workspaces">Tutorial: Getting started with multi-module workspaces</a></li>
<li><a href="https://go.dev/ref/mod#workspaces">Go Modules Reference - Workspaces</a></li>
<li><a href="https://go.dev/ref/mod#go-work-init">Go Modules Reference - go work &hellip; 命令</a></li>
<li><a href="https://pkg.go.dev/cmd/go#hdr-Workspace_maintenance">Go cmd - Workspace maintenance</a></li>
<li><a href="https://go.googlesource.com/proposal/+/master/design/45713-workspace.md#proposal-the-flag">Proposal: Multi-Module Workspaces in cmd/go</a></li>
</ul></li>
</ul>

<h2 id="安装-go-1-18">安装 Go 1.18</h2>

<p>前往 <a href="https://go.dev/dl/">下载地址</a> ，下载安装 Go 1.18。</p>

<h2 id="实验代码">实验代码</h2>

<p>本文对应实验代码位于：<a href="https://github.com/rectcircle/go-1-18-feature">github rectcircle/go-1-18-feature</a></p>

<h2 id="泛型">泛型</h2>

<blockquote>
<p>官方提案：<a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md">Type Parameters Proposal</a></p>
</blockquote>

<h3 id="概述">概述</h3>

<p>在语言特性方面，Go 1.18 带来了 Go 开发者期待已久的泛型。</p>

<ul>
<li><code>泛型函数</code>：函数可以有一个使用方括号的附加的<code>类型参数列表</code>，这些<code>类型参数</code>可以被常规参数和函数体使用，包含<code>类型参数列表</code>的函数叫<code>泛型函数</code>。例如：<code>func F[T any](p T) { ... }</code>，从该例可以看出

<ul>
<li>和 C++ / Java 不同，Go 类型参数是通过方括号 <code>[]</code> 包裹的。</li>
<li>类型参数 <code>T</code> 被常规参数 <code>p</code> 使用，是 p 的类型。</li>
</ul></li>
<li><code>泛型类型</code>：类型也可以有一个<code>类型参数列表</code>，包含<code>类型参数列表</code>的类型叫做<code>泛型类型</code>，例如：<code>type M[T any] []T</code>。</li>
<li><code>类型约束</code>：每个<code>类型参数</code>都有一个<code>类型约束</code>，类型约束必须是一个接口类型，如 <code>func F[T Constraint](p T) { ... }</code>。</li>
<li><code>any</code> 类型：新的预声明名称 <code>any</code> 表示，允许任何类型的类型约束。</li>
<li>被用作<code>类型约束</code>的接口类型：可以通过嵌入额外元素的方式，来限制某个<code>类型参数</code>必须是满足某些约束的集合，这些嵌入的元素可以是：

<ul>
<li><code>T</code> 约束为具体类型 <code>T</code></li>
<li><code>~T</code> <a href="https://lingchao.xin/post/type-system-overview.html#%E6%A6%82%E5%BF%B5-%E5%BA%95%E5%B1%82%E7%B1%BB%E5%9E%8B">底层类型</a>为 <code>T</code> 的所有类型</li>
<li><code>T1 | T2 | ...</code> 表示为以 <code>|</code> 分割列出的的元素</li>
</ul></li>
<li>调用<code>泛型函数</code>：

<ul>
<li>需要传递类型参数，如果类型推断可以推断出类型时，可以忽略（类型推断）。</li>
<li>普通参数的类型为类型参数时，只能使用类型符合该类型参数约束的变量调用该函数。</li>
</ul></li>
<li>实例化<code>泛型类型</code>：需要传递类型参数。</li>
</ul>

<h3 id="类型参数">类型参数</h3>

<p>泛型的核心是类型参数的定义。和 C++ / Java 不同，Go 类型参数是通过方括号 <code>[]</code> 包裹的。</p>

<p>普通函数参数除了参数名外，还拥有类型声明类似。类型参数的构成也由两半部分组成：</p>

<ul>
<li>类型参数名，一般为大写字母，如 <code>T</code>。</li>
<li>类型参数约束，必须是一个接口类型，更多关于类型参数约束，参见：<a href="#类型参数约束">下文</a>。</li>
</ul>

<p>在 Go 1.18 中，类型参数可以用于函数和类型定义中（注意，方法不支持）：</p>

<ul>
<li>泛型函数 <code>func FunctionName[T Constraint](param T) { ... }</code></li>
<li>泛型类型 <code>type TypeName[T Constraint] ...</code></li>
</ul>

<h3 id="泛型函数">泛型函数</h3>

<h4 id="定义泛型函数">定义泛型函数</h4>

<p>包含类型参数列表的函数被称为泛型函数，下面有一些泛型函数的例子：</p>

<p><code>01-generics/01-generic-function.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#75715e">// Print 打印切片的元素。
</span><span style="color:#75715e">// Print 有一个类型参数 T，且有一个（非类型）普通参数 s，
</span><span style="color:#75715e">// s 是一个元素类型为 T 的切片。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Print</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>](<span style="color:#a6e22e">s</span> []<span style="color:#a6e22e">T</span>) {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">s</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">v</span>)
	}
}

<span style="color:#75715e">// Print2 打印两个接片的元素
</span><span style="color:#75715e">// Print 有两个类型参数 T1 和 T2，且有两个（非类型）普通参数 s1 和 s2，
</span><span style="color:#75715e">// s1  是一个元素类型为 T1 的切片，s2  是一个元素类型为 T1 的切片。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Print2</span>[<span style="color:#a6e22e">T1</span>, <span style="color:#a6e22e">T2</span> <span style="color:#a6e22e">any</span>](<span style="color:#a6e22e">s1</span> []<span style="color:#a6e22e">T1</span>, <span style="color:#a6e22e">s2</span> []<span style="color:#a6e22e">T2</span>) {
	<span style="color:#a6e22e">Print</span>(<span style="color:#a6e22e">s1</span>)
	<span style="color:#a6e22e">Print</span>(<span style="color:#a6e22e">s2</span>)
}

<span style="color:#75715e">// Print2 打印两个接片的元素
</span><span style="color:#75715e">// Print 有一个类型参数 T，且有两个（非类型）普通参数 s1 和 s2，
</span><span style="color:#75715e">// s1, s2 都是一个元素类型为 T 的切片
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Print2Same</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>](<span style="color:#a6e22e">s1</span> []<span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">s2</span> []<span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">Print</span>(<span style="color:#a6e22e">s1</span>)
	<span style="color:#a6e22e">Print</span>(<span style="color:#a6e22e">s2</span>)
}</code></pre></div>
<h4 id="调用泛型函数">调用泛型函数</h4>

<p>泛型函数的调用方式与普通函数的调用方式类似，区别在于在函数名后紧跟着一个类型参数列表。</p>

<p>注意，绝大多数情况下，Go 编译器可以通过函数参数推断出参数类型，则这个类型参数列表就可以省略（语法上 Java 的类型推断仍然需要些 <code>&lt;&gt;</code>，而 Go 语言不需要再写 <code>[]</code>），这种特性叫类型推断。</p>

<p><code>01-generics/01-generic-function_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePrint</span>() {
	<span style="color:#75715e">// 使用一个 []int 参数调用 Print。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// print有一个类型参数T，我们要传递一个[]int，
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 所以通过 Print[int] 来向函数 Print 的类型参数 T 传递 int，作为其参数。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 此时函数 Print[int] 需要一个 []int 作为参数。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Print</span>[<span style="color:#66d9ef">int</span>]([]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>}) <span style="color:#75715e">// 可以省略，int 类型参数。因为编译器可以从参数列表中推断
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePrint2</span>() {
	<span style="color:#a6e22e">Print2</span>([]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>}, []<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">5</span>, <span style="color:#ae81ff">6</span>})
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 4
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 5
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 6
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePrint2Same</span>() {
	<span style="color:#a6e22e">Print2Same</span>([]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>}, []<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">4</span>, <span style="color:#ae81ff">5</span>, <span style="color:#ae81ff">6</span>})
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 4
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 5
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 6
</span><span style="color:#75715e"></span>}</code></pre></div>
<h3 id="类型参数约束">类型参数约束</h3>

<p>和 Java / C++ 不同，在 Go 1.18 中，类型参数必须有一个显式的约束，且这个约束必须是一个接口类型 或者 类型约束字面量。</p>

<h4 id="允许任意类型的约束">允许任意类型的约束</h4>

<p>很多场景，并不需要对泛型参数进行约束，换句话来说，这种约束就是允许任意类型。</p>

<p>因为类型参数约束必须是一个接口类型，所以这种允许任意类型的约束自然是可以通过空接口 <code>interface{}</code> 实现的。但是到处编写 <code>interface{}</code> 太过冗长，Go 1.18 添加了一个 <code>interface{}</code> 别名 <a href="https://go.dev/ref/spec#Predeclared_identifiers"><code>any</code></a>。可以简单的认为 <code>any</code> 和 <code>interface{}</code> 等价。</p>

<blockquote>
<p>当然，<code>any</code> 的出现不仅仅对泛型编程有用，对简化冗长的 <code>interface{}</code> 非常有用。</p>
</blockquote>

<p>关于 <code>any</code> 的使用，上文已给出例子，这里可用 <code>interface{}</code> 替代。</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;reflect&#34;</span>
)

<span style="color:#75715e">// Print 打印切片的元素。
</span><span style="color:#75715e">// Print 有一个类型参数 T，且有一个（非类型）普通参数 s，
</span><span style="color:#75715e">// s 是一个为类型为类型参数 T 的切片。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">PrintInterface</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">interface</span>{}](<span style="color:#a6e22e">s</span> []<span style="color:#a6e22e">T</span>) {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">s</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">v</span>)
	}
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePrintInterface</span>() {
	<span style="color:#75715e">// 使用一个 []int 参数调用 Print。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// print有一个类型参数T，我们要传递一个[]int，
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 所以通过 Print[int] 来向函数 Print 的类型参数 T 传递 int，作为其参数。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 此时函数 Print[int] 需要一个 []int 作为参数。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">PrintInterface</span>([]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>})
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>}</code></pre></div>
<h4 id="定义约束">定义约束</h4>

<p>在 Go 1.17 及之前，接口是<strong>方法集合</strong>的声明。由两个部分组成</p>

<ul>
<li>0 或 多个 方法声明</li>
<li>0 或 多个 接口类型（嵌入的接口）</li>
</ul>

<p>其 <a href="https://go.dev/ref/spec#Notation">EBNF</a> 的<a href="https://go.googlesource.com/go/+/refs/tags/go1.17.8/doc/go_spec.html#1236">语法</a>为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ebnf" data-lang="ebnf"><span style="color:#66d9ef">InterfaceType      </span><span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;interface&#34;</span> <span style="color:#e6db74">&#34;{&#34;</span> { ( <span style="color:#66d9ef">MethodSpec </span>| <span style="color:#66d9ef">InterfaceTypeName </span>) <span style="color:#e6db74">&#34;;&#34;</span> } <span style="color:#e6db74">&#34;}&#34;</span> .
<span style="color:#66d9ef">MethodSpec         </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">MethodName Signature </span>.
<span style="color:#66d9ef">MethodName         </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">identifier </span>.
<span style="color:#66d9ef">InterfaceTypeName  </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">TypeName </span>.</code></pre></div>
<p>在 Go 1.18，接口的语法和语义都得到的了扩充，即接口是<strong>类型集合</strong>的声明，由如下两个部分组成：</p>

<ul>
<li>0 或 多个 方法声明</li>
<li>0 或 多个 类型集，可以分为两类

<ul>
<li>嵌入的接口</li>
<li>【Go 1.18 新增】类型约束字面量

<ul>
<li>嵌入的非接口类型</li>
<li>嵌入的非接口类型的底层类型 (新操作符 <code>~</code>，参见下文)</li>
<li>嵌入的任意类型（包含方法的接口除外）或 非接口类型的底层类型 的联合（union） (操作符 <code>|</code>，参见下文)</li>
</ul></li>
</ul></li>
</ul>

<p>其 <a href="https://go.dev/ref/spec#Notation">EBNF</a> 的<a href="https://go.googlesource.com/go/+/refs/tags/go1.18/doc/go_spec.html#1198">语法</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ebnf" data-lang="ebnf"><span style="color:#66d9ef">InterfaceType  </span><span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;interface&#34;</span> <span style="color:#e6db74">&#34;{&#34;</span> { <span style="color:#66d9ef">InterfaceElem </span><span style="color:#e6db74">&#34;;&#34;</span> } <span style="color:#e6db74">&#34;}&#34;</span> .
<span style="color:#66d9ef">InterfaceElem  </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">MethodElem </span>| <span style="color:#66d9ef">TypeElem </span>.
<span style="color:#66d9ef">MethodElem     </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">MethodName Signature </span>.
<span style="color:#66d9ef">MethodName     </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">identifier </span>.
<span style="color:#66d9ef">TypeElem       </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">TypeTerm </span>{ <span style="color:#e6db74">&#34;|&#34;</span> <span style="color:#66d9ef">TypeTerm </span>} .
<span style="color:#66d9ef">TypeTerm       </span><span style="color:#f92672">=</span> <span style="color:#66d9ef">Type </span>| <span style="color:#66d9ef">UnderlyingType </span>.
<span style="color:#66d9ef">UnderlyingType </span><span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&#34;</span> <span style="color:#66d9ef">Type </span>.</code></pre></div>
<p>Go 1.18 的接口，可以分为两类：</p>

<ul>
<li><strong>运行时接口</strong>：只包含方法集的接口，这种接口可以作为类型参数的约束（<code>func F[T I](t I) ...</code>），也可以作为变量类型（<code>var a = I(nil)</code> 合法）。在语法上表现为，只使用 Go 1.17 语法的接口。换言之接口声明只包含如下元素：

<ul>
<li>0 或 多个 方法声明</li>
<li>0 或 多个 接口类型（嵌入的接口）</li>
</ul></li>
<li><strong>编译时接口</strong>：专用于类型参数约束的接口，这种接口只可以作为类型参数的约束（<code>func F[T I](t I) ...</code>），不可以作为变量类型（<code>var a = I(nil)</code> 非法），即使用 Go 1.18 新增的语法特性的接口。换言之接口声明包含了如下类型约束字面量：

<ul>
<li>约束为具体类型相同（嵌入的非接口类型）</li>
<li>约束为底层类型相同（嵌入的使用新的前缀操作符 <code>~</code> 修饰的非接口类型）</li>
<li>约束为类型联合（union）（非接口类型 或 使用新的前缀操作符 <code>~</code> 修饰的非接口类型 或不包含方法的编译时接口 的列表，该列表使用中缀操作符 <code>|</code> 分割)</li>
</ul></li>
</ul>

<p>不管是编译时接口还是运行时接口，都可以作为类型参数的约束。也就是说，在 Go 1.18 中，定义约束本质上定义的是接口。</p>

<h4 id="运行时接口约束">运行时接口约束</h4>

<p>运行时接口就是 Go 1.17 就支持的普通接口，其可以作为类型参数的约束，参见下文示例：</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// Stringer 是一个类型约束。约束为 Stringer 的类型参数意味着：该类型必须有一个 String 方法。
</span><span style="color:#75715e">// 在泛型函数中，使用类型为该类型参数的参数，允许在该变量上调用 String 方法。
</span><span style="color:#75715e">// （这定义了与标准库的 fmt.Stringer 类型相同的接口，实际代码可能会简单地使用 fmt.Stringer。）
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Stringer</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span>
}

<span style="color:#75715e">// Stringify 在 s 的每个元素上调用 String 方法，
</span><span style="color:#75715e">// 并返回结果
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Stringify</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">Stringer</span>](<span style="color:#a6e22e">s</span> []<span style="color:#a6e22e">T</span>) (<span style="color:#a6e22e">ret</span> []<span style="color:#66d9ef">string</span>) {
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">s</span> {
		<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">v</span>.<span style="color:#a6e22e">String</span>())
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ret</span>
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleStringify</span>() {
	<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>{}, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>{}, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>{}
	<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">WriteString</span>(<span style="color:#e6db74">&#34;a&#34;</span>)
	<span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">WriteString</span>(<span style="color:#e6db74">&#34;b&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">WriteString</span>(<span style="color:#e6db74">&#34;c&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">Stringify</span>([]<span style="color:#f92672">*</span><span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>{<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>, <span style="color:#a6e22e">c</span>}))
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// [a b c]
</span><span style="color:#75715e"></span>}</code></pre></div>
<h4 id="编译时接口约束">编译时接口约束</h4>

<h5 id="约束为具体类型">约束为具体类型</h5>

<p>语法为：嵌入的非接口类型。</p>

<p>语义为：被约束的参数必须是该类型，即</p>

<ul>
<li>允许：类型相同</li>
<li>允许：该具体类型的类型别名</li>
<li>不允许：类型不同，底层类型不同是</li>
</ul>

<p>单独使用该语法，对于开发者而言基本上没有任何意义。</p>

<p>具体参见下方示例：</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// MyInt 定义一个类型约束，表示必须是 int 类型
</span><span style="color:#75715e">// 这约束就等价于 int，等于限定死类型参数必须是 int
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyInt</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#66d9ef">int</span>
}

<span style="color:#75715e">// IntAdd2 两个 int 相加
</span><span style="color:#75715e">// 这个函数声明完全等价于 func IntAdd2(x, y int) int
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">IntAdd2</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">MyInt</span>](<span style="color:#a6e22e">x</span>, <span style="color:#a6e22e">y</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">y</span>
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleIntAdd2</span>() {
	<span style="color:#75715e">// 底层类型相同是不行的，如下两句会报错
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// type MyIntType int
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// fmt.Println(IntAdd2(MyIntType(1), MyIntType(2)))
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 别名是可以的
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntAlias</span> = <span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">IntAdd2</span>(<span style="color:#a6e22e">MyIntAlias</span>(<span style="color:#ae81ff">1</span>), <span style="color:#a6e22e">MyIntAlias</span>(<span style="color:#ae81ff">2</span>)))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">IntAdd2</span>(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>))
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>我们甚至可以构造出一种约束，这个约束不可能存在一个满足要求的类型。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// MyIntString 定义一个需同时是 string 和 int 的类型约束
</span><span style="color:#75715e">// 显然没有这种类型
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntString</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#66d9ef">int</span>
	<span style="color:#66d9ef">string</span>
}

<span style="color:#75715e">// 这个函数不可能被调用，因为不可能存在既是 int 又是 string 的类型
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">PrintMyIntString</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">MyIntString</span>](<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">x</span>)
}

<span style="color:#75715e">// MyIntWithAddOne 定义一个类型必须是 int 的类型约束，且包含一个 AddOne 方法
</span><span style="color:#75715e">// 显然没有这种类型
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntWithAddOne</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">AddOne</span>(<span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span>
}</code></pre></div>
<h5 id="约束为底层类型相同">约束为底层类型相同</h5>

<p>语法为：嵌入的使用新的前缀操作符 <code>~</code> 修饰的非接口类型。</p>

<p>语义为：被约束的参数和波浪线后面的类型的底层类型必须相同，即</p>

<ul>
<li>允许：类型相同</li>
<li>允许：类型不同，底层类型不同是</li>
<li>允许：该具体类型的类型别名</li>
</ul>

<p>关于底层类型概念，参见：<a href="https://lingchao.xin/post/type-system-overview.html#%E6%A6%82%E5%BF%B5-%E5%BA%95%E5%B1%82%E7%B1%BB%E5%9E%8B">博客</a></p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// MyUint 定义一个类型约束，表示被约束参数的底层类型必须是 uint
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyUint</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">uint</span>
}

<span style="color:#75715e">// IntAdd2 底层类型为 uint 的两个变量相加
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">UintAdd2</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">MyUint</span>](<span style="color:#a6e22e">x</span>, <span style="color:#a6e22e">y</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">y</span>
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleUintAdd2</span>() {
	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntType</span> <span style="color:#66d9ef">uint</span>
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">UintAdd2</span>(<span style="color:#a6e22e">MyIntType</span>(<span style="color:#ae81ff">1</span>), <span style="color:#a6e22e">MyIntType</span>(<span style="color:#ae81ff">2</span>)))
	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntAlias</span> = <span style="color:#66d9ef">uint</span>
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">UintAdd2</span>(<span style="color:#a6e22e">MyIntAlias</span>(<span style="color:#ae81ff">1</span>), <span style="color:#a6e22e">MyIntAlias</span>(<span style="color:#ae81ff">2</span>)))
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">UintAdd2</span>(uint(<span style="color:#ae81ff">1</span>), uint(<span style="color:#ae81ff">2</span>)))
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 3
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>该约束还是很有用的，设想一个库函数，这个函数要求传入一个参数，要求这个参数的底层类型为某个类型，且需要额外包含某些指定方法。</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// MyIntWithAddAddOne 定义一个类型约束，表示被约束参数的底层类型必须是 int，且包含一个 Add 方法
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntWithAdd</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">Add</span>(<span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">MyIntWithAddAddOne</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">MyIntWithAdd</span>](<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>) <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#ae81ff">1</span>)
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntAddType</span> <span style="color:#66d9ef">int</span>

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#a6e22e">MyIntAddType</span>) <span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> int(<span style="color:#a6e22e">a</span>) <span style="color:#f92672">+</span> <span style="color:#a6e22e">b</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleMyIntWithAddAddOne</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">MyIntWithAddAddOne</span>(<span style="color:#a6e22e">MyIntAddType</span>(<span style="color:#ae81ff">1</span>)))
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2
</span><span style="color:#75715e"></span>}</code></pre></div>
<h5 id="约束为类型联合-union">约束为类型联合（union）</h5>

<p>语法为：一个使用中缀操作符 <code>|</code> 分割的列表，该列表每个元素是如下三种情况之一：</p>

<ul>
<li>非接口类型</li>
<li>使用新的前缀操作符 <code>~</code> 修饰的非接口类型</li>
<li>不包含方法的编译时接口类型</li>
</ul>

<p>语义为：被约束的参数需满足该被 <code>|</code> 分割的类型列表之一。</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;reflect&#34;</span>
)

<span style="color:#75715e">// MyUnion 定义一个 Union 的类型约束，表示被约束参数的类型满足如下三者之一
</span><span style="color:#75715e">// a) bool
</span><span style="color:#75715e">// b) 底层类型和 int 相同
</span><span style="color:#75715e">// c) MyUint，即 ~uint，底层类型和 uint 相同
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyUnion</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#66d9ef">bool</span> | <span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">int</span> | <span style="color:#a6e22e">MyUint</span>
}

<span style="color:#75715e">// PrintMyUnionAndType 打印 MyUnion 的类型和值
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">PrintMyUnionAndType</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">MyUnion</span>](<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>) {
	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">any</span>(<span style="color:#a6e22e">x</span>).(<span style="color:#66d9ef">type</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#66d9ef">bool</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;bool = %t\n&#34;</span>, <span style="color:#a6e22e">v</span>)
	<span style="color:#66d9ef">case</span> <span style="color:#66d9ef">int</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;int = %d\n&#34;</span>, <span style="color:#a6e22e">v</span>)
	<span style="color:#66d9ef">case</span> <span style="color:#66d9ef">uint</span>:
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;uint = %d\n&#34;</span>, <span style="color:#a6e22e">v</span>)
	<span style="color:#66d9ef">default</span>:
		<span style="color:#75715e">// 底层类型概念：https://lingchao.xin/post/type-system-overview.html#%E6%A6%82%E5%BF%B5-%E5%BA%95%E5%B1%82%E7%B1%BB%E5%9E%8B
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 底层类型 issue： https://github.com/golang/go/issues/39574
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">v</span>).<span style="color:#a6e22e">Kind</span>() {
		<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">Int</span>:
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;~int = %d\n&#34;</span>, <span style="color:#a6e22e">v</span>)
		<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">Uint</span>:
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;~uint = %d\n&#34;</span>, <span style="color:#a6e22e">v</span>)
		<span style="color:#66d9ef">default</span>:
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;dead code\n&#34;</span>)
		}
	}
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExamplePrintMyUnionAndType</span>() {
	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyInt</span> <span style="color:#66d9ef">int</span>
	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyUint</span> <span style="color:#66d9ef">uint</span>
	<span style="color:#a6e22e">PrintMyUnionAndType</span>(<span style="color:#66d9ef">true</span>)
	<span style="color:#a6e22e">PrintMyUnionAndType</span>(<span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">PrintMyUnionAndType</span>(<span style="color:#a6e22e">MyInt</span>(<span style="color:#ae81ff">1</span>))
	<span style="color:#a6e22e">PrintMyUnionAndType</span>(uint(<span style="color:#ae81ff">2</span>))
	<span style="color:#a6e22e">PrintMyUnionAndType</span>(<span style="color:#a6e22e">MyUint</span>(<span style="color:#ae81ff">1</span>))
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// bool = true
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int = 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// ~int = 1
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// uint = 2
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// ~uint = 1
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>union 不允许是包含方法的接口类型</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyIntWithAdd</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">Add</span>(<span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">int</span>
}

<span style="color:#75715e">// 如下将报错
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyUnionInvalidWithInterfaceHasMethod1</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#75715e">// cannot use xxx.Stringer in union (fmt.Stringer contains methods)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// https://pkg.go.dev/golang.org/x/tools/internal/typesinternal?utm_source%3Dgopls#InvalidUnion
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">bool</span> | <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Stringer</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyUnionInvalidWithInterfaceHasMethod2</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#75715e">// cannot use xxx.MyIntWithAdd in union (xxx.MyIntWithAdd contains methods)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// https://pkg.go.dev/golang.org/x/tools/internal/typesinternal?utm_source%3Dgopls#InvalidUnion
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">bool</span> | <span style="color:#a6e22e">MyIntWithAdd</span>
}</code></pre></div>
<h5 id="编译时接口的限制">编译时接口的限制</h5>

<p>编译时接口，仅能用于类型参数约束，不能作为变量参数类型。</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyInt</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#66d9ef">int</span>
}

<span style="color:#75715e">// 如下将报错
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// cannot use interface MyInt in conversion (contains specific type constraints or is comparable)
</span><span style="color:#75715e">// https://pkg.go.dev/golang.org/x/tools/internal/typesinternal?utm_source%3Dgopls#MisplacedConstraintIface
</span><span style="color:#75715e"></span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">MyInt1</span> = <span style="color:#a6e22e">MyInt</span>(<span style="color:#ae81ff">1</span>)</code></pre></div>
<h4 id="类型约束字面量">类型约束字面量</h4>

<blockquote>
<p><a href="https://go.dev/ref/spec#Type_parameter_declarations">Spec - Type parameter declarations</a></p>
</blockquote>

<p>类型约束除了上文提到的是一个定义好的接口之外，也可以直接使用，字面量的形式，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">[<span style="color:#a6e22e">P</span> <span style="color:#a6e22e">any</span>]
[<span style="color:#a6e22e">S</span> <span style="color:#66d9ef">interface</span>{ <span style="color:#960050;background-color:#1e0010">~</span>[]<span style="color:#66d9ef">byte</span>|<span style="color:#66d9ef">string</span> }]
[<span style="color:#a6e22e">S</span> <span style="color:#960050;background-color:#1e0010">~</span>[]<span style="color:#a6e22e">E</span>, <span style="color:#a6e22e">E</span> <span style="color:#a6e22e">any</span>]
[<span style="color:#a6e22e">P</span> <span style="color:#a6e22e">Constraint</span>[<span style="color:#66d9ef">int</span>]]
[<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">any</span>]</code></pre></div>
<p>注意类型约束字面量不允许使用 <code>type</code> 定义为一个类型，即如下非法</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Constraint</span> <span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">int</span>         <span style="color:#f92672">//</span> <span style="color:#a6e22e">illegal</span>: <span style="color:#960050;background-color:#1e0010">~</span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">inside</span> <span style="color:#a6e22e">a</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">parameter</span> <span style="color:#a6e22e">list</span></code></pre></div>
<h4 id="comparable-预定义约束">comparable 预定义约束</h4>

<p>Go 1.18 添加一个新的预定义标识符 <code>comparable</code>，该标识符是一个用作类型参数约束的编译时接口，因此该标识符只能在类型参数约束中使用，无法作为变量参数类型。</p>

<p>哪些类型满足该标识符，参见 <a href="https://go.dev/ref/spec#Comparison_operators">Spec - Comparison operators</a>。</p>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Equals</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">comparable</span>](<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#a6e22e">T</span>) <span style="color:#66d9ef">bool</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">b</span>
}</code></pre></div>
<p><code>01-generics/02-constraints_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleEquals</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">Equals</span>(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>))
	<span style="color:#75715e">// fmt.Println(Equals([]int{1}, []int{2})) //  切片不可比较
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">Equals</span>([<span style="color:#ae81ff">1</span>]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">1</span>}, [<span style="color:#ae81ff">1</span>]<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">2</span>})) <span style="color:#75715e">// 长度相同数组可比较
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// false
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// false
</span><span style="color:#75715e"></span>}</code></pre></div>
<h4 id="从逻辑组合角度看">从逻辑组合角度看</h4>

<p>（1）类型参数约束总的来看支持 <code>与</code> 和 <code>或</code> 两种逻辑运算来将具体约束元素进行组合。比如想实现一个约束 <code>comparable &amp; (int | uint)</code>，此时语法为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ComparableInt</span> <span style="color:#66d9ef">interface</span> {
    <span style="color:#a6e22e">comparable</span>
    <span style="color:#66d9ef">int</span> | <span style="color:#66d9ef">uint</span>
}</code></pre></div>
<p>因此，接口声明中的每一行通过 <code>与</code> 逻辑运算组合，<code>或</code> 运算通过在每一行中通过 <code>|</code> 标识符组合。</p>

<p>（2）约束元素，支持如下几种约束元素：</p>

<ul>
<li>方法（不支持 <code>或</code> 运算）</li>
<li>具体非接口类型</li>
<li>非接口底层类型</li>
</ul>

<p>（3）根据（1）（2）可以得知，Go 的类型约束的表达能力为（启用 <code>|&amp;</code> 表示都可以与或都可以）：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">方法0~n个相互与 &amp; ( 具体非接口类型0~n个相互与or或 |&amp; 非接口底层类型0~n个相互与or或)</pre></div>
<h3 id="函数体中使用被类型参数约束的参数">函数体中使用被类型参数约束的参数</h3>

<p>在函数体中，我们需要对函数体进行操作。普通函数参数的操作直接根据函数类型就可以判断出可以做哪些操作，比如：</p>

<ul>
<li><code>(a int)</code> 就可以对 a 参数进行算数运算操作</li>
<li><code>(s fmt.Stringer)</code> 就可以调用 <code>s</code> 的 <code>String</code> 方法</li>
</ul>

<p>对于通过类型参数约束的函数参数，也是类似，编译器会检查对该参数的操作是否满足类型参数的约束，换句话枚举泛型参数的所有可能性，该操作都不可能出现未定义的情况。</p>

<ul>
<li><code>[T any](a T)</code>，此时 <code>a</code> 就相当于 <code>interface{}</code>，如果想操作，就只能通过 <code>any(a)</code> 或 <code>interface{}(a)</code> 转换为 <code>interface{}</code> 类型然后通过反射操作，当然下面的所有场景也都可以这么干，但是泛型的目的之一就是减少运行时反射，并不推荐这么做。</li>
<li><code>[T fmt.Stringer](a T)</code>，相当于 <code>(s fmt.Stringer)</code>，就可以调用 <code>s</code> 的 <code>String</code> 方法。</li>
<li><code>[T uint | int](a, b T)</code>，<code>a</code> 和 <code>b</code> 类型都为 <code>T</code>，显然 <code>a</code> 和 <code>b</code> 可以进行算数运算。针对 <code>|</code> 运算，要求对参数的操作必须同时满足这两者的约束。</li>
<li><code>[T1, T2 int64 | int](a T1, b T2)</code>，<code>a</code> 和 <code>b</code> 类型约束都为 <code>int64 | int</code>，但是两者类型不同，因此直接进行算数运算，需要转换同一类型进行运算 <code>int64(a) + int64(b)</code></li>
<li><code>[T StructA](a T)</code>，假设 <code>StructA</code> 是个结构体，拥有方法 <code>A()</code>，在 Go 1.18 中，是无法调用 <code>a.A()</code> 的，参见下文：<a href="#go-118-限制">限制章节</a></li>
<li><code>[T A1 | A2](a T)</code> ，假设类型 <code>A1</code> 和 <code>A2</code> 都拥有方法 <code>A()</code>，Go 1.18 中，是无法调用的 <code>a.A()</code> 的，参见下文：<a href="#go-118-限制">限制章节</a>：</li>
</ul>

<p><code>01-generics/02-constraints.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> (
	<span style="color:#a6e22e">A1</span> <span style="color:#66d9ef">struct</span>{}
	<span style="color:#a6e22e">A2</span> <span style="color:#66d9ef">struct</span>{}
)

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">A1</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> { <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;A1&#34;</span> }
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">A2</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> { <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;A2&#34;</span> }

<span style="color:#75715e">// AString A1 和 A2 都拥有 String 方法，但是在 Go 1.18 中编译仍然报错
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">AString1</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">A1</span> | <span style="color:#a6e22e">A2</span>](<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">String</span>() <span style="color:#75715e">// Error: x.String undefined (type T has no field or method String) https://pkg.go.dev/golang.org/x/tools/internal/typesinternal?utm_source=gopls#MissingFieldOrMethod
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">// 显式的声明方法， x.String() 才不会报错
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">AString1</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">A1</span> | <span style="color:#a6e22e">A2</span>
	<span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span>
	<span style="color:#75715e">// fmt.Stringer // 这个写法也行
</span><span style="color:#75715e"></span>}](<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>,
) <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">String</span>()
}</code></pre></div>
<h3 id="泛型类型">泛型类型</h3>

<p>除了函数支持类型参数外，Go 1.18 中，类型也支持类型参数，语法为 <code>type TypeName[...] ...</code>。</p>

<h4 id="定义泛型类型">定义泛型类型</h4>

<p>本部分源码位于 <code>01-generics/03-generic-types.go</code>。</p>

<p>下面是定义一个泛型的简单的示例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// Vector 是任何元素类型的切片。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Vector</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>] []<span style="color:#a6e22e">T</span>

<span style="color:#75715e">// Push 将元素添加到 Vector 的末尾。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">v</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Vector</span>[<span style="color:#a6e22e">T</span>]) <span style="color:#a6e22e">Push</span>(<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">T</span>) {
	<span style="color:#f92672">*</span><span style="color:#a6e22e">v</span> = append(<span style="color:#f92672">*</span><span style="color:#a6e22e">v</span>, <span style="color:#a6e22e">x</span>)
}</code></pre></div>
<p>泛型类型允许引用，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#75715e">// List 一个通用的链表类型
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">List</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>] <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">next</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">List</span>[<span style="color:#a6e22e">T</span>] <span style="color:#75715e">// 引用自身
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">val</span>  <span style="color:#a6e22e">T</span>
}</code></pre></div>
<p>泛型类型定义支持任意类型的定义，如接口，甚至指针。例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// Adder 泛型接口
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Adder</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>] <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span>
}

<span style="color:#75715e">// Object 泛型指针
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Object</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">any</span>] <span style="color:#f92672">*</span><span style="color:#a6e22e">T</span></code></pre></div>
<p>注意，泛型类型的类型参数不支持直接替换，如下例子将报错：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">T</span>[<span style="color:#a6e22e">P</span> <span style="color:#a6e22e">any</span>] <span style="color:#a6e22e">P</span> <span style="color:#f92672">//</span> <span style="color:#a6e22e">Error</span>: <span style="color:#a6e22e">cannot</span> <span style="color:#a6e22e">use</span> <span style="color:#a6e22e">a</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">parameter</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">RHS</span> <span style="color:#a6e22e">in</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">declaration</span> <span style="color:#a6e22e">https</span>:<span style="color:#f92672">//</span><span style="color:#a6e22e">pkg</span>.<span style="color:#66d9ef">go</span>.<span style="color:#a6e22e">dev</span><span style="color:#f92672">/</span><span style="color:#a6e22e">golang</span>.<span style="color:#a6e22e">org</span><span style="color:#f92672">/</span><span style="color:#a6e22e">x</span><span style="color:#f92672">/</span><span style="color:#a6e22e">tools</span><span style="color:#f92672">/</span><span style="color:#a6e22e">internal</span><span style="color:#f92672">/</span><span style="color:#a6e22e">typesinternal</span><span style="color:#960050;background-color:#1e0010">#</span><span style="color:#a6e22e">MisplacedTypeParam</span></code></pre></div>
<p>类型参数在类型声明中可以作为一个已定义的类型使用。可以作为，切片的元素类型，结构体字段的类型（上文已演示），此外还可以 Map 的键或者值类型，例如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyMap</span>[<span style="color:#a6e22e">K</span> <span style="color:#a6e22e">comparable</span>, <span style="color:#a6e22e">V</span> <span style="color:#a6e22e">any</span>] <span style="color:#66d9ef">map</span>[<span style="color:#a6e22e">K</span>]<span style="color:#a6e22e">V</span></code></pre></div>
<h4 id="泛型类型的实例化">泛型类型的实例化</h4>

<p>泛型类型的实例化表示，为类型参数指定具体一个类型。语法为 <code>GenericType[TypeName]</code>。</p>

<p>下面列了一些场景，源码位于 <code>01-generics/03-generic-types_test.go</code>。</p>

<p>(1) 可以通过 <code>type</code> 将泛型类型实例化为一个普通类型。（和普通类型类似，方法无法调用）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">VectorBool</span> <span style="color:#a6e22e">Vector</span>[<span style="color:#66d9ef">bool</span>]

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleVector</span>() {
	<span style="color:#75715e">// v0 := VectorBool{}
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// v0.Push(true) // Error: v0.Push undefined (type VectorBool has no field or method Push) https://pkg.go.dev/golang.org/x/tools/internal/typesinternal?utm_source=gopls#MissingFieldOrMethod
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>(2) 和普通类型类似，通过 <code>:=</code> 或 <code>var</code> 可以定义一个变量，也可以将泛型类型实例也支持嵌入其他结构体，此外通过反射获取到的类型名就是 <code>GenericType[TypeName]</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;reflect&#34;</span>
)

<span style="color:#75715e">// ...
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyVectorBool</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">Vector</span>[<span style="color:#66d9ef">bool</span>]
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExampleVector</span>() {
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 使用 := 实例化 Vector 为 Vector[int]
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 此时 v1 的类型就是 Vector[int] 等价于 type Vector[int] []int ，把 Vector[int] 看成一个标识符
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">v1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Vector</span>[<span style="color:#66d9ef">int</span>]{}
	<span style="color:#a6e22e">v1</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">v1</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#ae81ff">2</span>)
	<span style="color:#a6e22e">v1</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#ae81ff">3</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">v1</span>)
	<span style="color:#a6e22e">_</span> = []int(<span style="color:#a6e22e">v1</span>) <span style="color:#75715e">// 底层类型相同，可以这样转换
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;v1 reflect: type = %s, kind = %s\n&#34;</span>, <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">v1</span>), <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">ValueOf</span>(<span style="color:#a6e22e">v1</span>).<span style="color:#a6e22e">Kind</span>())

	<span style="color:#75715e">// 使用 var 实例化 Vector 为 Vector[string]
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">v2</span> <span style="color:#a6e22e">Vector</span>[<span style="color:#66d9ef">string</span>]
	<span style="color:#a6e22e">v2</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#e6db74">&#34;a&#34;</span>)
	<span style="color:#a6e22e">v2</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#e6db74">&#34;b&#34;</span>)
	<span style="color:#a6e22e">v2</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#e6db74">&#34;c&#34;</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">v2</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;v2 reflect: type = %s, kind = %s\n&#34;</span>, <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">v2</span>), <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">ValueOf</span>(<span style="color:#a6e22e">v2</span>).<span style="color:#a6e22e">Kind</span>())

	<span style="color:#75715e">// 嵌入结构体
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">v3</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">MyVectorBool</span>{}
	<span style="color:#a6e22e">v3</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#66d9ef">true</span>)
	<span style="color:#a6e22e">v3</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#66d9ef">false</span>)
	<span style="color:#a6e22e">v3</span>.<span style="color:#a6e22e">Push</span>(<span style="color:#66d9ef">true</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">v3</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;v3 reflect: type = %s, kind = %s\n&#34;</span>, <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">v3</span>), <span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">ValueOf</span>(<span style="color:#a6e22e">v3</span>).<span style="color:#a6e22e">Kind</span>())
	<span style="color:#75715e">// output:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// [1 2 3]
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// v1 reflect: type = generics.Vector[int], kind = slice
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// [a b c]
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// v2 reflect: type = generics.Vector[string], kind = slice
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// {[true false true]}
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// v3 reflect: type = generics.MyVectorBool, kind = struct
</span><span style="color:#75715e"></span>}</code></pre></div>
<h3 id="反射">反射</h3>

<p>Go 语言引入的反射并没有对反射库进行任何修改。</p>

<p>从这一点可以看出，Go 的泛型完全发生在编译阶段。</p>

<h3 id="标准库">标准库</h3>

<p>在 Go 1.18 中，Go 标准库没有为泛型进行相关改造（泛型是 Go 1 发布依赖最大的语言特性，官方比较谨慎，参见<a href="https://github.com/golang/go/issues/48918">issue</a>）。相关内容还在实验阶段，相关实验库参见：</p>

<ul>
<li><a href="https://pkg.go.dev/golang.org/x/exp/constraints">golang.org/x/exp/constraints</a></li>
<li><a href="https://pkg.go.dev/golang.org/x/exp/slices">golang.org/x/exp/slices</a></li>
<li><a href="https://pkg.go.dev/golang.org/x/exp/maps">golang.org/x/exp/maps</a></li>
</ul>

<h3 id="go-1-18-限制">Go 1.18 限制</h3>

<ul>
<li>a. 类型参数不支持在方法中使用，官方<strong>希望</strong>在 Go 1.19 中支持。</li>
<li>b. 类型参数不支持 <code>real</code>、<code>imag</code> 和 <code>complex</code> ，官方<strong>希望</strong>在 Go 1.19 中支持。</li>
<li>c. 类型为类型参数的函数函数，只能调用接口中显式声明的参数，无法调用结构体的方法，官方<strong>希望</strong>在 Go 1.19 中支持。</li>
<li>d. 类型为类型参数的函数函数，无法调用结构体的字段，官方<strong>可能</strong>在 Go 1.19 中支持。</li>
<li>e. 不允许将类型参数及其指针形式嵌入到结构类型中。同样，不允许在接口类型中嵌入类型参数。官方目前还<strong>不清楚</strong>这些是否会在未来被允许。</li>
<li>f. <code>|</code> 的类型联合，不允许接收包含方法的接口。官方目前还<strong>不清楚</strong>这些是否会在未来被允许。</li>
</ul>

<p>实例参加下文</p>

<p><code>01-generics/04-limit.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">generics</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">B</span> <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">A</span>) <span style="color:#a6e22e">Print</span>() { <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;a&#34;</span>) }

<span style="color:#75715e">// a. Error: method must have no type parameters
</span><span style="color:#75715e">// func (a A) Print[T any](a T)  {
</span><span style="color:#75715e">// 	fmt.Println(a)
</span><span style="color:#75715e">// }
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// b. Error: complex (built-in) is not a type
</span><span style="color:#75715e">// func PrintComplex[T complex](a T) {
</span><span style="color:#75715e">// 	fmt.Println(a)
</span><span style="color:#75715e">// }
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// c. Error: a.Print undefined (type T has no field or method Print)
</span><span style="color:#75715e">// func PrintA[T A](a T) {
</span><span style="color:#75715e">// 	a.Print()
</span><span style="color:#75715e">// }
</span><span style="color:#75715e">// c. Go 1.18 处理方式为手动声明方法
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">PrintA</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">A</span>
	<span style="color:#a6e22e">Print</span>()
}](<span style="color:#a6e22e">a</span> <span style="color:#a6e22e">T</span>,
) {
	<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">Print</span>()
}

<span style="color:#75715e">// d. Error: a.B undefined (type T has no field or method B)
</span><span style="color:#75715e">// func PrintB[T A](a T) {
</span><span style="color:#75715e">// 	fmt.Println(a.B)
</span><span style="color:#75715e">// }
</span><span style="color:#75715e">// d. Go 1.18 处理方式为通过 any 转换
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">PrintB</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">A</span>](<span style="color:#a6e22e">a</span> <span style="color:#a6e22e">T</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">any</span>(<span style="color:#a6e22e">a</span>).(<span style="color:#a6e22e">A</span>).<span style="color:#a6e22e">B</span>)
}

<span style="color:#75715e">// e. Error: embedded field type cannot be a (pointer to a) type parameter
</span><span style="color:#75715e">// type EmbeddedType[T any] struct {
</span><span style="color:#75715e">// 	T
</span><span style="color:#75715e">// 	A int
</span><span style="color:#75715e">// }
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// f. Error: cannot use fmt.Stringer in union (fmt.Stringer contains methods)
</span><span style="color:#75715e">// func PrintString[T string | fmt.Stringer](s T) {
</span><span style="color:#75715e"></span><span style="color:#f92672">//</span> }</code></pre></div>
<h3 id="泛型的实例">泛型的实例</h3>

<p>提案中，给了很多可以有意义的实例，参见，<a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#examples">提案 - 例子</a></p>

<ul>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#map_reduce_filter">Map/Reduce/Filter</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#map-keys">Map keys</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#sets">Sets</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#sort">Sort</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#channels">Channels</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#containers">Containers</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#append">Append</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#metrics">Metrics</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#list-transform">List transform</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#dot-product">Dot product</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#absolute-difference">Absolute difference</a></li>
</ul>

<h2 id="fuzzing-单元测试">Fuzzing 单元测试</h2>

<p>Fuzzing 单元测试，即 case 单元测试。官方有详细的文档：</p>

<ul>
<li><a href="https://go.dev/doc/tutorial/fuzz">Tutorial: Getting started with fuzzing</a></li>
<li><a href="https://go.dev/doc/fuzz/">Go Fuzzing</a></li>
</ul>

<p>假设我们要写一个字符串翻转的函数，并向对这个函数进行随机输入测试，此时就可以使用 Fuzzing 单元测试。</p>

<p>使用 Fuzzing 单元测试时需要注意，由于测试输入是随机的，因此我们没法枚举出输出，只能通过一些测试函数的特性来编写测试程序。</p>

<p>在这个例子中，字符串翻转函数可以通过如下特征编写测试函数：</p>

<ul>
<li>连续调用两次 <code>Reverse</code> 函数的输出和输入一致。</li>
<li>反转后的字符串将其状态保留为有效的 UTF-8。</li>
</ul>

<p><code>02-fuzzing/fuzzing.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;unicode/utf8&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">input</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;The quick brown fox jumped over the lazy dog&#34;</span>
	<span style="color:#a6e22e">rev</span>, <span style="color:#a6e22e">revErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">input</span>)
	<span style="color:#a6e22e">doubleRev</span>, <span style="color:#a6e22e">doubleRevErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">rev</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;original: %q\n&#34;</span>, <span style="color:#a6e22e">input</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;reversed: %q, err: %v\n&#34;</span>, <span style="color:#a6e22e">rev</span>, <span style="color:#a6e22e">revErr</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;reversed again: %q, err: %v\n&#34;</span>, <span style="color:#a6e22e">doubleRev</span>, <span style="color:#a6e22e">doubleRevErr</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">s</span>) {
		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;input is not valid UTF-8&#34;</span>)
	}
	<span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> []rune(<span style="color:#a6e22e">s</span>)
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">r</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">r</span>)<span style="color:#f92672">/</span><span style="color:#ae81ff">2</span>; <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> = <span style="color:#a6e22e">i</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
		<span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">j</span>] = <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">j</span>], <span style="color:#a6e22e">r</span>[<span style="color:#a6e22e">i</span>]
	}
	<span style="color:#66d9ef">return</span> string(<span style="color:#a6e22e">r</span>), <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p>编写 Fuzz 测试函数：</p>

<ul>
<li>函数名以 <code>Fuzz</code> 开头，函数签名为 <code>func (f *testing.F)</code>.</li>
<li>调用 <code>f.Add(...)</code> ，提供默认情况下的测试样例，并告诉驱动器参数的类型。</li>
<li>调用 <code>f.Fuzz</code> 传递一个函数，该函数的声明为 <code>func(t *testing.T, ...)</code>，其中 <code>...</code> 和 上一步 <code>f.Add(...)</code> 的类型一致。</li>
<li>编写测试 case 即可</li>
</ul>

<p><code>02-fuzzing/fuzzing_test.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;testing&#34;</span>
	<span style="color:#e6db74">&#34;unicode/utf8&#34;</span>
)

<span style="color:#75715e">// go test -fuzz=Fuzz -fuzztime 2s -run ^FuzzReverse$ ./02-fuzzing
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">FuzzReverse</span>(<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">F</span>) {
	<span style="color:#75715e">// 1. 提供默认情况下的测试样例
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 2. 告诉驱动器参数的类型
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">testcases</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;Hello, world&#34;</span>, <span style="color:#e6db74">&#34; &#34;</span>, <span style="color:#e6db74">&#34;!12345&#34;</span>}
	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">tc</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">testcases</span> {
		<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">tc</span>) <span style="color:#75715e">// Use f.Add to provide a seed corpus
</span><span style="color:#75715e"></span>	}
	<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Fuzz</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">orig</span> <span style="color:#66d9ef">string</span>) { <span style="color:#75715e">// 2~n 个参数需要和上面 f.Add 类型一致
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">rev</span>, <span style="color:#a6e22e">err1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">orig</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span>
		}
		<span style="color:#a6e22e">doubleRev</span>, <span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Reverse</span>(<span style="color:#a6e22e">rev</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span>
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">orig</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">doubleRev</span> {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Before: %q, after: %q&#34;</span>, <span style="color:#a6e22e">orig</span>, <span style="color:#a6e22e">doubleRev</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">orig</span>) <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">utf8</span>.<span style="color:#a6e22e">ValidString</span>(<span style="color:#a6e22e">rev</span>) {
			<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;Reverse produced invalid UTF-8 string %q&#34;</span>, <span style="color:#a6e22e">rev</span>)
		}
	})
}</code></pre></div>
<p>运行测试函数：</p>

<ul>
<li>普通运行，即只使用 <code>f.Add</code> 提供的测试样例：<code>go test  -run ^FuzzReverse$ ./02-fuzzing</code></li>
<li>随机 case 运行，使用随机 case 进行测试：<code>go test -fuzz=Fuzz -fuzztime 2s -run ^FuzzReverse$ ./02-fuzzing</code></li>
</ul>

<p>请注意，模糊测试会消耗大量内存，并可能会影响机器运行时的性能。另外，模糊引擎在运行时会将扩展测试覆盖率的值写入 $GOCACHE/fuzz 中的模糊缓存目录。目前对可以写入模糊缓存的文件数或总字节数没有限制，因此可能会占用大量存储空间（可能为数 GB）。</p>

<h2 id="工作空间">工作空间</h2>

<h3 id="官方文档">官方文档</h3>

<ul>
<li><a href="https://pkg.go.dev/cmd/go#hdr-Workspace_maintenance">go work 命令</a></li>
<li><a href="https://go.dev/doc/tutorial/workspaces">Tutorial: Getting started with multi-module workspaces</a></li>
<li><a href="https://go.dev/ref/mod#workspaces">Go Modules Reference - Workspaces</a></li>
</ul>

<h3 id="背景">背景</h3>

<p>在小型 Go 项目开发中，项目由两个部分组成：</p>

<ul>
<li>一个项目代码仓库：即一个 Go Module，一般存储在一个 git 仓库。作为项目代码，本项目开发人员需要在该代码仓库上开发代码。</li>
<li>多个项目依赖：多个外部 Go Module，声明在项目代码 <code>go.mod</code> 文件中。作为项目的依赖，本项目开发人员不需要修改这些外部模块的代码，只需要管理这些依赖的版本。</li>
</ul>

<p>此时，在本项目的开发人员的设备中，其项目代码结构如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── dir1
├── dir2
├── dir3
├── ...
├── go.mod
└── go.sum</pre></div>
<p>以上这种项目结构，存在一些问题：</p>

<ol>
<li>假设，在设计中，我们要求目录 dir2 单向依赖目录 dir1。但是在同一个 module 中，Go 编译器不能提供这种保证。在小型项目中，这种约束可以通过研发人员的意识进行约束，但是在大型项目中，没有工具层面的约束，是无法保证以上约束的。</li>
<li>在中大型项目中的设计中，项目可以按照项目的特点划分成多个相互独立的部分，这些部分之间的依赖关系是一张有向无环图，这些部分在 Go 语言中对应的概念就是模块 (module)。</li>
<li>某个项目是需要部分公开的，部分闭源的，此时我们就需要将项目拆分为多个部分，这些部分在 Go 语言中对应的概念就是模块 (module)。</li>
</ol>

<p>为了解决上述问题，项目就会被拆成多个 module，此时我们的项目变成如下两个部分组成：</p>

<ul>
<li>多个项目 module：这些 module 可能处于同一个 git 仓库，也可能处于不同的 git 仓库。作为项目代码，本项目开发人员完成一个 feature 可能需要编辑多个 module。</li>
<li>多个项目依赖：多个外部 Go Module，声明在项目代码 <code>go.mod</code> 文件中。作为项目的依赖，本项目开发人员不需要修改这些外部模块的代码，只需要管理这些依赖的版本。</li>
</ul>

<p>此时，在本项目的开发人员的设备中，则需要一个工作空间目录来管理这些项目 module：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── module1
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── go.mod
│   └── go.sum
├── module2
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── main.go
│   ├── go.mod
│   └── go.sum
└── ...</pre></div>
<p>此时又带来了一个问题：假设一个需求，我们需要同时更改多 module。以上文结构为例，module2 依赖 module1，我们修改了 module1，在 module2 中 go 命令是看不到的这些变更的，因为 module2 看到到仍然是 module1 在代码仓库中的旧版本。</p>

<p>为了解决这个问题，我们可以使用 <code>go mod replace</code> 语法，以上文结构为例，module2 依赖 module1，此时我们需要在 <code>module2/go.mod</code> 目录下添加如下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-gomod" data-lang="gomod">replace module1全名 =&gt; ../module1</code></pre></div>
<p>此时，在 module2 中 go 命令就可以看到磁盘中的 module1 目录的变更了（module2 作为依赖被其他 module 依赖时， <code>replace</code> 语句会被忽略，因此这个改动不会破坏下游使用者的依赖图）</p>

<p>replace 并没有完美的解决了问题，还存在如下问题：</p>

<ol>
<li>由于 go module 机制会忽略依赖的 <code>replace</code>，因此本项目依赖图上的所有节点的 go mod 文件都需要对其直接依赖和间接依赖的父节点添加 <code>replace</code>，在项目模块多的项目，维护这些 <code>replace</code> 成本比较高。比如 <code>c -&gt; b -&gt; a</code>，<code>e -&gt; d -&gt; b -&gt; a</code>，此时：

<ul>
<li><code>b</code> 的 <code>go.mod</code> 需要 replace <code>a</code></li>
<li><code>c</code> 的 <code>go.mod</code> 需要 replace <code>a</code>、<code>b</code></li>
<li><code>d</code> 的 <code>go.mod</code> 需要 replace <code>a</code>、<code>b</code></li>
<li><code>e</code> 的 <code>go.mod</code> 需要 replace <code>a</code>、<code>b</code>、<code>d</code></li>
</ul></li>
<li>由于 go 命令只能包含够 <code>go.mod</code> 的目录下识别当前 module。因此如果想要执行 module2 的 main 函数，还需要手动 <code>cd module2</code>，然后再执行 <code>go run ./</code>，这十分麻烦。</li>
</ol>

<h3 id="概述-1">概述</h3>

<p>为了解决如上两个问题， <code>Go 1.18</code> 带来了 worksace 的概念。在文件系统中，go workspace 是一个包含 <code>go.work</code> 的目录，这个目录中包含多个 module 的目录。</p>

<p>引入 workspace 后，上文的目录结构编程了如下结构：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── go.work
├── module1
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── go.mod
│   └── go.sum
├── module2
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── main.go
│   ├── go.mod
│   └── go.sum
└── ...</pre></div>
<p>go.work 的内容如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-gowork" data-lang="gowork">go 1.18

use (
	./module1
	./module2
    // ...
)</code></pre></div>
<p>同时，删掉 <code>module2/go.mod</code> 的相关 <code>replace</code> 语句。</p>

<p>此时，上文提到的两个问题都得到了解决。</p>

<ol>
<li><p>存在复杂的依赖关系时，不需要在每个 module 的 <code>go.mod</code> 文件中编写 <code>replace</code> 语句了，只要在 <code>go.work</code> 中将所有的项目模块通过 <code>use</code> 语句声明即可。比如，比如 <code>c -&gt; b -&gt; a</code>，<code>e -&gt; d -&gt; b -&gt; a</code>，我们只需要在 <code>go.work</code> 中添加：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-gowork" data-lang="gowork">use (
    ./a模块在本地的路径
    ./b模块在本地的路径
    ./c模块在本地的路径
    ./d模块在本地的路径
    ./e模块在本地的路径
)</code></pre></div></li>

<li><p>只要是在 workspace 目录及其子目录中执行 go 命令，go 命令都会首先向上递归查找 <code>go.work</code> 文件，如果找到了，则会加载本地文件系统中对应的 module 目录，然后执行相关操作。因此如果想要执行 module2 的 main 函数，只需在 workspace 目录执行 <code>go run module2全名</code> 即可。</p></li>
</ol>

<h3 id="说明">说明</h3>

<h4 id="go-work-文件">go.work 文件</h4>

<p>当前目录或祖先目录包含 <code>go.work</code> 文件时，表示开启当前目录处在一个 workspace 中。<code>go.work</code> 文件包含如下几个部分：</p>

<ul>
<li><code>go version</code>，go 版本声明，如 <code>go 1.18</code>。</li>
<li><code>use ...</code>，需要加载的本地 module 目录，这些目录必须包含 <code>go.mod</code> 文件，如 <code>./module1</code>。</li>
<li><code>replace ...</code>，和 <code>go.mod</code> 中的 <code>replace</code> 类似，配置的是这个工作空间的 replace，会应用到工作空间下的所有模块。</li>
</ul>

<h4 id="go-work-命令">go work 命令</h4>

<blockquote>
<p>参见 <code>go help work</code></p>
</blockquote>

<ul>
<li><code>go work init [moddirs]</code> 初始化一个 workspace，即创建一个 <code>go.work</code> 文件，并将 <code>moddirs</code> 添加到 <code>use</code> 子句 下。</li>
<li><code>go work use [-r] [moddirs]</code> 将 module 目录添加到 <code>use</code> 子句下，目录下不存在 <code>go.mod</code> 将忽略，如果 <code>-r</code> 参数被指定，则递归搜索该目录下的所有 module 目录。</li>
<li><code>go work edit [editing flags] [go.work]</code> 编辑 <code>go.work</code> 文件。</li>
<li><code>go work sync</code> 说明参见，<a href="https://go.dev/ref/mod#go-work-sync">官方文档</a>（没太理解这个命令的意义）。

<ul>
<li>按照官方文档的说法：该命令会计算 workspace 构建列表，并将其写入 workspace 下的每个 module 的 <code>go.mod</code> 文件。</li>
<li>实验观察下来，可能会发生如下现象：某些情况下，可能会生成一个 <code>go.work.sum</code> 文件。</li>
<li>经过实验，<code>go work sync</code> 的作用可能是： 类似于 go mod tidy，如果使用 <code>go get -u</code> 更新了一个依赖的版本，<code>go work sync</code> 会批量更新该 workspace 下的所有的 在 <code>use</code> 中声明的所有的相关 module 的 <code>go.mod</code> 文件。但是 <code>go.sum</code> 可能更新行为和 <code>go mod tidy</code> 不一致，因为部分 <code>sum</code> 会存储在 <code>go.work.sum</code> 中。该命令主要来更新间接 <code>go.mod</code> 的间接依赖 （<code>// indirect</code>）和 <code>go.sum</code>。</li>
</ul></li>
</ul>

<h3 id="ide-支持">IDE 支持</h3>

<p><a href="https://github.com/golang/vscode-go/blob/master/CHANGELOG.md#v0320---8-mar-2022">VSCode Go v0.32.0 - 8 Mar, 2022</a> 已完整支持了 <code>go.work</code> 。</p>

<h3 id="示例">示例</h3>

<p>有两个 module：<code>util</code> 和 <code>hello</code> ，<code>hello</code> 依赖 <code>util</code>，<code>hello</code> 和 <code>util</code> 属于同一个工作空间。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir <span style="color:#ae81ff">03</span>-workspace
mkdir util hello
cd hello <span style="color:#f92672">&amp;&amp;</span> go mod init github.com/rectcircle/go-1-18-feature/03-workspace/hello  <span style="color:#f92672">&amp;&amp;</span> cd ..
cd util <span style="color:#f92672">&amp;&amp;</span> go mod init github.com/rectcircle/go-1-18-feature/03-workspace/util  <span style="color:#f92672">&amp;&amp;</span> cd ..
go work init ./hello ./util</code></pre></div>
<p>更新 <code>03-workspace/hello/go.mod</code> 依赖，添加如下内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-gomod" data-lang="gomod">require github.com/rectcircle/go-1-18-feature/03-workspace/util v1.2.0</code></pre></div>
<p>此时实例 workspace 就搭建完成了，使用 VSCode 打开即可进行开发，且编译时 <code>hello</code> 使用的就是本地磁盘的 <code>util</code> 的代码。</p>

<p>执行 go 命令是，可以直接在 <code>03-workspace</code> 目录通过 <code>go run github.com/rectcircle/go-1-18-feature/03-workspace/hello</code> 即可运行 main 函数，而不需要去 <code>hello</code> 目录。</p>

<p>发布是需要为 <code>util</code> 添加 <code>03-workspace/hello/go.mod</code> 声明的 <code>tag</code>，如果 <code>tag</code> 不对，<code>hello</code> 的下游依赖这将出现错误。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 规划好版本号，03-workspace/hello/go.mod 的 对 util 的依赖的版本。</span>
<span style="color:#75715e"># git add commit  ...</span>
git push
git tag <span style="color:#ae81ff">03</span>-workspace/util/v1.2.0
git push --tags</code></pre></div>
<p>发布完成后，在进行下一步开发前，可以执行 <code>go work sync</code>，更新一下相关 module 的 <code>go.mod</code> 文件。</p>

<h3 id="最佳实践">最佳实践</h3>

<h4 id="多仓场景">多仓场景</h4>

<p>每个项目建议采用 1 + n 个仓库，即 1 个 workspace 开发仓库 和 n 个 module 仓库。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.                     // workspace 仓库
├── .git
├── go.work
├── module1           // 单独的 git 仓库，在 workspace 中是一个 git submodule
│   ├── .git
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── go.mod
│   └── go.sum
├── module2           // 单独的 git 仓库，在 workspace 中是一个 git submodule
│   ├── .git
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── main.go
│   ├── go.mod
│   └── go.sum
└── ...</pre></div>
<h4 id="单仓场景">单仓场景</h4>

<p>即 monorepo 模式，使用每个项目对应一个仓库，仓库里有多个 module。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.                     // 项目仓库
├── .git
├── go.work
├── module1           // 子目录
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── go.mod
│   └── go.sum
├── module2           // 子目录
│   ├── dir1
│   ├── dir2
│   ├── ...
│   ├── main.go
│   ├── go.mod
│   └── go.sum
└── ...</pre></div>
<p>推荐使用这种模式，这种模式同时支持 library 和 binary 开发。</p>

<p>如果 monorepo 存在会被外部依赖的 module，建议使用 <code>git tag</code> 来管理版本，且所有 module 同步更新版本号，防止混乱。此时发布的流程是：</p>

<ul>
<li>更新本 monorepo 所有 module 所依赖的本 monorepo 下的模块的版本号，为本次要发布的版本号，如 <code>cd modulex &amp;&amp; go mod edit -require=module全名@x.x.x</code>。</li>
<li>提交代码到远端仓库，注意，此时尚未发布。</li>
<li>为本 monorepo 所有 module 打  <code>git tag</code> 为本次版本号，如 <code>git tag modulex/vx.x.x</code>。</li>
<li>提交 tag 到远端 <code>git push --tags</code>。</li>
</ul>

<p>如上流程少有繁琐，不过流程固定，可以写一个脚本自动化的执行。</p>

<p>注意</p>

<ul>
<li>以上流程基本上没有问题，但是相关 module 间接依赖，更新会存在延迟（因为，只有 module 发布了，才能运行 go work sync 更新这些 module 的 go.mod，是个先有鸡还是先有蛋的问题）。但是并不影响将这些 module 作为 library 依赖的下游 module。原理参见博客：<a href="/posts/go-improve/#8-高级话题">Go 提升 - Go module - 高级话题</a></li>
<li>不要把 <code>go.work</code> 添加到 gitignore 中（<code>go.work.sum</code> 是否需要不确定）</li>
</ul>

<h2 id="其他">其他</h2>

<h3 id="编译优化">编译优化</h3>

<ul>
<li>添加新的 <code>GOAMD64</code> 环境变量以使用更新的指令集版本，参见：<a href="https://golang.org/wiki/MinimumRequirements#amd64">Go Wiki</a></li>
</ul>

<h3 id="go-命令">Go 命令</h3>

<ul>
<li><code>go get</code> 目前只用来修改 <code>go.mod</code> 文件，如果想安装二进制工具，需使用 <code>go install</code></li>
</ul>

<p>更多参见：<a href="https://go.dev/doc/go1.18#go-command">发行文档 - Go Comaand</a></p>

<h3 id="其他更新">其他更新</h3>

<p>其他开发人员可能感知不明显的特性，参见：<a href="https://go.dev/doc/go1.18">发行文档</a></p>
]]></description></item><item><title>容器核心技术（六） PID Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-6-namespace-pid/</link><pubDate>Thu, 17 Mar 2022 22:00:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-6-namespace-pid/</guid><description type="html"><![CDATA[

<blockquote>
<p>手册页面：<a href="https://man7.org/linux/man-pages/man7/pid_namespaces.7.html">pid namespaces</a>。</p>
</blockquote>

<h2 id="背景知识">背景知识</h2>

<h3 id="信号">信号</h3>

<p>信号是类 Unix 操作系统一种进程间通知的机制（参见：手册 <a href="https://man7.org/linux/man-pages/man7/signal.7.html">signal(7)</a>）。本部分涉及的为：</p>

<ul>
<li>用来协调多个进程的执行，如监听子孙进程的状态变更 <code>SIGCHLD</code>，默认忽略。需要注意的是，如果一个进程退出后，其父进程进程没有处理 <code>SIGCHLD</code> 信号，则该进程占用 PCB 将不会释放，此时该进程被称为僵尸进程。</li>
<li>无法覆盖的特权信号（包括 1 号进程同样无法覆盖），<code>SIGKILL</code> （终止） 和 <code>SIGSTOP</code> （挂起，需通过 <code>SIGCONT</code> 信号唤醒）</li>
<li><code>SIGTERM</code>，可以覆盖默认的行为，一般用于优雅退出</li>
</ul>

<h3 id="1-进程">1 进程</h3>

<p>1 号进程是内核创建的第 1 个用户态进程，内核对该进程的有特殊处理。</p>

<h4 id="1-进程和进程树">1 进程和进程树</h4>

<p>在 Unix 类系统中，进程会组成一颗进程树，其根节点是 0 号进程。每个进程都有一个父进程，有 0 个或多个子进程。</p>

<p>0 号进程是内核进程，内核进程会创建 1 号进程，1 号进程是第一个用户态进程。</p>

<p>一个进程通过 fork/clone 系统调用创建一个子进程，一个进程的父进程为 fork 该进程的进程。</p>

<p>当一个进程的父进程退出了，为了维持进程树的关系，该进程的父进程将会被设置为 1 号进程。这种父进程变化为 1 号进程的进程被称为孤儿进程。这个过程可以叫做：1 号进程（如果该进程的祖先进程使用 <a href="https://man7.org/linux/man-pages/man2/prctl.2.html"><code>prctl(2) 系统调用</code></a> 和 <code>PR_SET_CHILD_SUBREAPER</code> 进行标记，则该进程会进行收养）收养了该孤儿进程。</p>

<h4 id="1-号进程和信号">1 号进程和信号</h4>

<ul>
<li>1 号进程只能收到一种信号，即 1 号进程注册了信号处理器的信号。参见：<a href="https://man7.org/linux/man-pages/man2/kill.2.html#NOTES">kill(2)</a>。因此，默认情况下 <code>kill -15 1</code> 是收不到信号的。</li>
<li>由于 <code>SIGKILL</code> 和 <code>SIGSTOP</code> 两个特权信号是无法覆盖的，所以任何情况下，其子进程向 1 号进程发送这两个信号都是无效的（父 PID Namespace 的进程发送是可以的）。</li>
<li>通过 <a href="https://man7.org/linux/man-pages/man2/reboot.2.html">reboot(2)</a> （<code>LINUX_REBOOT_CMD_CAD_OFF</code>）关闭 CAD （Ctrl-Alt-Del） 快捷键时，CAD 将会向 1 号进程发送 <code>SIGINT</code> 信号</li>
</ul>

<h3 id="proc-文件系统"><code>/proc</code> 文件系统</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man5/proc.5.html">proc(5)</a></p>
</blockquote>

<p>进程文件系统，通过 <code>mount -t proc proc /proc</code> 调用。top、ps 等命令都是通过该文件系统实现的。</p>

<h3 id="unix-domain-socket">Unix domain socket</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/unix.7.html">unix(7)</a></p>
</blockquote>

<p>遵循 Socket API 的 进程通讯方式，相比于网络层面的 Socket API 接口，性能是更好。</p>

<h2 id="描述">描述</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/pid_namespaces.7.html">pid_namespaces(7)</a></p>
</blockquote>

<ul>
<li>通过 <code>CLONE_NEWPID</code> 可以创建一个 PID Namespace

<ul>
<li><code>clone(2)</code> 系统调用产生的进程就是该 PID Namespace 的<strong>第一个进程</strong></li>
<li><code>setns(2)</code> 系统调用后，再调用 <code>fork/clone</code> 系统调用（不需要指定 <code>CLONE_NEWPID</code>），产生的进程就是该 PID Namespace 的进程。注意：调用 <code>setns(2)</code> 的进程的 PID Namespace 不会发生变化。</li>
<li><code>unshare(2)</code> 系统调用后，再调用 <code>fork/clone</code> 系统调用（不需要指定 <code>CLONE_NEWPID</code>），产生的进程就是该 PID Namespace 的进程，第一次调用时产生的进程，就是该 PID Namespace 的<strong>第一个进程</strong>。注意：调用 <code>unshare(2)</code> 的进程的 PID Namespace 不会发生变化。</li>
</ul></li>
<li>PID Namespace 支持嵌套

<ul>
<li>最大 32 层</li>
<li>当前 PID Namespace 可见所有子孙 Namespace 的进程，可见意味着可以 kill、设置优先级</li>
<li>当前 PID Namespace 无法看到祖先 Namespace 下的进程</li>
<li>一个进程在每一层 PID Namespace 都有一个 PID，进程自身调用 <code>getpid</code> 看到的是当前 PID Namespace 的 PID</li>
<li>如果当前 PID Namespace 的进程的父进程也是当前 PID Namespace 内的进程，则  <code>getppid(2)</code> 返回该父进程在该 PID Namespace 的 PID</li>
<li>如果当前 PID Namespace 的进程的父进程不是当前 PID Namespace 内的进程，则  <code>getppid(2)</code> 返回该父进程返回 0 （<code>setns(2)</code> 和 <code>unshare(2)</code> 语义造成的）</li>
</ul></li>
</ul>

<p><img src="/image/container-core-tech-namespace-pid-create-or-join.png" alt="image" /></p>

<ul>
<li><code>setns(2)</code> 和 <code>unshare(2)</code> 语义，由于一个进程的 PID Namespace 从创建的那一刻就固定了，所以 <code>setns(2)</code> 和 <code>unshare(2)</code>，并不会影响当前进程的 PID Namespace（ 仅仅修改 <code>/proc/[pid]/ns/pid_for_children</code> 文件）。（试想一下，如果 PID Namespace 发生了变化，那么他们的进程号就变了，而很多程序假设自身的进程号不会发生变化的，这样就破坏了兼容性）</li>
<li>新的 PID Namespace 的<strong>第一个进程</strong>的进程号为 <code>1</code>，即在该 PID Namespace 中，该进程就是受内核特殊处理的 1 号进程（参见上文：1 号进程和信号，1 号进程和进程树），此外还需要注意：

<ul>
<li>该 PID Namespace 内的进程永远无法通过 <code>kill -9</code> 杀死 1 号进程。</li>
<li>祖先 PID Namespace 的进程可以向该 1 号进程通过 <code>kill -9</code> 发送信号，此时该进程的行为和普通进程一致。但是有一点需要注意的是（<strong>手册也没有阐述</strong> <code>5.10.0-11-amd64</code> 稳定复现）：

<ul>
<li>如果该 PID Namespace 存在一个 <code>进程 a</code>，其父进程不在该 PID Namespace 中 （即：通过 <code>setns(2)</code> 创建到该 Namespace 中），且这个父进程没有处理 <code>SIGCHLD</code> 信号。此时 <code>kill -9</code> 1 号进程</li>
<li><code>进程 a</code> 将变成僵尸进程，该名字空间下的所有进程都将无响应</li>
<li>该 PID Namespace 处于可不加入状态（即：通过 <code>setns(2)</code> 创建将报错 <code>ENOMEM</code>）</li>
<li>只有 <code>进程 a</code> 真正退出，该 PID Namespace 的其他进程才能退出</li>
</ul></li>
<li>如果某 PID Namespace 的 1 号进程退出了，则整个 PID Namespace 所有进程将被杀死，也就是说这个 PID Namespace 已经消失了。

<ul>
<li>内核会向该 PID Namespace 下的所有进程发送 <code>SIGKILL</code> (9) 信号</li>
<li>无法再在该 Namespace 中 <code>fork</code> 进程，比如：之前通过调用了 <code>setns(2)</code> 和 <code>unshare(2)</code> 将该进程 <code>/proc/[pid]/ns/pid_for_children</code> 设置为一个 1 号进程现在已经退出的 PID Namespace，然后执行 <code>fork</code>，此时会报 <code>ENOMEM</code> 错误。</li>
</ul></li>
<li>在非 Init PID Namespace 调用 <a href="https://man7.org/linux/man-pages/man2/reboot.2.html"><code>reboot(2)</code></a> 行为不同，调用后，1 号进程将直接被终止，该进程的父进程 <a href="https://man7.org/linux/man-pages/man2/wait.2.html"><code>wait(2)</code></a> 收到子进程的退出信号为 <code>SIGHUP</code> 或 <code>SIGINT</code> （由参数决定） 信号（通过 <code>WTERMSIG(wstatus)</code> 获得）</li>
<li>PID Namespace 1 号进程收养孤儿机制

<ul>
<li><code>getppid(2)</code> 不为 0 的会被当前 PID Namespace 的 1 号进程收养</li>
<li><code>getppid(2)</code> 为 0 的不会被当前 PID Namespace 的 1 号进程收养，而是被之前父 PID Namespace 所在的 PID Namespace 的 1 号进程收养 （产生这种进程的原因还是 <code>setns(2)</code> 和 <code>unshare(2)</code> 语义造成的），在当前 PID Namespace 看来，该进程的 <code>getppid(2)</code> 仍为 0</li>
</ul></li>
</ul></li>
<li><code>/proc</code>

<ul>
<li>显示的是在执行 mount 时刻进程所属的 PID Namespace 下的可见的进程（包含子孙进程）。</li>
<li>一个常见做法是，PID Namespace 配合 Mount Namespace 使用，执行 <code>mount -t proc proc /proc</code>，将当前 PID Namespace 的进程信息挂载进去。（如果不这么做，<code>/proc/self</code> 看到的还是该进程在父 PID Namespace 中的信息）</li>
<li><code>/proc/sys/kernel/ns_last_pid</code> 是当前 PID Namespace 的 last pid，可以通过更改该文件的值，来配置即将创建的进程的 ID（从 <code>ns_last_pid + 1</code> 开始查找）</li>
</ul></li>
<li>杂项

<ul>
<li><code>SCM_CREDENTIALS</code> <code>unix(7)</code> 会翻译成对应的 PID Namespace 的 PID</li>
</ul></li>
</ul>

<p><img src="/image/container-core-tech-namespace-pid-proccess-tree-and-operate.png" alt="image" /></p>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<p>为了验证 PID Namespace 的能力，按照时序进行如下操作：</p>

<ul>
<li>(0s) <code>主进程</code> 启动一个具有新 PID Namespace 和 Mount Namespace 的 <code>进程(a)</code>，主进程 sleep 1s</li>
<li>(0s) <code>进程(a)</code> 会先挂载 <code>/proc</code>， sleep 2s</li>
<li>(1s) <code>主进程</code> 构造一个孤儿进程，<code>进程(b)</code>，该进程在新 PID Namespace 中，其 ppid 为 0，在父 PID Namespace 中其 ppid 为 1</li>
<li>(2s) <code>主进程</code> 构造 <code>进程(c)</code>，该进程在新 PID Namespace 中，其 ppid 为 0，，在父 PID Namespace 中其 ppid 为 <code>主进程</code></li>
<li>(3s) <code>子进程(a)</code> 执行命令：

<ul>
<li>构造一个 <code>孤儿进程(d)</code>，在该 PID Namespace，其 ppid 为 1</li>
<li>观察 <code>/proc</code> 目录</li>
<li>观察该 PID Namespace 的所有进程</li>
<li>尝试 <code>kill -9 1</code></li>
<li>再次观察该 PID Namespace 的所有进程</li>
<li><code>exec sleep infinity</code></li>
</ul></li>
<li>(4s) 然后 fork <code>子进程(e)</code>，该进程在主进程初始状态的 PID Namespace 中，执行命令：

<ul>
<li>观察 <code>/proc</code> 目录</li>
<li>观察所有 sleep 进程</li>
<li>尝试 <code>kill -9 进程(a)</code></li>
<li>再次观察观察所有 sleep 进程</li>
</ul></li>
<li>(5s) 主进程退出</li>
</ul>

<p>该实验在第 3s 末进程状态应该为：</p>

<p><img src="/image/container-core-tech-namespace-pid-exp.png" alt="image" /></p>

<p>注意，上图描述的是 C 语言版本可以达到的效果：</p>

<ul>
<li>在 Go 语言 和 Shell 描述的实验代码中，<code>进程 c</code> 的父进程应该是 <code>nsenter</code>。这个 <code>nsenter</code> 的 PID Namespace 为 main，这个 <code>nsenter</code> 的父进程为 main。</li>
</ul>

<p>此外，阅读下列实验代码，可以关注注释中的如下内容：</p>

<ul>
<li><code>seq: xxs</code> 标明时序过程。</li>
<li><code>进程 x</code> 表示相关语句来生成实验设计中的进程</li>
</ul>

<h3 id="源码">源码</h3>

<h4 id="c-语言描述">C 语言描述</h4>

<p>注意事项</p>

<ul>
<li>父进程需要处理 <code>SIGCHLD</code> 信号，否则主进程 <code>kill -9</code> 新 PID Namespace 的 1 号进程（即 <code>进程 a</code>）时，上文实验设计 <code>进程 c</code> 变成僵尸进程，导致新 PID Namespace 的所有进程无响应</li>
<li>使用 <a href="https://man7.org/linux/man-pages/man3/sleep.3.html"><code>sleep(3) 库函数</code></a> 会被 <code>SIGCHLD</code> 信号中断，导致时序不符合预期。因此需要使用 <a href="https://man7.org/linux/man-pages/man2/nanosleep.2.html"><code>nanosleep(2) 系统调用</code></a> 手动实现一个专门的 sleep。</li>
</ul>

<p>实验代码如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/04-pid/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE	   </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt; // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;  // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/syscall.h&gt; // For SYS_pidfd_open</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;time.h&gt;	   // For nanosleep(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;	   // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;	   // For SIGCHLD</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;	   // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For execv(3), sleep(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;    // For exit(3), system(3)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>

<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">my_sleep</span>(<span style="color:#66d9ef">int</span> sec)
{
	<span style="color:#66d9ef">struct</span> timespec t <span style="color:#f92672">=</span> {
		.tv_sec <span style="color:#f92672">=</span> sec,
		.tv_nsec <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>};
	<span style="color:#75715e">// sleep 会被信号打断，因此通过 nanosleep 重新实现一下
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// https: // man7.org/linux/man-pages/man2/nanosleep.2.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">while</span> (nanosleep(<span style="color:#f92672">&amp;</span>t, <span style="color:#f92672">&amp;</span>t) <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>)
		;
}

<span style="color:#75715e">// 进程 a：当前 bash，最终为 sleep infinity
</span><span style="color:#75715e">// 进程 d：nohup sleep infinity 孤儿进程在该 PID Namespace 中，其 ppid 为 1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> proccess_a_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;bash -c &#39;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#39; \
</span><span style="color:#e6db74">	&amp;&amp; echo $$ \
</span><span style="color:#e6db74">	&amp;&amp; ls /proc \
</span><span style="color:#e6db74">	&amp;&amp; ps -o pid,ppid,cmd \
</span><span style="color:#e6db74">	&amp;&amp; kill -9 1\
</span><span style="color:#e6db74">	&amp;&amp; ps -o pid,ppid,cmd \
</span><span style="color:#e6db74">	&amp;&amp; exec sleep infinity \
</span><span style="color:#e6db74">	&#34;</span>,
	NULL};

<span style="color:#75715e">// 进程 b： 在该 PID Namespace 中，构造一个孤儿进程，其 ppid 为 0，在父 PID Namespace 中 为 1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>proccess_b_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-c&#34;</span>,
	<span style="color:#e6db74">&#34;&#34;</span>,
	NULL};

<span style="color:#75715e">// 进程 c： sleep infinity 进程在该 PID Namespace 中，其 ppid 为 0，在父 PID Namespace 中 ppid 为 主进程
</span><span style="color:#75715e"></span><span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> proccess_c_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-c&#34;</span>,
	<span style="color:#e6db74">&#34;exec sleep infinity&#34;</span>,
	NULL};

<span style="color:#75715e">// 进程 e：
</span><span style="color:#75715e"></span><span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> proccess_e_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;ls /proc \
</span><span style="color:#e6db74">	&amp;&amp; ps -eo pid,ppid,cmd | grep sleep | grep -v grep  \
</span><span style="color:#e6db74">	&amp;&amp; kill -9 $(ps -eo pid,ppid | grep $PPID | awk &#39;{print $1}&#39; | sed -n &#39;2p&#39;) \
</span><span style="color:#e6db74">	&amp;&amp; ps -eo pid,ppid,cmd | grep sleep | grep -v grep \
</span><span style="color:#e6db74">	&#34;</span>,
	NULL};

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args)
{
	<span style="color:#75715e">// seq: 0s
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(NULL, <span style="color:#e6db74">&#34;/&#34;</span>, NULL , MS_SLAVE <span style="color:#f92672">|</span> MS_REC, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-MS_SLAVE&#34;</span>);
	<span style="color:#75715e">// 挂载当前 PID Namespace 的 proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount -t proc proc /proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// mount 函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    int mount(const char *source, const char *target,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const char *filesystemtype, unsigned long mountflags,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const void *data);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/mount.2.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(<span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#e6db74">&#34;/proc&#34;</span>, <span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#ae81ff">0</span>, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-proc&#34;</span>);
	my_sleep(<span style="color:#ae81ff">3</span>);
	<span style="color:#75715e">// seq: 3s
</span><span style="color:#75715e"></span>	printf(<span style="color:#e6db74">&#34;=== new pid namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	execv(proccess_a_args[<span style="color:#ae81ff">0</span>], proccess_a_args);
	perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
	exit(EXIT_FAILURE);
}

pid_t <span style="color:#a6e22e">fork_proccess</span>(<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> <span style="color:#f92672">*</span>argv)
{
	pid_t p <span style="color:#f92672">=</span> fork();
	<span style="color:#66d9ef">if</span> (p <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
	{
		execv(argv[<span style="color:#ae81ff">0</span>], argv);
		perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
		exit(EXIT_FAILURE);
	}
	<span style="color:#66d9ef">return</span> p;
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">set_pid_namespace</span>(pid_t pid) {
	<span style="color:#66d9ef">int</span> fd <span style="color:#f92672">=</span> syscall(SYS_pidfd_open, pid, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (fd <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;pidfd_open&#34;</span>);
	<span style="color:#66d9ef">if</span> (setns(fd, CLONE_NEWPID) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;setns&#34;</span>);
	close(fd);
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">print_child_handler</span>(<span style="color:#66d9ef">int</span> sig) {
	<span style="color:#66d9ef">int</span> wstatus;
	pid_t pid;
	<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/waitpid.2.html
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 获取子进程退出情况
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">while</span> ((pid<span style="color:#f92672">=</span>waitpid(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#f92672">&amp;</span>wstatus, WNOHANG)) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) {
		printf(<span style="color:#e6db74">&#34;*** pid %d exit by %d signal</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, pid, WTERMSIG(wstatus));
	}
}

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">register_signal_handler</span>() {
	<span style="color:#75715e">// 处理 SIGCHLD 信号，解决僵尸进程阻塞 Namespace 进程退出的情况。
</span><span style="color:#75715e"></span>	signal(SIGCHLD, print_child_handler);
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[])
{
	<span style="color:#75715e">// seq: 0s
</span><span style="color:#75715e"></span>	printf(<span style="color:#e6db74">&#34;=== main: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, getpid());
	<span style="color:#75715e">// 注册 SIGCHLD 处理程序，会产生僵尸进程，而导致 PID Namespace 无法退出
</span><span style="color:#75715e"></span>	register_signal_handler();
	<span style="color:#75715e">// 为子进程提供申请函数栈
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
							 PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
							 MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
							 <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
		errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 PID Namespace（CLONE_NEWPID），并执行 new_namespace_func 函数
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// clone 库函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 		  /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	pid_t pa <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWNS <span style="color:#f92672">|</span> CLONE_NEWPID, NULL); <span style="color:#75715e">// 进程 a
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (pa <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone-PA&#34;</span>);
	printf(<span style="color:#e6db74">&#34;=== PA: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, pa);

	my_sleep(<span style="color:#ae81ff">1</span>);
	<span style="color:#75715e">// seq: 1s
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 构造 进程 b
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">char</span> buf[<span style="color:#ae81ff">256</span>];
	<span style="color:#75715e">// 通过 nsenter 进入进程 a 的 PID Namespace
</span><span style="color:#75715e"></span>	sprintf(buf, <span style="color:#e6db74">&#34;exec nsenter -p -t %d bash -c &#39;echo === PB: </span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">$$ in new pid namespace</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74"> &amp;&amp; exec sleep infinity&#39;&#34;</span>, pa);
	proccess_b_args[<span style="color:#ae81ff">2</span>] <span style="color:#f92672">=</span> buf;
	pid_t pbp <span style="color:#f92672">=</span> fork_proccess(proccess_b_args);
	<span style="color:#66d9ef">if</span> (pbp <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone-PB&#34;</span>);
	my_sleep(<span style="color:#ae81ff">1</span>);

	<span style="color:#75715e">// seq: 2s
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 此时 kill 掉 nsenter 进程，sleep infinity 就能称为满足条件的进程 b
</span><span style="color:#75715e"></span>	kill(pbp, SIGKILL);

	<span style="color:#75715e">// 主进程 setns PID Namespace 为 进程 a
</span><span style="color:#75715e"></span>	set_pid_namespace(pa);
	<span style="color:#75715e">// fork 进程 c
</span><span style="color:#75715e"></span>	pid_t pc <span style="color:#f92672">=</span> fork_proccess(proccess_c_args);
	<span style="color:#66d9ef">if</span> (pc <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone-PC&#34;</span>);
	printf(<span style="color:#e6db74">&#34;=== PC: %d</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, pc);

	my_sleep(<span style="color:#ae81ff">2</span>);
	<span style="color:#75715e">// seq: 4s
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 恢复主进程 PID Namespace
</span><span style="color:#75715e"></span>	set_pid_namespace(<span style="color:#ae81ff">1</span>);
	printf(<span style="color:#e6db74">&#34;=== old pid namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	pid_t pe <span style="color:#f92672">=</span> fork_proccess(proccess_e_args);

	my_sleep(<span style="color:#ae81ff">1</span>);
	<span style="color:#75715e">// seq: 5s
</span><span style="color:#75715e"></span>
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<h4 id="go-语言描述">Go 语言描述</h4>

<p>注意事项：</p>

<ul>
<li><p>Go 不能直接使用 setns 系统调用（因为 setns 不支持多线程调用，而 go runtime 是多线程），因此还是通过 nsenter 命令实现 <code>进程 c</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run src/go/01-namespace/04-pid/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;os/signal&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>
)

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">proccess_a_args</span> = []<span style="color:#66d9ef">string</span>{
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;bash -c &#39;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#39; &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; echo $$ &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; ls /proc &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; ps -o pid,ppid,cmd &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; kill -9 1 &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; ps -o pid,ppid,cmd &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; exec sleep infinity &#34;</span>,
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">proccess_e_args</span> = []<span style="color:#66d9ef">string</span>{
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;ls /proc &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; ps -eo pid,ppid,cmd | grep sleep | grep -v grep &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; kill -9 $(ps -eo pid,ppid | grep $PPID | awk &#39;{print $1}&#39; | sed -n &#39;2p&#39;) &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; ps -eo pid,ppid,cmd | grep sleep | grep -v grep &#34;</span>,
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">asyncExec</span>(<span style="color:#a6e22e">name</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">arg</span> <span style="color:#f92672">...</span><span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">arg</span><span style="color:#f92672">...</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccess</span>() <span style="color:#66d9ef">int</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 PID Namespace（syscall.CLONE_NEWPID
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWNS</span> | <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWPID</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Start</span>()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Process</span>.<span style="color:#a6e22e">Pid</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>() {
	<span style="color:#75715e">// seq: 0s
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_SLAVE</span>|<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_REC</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 挂载当前 PID Namespace 的 proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount -t proc proc /proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man8/mount.8.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#e6db74">&#34;/proc&#34;</span>, <span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#ae81ff">0</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">3</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)

	<span style="color:#75715e">// seq: 3s
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;=== new pid namespace process ===&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Exec</span>(<span style="color:#a6e22e">proccess_a_args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">proccess_a_args</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">registerSignalhandler</span>() {
	<span style="color:#75715e">// 处理 SIGCHLD 信号，解决僵尸进程阻塞 Namespace 进程退出的情况。
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">sigs</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>, <span style="color:#ae81ff">1</span>)
	<span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">sigs</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGCHLD</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#66d9ef">for</span> {
			<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">sigs</span>
			<span style="color:#66d9ef">for</span> {
				<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wstatus</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WaitStatus</span>
				<span style="color:#a6e22e">pid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Wait4</span>(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">wstatus</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">WNOHANG</span>, <span style="color:#66d9ef">nil</span>)
				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">pid</span> <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">pid</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
					<span style="color:#66d9ef">break</span>
				}
				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;*** pid %d exit by %d signal\n&#34;</span>, <span style="color:#a6e22e">pid</span>, <span style="color:#a6e22e">wstatus</span>.<span style="color:#a6e22e">Signal</span>())
			}
		}
	}()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">mainProccess</span>() {
	<span style="color:#75715e">// seq: 0s
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== main: %d\n&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getpid</span>())
	<span style="color:#75715e">// 注册 SIGCHLD 处理程序，会产生僵尸进程，而导致 PID Namespace 无法退出
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">registerSignalhandler</span>()
	<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 PID Namespace 的进程
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pa</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccess</span>()
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;=== PA: %d\n&#34;</span>, <span style="color:#a6e22e">pa</span>)

	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// seq: 1s
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 构造 进程 b
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 通过 nsenter 进入进程 a 的 PID Namespace
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">pbp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">asyncExec</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;exec nsenter -p -t %d bash -c &#39;echo === PB: \&#34;$$ in new pid namespace\&#34; &amp;&amp; exec sleep infinity&#39;&#34;</span>, <span style="color:#a6e22e">pa</span>))
	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// 此时 kill 掉 nsenter 进程，sleep infinity 就能称为满足条件的进程 b
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Kill</span>(<span style="color:#a6e22e">pbp</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGKILL</span>)

	<span style="color:#75715e">// seq: 2s
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 构造进程 c
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// Go 不能直接使用 setns 系统调用（因为 setns 不支持多线程调用，而 go runtime 是多线程），因此还是通过 nsenter 命令实现
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">asyncExec</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;exec nsenter -p -t %d bash -c &#39;echo === PC: \&#34;$$ in new pid namespace\&#34; &amp;&amp; exec sleep infinity&#39;&#34;</span>, <span style="color:#a6e22e">pa</span>))

	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// seq: 4s
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;=== old pid namespace process ===&#34;</span>)
	<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">asyncExec</span>(<span style="color:#a6e22e">proccess_e_args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">proccess_e_args</span>[<span style="color:#ae81ff">1</span>:]<span style="color:#f92672">...</span>)

	<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
	<span style="color:#75715e">// seq: 5s
</span><span style="color:#75715e"></span>
	<span style="color:#66d9ef">return</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#a6e22e">mainProccess</span>()
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#a6e22e">newNamespaceProccessFunc</span>()
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div></li>
</ul>

<h4 id="shell-描述">Shell 描述</h4>

<p><code>src/shell/01-namespace/04-pid/main.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># sudo ./src/shell/01-namespace/04-pid/main.sh</span>

<span style="color:#75715e"># 注意：该脚本运行于进程为 main</span>
<span style="color:#75715e"># 设置 main 进程的 /proc/[pid]/ns/pid_for_children</span>
<span style="color:#75715e"># mount namespace 不能在此设置，因为 mount namespace 会立即生效</span> 
exec unshare -p bash <span style="color:#66d9ef">$(</span>cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>; pwd<span style="color:#66d9ef">)</span>/seq00.sh</code></pre></div>
<p><code>src/shell/01-namespace/04-pid/seq00.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># 注意：该脚本运行于进程为 main</span>

<span style="color:#75715e"># seq: 0s</span>

echo <span style="color:#e6db74">&#34;=== main: </span>$$<span style="color:#e6db74">&#34;</span>
<span style="color:#75715e"># bash 默认处理了 SIGCHLD 信号，因此不需要处理信号</span>

<span style="color:#75715e">### 构造进程 a</span>
<span style="color:#75715e"># 创建一个新的 mount namespace</span>
unshare -m bash -c <span style="color:#e6db74">&#39;mount -t proc proc /proc \
</span><span style="color:#e6db74">    &amp;&amp; sleep 3 \
</span><span style="color:#e6db74">    &amp;&amp; echo &#34;=== new pid namespace process ===&#34; \
</span><span style="color:#e6db74">    &amp;&amp; set -x \
</span><span style="color:#e6db74">    &amp;&amp; bash -c &#34;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#34; \
</span><span style="color:#e6db74">    &amp;&amp; echo $$ \
</span><span style="color:#e6db74">    &amp;&amp; ls /proc \
</span><span style="color:#e6db74">    &amp;&amp; ps -o pid,ppid,cmd \
</span><span style="color:#e6db74">    &amp;&amp; kill -9 1 \
</span><span style="color:#e6db74">    &amp;&amp; ps -o pid,ppid,cmd \
</span><span style="color:#e6db74">    &amp;&amp; exec sleep infinity \
</span><span style="color:#e6db74">&#39;</span> &amp;

pa<span style="color:#f92672">=</span>$!
echo <span style="color:#e6db74">&#34;=== PA: </span>$pa<span style="color:#e6db74">&#34;</span>
sleep <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># seq: 1s</span>

<span style="color:#75715e"># 恢复 main 进程 /proc/[pid]/ns/pid_for_children 为初始状态</span>
exec nsenter -p -t <span style="color:#ae81ff">1</span> bash <span style="color:#66d9ef">$(</span>cd <span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$0<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>; pwd<span style="color:#66d9ef">)</span>/seq01.sh $pa</code></pre></div>
<p><code>src/shell/01-namespace/04-pid/seq01.sh</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># 注意：该脚本运行于 main 进程的子进程，其 PID Namespace 和 main 进程相同</span>

pa<span style="color:#f92672">=</span>$1

<span style="color:#75715e"># seq: 1s</span>

<span style="color:#75715e">### 构造进程 b</span>
nsenter -p -t $pa bash -c <span style="color:#e6db74">&#39;echo &#34;=== PB: $$ in new pid namespace&#34; &amp;&amp; exec sleep infinity&#39;</span> &amp;
pbp<span style="color:#f92672">=</span>$! <span style="color:#75715e"># 进程 b 的父进程</span>
sleep <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># seq: 2s</span>

kill -9 $pbp <span style="color:#75715e"># kill 进程 b 的父进程，进程 b 构造完成</span>

<span style="color:#75715e">### 构造进程 c</span>
nsenter -p -t $pa bash -c <span style="color:#e6db74">&#39;echo &#34;=== PC: $$ in new pid namespace&#34; &amp;&amp; exec sleep infinity&#39;</span> &amp;

sleep <span style="color:#ae81ff">2</span>

<span style="color:#75715e"># seq: 4s</span>

echo <span style="color:#e6db74">&#34;=== old pid namespace process ===&#34;</span>
set -x
ls /proc
ps -eo pid,ppid,cmd | grep sleep | grep -v grep
kill -9 $pa
ps -eo pid,ppid,cmd | grep sleep | grep -v grep</code></pre></div>
<h3 id="输出及分析">输出及分析</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== main: 4683
=== PA: 4684
=== PB: 2 in new pid namespace
=== PC: 4708
*** pid 4697 exit by 9 signal
=== new pid namespace process ===
+ bash -c &#39;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#39;
+ echo 1
1
+ ls /proc
1  6          bus       cpuinfo    dma            fb           iomem     kcore      kpagecgroup  locks    mounts        partitions   self      swaps          thread-self  version
2  acpi       cgroups   crypto     driver         filesystems  ioports   keys       kpagecount   meminfo  mtrr          pressure     slabinfo  sys            timer_list   vmallocinfo
3  asound     cmdline   devices    dynamic_debug  fs           irq       key-users  kpageflags   misc     net           sched_debug  softirqs  sysrq-trigger  tty          vmstat
5  buddyinfo  consoles  diskstats  execdomains    interrupts   kallsyms  kmsg       loadavg      modules  pagetypeinfo  schedstat    stat      sysvipc        uptime       zoneinfo
+ ps -o pid,ppid,cmd
	PID    PPID CMD
	  1       0 /bin/bash -xc bash -c &#39;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#39; ?&amp;&amp; echo $$ ?&amp;&amp; ls /proc ?&amp;&amp; ps -o pid,ppid,cmd ?&amp;&amp; kill -9 1?&amp;&amp; ps -o pid,ppid,cmd ?&amp;&amp; exec sleep infini
	  2       0 sleep infinity
	  3       0 sleep infinity
	  5       1 sleep infinity
	  7       1 ps -o pid,ppid,cmd
+ kill -9 1
+ ps -o pid,ppid,cmd
	PID    PPID CMD
	  1       0 /bin/bash -xc bash -c &#39;nohup sleep infinity &gt;/dev/null 2&gt;&amp;1 &amp;&#39; ?&amp;&amp; echo $$ ?&amp;&amp; ls /proc ?&amp;&amp; ps -o pid,ppid,cmd ?&amp;&amp; kill -9 1?&amp;&amp; ps -o pid,ppid,cmd ?&amp;&amp; exec sleep infini
	  2       0 sleep infinity
	  3       0 sleep infinity
	  5       1 sleep infinity
	  8       1 ps -o pid,ppid,cmd
+ exec sleep infinity
=== old pid namespace process ===
+ ls /proc
1    11    15    204   24    278  3     3853  4426  4683  48   585  691        cmdline    dynamic_debug  irq          kpageflags  net           softirqs       tty
10   110   1558  2072  2445  280  302   3854  4458  4684  489  6    717        consoles   execdomains    kallsyms     loadavg     pagetypeinfo  stat           uptime
104  1183  17    2078  25    283  306   4     45    4698  49   62   9          cpuinfo    fb             kcore        locks       partitions    swaps          version
105  12    18    2079  253   285  311   4193  4586  47    490  621  acpi       crypto     filesystems    keys         meminfo     pressure      sys            vmallocinfo
106  13    183   21    270   287  318   429   46    4708  50   641  asound     devices    fs             key-users    misc        sched_debug   sysrq-trigger  vmstat
107  14    19    22    272   290  3762  43    462   4710  51   65   buddyinfo  diskstats  interrupts     kmsg         modules     schedstat     sysvipc        zoneinfo
108  147   2     229   274   291  3764  44    463   4714  52   652  bus        dma        iomem          kpagecgroup  mounts      self          thread-self
109  148   20    23    277   294  3852  4413  4682  4715  575  66   cgroups    driver     ioports        kpagecount   mtrr        slabinfo      timer_list
+ grep -v grep
+ grep sleep
+ ps -eo pid,ppid,cmd
   4684    4683 sleep infinity
   4698       1 sleep infinity
   4708    4683 sleep infinity
   4710    4684 sleep infinity
++ sed -n 2p
++ awk &#39;{print $1}&#39;
++ grep 4683
++ ps -eo pid,ppid
+ kill -9 4684
*** pid 4708 exit by 9 signal
*** pid 4684 exit by 9 signal
+ grep -v grep
+ grep sleep
+ ps -eo pid,ppid,cmd
*** pid 4714 exit by 0 signal</pre></div>
<p>分析上面输出日志可以看出，第 3s 末进程关系和实验描述一致：</p>

<p><img src="/image/container-core-tech-namespace-pid-exp-result.png" alt="image" /></p>

<p>注意，以上输出是 C 语言版本的 Go 和 Shell 版本略有不同：</p>

<ul>
<li>在 Go 语言场景，进程 B 的 PID 为 <code>5</code> 而不是 <code>2</code>，因为 Go 会启动多个线程，这些线程会占用一些 PID。</li>
<li>在 Shell 语言场景，进程 B 的 PID 也不为 <code>2</code>，因为在 <code>seq00.sh</code> 脚本里面的一部分外部命令也占用了部分进程号。</li>
</ul>
]]></description></item><item><title>容器核心技术（五） IPC Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-5-namespace-ipc/</link><pubDate>Wed, 16 Mar 2022 23:58:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-5-namespace-ipc/</guid><description type="html"><![CDATA[

<blockquote>
<p>手册页面：<a href="https://man7.org/linux/man-pages/man7/ipc_namespaces.7.html">ipc namespaces</a>。</p>
</blockquote>

<h2 id="背景知识">背景知识</h2>

<blockquote>
<p><a href="https://zh.wikipedia.org/wiki/%E8%A1%8C%E7%A8%8B%E9%96%93%E9%80%9A%E8%A8%8A">WIKI</a></p>
</blockquote>

<p>IPC （Inter-Process Communication，进程间通讯） 有很多中方式，在 Linux 中主要有：</p>

<ul>
<li>文件系统</li>
<li>Signal 信号</li>
<li>Pipe 管道</li>
<li>FIFO 命名管道</li>
<li>System V IPC

<ul>
<li>消息队列</li>
<li>信号量</li>
<li>共享内存</li>
</ul></li>
<li>POSIX IPC

<ul>
<li>消息队列</li>
<li>信号量</li>
<li>共享内存</li>
</ul></li>
<li>网络 Socket</li>
<li>Unix Domain Socket</li>
</ul>

<p>更多参见：<a href="/posts/linux-ipc/">Linux IPC</a></p>

<h2 id="描述">描述</h2>

<p>IPC Namespace 主要隔离了如下全局资源：</p>

<ul>
<li>System V IPC，包括消息队列、信号量、共享内存。</li>
<li>POSIX IPC 的 消息队列，不包括信号量、共享内存（原因是信号量和共享内存是基于 tmpfs 文件系统的，已经通过 mount namespace 隔离过了）。</li>
</ul>

<p>不同的 IPC Namespace 的如下 <code>/proc</code> 接口是隔离的：</p>

<ul>
<li><code>/proc/sys/fs/mqueue</code> POSIX message queue 接口。</li>
<li><code>/proc/sys/kernel</code> 中的 System V IPC 接口，即：msgmax、msgmnb、msgmni、sem、shmall、shmmax、shmmni 和shm_rmid_forced。</li>
<li>System V IPC 接口 <code>/proc/sysvipc</code>。</li>
</ul>

<p>当 IPC 命名空间被销毁时（即，当最后一个进程是命名空间的成员终止），所有 IPC 对象在命名空间被自动销毁。</p>

<p>使用 IPC 命名空间需要一个配置有的内核 CONFIG_IPC_NS 选项。</p>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<p>启动一个具有新 IPC Namespace 的子进程，这个进程会设置 创建一个 System V 消息队列。然后分别在父子两个进程观察系统消息队列列表。</p>

<h3 id="源码">源码</h3>

<h4 id="c-语言描述">C 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/03-ipc/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE	   </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt; // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;  // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;	   // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;	   // For SIGCHLD constant</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;	   // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For execv(3), sleep(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;    // For exit(3), system(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/ipc.h&gt;   // For ftok(3), key_t</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/msg.h&gt;   // For msgget(2)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> child_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;ipcs -q&#34;</span>,
	NULL};

<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">create_msg_queue</span>() 
{
	key_t k <span style="color:#f92672">=</span> ftok(<span style="color:#e6db74">&#34;/system_v_msg_queue_test_1&#34;</span>, <span style="color:#ae81ff">1</span>);
	<span style="color:#66d9ef">int</span> msgid <span style="color:#f92672">=</span> msgget(k, IPC_CREAT);
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args)
{
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(NULL, <span style="color:#e6db74">&#34;/&#34;</span>, NULL , MS_SLAVE <span style="color:#f92672">|</span> MS_REC, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-MS_SLAVE&#34;</span>);
	<span style="color:#75715e">// 挂载当前 PID Namespace 的 proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount -t proc proc /proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// mount 函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    int mount(const char *source, const char *target,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const char *filesystemtype, unsigned long mountflags,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const void *data);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/mount.2.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(<span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#e6db74">&#34;/proc&#34;</span>, <span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#ae81ff">0</span>, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-proc&#34;</span>);
	<span style="color:#75715e">// 创建一个 System V 消息队列
</span><span style="color:#75715e"></span>	create_msg_queue();
	printf(<span style="color:#e6db74">&#34;=== new ipc namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
	perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
	exit(EXIT_FAILURE);
}

pid_t <span style="color:#a6e22e">old_namespace_exec</span>()
{
	pid_t p <span style="color:#f92672">=</span> fork();
	<span style="color:#66d9ef">if</span> (p <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
	{
		printf(<span style="color:#e6db74">&#34;=== old namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
		execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
		perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
		exit(EXIT_FAILURE);
	}
	<span style="color:#66d9ef">return</span> p;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
	<span style="color:#75715e">// 为子进程提供申请函数栈
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
							 PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
							 MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
							 <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
		errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 IPC Namespace（CLONE_NEWIPC），并执行 new_namespace_func 函数
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// clone 库函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 		  /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 为了测试方便，同时创建 Mount Namespace 和 PID Namespace
</span><span style="color:#75715e"></span>	pid_t p1 <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWNS <span style="color:#f92672">|</span> CLONE_NEWIPC <span style="color:#f92672">|</span> CLONE_NEWPID, NULL);
	<span style="color:#66d9ef">if</span> (p1 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone&#34;</span>);
	sleep(<span style="color:#ae81ff">5</span>);
	<span style="color:#75715e">// 创建新的进程（不创建 Namespace），并执行测试命令
</span><span style="color:#75715e"></span>	pid_t p2 <span style="color:#f92672">=</span> old_namespace_exec();
	<span style="color:#66d9ef">if</span> (p2 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;fork&#34;</span>);
	waitpid(p1, NULL, <span style="color:#ae81ff">0</span>);
	waitpid(p2, NULL, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<h4 id="go-语言描述">Go 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run ./src/go/01-namespace/03-ipc/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>

	<span style="color:#a6e22e">script</span> = <span style="color:#e6db74">&#34;ipcs -q&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#a6e22e">tip</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">tip</span>)
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-cx&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 IPC Namespace（syscall.CLONE_NEWIPC）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 为了测试方便，同时创建 Mount Namespace 和 PID Namespace
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWNS</span> | <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWPID</span> | <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWIPC</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">createMsgQueue</span>() {
	<span style="color:#75715e">// key_t k = ftok(&#34;/system_v_msg_queue_test_1&#34;, 1);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int msgid = msgget(k, IPC_CREAT);
</span><span style="color:#75715e"></span>
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;ipcmk -Q&#34;</span>).<span style="color:#a6e22e">Run</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_SLAVE</span>|<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_REC</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 挂载当前 PID Namespace 的 proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount -t proc proc /proc
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man8/mount.8.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#e6db74">&#34;/proc&#34;</span>, <span style="color:#e6db74">&#34;proc&#34;</span>, <span style="color:#ae81ff">0</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 创建一个 System V 消息队列
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">createMsgQueue</span>()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== new ipc namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">oldNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== old namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 IPC Namespace 的进程
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccess</span>()
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
		<span style="color:#75715e">// 3. 创建新的进程（不创建 Namespace），并执行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">oldNamespaceProccess</span>()
		<span style="color:#a6e22e">err1</span>, <span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r1</span>, <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r2</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err1</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err2</span>)
		}
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">newNamespaceProccessFunc</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div>
<h4 id="shell-描述">Shell 描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># sudo ./src/shell/01-namespace/03-ipc/main.sh</span>

script<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ipcs -q&#34;</span>

<span style="color:#75715e"># unshare -m -i -p 创建新进程，并为该进程创建一个 ipc Namespace（-i）</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html</span>
<span style="color:#75715e"># 为了测试方便，同时创建 Mount Namespace (-m) 和 PID Namespace (-p)</span>

<span style="color:#75715e"># 注意 unshare 会自动取消进程的所有共享，因此不需要手动执行：mount --make-rprivate /</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html 的 --propagation 参数说明</span>

<span style="color:#75715e"># mount -t proc proc /proc 挂载 proc 文件系统，等价于 mount(&#34;proc&#34;, &#34;/proc&#34;, &#34;proc&#34;, 0, NULL) 系统调用</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man8/mount.8.html</span>

<span style="color:#75715e"># ipcmk -Q 创建一个 System V 消息队列</span>

<span style="color:#75715e"># 注意：bash 的最后一条命令将不会 fork 进程，所以在最后补充一个 sleep ，让命令在新的进程执行！</span>
<span style="color:#75715e"># https://unix.stackexchange.com/questions/466496/why-is-there-no-apparent-clone-or-fork-in-simple-bash-command-and-how-its-done</span>
unshare -m -i -p /bin/bash -c <span style="color:#e6db74">&#34;/bin/bash -c &#39;mount -t proc proc /proc \
</span><span style="color:#e6db74">	&amp;&amp; ipcmk -Q \
</span><span style="color:#e6db74">	&amp;&amp; echo \&#34;=== new ipc namespace process ===\&#34; &amp;&amp; set -x &amp;&amp; </span>$script<span style="color:#e6db74">&#39; &amp;&amp; sleep 10&#34;</span> &amp;
pid1<span style="color:#f92672">=</span>$!

sleep <span style="color:#ae81ff">5</span>
<span style="color:#75715e"># 创建新的进程（不创建 Namespace），并执行测试命令</span>
/bin/bash -c <span style="color:#e6db74">&#34;echo &#39;=== old namespace process ===&#39; &amp;&amp; set -x &amp;&amp; </span>$script<span style="color:#e6db74">&#34;</span> &amp;
pid2<span style="color:#f92672">=</span>$!

wait $pid1
wait $pid2</code></pre></div>
<h3 id="输出及分析">输出及分析</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== new ipc namespace process ===
+ ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x8a0d2334 0          root       644        0            0           

=== old namespace process ===
+ ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    </pre></div>
<p>可以看出 new ipc namespace 中创建的 System V 消息队列，在初始 Namespace 中并不存在。</p>
]]></description></item><item><title>【持续更新】网络代理</title><link>https://www.rectcircle.cn/posts/net-proxy/</link><pubDate>Mon, 14 Mar 2022 14:05:59 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/net-proxy/</guid><description type="html"><![CDATA[

<h2 id="http-tunnel">HTTP Tunnel</h2>

<blockquote>
<p>Wiki: <a href="https://zh.wikipedia.org/wiki/HTTP%E9%9A%A7%E9%81%93">HTTP隧道</a>，又称 “HTTP 正向代理”。</p>
</blockquote>

<h3 id="原理">原理</h3>

<blockquote>
<p>参见：<a href="https://imququ.com/post/web-proxy.html">博客</a></p>
</blockquote>

<p>利用 HTTP 协议标准的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/CONNECT"><code>CONNECT</code> 方法</a>实现的。</p>

<p><img src="/image/http_tunnel.png.webp" alt="image" /></p>

<h3 id="特性">特性</h3>

<ul>
<li>同时支持 HTTP / HTTPS</li>
<li>支持多级代理</li>
<li>基于 HTTP 协议原生支持</li>
<li>需要客户端显示的配置</li>
<li>只适用于临时局部的代理，不推荐用于全局代理（即通过 <code>http_proxy</code> 全局变量的形式）</li>
</ul>

<h3 id="客户端">客户端</h3>

<ul>
<li>Unix 命令行：通过 <code>http_proxy</code>、<code>https_proxy</code>、<code>no_proxy</code> 环境变量设置（全大写也可以）。</li>
<li>操作系统：网络 -&gt; 代理配置进行配置</li>
<li>软件层面：设置 -&gt; HTTP Proxy</li>
<li>编程语言 HTTP Client：搜索 <code>XXX HTTP Client Proxy</code>，<code>XXX</code> 为 编程语言名（一般情况下会默认识别 Unix 命令行的环境变量）</li>
</ul>

<p>注意：<code>http_proxy</code>、<code>https_proxy</code>、<code>no_proxy</code> 环境变量一种松散的业界约定俗称，并没有统一强制的规范，所以极度不统一，并且不是所有命令行工具和各个编程语言的客户端都支持的。下面表述一些工具和库的情况：</p>

<ul>
<li>Linux 下的 wget 和 curl 支持后文描述的所有格式：

<ul>
<li><a href="https://www.gnu.org/software/wget/manual/html_node/Proxies.html">manual: wget Proxies</a></li>
<li><a href="https://curl.se/docs/manpage.html">maual: curl Environment</a></li>
</ul></li>
<li><code>http_proxy</code> 该环境变量的值，其兼容性下：

<ul>
<li><code>http://proxyHost:proxyPort</code> 一般所有的工具/库都支持</li>
<li><code>proxyHost:proxyPort</code> 一般所有工具/库都支持</li>
<li><code>http://user:password@proxyHost:proxyPort</code> 很多工具/库不支持</li>
</ul></li>
<li><code>https_proxy</code> 该环境变量的情况和 <code>http_proxy</code> 类似即可以配置为 <code>http</code> 协议的代理服务，但是需要注意的是：

<ul>
<li><code>http://proxyHost:proxyPort</code> 形式，很多 nodejs 的 http 客户端默认不支持，因为在 nodejs 社区认为 https over http tunnel 不安全，如果希望不报错，需要手动配置 proxy，并指定 <code>strictSSL</code> 为 false 才允许。</li>
<li><code>https://proxyHost:proxyPort</code> 这要求代理服务器支持 https，不太清楚其兼容性，。</li>
</ul></li>
<li><code>no_proxy</code> 该环境变量的值，一般情况下是用逗号分隔的host列表，其兼容性如下：

<ul>
<li>部分工具/库直接不支持识别该环境变量</li>
<li><code>example.com</code> 一般支持 <code>no_proxy</code> 环境变量的工具/库都支持，但是作者理解可能不一样：

<ul>
<li><a href="https://github.com/axios/axios">nodejs axios</a>、curl、wget 认为：严格匹配，即 <code>abc.example.com</code> 匹配不上，所以仍然进行代理。</li>
<li><a href="https://github.com/request/request">nodejs request</a> 认为：匹配所有子域，即 <code>abc.example.com</code> 会匹配上，不进行所以不进行代理。</li>
</ul></li>
<li><code>.example.com</code> 只有部分库支持：

<ul>
<li><a href="https://github.com/request/request">nodejs axios</a>、curl、wget 认为：匹配所有子域，即 <code>abc.example.com</code> 会匹配上，不进行所以不进行代理。</li>
<li><a href="https://github.com/axios/axios">nodejs request</a> 不支持该语法，且，只要存在该形式，其认为 <code>no_proxy</code> 非法，这个 <code>no_proxy</code> 都不生效。</li>
</ul></li>
<li><code>1.2.3.4</code> IP 地址，多数库工具支持。</li>
<li><code>10.0.0.0/24</code> 网段格式，部分工具/库支持。</li>
</ul></li>
</ul>

<h3 id="服务端">服务端</h3>

<h4 id="tinyproxy">tinyproxy</h4>

<blockquote>
<p>官网：<a href="https://tinyproxy.github.io/">tinyproxy</a></p>
</blockquote>

<h5 id="编译安装">编译安装</h5>

<blockquote>
<p>参考：<a href="https://www.yuncongz.com/archives/6.html">博客</a></p>
</blockquote>

<p>（推荐）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 下载源码</span>
<span style="color:#75715e"># 前往 https://github.com/tinyproxy/tinyproxy/releases 获取连接</span>
wget https://github.com/tinyproxy/tinyproxy/releases/download/1.11.0/tinyproxy-1.11.0.tar.gz -O tinyproxy.tar.gz
tar -xzvf tinyproxy.tar.gz
cd tinyproxy-*
<span style="color:#75715e"># 安装编译依赖</span>
yum install -y gcc
<span style="color:#75715e"># 编译安装</span>
./configure
make
make install</code></pre></div>
<p>配置开启自动启动 <code>vim /usr/local/etc/tinyproxy/tinyproxy.conf</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#75715e"># 将下面直接注释掉，允许所有ip访问</span>
<span style="color:#75715e">#Allow 127.0.0.1</span>
<span style="color:#75715e">#Allow ::1</span>

<span style="color:#75715e"># 设置用户名密码</span>
<span style="color:#a6e22e">BasicAuth username password</span>

<span style="color:#75715e"># 顺便将下面两行取消注释，后面有用到</span>
<span style="color:#a6e22e">PidFile &#34;/usr/local/var/run/tinyproxy/tinyproxy.pid&#34;</span>
<span style="color:#a6e22e">LogFile &#34;/usr/local/var/log/tinyproxy/tinyproxy.log&#34;</span></code></pre></div>
<p>创建 service 文件 <code>vi /usr/lib/systemd/system/tinyproxy.service</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-service" data-lang="service"><span style="color:#66d9ef">[Unit]</span>
<span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">Startup script for the tinyproxy server</span>
<span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.target</span>
 
<span style="color:#66d9ef">[Service]</span>
<span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">forking</span>
<span style="color:#a6e22e">PIDFile</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/var/run/tinyproxy/tinyproxy.pid</span>
<span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/tinyproxy -c /usr/local/etc/tinyproxy/tinyproxy.conf</span>
<span style="color:#a6e22e">ExecReload</span><span style="color:#f92672">=</span><span style="color:#e6db74">/bin/kill -HUP $MAINPID</span>
<span style="color:#a6e22e">KillMode</span><span style="color:#f92672">=</span><span style="color:#e6db74">process</span>
 
<span style="color:#66d9ef">[Install]</span>
<span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span></code></pre></div>
<p>准备数据目录</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p /usr/local/var/run/tinyproxy
mkdir -p /usr/local/var/log/tinyproxy
touch /usr/local/var/log/tinyproxy/tinyproxy.log
touch /usr/local/var/run/tinyproxy/tinyproxy.pid</code></pre></div>
<p>配置开机自启</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">systemctl enable tinyproxy
systemctl start tinyproxy
systemctl status tinyproxy</code></pre></div>
<h5 id="包管理器安装">包管理器安装</h5>

<p>（不推荐，版本可能过旧）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># CentOS / EPEL / Fedora</span>
yum install tinyproxy
<span style="color:#75715e"># Debian / Ubuntu</span>
apt-get install tinyproxy 
<span style="color:#75715e"># openSUSE</span>
zypper in tinyproxy
<span style="color:#75715e"># Arch</span>
pacman -S tinyproxy
<span style="color:#75715e"># Mac</span>
brew install tinyproxy</code></pre></div>
<h5 id="配置项">配置项</h5>

<p><code>vim /etc/tinyproxy/tinyproxy.conf</code> （编译安装路径为 <code>/usr/local/etc/tinyproxy/tinyproxy.conf</code>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-conf" data-lang="conf"># User/Group: 设置 tinyproxy 进程的用户名和组名，可以是 UID 或 GID 号
User nobody
Group nobody

# Port: 绑定的端口。如果端口小于 1024，则需要使用 root tinyproxy
Port 8888

# Listen: 监听的网卡设备，默认为所有，即 0.0.0.0
#Listen 192.168.0.1

# Bind: This allows you to specify which interface will be used for
# outgoing connections.  This is useful for multi-home&#39;d machines where
# you want all traffic to appear outgoing from one particular interface.
#Bind 192.168.0.1

# BindSame: If enabled, tinyproxy will bind the outgoing connection to the
# ip address of the incoming connection.
#BindSame yes

# Timeout: 一个连接在被 tinyproxy 关闭之前允许处于不活动状态的最大秒数。
Timeout 600

# ErrorFile: 定义发生给定 HTTP 错误时要发送的 HTML 文件。 您可能需要为您的特定安装自定义位置。 通常要检查的位置是：
#   /usr/local/share/tinyproxy
#   /usr/share/tinyproxy
#   /etc/tinyproxy
#ErrorFile 404 &#34;/usr/local/share/tinyproxy/404.html&#34;
#ErrorFile 400 &#34;/usr/local/share/tinyproxy/400.html&#34;
#ErrorFile 503 &#34;/usr/local/share/tinyproxy/503.html&#34;
#ErrorFile 403 &#34;/usr/local/share/tinyproxy/403.html&#34;
#ErrorFile 408 &#34;/usr/local/share/tinyproxy/408.html&#34;

# DefaultErrorFile: 如果没有使用 ErrorFile 关键字为发生的 HTTP 错误定义 HTML 文件，则发送的 HTML 文件。
DefaultErrorFile &#34;/usr/local/share/tinyproxy/default.html&#34;

# StatHost: 这将配置被视为统计主机的主机名或 IP 地址：每当收到对该主机的请求时，Tinyproxy 将返回一个内部统计页面，而不是将请求转发到该主机。 StatHost 的默认值为 tinyproxy.stats。
# 通过在 /etc/hosts 添加配置在浏览器中直接访问
# 代理IP地址 tinyproxy.stats
#StatHost &#34;tinyproxy.stats&#34;


# StatFile：向 stathost 发出请求时发送的 HTML 文件。 如果此文件不存在，则在 tinyproxy 中硬编码基本页面。
StatFile &#34;/usr/local/share/tinyproxy/stats.html&#34;

# LogFile：允许您指定应记录信息的位置。 如果您希望登录到 syslog，请禁用此功能并启用 Syslog 指令。 这些指令是相互排斥的。 如果既没有指定 Syslog 也没有指定 LogFile，则输出到 stdout。
LogFile &#34;/usr/local/var/log/tinyproxy/tinyproxy.log&#34;

# Syslog: 告诉 tinyproxy 使用 syslog 而不是日志文件。 如果正在使用 Logfile 指令，则不得启用此选项。 这两个指令是互斥的。
#Syslog On

# LogLevel: Warning
# Set the logging level. Allowed settings are:
#       Critical        (least verbose)
#       Error
#       Warning
#       Notice
#       Connect         (to log connections without Info&#39;s noise)
#       Info            (most verbose)
# LogLevel 从设置级别及更高级别记录。 例如，如果将 LogLevel 设置为 Warning，则将输出从 Warning 到 Critical 的所有日志消息，但会抑制 Notice 及以下的消息。
LogLevel Info

# PidFile: 将主要 tinyproxy 线程的 PID 写入此文件，以便将其用于信号目的。 如果未指定，则不会写入 pidfile。
PidFile &#34;/usr/local/var/run/tinyproxy/tinyproxy.pid&#34;

# XTinyproxy: 告诉 Tinyproxy 包含 X-Tinyproxy 标头，其中包含客户端的 IP 地址。
#XTinyproxy Yes

# Upstream:
#
# 打开上游代理支持。
#
# 上游规则允许您根据正在访问的站点的主机/域有选择地路由上游连接。
#
# 语法: 上游类型 (user:pass@)ip:port (&#34;domain&#34;)
# 或:     upstream none &#34;domain&#34;
# 括号中的部分是可选的。
# 可能的上游类型类型有 http、socks4、socks5、none
#
# 举例:
#  # 与测试域的连接通过 testproxy
#  upstream http testproxy:8008 &#34;.test.domain.invalid&#34;
#  upstream http testproxy:8008 &#34;.our_testbed.example.com&#34;
#  upstream http testproxy:8008 &#34;192.168.128.0/255.255.254.0&#34;
#
#  # 使用基本身份验证的上游代理
#  upstream http user:pass@testproxy:8008 &#34;.test.domain.invalid&#34;
#
#  # 内部网站和不合格主机没有上游代理
#  upstream none &#34;.internal.example.com&#34;
#  upstream none &#34;www.example.com&#34;
#  upstream none &#34;10.0.0.0/8&#34;
#  upstream none &#34;192.168.0.0/255.255.254.0&#34;
#  upstream none &#34;.&#34;
#
#  # connection to these boxes go through their DMZ firewalls
#  upstream http cust1_firewall:8008 &#34;testbed_for_cust1&#34;
#  upstream http cust2_firewall:8008 &#34;testbed_for_cust2&#34;
#
#  # default upstream is internet firewall
#  upstream http firewall.internal.example.com:80
#
# 您也可以使用 SOCKS4/SOCKS5 上游代理：
#  upstream socks4 127.0.0.1:9050
#  upstream socks5 socksproxy:1080
#
# The LAST matching rule wins the route decision.  As you can see, you
# can use a host, or a domain:
#  name     matches host exactly
#  .name    matches any host in domain &#34;name&#34;
#  .        matches any host with no domain (in &#39;empty&#39; domain)
#  IP/bits  matches network/mask
#  IP/mask  matches network/mask
#
#Upstream http some.remote.proxy:port

#
# MaxClients：这是将创建的绝对最大线程数。 也就是说，只能同时连接MaxClients个客户端。
#
MaxClients 100

#
# Allow: 匹配允许哪些客户端连接，如果存在一条 Allow，则不匹配的将直接拒绝。
#
# 控件的顺序很重要。 所有传入的连接都根据基于顺序的控件进行测试。
#
#Allow 127.0.0.1
#Allow ::1

# BasicAuth：用于访问代理的 HTTP &#34;Basic Authentication&#34;。 如果指定了任何条目，则仅向经过身份验证的用户授予访问权限。
BasicAuth user password

#
# AddHeader: 将指定的标头添加到 Tinyproxy 发出的传出 HTTP 请求。 请注意，此选项不适用于 HTTPS 流量，因为 Tinyproxy 无法控制交换的标头。
#
#AddHeader &#34;X-My-Header&#34; &#34;Powered by Tinyproxy&#34;

#
# ViaProxyName: HTTP RFC 需要 Via 标头，但使用真实主机名是一个安全问题。 如果启用了以下指令，则提供的字符串将用作 Via 标头中的主机名； 否则，将使用服务器的主机名。
#
ViaProxyName &#34;tinyproxy&#34;

#
# DisableViaHeader: 当设置为 yes 时，Tinyproxy 不会将 Via 标头添加到请求中。 这实际上将 Tinyproxy 置于隐身模式。 请注意，RFC 2616 要求代理设置 Via 标头，因此启用此选项会破坏合规性。 除非您知道自己在做什么，否则不要禁用 Via 标头...
#
#DisableViaHeader Yes

#
# Filter: 这允许您指定过滤器文件的位置。
# 下面是 filter 文件的示例
# # filter exactly cnn.com
# ^cnn\.com$
# 
# # filter all subdomains of cnn.com, but not cnn.com itself
# .*\.cnn.com$
# 
# # filter any domain that has cnn.com in it, like xcnn.comfy.org
# cnn\.com
# 
# # filter any domain that ends in cnn.com
# cnn\.com$
# 
# # filter any domain that starts with adserver
# ^adserver
#Filter &#34;/usr/local/etc/tinyproxy/filter&#34;


# FilterURLs: 将连接的 URL 而不是默认的 domain 进程匹配。
#FilterURLs On

# FilterExtended: 使用 POSIX 扩展的正则表达式而不是 Basic 正则。
#FilterExtended On


# FilterCaseSensitive: 使用区分大小写的正则表达式。
#FilterCaseSensitive On

# FilterDefaultDeny: 更改过滤系统的默认策略。 如果此指令被注释掉，或者设置为 No，则表示只禁用 Filter 文件匹配到的连接，未匹配到的允许连接。
# 如果配置为 Yes，则表示只允许 Filter 文件匹配到的连接，未匹配的将禁用。
# 也就是说： Yes 是白名单， No (默认) 是黑名单。
#FilterDefaultDeny Yes

# Anonymous: 如果存在 Anonymous 关键字，则启用匿名代理。The headers listed with `Anonymous` are allowed through, while all others are denied. 如果不存在 Anonymous 关键字，则允许所有标头通过。 您必须在 Header 周围加上引号。
#
# 大多数网站都需要启用 Cookie 才能正常工作，因此如果您访问这些网站，则需要允许 Cookies 通过。
#
#Anonymous &#34;Host&#34;
#Anonymous &#34;Authorization&#34;
#Anonymous &#34;Cookie&#34;

#
# ConnectPort: 这是使用 CONNECT 方法时 tinyproxy 允许的端口列表。 要完全禁用 CONNECT 方法，请将值设置为 0。如果未找到 ConnectPort 行，则允许所有端口。
#
# SSL 使用以下两个端口。
#
#ConnectPort 443
#ConnectPort 563

#
# 配置一个或多个 ReversePath 指令以启用反向代理支持。 使用反向代理可以使多个站点看起来好像它们是单个站点的一部分。
#
# 如果您取消注释以下两个指令并在您自己的计算机上的端口 8888 上运行 tinyproxy，您可以使用 http://localhost:8888/google/ 访问 Google 和使用 http://localhost:8888/wired/news/ 访问有线新闻。 除非您取消注释 ReverseMagic，否则它们都不会真正起作用，因为它们使用绝对链接。
#
#ReversePath &#34;/google/&#34; &#34;http://www.google.com/&#34;
#ReversePath &#34;/wired/&#34;  &#34;http://www.wired.com/&#34;

#
# 当使用 tinyproxy 作为反向代理时，强烈建议通过取消注释下一个指令来关闭普通代理。
#
#ReverseOnly Yes

# 使用 cookie 跟踪反向代理映射。 如果您需要反向代理具有绝对链接的站点，您必须取消注释。
# 这里的意思是通过 cookie 来区分反向代理，需要配合 ReverseBaseURL 使用。
#ReverseMagic Yes

# 用于访问此反向代理的 URL。 该 URL 用于重写 HTTP 重定向，以便它们不会转义代理。 如果您有反向代理链，则需要将最外层的 URL 放在这里（最终用户在他/她的浏览器中键入的地址）。
# 如果未设置，则不会发生重 rewrite 302。
#ReverseBaseURL &#34;http://localhost:8888/&#34;</code></pre></div>
<h5 id="测试">测试</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 观察出网 IP</span>
curl -x http://username:password@ip:port https://ifconfig.me</code></pre></div>
<h2 id="http-s-by-mitm">HTTP(s) by MITM</h2>

<blockquote>
<p>Wiki：<a href="https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB">MITM 中文</a> | <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">MITM 英文</a>，又称 “HTTP 透明代理”，属于 “反向代理”。</p>
</blockquote>

<h3 id="原理-1">原理</h3>

<p>代理服务器工作，从 TCP 流中获取 HTTP(s) 协议的目标 Host 信息，将流量转发到目标地址：</p>

<ul>
<li>HTTP 协议，从 Header 中获取目标 Host</li>
<li>HTTPS 协议，从 <a href="https://zh.wikipedia.org/zh-hans/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E6%8C%87%E7%A4%BA">SNI</a> 中获取目标 Host</li>
</ul>

<h3 id="特性-1">特性</h3>

<ul>
<li>利用中间人攻击的原理实现的代理</li>
<li>同时支持 HTTP / HTTPS (未启用 <a href="https://zh.wikipedia.org/wiki/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E6%8C%87%E7%A4%BA#%E5%8A%A0%E5%AF%86%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E6%8C%87%E7%A4%BA">ESNI/ECH</a> 情况)</li>
<li>客户端需要配置自定义 DNS 或者 iptable 将流量打到代理服务器</li>
<li>服务端看到的 Client IP 是代理服务器的 IP</li>
</ul>

<h3 id="客户端-1">客户端</h3>

<p>自定义 DNS (如 <a href="https://thekelleys.org.uk/dnsmasq/doc.html">dnsmasq</a>)。这里简单通过 <code>/etc/hosts</code> 进行配置，假设我们要代理 <code>ifconfig.me</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">代理服务器IP ifconfig.me</pre></div>
<h3 id="服务端-1">服务端</h3>

<h4 id="nginx">Nginx</h4>

<blockquote>
<p>参考：<a href="https://blog.mmf.moe/post/nginx-dnsmasq-sni-proxy/">博客</a></p>
</blockquote>

<h5 id="包管理器安装-1">包管理器安装</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># CentOS / EPEL / Fedora</span>
yum install nginx
<span style="color:#75715e"># Debian / Ubuntu</span>
apt-get install nginx</code></pre></div>
<h5 id="配置">配置</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">stream</span> {
  <span style="color:#f92672">server</span> {
    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span>;
    <span style="color:#f92672">ssl_preread</span> <span style="color:#66d9ef">on</span>;
    <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
    <span style="color:#f92672">proxy_pass</span> $ssl_preread_server_name:$server_port;
  }
}

<span style="color:#66d9ef">http</span> {

  <span style="color:#75715e"># include /etc/nginx/conf.d/*.conf;
</span><span style="color:#75715e"></span>
  <span style="color:#f92672">server</span> {
    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span> <span style="color:#e6db74">default_server</span>;
    <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
      <span style="color:#f92672">proxy_pass</span> $scheme://$host$request_uri;
      <span style="color:#75715e"># 以下两个都不需要，做的不是透明代理
</span><span style="color:#75715e"></span>      <span style="color:#75715e"># proxy_bind $remote_addr transparent;
</span><span style="color:#75715e"></span>      <span style="color:#75715e"># proxy_set_header Host $host;
</span><span style="color:#75715e"></span>    }
  }
}</code></pre></div>
<p>应用配置</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nginx -s reload</code></pre></div>
<h5 id="docker-方式">Docker 方式</h5>

<p>docker 配置 <code>nginx.conf</code> 文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">user</span>  <span style="color:#e6db74">nginx</span>;
<span style="color:#66d9ef">worker_processes</span>  <span style="color:#e6db74">auto</span>;

<span style="color:#66d9ef">error_log</span>  <span style="color:#e6db74">/var/log/nginx/error.log</span> <span style="color:#e6db74">notice</span>;
<span style="color:#66d9ef">pid</span>        <span style="color:#e6db74">/var/run/nginx.pid</span>;


<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span>  <span style="color:#ae81ff">1024</span>;
}


<span style="color:#66d9ef">stream</span> {
    <span style="color:#f92672">log_format</span> <span style="color:#e6db74">main</span> <span style="color:#e6db74">&#39;</span>$remote_addr  <span style="color:#e6db74">[</span>$time_local] $ssl_preread_server_name <span style="color:#e6db74">&#39;</span>
                     <span style="color:#e6db74">&#39;</span>$protocol $status $bytes_sent $bytes_received <span style="color:#e6db74">&#39;</span>
                     <span style="color:#e6db74">&#39;</span>$session_time&#39;;
    <span style="color:#f92672">access_log</span>  <span style="color:#e6db74">/var/log/nginx/access.log</span>  <span style="color:#e6db74">main</span>;

    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span>;
        <span style="color:#f92672">ssl_preread</span> <span style="color:#66d9ef">on</span>;
        <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
        <span style="color:#f92672">proxy_pass</span> $ssl_preread_server_name:$server_port;
    }
}


<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">include</span>       <span style="color:#e6db74">/etc/nginx/mime.types</span>;
    <span style="color:#f92672">default_type</span>  <span style="color:#e6db74">application/octet-stream</span>;

    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;

    <span style="color:#f92672">access_log</span>  <span style="color:#e6db74">/var/log/nginx/access.log</span>  <span style="color:#e6db74">main</span>;

    <span style="color:#f92672">sendfile</span>        <span style="color:#66d9ef">on</span>;
    <span style="color:#75715e">#tcp_nopush     on;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">keepalive_timeout</span>  <span style="color:#ae81ff">65</span>;

    <span style="color:#75715e">#gzip  on;
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">#include /etc/nginx/conf.d/*.conf;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span> <span style="color:#e6db74">default_server</span>;
        <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">proxy_pass</span> $scheme://$host$request_uri;
            <span style="color:#75715e"># 以下两个都不需要，做的不是透明代理
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># proxy_bind $remote_addr transparent;
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># proxy_set_header Host $host;
</span><span style="color:#75715e"></span>        }
    }

}</code></pre></div>
<p>docker 启动命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 启动</span>
docker run --name nginx-proxy -p <span style="color:#ae81ff">80</span>:80 -p <span style="color:#ae81ff">443</span>:443 -p <span style="color:#ae81ff">563</span>:563 -v <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx <span style="color:#75715e"># 后台运行</span>
<span style="color:#75715e"># 删除停止（后台运行时）</span>
docker rm -f nginx-proxy</code></pre></div>
<h5 id="测试-1">测试</h5>

<p>前置条件：参见客户端配置 ，配置 <code>/etc/hosts</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 观察出网 IP</span>
curl -v https://ifconfig.me
curl -v http://ifconfig.me</code></pre></div>
<h5 id="配置只代理指定域名">配置只代理指定域名</h5>

<ul>
<li><a href="http://nginx.org/en/docs/stream/ngx_stream_ssl_preread_module.html">https by map</a></li>
<li>http by server_name: <a href="https://segmentfault.com/a/1190000021771733">博客</a> | <a href="http://nginx.org/en/docs/http/server_names.html">官方</a></li>
</ul>

<p>下文以只代理 google.com 为例。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-nginx" data-lang="nginx"><span style="color:#66d9ef">user</span>  <span style="color:#e6db74">nginx</span>;
<span style="color:#66d9ef">worker_processes</span>  <span style="color:#e6db74">auto</span>;

<span style="color:#66d9ef">error_log</span>  <span style="color:#e6db74">/var/log/nginx/error.log</span> <span style="color:#e6db74">notice</span>;
<span style="color:#66d9ef">pid</span>        <span style="color:#e6db74">/var/run/nginx.pid</span>;


<span style="color:#66d9ef">events</span> {
    <span style="color:#f92672">worker_connections</span>  <span style="color:#ae81ff">1024</span>;
}


<span style="color:#66d9ef">stream</span> {
    <span style="color:#f92672">log_format</span> <span style="color:#e6db74">main</span> <span style="color:#e6db74">&#39;</span>$remote_addr  <span style="color:#e6db74">[</span>$time_local] $ssl_preread_server_name <span style="color:#e6db74">&#39;</span>
                     <span style="color:#e6db74">&#39;</span>$protocol $status $bytes_sent $bytes_received <span style="color:#e6db74">&#39;</span>
                     <span style="color:#e6db74">&#39;</span>$session_time&#39;;
    <span style="color:#f92672">access_log</span>  <span style="color:#e6db74">/var/log/nginx/access.log</span>  <span style="color:#e6db74">main</span>;
    
    <span style="color:#f92672">map</span> $ssl_preread_server_name $name {
        <span style="color:#f92672">google.com</span> <span style="color:#e6db74">google.com</span>;
    }

    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span>;
        <span style="color:#f92672">ssl_preread</span> <span style="color:#66d9ef">on</span>;
        <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
        <span style="color:#f92672">proxy_pass</span> $name:$server_port;
    }
}

<span style="color:#66d9ef">http</span> {
    <span style="color:#f92672">include</span>       <span style="color:#e6db74">/etc/nginx/mime.types</span>;
    <span style="color:#f92672">default_type</span>  <span style="color:#e6db74">application/octet-stream</span>;

    <span style="color:#f92672">log_format</span>  <span style="color:#e6db74">main</span>  <span style="color:#e6db74">&#39;</span>$remote_addr <span style="color:#e6db74">-</span> $remote_user <span style="color:#e6db74">[</span>$time_local] <span style="color:#e6db74">&#34;</span>$request&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;</span>$status $body_bytes_sent <span style="color:#e6db74">&#34;</span>$http_referer&#34; <span style="color:#e6db74">&#39;</span>
                      <span style="color:#e6db74">&#39;&#34;</span>$http_user_agent&#34; <span style="color:#e6db74">&#34;</span>$http_x_forwarded_for&#34;&#39;;

    <span style="color:#f92672">access_log</span>  <span style="color:#e6db74">/var/log/nginx/access.log</span>  <span style="color:#e6db74">main</span>;

    <span style="color:#f92672">sendfile</span>        <span style="color:#66d9ef">on</span>;
    <span style="color:#75715e">#tcp_nopush     on;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">keepalive_timeout</span>  <span style="color:#ae81ff">65</span>;

    <span style="color:#75715e">#gzip  on;
</span><span style="color:#75715e"></span>
    <span style="color:#75715e">#include /etc/nginx/conf.d/*.conf;
</span><span style="color:#75715e"></span>
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
        <span style="color:#f92672">resolver</span> <span style="color:#ae81ff">8</span><span style="color:#e6db74">.8.8.8</span>;
        
        <span style="color:#f92672">server_name</span> <span style="color:#e6db74">google.com</span>;
        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">proxy_pass</span> $scheme://$host$request_uri;
            <span style="color:#75715e"># 以下两个都不需要，做的不是透明代理
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># proxy_bind $remote_addr transparent;
</span><span style="color:#75715e"></span>            <span style="color:#75715e"># proxy_set_header Host $host;
</span><span style="color:#75715e"></span>        }
    }
    
    <span style="color:#f92672">server</span> {
        <span style="color:#f92672">listen</span>       <span style="color:#ae81ff">80</span> <span style="color:#e6db74">default_server</span>;    

        <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
            <span style="color:#f92672">return</span> <span style="color:#ae81ff">404</span>;
            <span style="color:#f92672">root</span>   <span style="color:#e6db74">/usr/share/nginx/html</span>;
            <span style="color:#f92672">index</span>  <span style="color:#e6db74">index.html</span> <span style="color:#e6db74">index.htm</span>;
        }

        <span style="color:#f92672">error_page</span>   <span style="color:#ae81ff">500</span> <span style="color:#ae81ff">502</span> <span style="color:#ae81ff">503</span> <span style="color:#ae81ff">504</span>  <span style="color:#e6db74">/50x.html</span>;
        <span style="color:#f92672">location</span> = <span style="color:#e6db74">/50x.html</span> {
            <span style="color:#f92672">root</span>   <span style="color:#e6db74">/usr/share/nginx/html</span>;
        }
    }
}</code></pre></div>]]></description></item><item><title>Linux IPC</title><link>https://www.rectcircle.cn/posts/linux-ipc/</link><pubDate>Sun, 13 Mar 2022 01:28:48 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-ipc/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>IPC（Inter-Process Communication，进程间通讯），有多种。</p>

<p>系统学习：<a href="https://www.cnblogs.com/philip-tell-truth/p/6284475.html">https://www.cnblogs.com/philip-tell-truth/p/6284475.html</a></p>

<h2 id="管道-pipe">管道 (pipe)</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/pipe.7.html"><code>pipe(7) 文档</code></a> | <a href="https://man7.org/linux/man-pages/man2/pipe.2.html"><code>pipe(2) 系统调用</code></a></p>
</blockquote>

<p>管道是 Unix 系统 最古老的一种 IPC 机制。该机制的特点是：</p>

<ul>
<li>通讯机制是半双工的，即 Pipe 的两个端点一个只能写入，另一个只能读取。</li>
<li>管道只能在具有公共祖先的进程间通讯，通常，一个管道由一个进程创建，在进程调用 fork 后，这个管道在父子进程通讯中使用。</li>
</ul>

<p>在 Linux 中管道又称匿名管道，而 FIFO 被称为命名管道，本部分介绍的是匿名管道。</p>

<p>Linux 中 pipe 的函数声明为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> pipe (<span style="color:#66d9ef">int</span> fd[<span style="color:#ae81ff">2</span>])
</code></pre></div>
<ul>
<li>参数 <code>fd</code> 是一个长度为 2 的数组，用来存放管道两个端点的文件描述符。

<ul>
<li><code>fd[0]</code> 为读取端点的文件描述符</li>
<li><code>fd[1]</code> 为写入端点的文件描述符</li>
</ul></li>
<li>返回值 <code>-1</code> 表示出错</li>
</ul>

<p>实例：一个进程创建一个管道由，在进程调用 fork 后，父进程发送 <code>hello world\n</code> 字符串，子进程读取。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/03-ipc/01-pipe.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For pipe(2), STDOUT_FILENO</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;limits.h&gt;    // For PIPE_BUF</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;   // For EXIT_FAILURE, exit</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;    // For perror</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;    // For strlen(3)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">void</span> <span style="color:#a6e22e">main</span>()
{
    <span style="color:#66d9ef">int</span> n;
    <span style="color:#66d9ef">int</span> fd[<span style="color:#ae81ff">2</span>];
    pid_t pid;
    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hello world</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>;
    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">int</span> MAXLINE <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span>;
    <span style="color:#66d9ef">char</span> line[MAXLINE];

    <span style="color:#66d9ef">if</span> (pipe(fd) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        errExit(<span style="color:#e6db74">&#34;pipe&#34;</span>);

    <span style="color:#66d9ef">if</span> ((pid <span style="color:#f92672">=</span> fork())<span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)
        errExit(<span style="color:#e6db74">&#34;fork&#34;</span>);

    <span style="color:#66d9ef">if</span> (pid <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) <span style="color:#75715e">// 父进程
</span><span style="color:#75715e"></span>    {
        close(fd[<span style="color:#ae81ff">0</span>]); <span style="color:#75715e">// 父进程不需要使用管道的读取端点，所以关闭它
</span><span style="color:#75715e"></span>        write(fd[<span style="color:#ae81ff">1</span>], msg, strlen(msg));
        wait(NULL);
    }
    <span style="color:#66d9ef">else</span> <span style="color:#75715e">// 子进程
</span><span style="color:#75715e"></span>    {
        close(fd[<span style="color:#ae81ff">1</span>]); <span style="color:#75715e">// 子进程不需要使用管道的写入端点，所以关闭它
</span><span style="color:#75715e"></span>        n <span style="color:#f92672">=</span> read(fd[<span style="color:#ae81ff">0</span>], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }
}
</code></pre></div>
<p>输出为：<code>hello world\n</code></p>

<h3 id="popen-和-pclose">popen 和 pclose</h3>

<blockquote>
<p>手册： <a href="https://man7.org/linux/man-pages/man3/popen.3.html"><code>popen(3) 库函数</code></a></p>
</blockquote>

<p><a href="https://man7.org/linux/man-pages/man3/popen.3.html"><code>popen(3) 库函数</code></a> 封装了，父进程创建一个子进程，写入子进程标准输入 或 读取子进程标准输出的能力（读写只能二选一）。</p>

<p>函数声明为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
FILE <span style="color:#f92672">*</span><span style="color:#a6e22e">popen</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>command, <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>type);
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">pclose</span>(FILE <span style="color:#f92672">*</span>stream);
</code></pre></div>
<p><code>popen</code> 实现原理为：</p>

<ul>
<li>调用 <code>pipe(pfd)</code> 系统调用创建一个管道</li>
<li>调用 <code>fork</code> 创建一个子进程

<ul>
<li>父进程根据 <code>type</code> 决定关闭 <code>pfd[0]</code> 或 <code>pfd[1]</code>，然后调用 <code>fdopen()</code> 库函数将另一个 <code>pfd[0]</code> 或 <code>pfd[1]</code> 封装成 <code>*FILE</code> 返回</li>
<li>子进程根据 <code>type</code> 决定关闭 <code>pfd[0]</code> 或 <code>pfd[1]</code>，然后调用 <code>dup2</code> 系统调用重定向标准输入/输出到 <code>pfd[0]</code> 或 <code>pfd[1]</code>，然后调用 <code>exec</code> 使用 <code>sh -c command</code> 执行命令。</li>
</ul></li>
</ul>

<h3 id="协同进程">协同进程</h3>

<p>一种比较常见的父子进程通讯方式。即，父进程创建两个管道，分别连接到子进程的标准输入和标准输出上，从而实现通讯。</p>

<p>该方式在只有 ksh 提供支持，sh，bash，csh 并不支持。</p>

<p>实现上需要注意标准 I/O 缓冲机制问题（全缓冲）</p>

<h3 id="shell-管道">shell 管道</h3>

<p>在 shell 中 <code>command1 | command2</code> 的语法，是通过 <a href="https://man7.org/linux/man-pages/man2/pipe.2.html"><code>pipe(2) 系统调用</code></a> 和 <a href="https://man7.org/linux/man-pages/man2/dup.2.html"><code>dup2(2) 系统调用</code></a> 实现的。即：</p>

<ul>
<li>当前进程标准输入，对接上级进程的标准输出</li>
<li>当前进程标准输出，对接下级进程的标准输入</li>
</ul>

<p>实现上需要注意标准 I/O 缓冲机制问题（全缓冲）</p>

<h3 id="注意事项">注意事项</h3>

<ul>
<li>管道被设计来用于一对一单向通讯。并发使用存在如下问题：存在消息长度存在 <code>PIPE_BUF</code> 限制（4096）。如果单次写入超过该限制，则会被拆成多次发送。在多写的场景，会发生消息不连续的问题。这种场景让程序正确运行实现上比较困难，因此不建议用在在并发场景中。</li>
<li>如果写一个写端已经关闭的管道，读端将将返回 <code>0</code> 表示文件结束（不是 <code>-1</code>，<code>-1</code> 表示错误）。</li>
<li>如果写一个读端已经关闭的管道，将触发 <code>SIGPIPE</code> 信号，该信号的默认处理器为终止进程。如果处理了该信号，则写端将返回 <code>-1</code>，并且设置 <code>errno</code> 为 <code>EPIPE</code>。</li>
<li>如果没有进程打开了管道，则这个管道会被自动销毁，数据将丢弃。</li>
<li>管道的缓冲器长度为 <code>PIPE_BUF</code> （4096），如果缓冲区满了，<code>write</code> 调用将阻塞（未设置非阻塞），直到读端消费掉缓冲区数据。</li>
</ul>

<h2 id="命名管道-fifo">命名管道 (FIFO)</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/fifo.7.html"><code>fifo(7) 文档</code></a> | <a href="https://man7.org/linux/man-pages/man1/mkfifo.1.html"><code>mkfifo(1) 命令</code></a> | <a href="https://man7.org/linux/man-pages/man3/mkfifo.3.html">mkfifo(3) 库函数</a></p>
</blockquote>

<p>FIFO (first-in first-out special file, named pipe) 又称命名管道，其特点和管道 (pipe) 相比</p>

<ul>
<li>相同点是：通讯机制是半双工的，即 Pipe 的两个端点一个只能写入，另一个只能读取。</li>
<li>不点在是：FIFO 会在文件系统中创建一个类型为 <code>fifo</code> 的文件，支持任意进程打开该文件进行通讯，而管道 (pipe) 只能在具有公共祖先的进程间通讯。</li>
</ul>

<p>Linux 中 FIFO 文件创建函数声明为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/types.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/stat.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">mkfifo</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>pathname, mode_t mode);

<span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;fcntl.h&gt;           /* Definition of AT_* constants */</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/stat.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">mkfifoat</span>(<span style="color:#66d9ef">int</span> dirfd, <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>pathname, mode_t mode);
</code></pre></div>
<p>创建完成后：</p>

<ul>
<li>使用 <a href="https://man7.org/linux/man-pages/man2/lstat.2.html"><code>stat(2) 系统调用</code></a>，使用 <code>S_IFIFO</code> 判断是否是 FIFO 文件。</li>
<li>使用 <a href="https://man7.org/linux/man-pages/man2/open.2.html"><code>open(2)</code> 系统调用</a>，即可打开一个 FIFO 文件。

<ul>
<li><code>O_RDONLY</code> 打开该 FIFO 的读端点</li>
<li><code>O_WRONLY</code> 打开该 FIFO 的写断点</li>
<li><code>O_NONBLOCK</code> 是否阻塞

<ul>
<li>如果未设置 <code>O_NONBLOCK</code>，<code>O_RDONLY</code> 要阻塞到有进程使用 <code>O_WRONLY</code> 打开的时候返回。</li>
<li>如果设置了 <code>O_NONBLOCK</code>，<code>O_RDONLY</code> 要会立即返回，如果没有写入进程，则返回 -1，errno 设置为 <code>ENXIO</code>。</li>
</ul></li>
</ul></li>
<li>使用 <a href="https://man7.org/linux/man-pages/man2/write.2.html"><code>write(2) 系统调用</code></a> 写入一个 fifo 文件描述符时，类似与管道。如果没有进程打开该 fifo 文件的读断点(<code>O_RDONLY</code>)，将触发 <code>SIGPIPE</code> 信号，该信号的默认处理器为终止进程。如果处理了该信号，则写端将返回 <code>-1</code>，并且设置 <code>errno</code> 为 <code>EPIPE</code>。</li>
<li>使用 <a href="https://man7.org/linux/man-pages/man2/read.2.html"><code>read(2) 系统调用</code></a> 读取一个 fifo 文件描述符时，类似与管道。如果没有进程打开该 fifo 文件的写断点(<code>O_WRONLY</code>)，读端将将返回 <code>0</code> 表示文件结束（不是 <code>-1</code>，<code>-1</code> 表示错误）。</li>
<li>如果没有进程打开 fifo 文件，这个文件将仍然在文件系统中保留，但 FIFO 中的数据已经被删除了。</li>
<li>管道的缓冲器长度为 <code>PIPE_BUF</code> （4096），如果缓冲区满了，<code>write</code> 调用将阻塞（未设置非阻塞），直到读端消费掉缓冲区数据。</li>
</ul>

<h3 id="用途-1-在-shell-中数据流转而无需落盘">用途 1：在 shell 中数据流转而无需落盘</h3>

<ul>
<li>可以通过 <a href="https://man7.org/linux/man-pages/man1/mkfifo.1.html"><code>mkfifo(1) 系统命令</code></a> 直接在文件系统中创建一个 fifo 文件。</li>
<li>可以通过 <code>| tee fifo文件</code>，<code>&lt;fifo文件</code>，<code>&gt;fifo文件</code> 重定向和管道符读写 fifo 文件，来以有向无环图的方式串联多个标准 IO 处理程序，并不产生任何磁盘数据。</li>
</ul>

<p>如 <code>apue</code> 书上的一个例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkfifo fifo1
prog3 &lt; fifo1 &amp;
prog1 &lt; infile | tee fifo1 | prog2</code></pre></div>
<p>在这个例子中，任务 DAG 为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                                        ---(fifo1)--&gt; prog3
                                       |
infile ----(pipe)---&gt; prog1 ---(tee)---
                                       |
                                        ---(pipe)---&gt; prog2</pre></div>
<h3 id="用途-2-编写客户端-服务器模型的程序">用途 2：编写客户端/服务器模型的程序</h3>

<p>不推荐，有其他更好的方法，如 Unix Domain Socket。</p>

<h2 id="system-v-xsi-ipc">System V (XSI) IPC</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/sysvipc.7.html">sysvipc(7)</a></p>
</blockquote>

<p>这一部分不详细介绍，只简要介绍函数声明吧</p>

<p>System V (XSI) IPC 来源于 System V Unix 系统。System V IPC 饱受批评的地方是：没有使用文件系统作为标识符，而是构造了自己的命名空间。</p>

<p>System V IPC 有三主要功能：</p>

<ul>
<li>消息队列</li>
<li>信号量</li>
<li>共享内存</li>
</ul>

<p><code>apue</code> 书作者认为 System V IPC 基本上没有什么优点：</p>

<ul>
<li>最基本的问题：System V IPC 结构在系统范围内生效，且没有引用计数。以消息队列为例，也就是说，当所有进程都退出了，这个消息队列仍然存在，只有调用了 <code>ipcrm</code> 命令或者 <code>msgctl(2)</code> 系统调用才会删除。</li>
<li>另一个问题：System V IPC 在文件系统中没有名字，无法使用任何文件系统相关的系统调用或命令（ls、rm、chmod 都不行）来访问消息队列。因此在内核中添加了数十个系统调用以及 <code>ipcmk(1)</code> <code>ipcs(1)</code>、<code>ipcrm(1)</code>、<code>lsipc</code> 等命令。不复用文件描述符机制，无法使用多路复用函数（select、poll）。</li>
<li>不认为 System V IPC 作者列出的优点有说服力。</li>
<li>性能上并不比其他 IPC 机制优秀。</li>
</ul>

<h3 id="共性">共性</h3>

<p>System V IPC 可以通过 <code>key_t</code> 和 <code>id</code> 来定位一个对象。</p>

<ul>
<li><code>key_t</code> 在 Linux 中为 <code>int</code></li>

<li><p><code>id</code> 类型为 <code>int</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/ipc.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// https://man7.org/linux/man-pages/man3/ftok.3.html
</span><span style="color:#75715e">// 由文件系统路径和项目 id （只会用到低 8 位）生成一个 key_t
</span><span style="color:#75715e"></span>key_t <span style="color:#a6e22e">ftok</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>pathname, <span style="color:#66d9ef">int</span> proj_id);
</code></pre></div></li>
</ul>

<p>一般情况下，转换路径为，<code>pathname, proj_id ---(ftok)---&gt; key_t ---(msgget/semget/shmget)---&gt; id</code>，获取到 <code>id</code> 后，就可以操作 System V IPC 对象了。</p>

<h3 id="消息队列">消息队列</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/msg.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/msgget.2.html
</span><span style="color:#75715e">// 创建或获取一个 System V 消息队列。
</span><span style="color:#75715e">// return 成功返回 id，失败返回 -1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">msgget</span>(key_t key, <span style="color:#66d9ef">int</span> msgflg);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/msgctl.2.html
</span><span style="color:#75715e">// 对消息队列进行控制操作，如删除，修改元数据等
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">msgctl</span>(<span style="color:#66d9ef">int</span> msqid, <span style="color:#66d9ef">int</span> cmd, <span style="color:#66d9ef">struct</span> msqid_ds <span style="color:#f92672">*</span>buf);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/msgsnd.2.html
</span><span style="color:#75715e">// 发送消息
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">msgsnd</span>(<span style="color:#66d9ef">int</span> msqid, <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>msgp, size_t msgsz, <span style="color:#66d9ef">int</span> msgflg);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/msgrcv.2.html
</span><span style="color:#75715e">// 接收消息
</span><span style="color:#75715e"></span>ssize_t <span style="color:#a6e22e">msgrcv</span>(<span style="color:#66d9ef">int</span> msqid, <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>msgp, size_t msgsz, <span style="color:#66d9ef">long</span> msgtyp,
               <span style="color:#66d9ef">int</span> msgflg);
</code></pre></div>
<h3 id="信号量">信号量</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/sem.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/semget.2.html
</span><span style="color:#75715e">// 创建或获取一个 System V 信号量。
</span><span style="color:#75715e">// return 成功返回 id，失败返回 -1
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">semget</span>(key_t key, <span style="color:#66d9ef">int</span> nsems, <span style="color:#66d9ef">int</span> semflg);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/semctl.2.html
</span><span style="color:#75715e">// 对信号量进行控制操作，如删除，修改元数据等
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">semctl</span>(<span style="color:#66d9ef">int</span> semid, <span style="color:#66d9ef">int</span> semnum, <span style="color:#66d9ef">int</span> cmd, ...);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/semop.2.html
</span><span style="color:#75715e">// 信号量操作
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">semop</span>(<span style="color:#66d9ef">int</span> semid, <span style="color:#66d9ef">struct</span> sembuf <span style="color:#f92672">*</span>sops, size_t nsops);
<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">semtimedop</span>(<span style="color:#66d9ef">int</span> semid, <span style="color:#66d9ef">struct</span> sembuf <span style="color:#f92672">*</span>sops, size_t nsops,
               <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">struct</span> timespec <span style="color:#f92672">*</span>timeout);
</code></pre></div>
<h3 id="共享内存">共享内存</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/shm.h&gt;</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/shmget.2.html
</span><span style="color:#75715e">// 创建或获取一个 System V 共享内存。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">shmget</span>(key_t key, size_t size, <span style="color:#66d9ef">int</span> shmflg);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/shmctl.2.html
</span><span style="color:#75715e">// 对共享内存进行控制操作，如删除，修改元数据等
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">shmctl</span>(<span style="color:#66d9ef">int</span> shmid, <span style="color:#66d9ef">int</span> cmd, <span style="color:#66d9ef">struct</span> shmid_ds <span style="color:#f92672">*</span>buf);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/shmat.2.html
</span><span style="color:#75715e">// 将现有的共享内存对象 Attach 到调用进程的地址空间。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">shmat</span>(<span style="color:#66d9ef">int</span> shmid, <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>shmaddr, <span style="color:#66d9ef">int</span> shmflg);

<span style="color:#75715e">// https://man7.org/linux/man-pages/man2/shmdt.2.html
</span><span style="color:#75715e">// 从调用进程的地址空间中 Detach 段。
</span><span style="color:#75715e"></span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">shmdt</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>shmaddr);
</code></pre></div>
<p>以上共享内存是支持在任意进程中实施的，但是如果是父进程和子孙进程间进行内存共享，可以通过如下方式实现：</p>

<ul>
<li>mmap 设置为 <code>MAP_SHARED</code> 并绑定 <code>/dev/zero</code>。</li>
<li>mmap 设置为 <code>MAP_SHARED | MAP_ANONYMOUS</code>，fd 字段设置为 <code>-1</code></li>
</ul>

<h2 id="posix-ipc">POSIX IPC</h2>

<p>POSIX IPC 是对 Systemc V IPC 的标准化产物。功能上和 Systemc V IPC 基本对等</p>

<h3 id="消息队列-1">消息队列</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/mq_overview.7.html">mq_overview(7)</a></p>
</blockquote>

<p>API 和 系统调用</p>

<table>
<thead>
<tr>
<th>glibc 库函数</th>
<th>系统调用</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_open.3.html">mq_open(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_open.2.html">mq_open(2)</a></td>
<td>创建或打开一个消息队列</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_getattr.3.html">mq_getattr(3)</a></td>
<td>mq_getsetattr(2)</td>
<td>获取属性</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_setattr.3.html">mq_setattr(3)</a></td>
<td>mq_getsetattr(2)</td>
<td>修改属性</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_notify.3.html">mq_notify(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_notify.2.html">mq_notify(2)</a></td>
<td>注册一个处理函数，当一个空的消息队列放入消息是，处理函数将被调用</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_receive.3.html">mq_receive(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_timedreceive.2.html">mq_timedreceive(2)</a></td>
<td>从消息队列中接收消息</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_send.3.html">mq_send(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_timedsend.2.html">mq_timedsend(2)</a></td>
<td>向消息队列中发送消息</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_timedreceive.3.html">mq_timedreceive(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_timedreceive.2.html">mq_timedreceive(2)</a></td>
<td>从消息队列中接收消息（支持超时）</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_timedsend.3.html">mq_timedsend(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_timedsend.2.html">mq_timedsend(2)</a></td>
<td>向消息队列中发送消息（支持超时）</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_close.3.html">mq_close(3)</a></td>
<td>close(2)</td>
<td>关闭消息队列</td>
</tr>

<tr>
<td><a href="https://man7.org/linux/man-pages/man3/mq_unlink.3.html">mq_unlink(3)</a></td>
<td><a href="https://man7.org/linux/man-pages/man2/mq_unlink.2.html">mq_unlink(2)</a></td>
<td>删除消息队列</td>
</tr>
</tbody>
</table>

<p>更多参见：<a href="https://man7.org/linux/man-pages/man7/mq_overview.7.html">mq_overview(7) 手册</a></p>

<h3 id="信号量-1">信号量</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/sem_overview.7.html">sem_overview(7)</a></p>
</blockquote>

<ul>
<li><a href="https://man7.org/linux/man-pages/man3/sem_open.3.html">sem_open(3)</a> 创建或打开一个信号量</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_post.3.html">sem_post(3)</a> 操作信号量</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_wait.3.html">sem_wait(3)</a> 如果一个值信号量当前为零，该操作将阻塞直到值大于零。</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_close.3.html">sem_close(3)</a> 关闭信号量</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_unlink.3.html">sem_unlink(3)</a> 删除信号量</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_init.3.html">sem_init(3)</a> 匿名信号量初始化</li>
<li><a href="https://man7.org/linux/man-pages/man3/sem_destroy.3.html">sem_destroy(3)</a> 匿名信号量销毁（在 free 内存前调用）</li>
</ul>

<p>在 Linux 上，命名信号量是在虚拟文件系统中创建的，通常安装在 /dev/shm 下，名称为 <code>sem.somename</code>。 （这就是信号量名称的限于 NAME_MAX-4 个字符，而不是 NAME_MAX 个字符的原因）</p>

<p>更多参见：<a href="https://man7.org/linux/man-pages/man7/sem_overview.7.html">sem_overview(7) 手册</a></p>

<h3 id="共享内存-1">共享内存</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/shm_overview.7.html">shm_overview(7)</a></p>
</blockquote>

<p>相关 API</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man3/shm_open.3.html">shm_open(3)</a> 创建或打开一个共享内存对象。类似于 <a href="https://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>。调用返回一个下面列出的其他接口需要使用的文件描述符。</li>
<li><a href="https://man7.org/linux/man-pages/man2/ftruncate.2.html">ftruncate(2)</a> 设置共享内存对象的大小。（一个新创建的共享内存对象的长度为零。）</li>
<li><a href="https://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a> 将共享内存对象映射到虚拟地址调用进程的空间。</li>
<li><a href="https://man7.org/linux/man-pages/man2/munmap.2.html">munmap(2)</a> 取消映射到当前进程虚拟内存空间的共享内存。</li>
<li><a href="https://man7.org/linux/man-pages/man3/shm_unlink.3.html">shm_unlink(3)</a> 删除共享内存对象名称。</li>
<li><a href="https://man7.org/linux/man-pages/man2/close.2.html">close(2)</a> 关闭由 <a href="https://man7.org/linux/man-pages/man3/shm_open.3.html">shm_open(3)</a> 分配的不再需要文件描述符。</li>
<li><a href="https://man7.org/linux/man-pages/man2/fstat.2.html">fstat(2)</a> 获取描述共享内存对象的统计结构。 此调用返回的信息包括对象的大小 (st_size)、权限 (st_mode)、所有者 (st_uid) 和组 (st_gid)。</li>
<li><a href="https://man7.org/linux/man-pages/man2/fchown.2.html">fchown(2)</a> 更改共享内存对象的所有权。</li>
<li><a href="https://man7.org/linux/man-pages/man2/fchmod.2.html">fchmod(2)</a> 更改共享内存对象的权限。</li>
</ul>

<p>在 Linux 中，共享内存是在 <a href="https://man7.org/linux/man-pages/man5/tmpfs.5.html"><code>tmpfs(5)</code></a> 虚拟文件系统中创建的，通常安装在 <code>/dev/shm</code> 下。自从内核 2.6.19，Linux 支持使用访问控制列表 (ACL) 来控制虚拟对象的权限文件系统。</p>

<p>更多参见：<a href="https://man7.org/linux/man-pages/man7/shm_overview.7.html">shm_overview(7) 手册</a></p>

<h2 id="socket">Socket</h2>

<p>TODO</p>

<h2 id="unix-domain-socket">Unix Domain Socket</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man7/unix.7.html">unix(7)</a></p>
</blockquote>

<p>TODO</p>
]]></description></item><item><title>容器核心技术（四） UTS Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-4-namespace-uts/</link><pubDate>Thu, 10 Mar 2022 23:58:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-4-namespace-uts/</guid><description type="html"><![CDATA[

<blockquote>
<p>手册页面：<a href="https://man7.org/linux/man-pages/man7/uts_namespaces.7.html">uts namespaces</a>。</p>
</blockquote>

<p>UTS (UNIX Time-Sharing System) Namespace 提供了个对 hostname 和 NIS domain name 这两个系统标识符的的隔离。</p>

<h2 id="背景知识">背景知识</h2>

<p>在 Linux 中，<code>hostname</code> 和 <code>domainname</code> 有很多种，需要区分清楚：</p>

<ul>
<li><code>system hostname</code> (在 Linux 内核语境下直接叫 <code>hostname</code>)

<ul>
<li>获取

<ul>
<li><a href="https://man7.org/linux/man-pages/man1/hostname.1.html"><code>hostname(1) 命令</code></a> （无参数）</li>
<li><a href="https://man7.org/linux/man-pages/man2/gethostname.2.html"><code>gethostname(2) 系统调用</code></a></li>
</ul></li>
<li>配置

<ul>
<li><a href="https://man7.org/linux/man-pages/man1/hostname.1.html"><code>hostname(1) 命令</code></a> （加一个参数）或者 <code>--file</code></li>
<li><a href="https://man7.org/linux/man-pages/man2/sethostname.2.html"><code>sethostname(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man5/hostname.5.html"><code>/etc/hostname(5) 配置文件</code></a> ，在系统启动时配置一次</li>
</ul></li>
</ul></li>
<li><code>FQDN</code> (Fully Qualified Domain Name，在域名解析语境下直接叫 hostname)，解释参见： <a href="https://man7.org/linux/man-pages/man7/hostname.7.html"><code>hostname(7)</code></a>

<ul>
<li>获取

<ul>
<li><a href="https://man7.org/linux/man-pages/man1/hostname.1.html"><code>hostname(1) 命令</code></a> <code>--fqdn</code> 参数</li>
<li><a href="https://linux.die.net/man/3/gethostbyname2"><code>gethostbyname2(3) 库函数</code></a></li>
</ul></li>
<li>设置 (<a href="https://man7.org/linux/man-pages/man1/hostname.1.html#DESCRIPTION">原文</a>)

<ul>
<li>默认通过 <a href="https://man7.org/linux/man-pages/man5/hosts.5.html"><code>/etc/hosts(5) 配置文件</code></a> 配置（每一行的格式为 <code>IP_address canonical_hostname [aliases...]</code>），值为 <a href="https://man7.org/linux/man-pages/man5/hosts.5.html">/etc/hosts(5)</a> 文件中 alias 为 <a href="https://man7.org/linux/man-pages/man5/hostname.5.html"><code>/etc/hostname(5)</code></a> 的那一行的 <code>canonical_hostname</code></li>
<li>具体取决于 <a href="https://man7.org/linux/man-pages/man5/host.conf.5.html"><code>/etc/host.conf(5) 配置文件</code></a></li>
<li>没有对应系统调用（域名解析属于网络协议层面）</li>
</ul></li>
</ul></li>
<li><code>DNS domainname</code>，为 FQDN 去掉 第一个 <code>.</code> 和之前的内容

<ul>
<li>获取

<ul>
<li><a href="https://man7.org/linux/man-pages/man1/hostname.1.html"><code>hostname(1) 命令</code></a> <code>-d</code> 参数</li>
<li><a href="https://linux.die.net/man/1/dnsdomainname"><code>dnsdomainname(1) 命令</code></a> <code>-d</code> 参数</li>
<li><a href="https://linux.die.net/man/3/gethostbyname2"><code>gethostbyname2(3) 库函数</code></a></li>
</ul></li>
<li>设置，参见 <code>FQDN</code> 设置</li>
</ul></li>
<li><code>NIS/YP domainname</code> (在 Linux 内核语境下直接叫 <code>domainname</code>，又称 <code>nisdomainname</code>、<code>ypdomainname</code> 、 <code>Local domain name</code>)

<ul>
<li>获取

<ul>
<li><a href="https://man7.org/linux/man-pages/man1/hostname.1.html"><code>hostname(1) 命令</code></a> <code>-y</code> 或 <code>--yp</code> 或 <code>--nis</code> 参数</li>
<li><a href="https://linux.die.net/man/1/domainname"><code>domainname(1) 命令</code></a>、<a href="https://linux.die.net/man/1/nisdomainname"><code>nisdomainname(1) 命令</code></a>、<a href="https://linux.die.net/man/1/ypdomainname"><code>ypdomainname(1) 命令</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/getdomainname.2.html"><code>getdomainname(2) 系统调用</code></a></li>
</ul></li>
<li>设置

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/setdomainname.2.html"><code>setdomainname(2) 系统调用</code></a></li>
</ul></li>
</ul></li>
</ul>

<p>举一个例子，比如：</p>

<ul>
<li><code>/etc/hostname</code> 内容为 <code>thishost</code></li>
<li><code>/etc/hosts</code> 存在一行 <code>127.0.1.1       thishost.mydomain.org  thishost</code></li>
</ul>

<p>此时</p>

<ul>
<li><code>system hostname</code> 为 <code>thishost</code></li>
<li><code>FQDN</code> 为 <code>thishost.mydomain.org</code></li>
<li><code>DNS domainname</code> 为 <code>mydomain.org</code></li>
<li><code>NIS/YP domainname</code> 为 <code>(none)</code></li>
</ul>

<p>可以得出如下关系：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">${FQDN} = ${system hostname} . ${DNS domainname}</pre></div>
<h2 id="描述">描述</h2>

<p>而 UTS Namespace 可以隔离的全局系统资源为：<code>system hostname</code> 和 <code>NIS/YP domainname</code>，设计的系统调用为：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/gethostname.2.html"><code>gethostname(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/sethostname.2.html"><code>sethostname(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/getdomainname.2.html"><code>getdomainname(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/setdomainname.2.html"><code>setdomainname(2) 系统调用</code></a></li>
</ul>

<p>下面，简单介绍下 <code>system hostname</code> 和 <code>NIS/YP domainname</code> 的应用。</p>

<ul>
<li><code>system hostname</code>

<ul>
<li>作为局域网邻居发现的标识符，可以通过 <code>${system hostname}.local</code> 直接访问该主机，更多参见：

<ul>
<li><a href="https://notes.leconiot.com/mdns.html">文章：在局域网建立.local域名</a></li>
<li><a href="https://blog.csdn.net/docdocadmin/article/details/112135459">文章：隐藏在网络邻居背后的协议,快来看看你家网络有几种?</a></li>
</ul></li>
</ul></li>
<li><code>NIS/YP domainname</code>

<ul>
<li>NIS 服务，更多参见：

<ul>
<li><a href="http://cn.linux.vbird.org/linux_server/0430nis.php">鸟哥的 Linux 私房菜：第十四章、账号控管： NIS 服务器</a></li>
</ul></li>
</ul></li>
</ul>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<p>为了验证 UTS Namespace 的能力，我们将启动一个具有新 UTS Namespace 的子进程，这个进程会设置 <code>hostname</code> 和 <code>domainname</code>。然后分别在父子两个进程观察 <code>hostname</code> 和 <code>domainname</code> 情况。</p>

<h3 id="源码">源码</h3>

<h4 id="c-语言描述">C 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/02-uts/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE	   </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt; // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;  // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;	   // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;	   // For SIGCHLD constant</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;	   // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;	   // For execv(3), sleep(3), sethostname(2), setdomainname(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;    // For exit(3), system(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;string.h&gt;    // For strlen(3)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> child_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;hostname &amp;&amp; hostname --nis || true&#34;</span>,
	NULL};

<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> new_hostname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;new-hostname&#34;</span>;
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> new_domainname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;new-domainname&#34;</span>;

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args)
{
	<span style="color:#66d9ef">if</span> (sethostname(new_hostname, strlen(new_hostname)) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> )
		errExit(<span style="color:#e6db74">&#34;sethostname&#34;</span>);
	<span style="color:#66d9ef">if</span> (setdomainname(new_domainname, strlen(new_domainname)) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;setdomainname&#34;</span>);
	printf(<span style="color:#e6db74">&#34;=== new uts namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
	perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
	exit(EXIT_FAILURE);
}

pid_t <span style="color:#a6e22e">old_namespace_exec</span>()
{
	pid_t p <span style="color:#f92672">=</span> fork();
	<span style="color:#66d9ef">if</span> (p <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
	{
		printf(<span style="color:#e6db74">&#34;=== old namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
		execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
		perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
		exit(EXIT_FAILURE);
	}
	<span style="color:#66d9ef">return</span> p;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
	<span style="color:#75715e">// 为子进程提供申请函数栈
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
							 PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
							 MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
							 <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
		errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 UTS Namespace（CLONE_NEWUTS），并执行 new_namespace_func 函数
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// clone 库函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 		  /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	pid_t p1 <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWUTS, NULL);
	<span style="color:#66d9ef">if</span> (p1 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone&#34;</span>);
	sleep(<span style="color:#ae81ff">5</span>);
	<span style="color:#75715e">// 创建新的进程（不创建 Namespace），并执行测试命令
</span><span style="color:#75715e"></span>	pid_t p2 <span style="color:#f92672">=</span> old_namespace_exec();
	<span style="color:#66d9ef">if</span> (p2 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;fork&#34;</span>);
	waitpid(p1, NULL, <span style="color:#ae81ff">0</span>);
	waitpid(p2, NULL, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<h4 id="go-语言描述">Go 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run ./src/go/01-namespace/02-uts/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>

	<span style="color:#a6e22e">script</span> = <span style="color:#e6db74">&#34;hostname &amp;&amp; hostname --nis || true&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#a6e22e">tip</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">tip</span>)
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-cx&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 UTS Namespace（syscall.CLONE_NEWUTS）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWUTS</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Sethostname</span>([]byte(<span style="color:#e6db74">&#34;new-hostname&#34;</span>)); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Setdomainname</span>([]byte(<span style="color:#e6db74">&#34;new-domainname&#34;</span>)); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== new uts namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">oldNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== old namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 UTS Namespace 的进程
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccess</span>()
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
		<span style="color:#75715e">// 3. 创建新的进程（不创建 Namespace），并执行测试命令
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">oldNamespaceProccess</span>()
		<span style="color:#a6e22e">err1</span>, <span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r1</span>, <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r2</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err1</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err2</span>)
		}
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
		<span style="color:#75715e">// 2. 该进程执行 newNamespaceProccessFunc，配置 hostname 和 domainname，并执行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">newNamespaceProccessFunc</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div>
<h4 id="shell-描述">Shell 描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># sudo ./src/shell/01-namespace/02-uts/main.sh</span>

script<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hostname &amp;&amp; hostname --nis || true&#34;</span>

<span style="color:#75715e"># 创建新进程，并为该进程创建一个 UTS Namespace（-u）</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html</span>

<span style="color:#75715e"># 设置新的 hostname 和 domainname</span>
unshare -u /bin/bash -c <span style="color:#e6db74">&#34;hostname new-hostname &amp;&amp; domainname new-domainname \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;=== new uts namespace process ===&#39; &amp;&amp; set -x &amp;&amp; </span>$script<span style="color:#e6db74">&#34;</span> &amp;
pid1<span style="color:#f92672">=</span>$!

sleep <span style="color:#ae81ff">5</span>
<span style="color:#75715e"># 创建新的进程（不创建 Namespace），并执行测试命令</span>
/bin/bash -c <span style="color:#e6db74">&#34;echo &#39;=== old namespace process ===&#39; &amp;&amp; set -x &amp;&amp; </span>$script<span style="color:#e6db74">&#34;</span> &amp;
pid2<span style="color:#f92672">=</span>$!

wait $pid1
wait $pid2</code></pre></div>
<h3 id="输出及分析">输出及分析</h3>

<p>按照代码上方注释，编译并运行，输出形如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== new uts namespace process ===
+ hostname
new-hostname
+ hostname --nis
new-domainname
=== old namespace process ===
+ hostname
debian
+ hostname --nis
hostname: Local domain name not set
+ true</pre></div>
<ul>
<li>具有新的 Mount Namespace 的进程打印的 hostname 和 domainname 发生了变化</li>
<li>旧的 Namespace 中进程打印的 hostname 和 domainname 没有受到影响</li>
</ul>
]]></description></item><item><title>容器核心技术（三） Mount Namespace</title><link>https://www.rectcircle.cn/posts/container-core-tech-3-namespace-mount/</link><pubDate>Thu, 10 Mar 2022 23:38:00 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-3-namespace-mount/</guid><description type="html"><![CDATA[

<blockquote>
<p>手册页面：<a href="https://man7.org/linux/man-pages/man7/mount_namespaces.7.html">mount namespaces</a>。</p>
</blockquote>

<h2 id="文件系统">文件系统</h2>

<p>关于 Linux 的文件系统，众所周知的有：一切皆文件的理念，以及 ext2，ext4 这些文件系统。</p>

<p>从设计角度来看，Linux 文件系统的遵循插件化/面向接口的原则，Linux 定义了两套接口：</p>

<ul>
<li>面向 Linux 使用者的，一切皆文件的，文件/目录树操作的系统调用。比如：打开文件 <code>open</code>，查看路径状态 <code>stat</code> 等</li>
<li>面向 Linux 开发者的，VFS （Virtual File System，虚拟文件系统）。上面提到的 ext2、ext4 以及网络文件系统 ceph、联合文件系统 overlay2 等都是 VFS 的一种实现。在实现上，开发者只需要实现有限几个函数，并编译成一个 Linux 模块，并插入到内核中即可为 Linux 添加一种新的 VFS 的实现。简单的实现参见：<a href="https://blog.csdn.net/qq_35536179/article/details/109013447">博客 1</a> | <a href="https://blog.csdn.net/dog250/article/details/100099936">博客 2</a></li>
</ul>

<p>此外 Linux 还提供了 <code>mount 系统调用</code>，来将 VFS 和目录树节点绑定。</p>

<h2 id="挂载-mount">挂载 (mount)</h2>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a> | <a href="https://man7.org/linux/man-pages/man8/mount.8.html"><code>mount(8) 命令</code></a></p>
</blockquote>

<h3 id="概述">概述</h3>

<p>目录树是 Linux 一种的全局系统资源，将一个文件系统绑定到目录树的一个节点的操作叫做挂载，即 <code>mount</code>。在 Linux 中，是通过 <a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a> 或 <a href="https://man7.org/linux/man-pages/man8/mount.8.html"><code>mount(8) 命令</code></a> 实现的。</p>

<p>这里先介绍几种在日常使用 Linux 过程中，常见的一些关于挂载的例子：</p>

<ul>
<li>挂载 一个 ext4 格式的文件系统（磁盘分区） 到某个目录上</li>
<li>挂载 一个 U 盘到某个目录上</li>
<li>挂载 一个 ISO 光盘镜像文件到某个目录上</li>
<li>挂载一个 <code>tmpfs</code> 到某个目录，tmpfs 是一种特殊的文件系统，一般用于缓存，数据存储在内存和 swap 中，系统重启后会丢失。</li>
</ul>

<p>在容器技术中，使用到的挂载主要是如下两种情况：</p>

<ul>
<li>bind 某一个目录（也可以是文件）到另一个目录（也可以是文件，类型需和源保持一致）。实现的效果类似于一个软链指向两一个目录，区别是，对于进程来说，是无法分辨出同一个文件的两个路径的关系。该能力是容器引擎实现挂载 host 目录或 volume 的核心技术。</li>
<li>将几个目录组成一套 overlay 文件系统，并挂载在某个目录，这是容器引擎实现镜像和容器数据存储的核心技术，后续文章有专门介绍。</li>
</ul>

<p>更多关于 Linux 支持 mount 的文件系统类型，参见： <code>/proc/filesystems</code> 文件。下面给出的是 <code>Debian11</code> 的 <code>/proc/filesystems</code> 文件内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nodev   sysfs
nodev   tmpfs
nodev   bdev
nodev   proc
nodev   cgroup
nodev   cgroup2
nodev   cpuset
nodev   devtmpfs
nodev   debugfs
nodev   tracefs
nodev   securityfs
nodev   sockfs
nodev   bpf
nodev   pipefs
nodev   ramfs
nodev   hugetlbfs
nodev   devpts
nodev   mqueue
nodev   pstore
        ext3
        ext2
        ext4
nodev   autofs
nodev   configfs
        fuseblk
nodev   fuse
nodev   fusectl
nodev   binfmt_misc</pre></div>
<p>注意：mount 的调用需要 <code>CAP_SYS_ADMIN</code> 权限。</p>

<h3 id="mount-和-目录树">mount 和 目录树</h3>

<p>众所周知，和 Window 文件访问需要先确定盘（设备）不同，Linux 的文件是以目录树的形式进行抽象的。</p>

<p>在 Linux 中，如果想让进程访问文件系统内部的文件，就必须将该文件系统绑定到在目录树的一个路径上（该路径被称为挂载点）。</p>

<p>站在目录树角度，目录树上每个节点有两种可能：a) 当前文件系统的内容 b) 另一个文件系统的挂载点。因此，挂载点也是组成了一颗挂载点树。</p>

<p>总的来说分别从文件系统、目录树和挂载点视角来看，如下图所示：</p>

<p><img src="/image/container-core-tech-fs-mount-tree.png" alt="image" /></p>

<p>即：<code>目录树 = 文件系统 + 挂载点</code>。</p>

<h3 id="mount-系统调用和命令">mount 系统调用和命令</h3>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a> | <code>[mount(8) 命令](https://man7.org/linux/man-pages/man8/mount.8.html)</code></p>
</blockquote>

<p>mount 系统调用和命令的参数可以分为五个类：</p>

<ul>
<li><code>type</code> 文件系统类型</li>
<li><code>source</code> 源，与 <code>type</code> 有关，有可能是 目录、块设备或者不需要 等等</li>
<li><code>target</code> 目标，即挂载点，绑定到目录树的路径，必填，一般情况下是一个目录（也可能是一个文件），<strong>注意：该路径必须在当前文件系统中存在</strong>。</li>
<li><code>data</code> 参数，与 <code>type</code> 有关，一般是是一串由逗号分隔的选项</li>
<li><code>mountflags</code> 附加标志

<ul>
<li>配置 mount 的操作类型

<ul>
<li><code>MS_REMOUNT</code> 重新挂载</li>
<li><code>MS_BIND</code> bind 挂载</li>
<li><code>MS_SHARED</code>、<code>MS_PRIVATE</code>、<code>MS_SLAVE</code>、<code>MS_UNBINDABLE</code>。改变一个挂载的传播类型</li>
<li><code>MS_MOVE</code> 将现有挂载移动到新位置</li>
<li>创建一个新的挂载：<code>mountflags</code> 不包括上述任何一项</li>
</ul></li>
<li>其他附加选项

<ul>
<li><code>MS_DIRSYNC</code> 所有文件系统的更新都应该立即完成写入磁盘。参见：<a href="https://man7.org/linux/man-pages/man8/mount.8.html">mount(8) dirsync</a></li>
<li><code>MS_LAZYTIME</code> 减少 inode 时间戳的磁盘更新（atime、mtime、ctime) 通过仅在内存中维护这些更改。这磁盘时间戳仅在以下情况下更新：

<ul>
<li>需要更新 inode 以进行一些更改与文件时间戳无关；</li>
<li>应用程序使用 fsync(2)、syncfs(2) 或同步（2）；</li>
<li>未删除的 inode 从内存中逐出；</li>
<li>自 inode 启动以来已超过 24 小时写入磁盘。</li>
</ul></li>
<li><code>MS_REC</code> 递归，与 MS_BIND 结合使用以创建递归绑定挂载；结合传播类型标志递归地改变所有的传播类型子树中的挂载。</li>
<li><code>MS_RDONLY</code> 只读模式</li>
<li>其他参见：<a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a></li>
</ul></li>
</ul></li>
</ul>

<h3 id="创建一个新的挂载点">创建一个新的挂载点</h3>

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a>：不使用 <code>MS_REMOUNT</code>, <code>MS_BIND</code>, <code>MS_MOVE</code>, <code>MS_SHARED</code>, <code>MS_PRIVATE</code>, <code>MS_SLAVE</code>, <code>MS_UNBINDABLE</code> 这些特殊参数的情况下为创建一个新的挂载。其他参数由 <code>type</code> 决定。</li>
<li><a href="https://man7.org/linux/man-pages/man8/mount.8.html"><code>mount(8) 命令</code></a>，参见文章：
<a href="https://segmentfault.com/a/1190000006878392">Linux mount （第一部分）</a>。</li>
</ul>

<h3 id="重新挂载已存在挂载点">重新挂载已存在挂载点</h3>

<p>允许更改现有挂载的 <code>mountflags</code> 和 <code>data</code> ，而无需卸载和重新安装文件系统。</p>

<ul>
<li>使用 <code>MS_REMOUNT</code> 标志</li>
<li>使用相同的 <code>target</code> 参数</li>
<li><code>source</code> 和 <code>filesystemtype</code> 参数将被忽略</li>
</ul>

<p>更多参见：<a href="https://man7.org/linux/man-pages/man2/mount.2.html"><code>mount(2) 系统调用</code></a></p>

<h3 id="创建一个-bind-挂载点">创建一个 bind 挂载点</h3>

<ul>
<li>使用 <code>MS_BIND</code> 标志</li>
<li><code>sourcec</code> 源目录</li>
<li><code>target</code> 目标目录</li>
<li><code>data</code> 忽略</li>
<li>默认情况只会绑定这个目录，而不会绑定这个目录下的其他挂载，可以通过 <code>MS_REC</code> 选项递归挂载</li>
</ul>

<p>经测试 bind 并不会造成递归。原理参见下文：mount 传播类型</p>

<h3 id="移动一个挂载点">移动一个挂载点</h3>

<ul>
<li>使用 <code>mountflags</code> 标志</li>
<li><code>source</code> 指定一个现有的mount</li>
<li><code>target</code> 指定该挂载的被搬迁新位置</li>
<li><code>mountflags</code> 参数中的其余位将被忽略，同样，<code>type</code> 和 <code>data</code> 也会被忽略。</li>
<li>这个操作是原子的：在任何时候子树的挂载都不会被卸载。</li>
</ul>

<h3 id="mount-传播类型">mount 传播类型</h3>

<h4 id="挂载点属性介绍">挂载点属性介绍</h4>

<blockquote>
<p>手册：<a href="https://man7.org/linux/man-pages/man5/proc.5.html">proc(5)</a></p>
</blockquote>

<p>挂载点列表以及每个挂载点的详细属性可以通过 <code>/proc/self/mountinfo</code> 文件查看，其每一行的格式为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
(1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)</pre></div>
<ul>
<li>(1)  mount ID，此挂载点的唯一 ID。</li>
<li>(2)  parent ID，此挂载点的父挂载点 ID。

<ul>
<li>如果此挂载点是挂载点树的根节点，parent ID = mount ID。</li>
<li>父挂载点指的是：从当前挂载点路径开始向上递归，找到的第一个挂载点。</li>
<li>如果当前挂载点的 parent 不在当前目录树，则这 parent ID 将不会出现在 <code>/proc/self/mountinfo</code> 文件中（比如 <code>chroot(2)</code>、<code>pivot_root(2)</code> 情况）。</li>
</ul></li>
<li>(4)  root: 将当前文件系统的那个目录（一般是 <code>/</code>），挂载到挂载点。</li>
<li>(5)  mount point: 挂载点路径。</li>
<li>(6)  mount options: <code>mount(2)</code> 的 <code>data</code> 参数</li>
<li>(7)  optional fields: 0 或多个以 <code>,</code> 分割的可选字段，每个字段格式为 <code>tag[:value]</code></li>
<li>其他略</li>
</ul>

<h4 id="bind-引入的问题">bind 引入的问题</h4>

<p>在引入 bind 之前，一个文件系统的内容只对应目录树上一个路径（不考虑硬链接/软链接）。</p>

<p>引入 bind 之后，一个文件系统的内容在目录树上就会对应多个路径。如：将 <code>/home/a</code> 目录 bind 到 <code>/home_a</code> 路径下 （对应下图 <code>1. bind</code>）。</p>

<p>此时。如果向对这些路径中的一个子目录中 bind 一个其他的目录，操作，其他路径是否可见呢？如：将 <code>/m2</code> bind 到 <code>/home_a/.m2</code>，<code>/home/a/.m2</code> 是否也自动绑定呢（对应下图 <code>2. bind</code> 后，<code>3.❓</code> 的情况）？</p>

<p><img src="/image/container-core-tech-fs-mount-tree-bind.png" alt="image" /></p>

<h4 id="传播特性-和-peer-group"><code>传播特性</code> 和 <code>peer group</code></h4>

<p>在 Linux 中，上文提到的 <code>3.❓</code> 的情况，由挂载点 <code>optional fields</code> 字段的 <code>${传播类型}:${peer group}</code> 决定。</p>

<p>先来看 <code>peer group</code>。<code>peer group</code> 是一个数字 ID，Linux 保证同一个文件系统的 <code>peer group</code> 是相同的（注意：这个 <code>peer group</code> 中必须有一个 <code>MS_SHARED</code>，否则 <code>peer group</code> 相同的所有挂载点的 <code>peer group</code> 都会被清空）。</p>

<p>以上图为例：执行完 <code>1.bind</code> 后，<code>/home_a</code> 和 <code>home</code> 属于同一个文件系统，所以其 <code>peer group</code> 是相同的。</p>

<p>接下来看 <code>传播类型</code> 字段，关于挂载点的传播类型有四种：</p>

<ul>
<li><code>shared</code> (<code>MS_SHARED</code>)，共享：

<ul>
<li>以当前挂载点的子目录作为 mount 的 <code>target</code> 或删除当前挂载点子目录的一个挂载，这个挂载事件会传播到具有相同的 <code>peer group</code> （意味着同一个的文件系统）的挂载点。</li>
<li>当前挂载会接收其他具有相同的 <code>peer group</code>（意味着同一个的文件系统） 的挂载事件。</li>
</ul></li>
<li><code>-</code> (<code>MS_PRIVATE</code>)，私有：

<ul>
<li>以当前挂载点的子目录作为 mount 的 <code>target</code> 或删除当前挂载点子目录的一个挂载，不会影响其他挂载点。</li>
<li>当前挂载不会接收任何其他具有相同的 <code>peer group</code>（意味着同一个的文件系统） 的挂载事件。</li>
</ul></li>
<li><code>master</code> (<code>MS_SLAVE</code>)，从模式：

<ul>
<li>以当前挂载点的子目录作为 mount 的 <code>target</code> 或删除当前挂载点子目录的一个挂载，不会影响其他挂载点。</li>
<li>当前挂载会接收其他具有相同的 <code>peer group</code>（意味着同一个的文件系统） 的挂载事件。</li>
</ul></li>
<li><code>unbindable</code> (<code>MS_UNBINDABLE</code>)，发送和接收的行为和 <code>MS_PRIVATE</code>，此外，还附加如下约束：

<ul>
<li>针对某个目录进行递归 bind 时（<code>MS_BIND | MS_REC</code>），如果该目录的子目录存在一个配置 <code>MS_UNBINDABLE</code> 的挂载点，将忽略。</li>
<li>直接 bind 该挂载点，将报错。</li>
</ul></li>
</ul>

<p>因此我们来枚举下上图操作 <code>2. bind</code> 后， <code>3.❓</code> 的情况：</p>

<table>
<thead>
<tr>
<th></th>
<th><code>/home</code> 挂载点 <code>MS_SHARED</code></th>
<th><code>/home</code> 挂载点 <code>MS_PRIVATE</code></th>
<th><code>/home</code> 挂载点 <code>MS_SLAVE</code></th>
</tr>
</thead>

<tbody>
<tr>
<td><code>/home_a</code> 挂载点 <code>MS_SHARED</code></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
</tr>

<tr>
<td><code>/home_a</code> 挂载点 <code>MS_PRIVATE</code></td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
</tr>

<tr>
<td><code>/home_a</code> 挂载点 <code>MS_SLAVE</code></td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
</tr>
</tbody>
</table>

<p>假设， <code>/home</code> 挂载点 <code>MS_SHARED</code> 且 <code>/home_a</code> 挂载点 <code>MS_SHARED</code>，此时相关挂载点的属性如下表所示：</p>

<table>
<thead>
<tr>
<th>ID</th>
<th>Parent ID</th>
<th>Root</th>
<th>mount point</th>
<th>optional fields</th>
<th>文件系统</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>26</td>
<td>1</td>
<td><code>/</code></td>
<td><code>/</code></td>
<td><code>shared:1</code></td>
<td><code>/</code></td>
<td>根目录挂载点</td>
</tr>

<tr>
<td>209</td>
<td>26</td>
<td><code>/</code></td>
<td><code>/home</code></td>
<td><code>shared:122</code></td>
<td><code>/home</code></td>
<td><code>/home</code> 挂载点</td>
</tr>

<tr>
<td>216</td>
<td>26</td>
<td><code>/</code></td>
<td><code>/m2</code></td>
<td><code>shared:126</code></td>
<td><code>/m2</code></td>
<td><code>/m2</code> 挂载点</td>
</tr>

<tr>
<td>223</td>
<td>26</td>
<td><code>/a</code></td>
<td><code>/home_a</code></td>
<td><code>shared:122</code></td>
<td><code>/home</code></td>
<td>操作 <code>1. bind</code></td>
</tr>

<tr>
<td>230</td>
<td>223</td>
<td><code>/</code></td>
<td><code>/home_a/.m2</code></td>
<td><code>shared:126</code></td>
<td><code>/m2</code></td>
<td>操作 <code>2. bind</code></td>
</tr>

<tr>
<td>231</td>
<td>209</td>
<td><code>/</code></td>
<td><code>/home/a/.m2</code></td>
<td><code>shared:126</code></td>
<td><code>/m2</code></td>
<td><code>3. ❓</code> 结果</td>
</tr>
</tbody>
</table>

<p>接下来，探讨创建一个挂载点的 <code>传播类型</code> 和 <code>peer group</code> 的初始化情况：</p>

<ul>
<li>第一步，确认挂载的 <code>source</code> 所在的挂载点（以 <code>1. bind</code> 操作为例，其挂载点为 <code>/home</code>）。</li>
<li>新的挂载点的 <code>传播类型</code> 和 <code>peer group</code> 为和第一步确认的挂载点保持一致。</li>
</ul>

<p>最后，探讨下一个挂载点的 <code>传播类型</code> 和 <code>peer group</code> 的变化情况：</p>

<ul>
<li>将一个 <code>MS_SHARED</code> 的挂载点设置为 <code>MS_SLAVE</code> 时，如果设置后，<code>peer group</code> 相同的挂载点不存在 <code>传播特性</code> 为 <code>MS_SHARED</code> 是，这个挂载点将直接变为 <code>MS_PRIVATE</code>（<code>peer group</code> 将丢失）。否则可以变为 <code>MS_SLAVE</code>。</li>
<li>将 <code>MS_SHARED</code> 或 <code>MS_SLAVE</code> 设为 <code>MS_PRIVATE</code> 或 <code>MS_UNBINDABLE</code>，<code>peer group</code> 将丢失。</li>
<li>将 <code>MS_PRIVATE</code> 或 <code>MS_UNBINDABLE</code> 设为 <code>MS_SLAVE</code> 将不生效</li>
<li>将 <code>MS_PRIVATE</code> 或 <code>MS_UNBINDABLE</code> 设为 <code>MS_SHARED</code> 将分配一个新的 <code>peer group</code></li>
</ul>

<h4 id="修改传播类型参数说明">修改传播类型参数说明</h4>

<ul>
<li><code>target</code> 填写要改变的挂载点</li>
<li><code>source</code>、<code>data</code>、<code>type</code> 忽略</li>
<li><code>mountflags</code> 上文已经介绍清楚

<ul>
<li><code>MS_SHARED</code></li>
<li><code>MS_PRIVATE</code></li>
<li><code>MS_SLAVE</code></li>
<li><code>MS_UNBINDABLE</code></li>
</ul></li>
</ul>

<h4 id="example">Example</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
abs_dir<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cd <span style="color:#66d9ef">$(</span>dirname $0<span style="color:#66d9ef">)</span>; pwd<span style="color:#66d9ef">)</span>
cd $abs_dir

<span style="color:#75715e"># 开始测试</span>
echo <span style="color:#e6db74">&#39;=== origin ===&#39;</span>
tree

sudo mount --bind source1 target1
echo <span style="color:#e6db74">&#39;=== bind ./source1 ./target1 ===&#39;</span>
tree

sudo mount --bind source2 target1/target2
echo <span style="color:#e6db74">&#39;=== / is share &amp; ./target1 is share ===&#39;</span>
echo <span style="color:#e6db74">&#39;=== bind ./source2 ./target1/target2 : ./source1/target2 ✅  ===&#39;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;/ / &#34;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;propagation&#34;</span>
tree

sudo umount target1/target2
sudo mount --make-slave target1
sudo mount --bind source2 source1/target2
echo <span style="color:#e6db74">&#39;=== / is share &amp; ./target1 is slave ===&#39;</span>
echo <span style="color:#e6db74">&#39;=== bind ./source2 ./source1/target2 : ./target1/target2/ ✅  ===&#39;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;/ / &#34;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;propagation&#34;</span>
tree

sudo umount source1/target2
sudo mount --bind source2 target1/target2
echo <span style="color:#e6db74">&#39;=== bind ./source2 ./target1/target2 : ./source1/target2 ❌ ===&#39;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;/ / &#34;</span>
cat /proc/self/mountinfo | grep <span style="color:#e6db74">&#34;propagation&#34;</span>
tree

sudo umount target1/target2
sudo umount target1</code></pre></div>
<p>输出</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== origin ===
.
├── source1
│   ├── source1
│   └── target2
│       └── target2
├── source2
│   └── mounted
├── target1
│   └── target1
└── test.sh

4 directories, 5 files
=== bind ./source1 ./target1 ===
.
├── source1
│   ├── source1
│   └── target2
│       └── target2
├── source2
│   └── mounted
├── target1
│   ├── source1
│   └── target2
│       └── target2
└── test.sh

5 directories, 6 files
=== / is share &amp; ./target1 is share ===
=== bind ./source2 ./target1/target2 : ./source1/target2 ✅  ===
26 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
209 26 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source1 /home/rectcircle/container-core-tech-experiment/data/propagation/target1 rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
216 209 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source2 /home/rectcircle/container-core-tech-experiment/data/propagation/target1/target2 rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
217 26 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source2 /home/rectcircle/container-core-tech-experiment/data/propagation/source1/target2 rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
.
├── source1
│   ├── source1
│   └── target2
│       └── mounted
├── source2
│   └── mounted
├── target1
│   ├── source1
│   └── target2
│       └── mounted
└── test.sh

5 directories, 6 files
=== / is share &amp; ./target1 is slave ===
=== bind ./source2 ./source1/target2 : ./target1/target2/ ✅  ===
26 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
209 26 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source1 /home/rectcircle/container-core-tech-experiment/data/propagation/target1 rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
216 26 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source2 /home/rectcircle/container-core-tech-experiment/data/propagation/source1/target2 rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
217 209 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source2 /home/rectcircle/container-core-tech-experiment/data/propagation/target1/target2 rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
.
├── source1
│   ├── source1
│   └── target2
│       └── mounted
├── source2
│   └── mounted
├── target1
│   ├── source1
│   └── target2
│       └── mounted
└── test.sh

5 directories, 6 files
=== bind ./source2 ./target1/target2 : ./source1/target2 ❌ ===
26 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
209 26 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source1 /home/rectcircle/container-core-tech-experiment/data/propagation/target1 rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
216 209 8:1 /home/rectcircle/container-core-tech-experiment/data/propagation/source2 /home/rectcircle/container-core-tech-experiment/data/propagation/target1/target2 rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro
.
├── source1
│   ├── source1
│   └── target2
│       └── target2
├── source2
│   └── mounted
├── target1
│   ├── source1
│   └── target2
│       └── mounted
└── test.sh

5 directories, 6 files</pre></div>
<h2 id="描述">描述</h2>

<h3 id="隔离">隔离</h3>

<p>Mount Namespace 实现了进程间挂载点树的隔离，即：不同 Namespace 的进程看到的挂载点树可以是不一样的（导致目录树不同），且这些进程中的挂载是相互不影响的。</p>

<h3 id="传播类型">传播类型</h3>

<blockquote>
<p>本部分主要在手册：<a href="https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES">mount_namespaces(7)</a> 阐述</p>
</blockquote>

<p>已该部分，已经在 《背景知识 —— mount 传播类型》阐述过了。</p>

<p>共享和传播在容器技术中应用参见：《场景 —— 某 Namespace 的进程为其他 Namespace Mount 文件系统》</p>

<h3 id="文件共享">文件共享</h3>

<p>Mount Namespace 隔离的是是挂载点树，而不是目录树，因此如果在两个不同 Mount Namespace 挂载了相同的文件系统，则该文件系统就在这两个 Mount Namespace 中实现了共享。两者对文件的修改上方都是可见的。这就是容器引擎可以通过宿主机目录共享数据的原因。</p>

<h3 id="相关系统调用和命令">相关系统调用和命令</h3>

<p>除了 《Namespace 概述》 描述的相关系统调用、函数、命令以及文档的手册外，本部分还涉及如下内容：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man8/mount.8.html"><code>mount(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man8/mount.8.html"><code>mount(8) 命令</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/umount.2.html"><code>umount(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man8/umount.8.html"><code>umount(8) 命令</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man2/pivot_root.2.html"><code>pivot_root(2) 系统调用</code></a></li>
<li><a href="https://man7.org/linux/man-pages/man8/pivot_root.8.html"><code>pivot_root(8) 系统调用</code></a></li>
</ul>

<p>特别说明，对于根目录挂载点的切换，需要通过 <a href="https://man7.org/linux/man-pages/man2/pivot_root.2.html"><code>pivot_root(2) 系统调用</code></a> 实现。</p>

<h2 id="实验">实验</h2>

<h3 id="实验设计">实验设计</h3>

<p>为了验证 Mount Namespace 的能力，我们将启动一个具有新 Mount Namespace 的 bash 的进程，这个进程将会使用 bind 挂载的方式将 <code>data/binding/source</code> 目录挂载到当前目录的 <code>data/binding/target</code> 目录，其中 <code>data/binding/source</code> 包含一个文件 <code>a</code>。并观察：</p>

<ul>
<li>具有新 Mount Namespace 的 bash 进程，看到 <code>data/binding/source</code> 目录和 <code>data/binding/target</code> 目录，内容一致</li>
<li>其他普通进程，看到的 <code>data/binding/source</code> 目录和 <code>data/binding/target</code> 目录，内容<strong>不</strong>一致</li>
</ul>

<p>此外还可以观察两个进程的 <code>mount</code> 命令的输出，以及 <code>readlink /proc/self/ns/mnt</code>、<code>cat /proc/self/mounts</code>、<code>cat /proc/self/mountinfo</code> 以及 <code>cat /proc/self/mountstats</code> 等的输出。</p>

<h3 id="源码">源码</h3>

<h4 id="c-语言描述">C 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/01-mount/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE	   </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt; // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;  // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;	   // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;	   // For SIGCHLD constant</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;	   // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For execv(3), sleep(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;    // For exit(3), system(3)</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> child_args[] <span style="color:#f92672">=</span> {
	<span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
	<span style="color:#e6db74">&#34;-xc&#34;</span>,
	<span style="color:#e6db74">&#34;ls data/binding/target \
</span><span style="color:#e6db74">	&amp;&amp; readlink /proc/self/ns/mnt \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mounts | grep data/binding/target || true \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mountinfo | grep data/binding/target || true \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mountstats | grep data/binding/target || true \
</span><span style="color:#e6db74">	&amp;&amp; sleep 10 \
</span><span style="color:#e6db74">	&#34;</span>,
	NULL};

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args)
{
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(NULL, <span style="color:#e6db74">&#34;/&#34;</span>, NULL , MS_SLAVE <span style="color:#f92672">|</span> MS_REC, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-MS_SLAVE&#34;</span>);
	<span style="color:#75715e">// 使用 MS_BIND 参数将 data/binding/source 挂载（绑定）到 data/binding/target
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount --bind data/binding/source data/binding/target
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// mount 函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//    int mount(const char *source, const char *target,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const char *filesystemtype, unsigned long mountflags,
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//              const void *data);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/mount.2.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> (mount(<span style="color:#e6db74">&#34;data/binding/source&#34;</span>, <span style="color:#e6db74">&#34;data/binding/target&#34;</span>, NULL, MS_BIND, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;mount-MS_BIND&#34;</span>);
	printf(<span style="color:#e6db74">&#34;=== new mount namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
	execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
	perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
	exit(EXIT_FAILURE);
}

pid_t <span style="color:#a6e22e">old_namespace_exec</span>()
{
	pid_t p <span style="color:#f92672">=</span> fork();
	<span style="color:#66d9ef">if</span> (p <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)
	{
		printf(<span style="color:#e6db74">&#34;=== old namespace process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
		execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
		perror(<span style="color:#e6db74">&#34;exec&#34;</span>);
		exit(EXIT_FAILURE);
	}
	<span style="color:#66d9ef">return</span> p;
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
	<span style="color:#75715e">// 为子进程提供申请函数栈
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
							 PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
							 MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
							 <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
		errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 Mount Namespace（CLONE_NEWNS），并执行 new_namespace_func 函数
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// clone 库函数声明为：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 		  /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	pid_t p1 <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWNS, NULL);
	<span style="color:#66d9ef">if</span> (p1 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;clone&#34;</span>);
	sleep(<span style="color:#ae81ff">5</span>);
	<span style="color:#75715e">// 创建新的进程（不创建 Namespace），并执行测试命令
</span><span style="color:#75715e"></span>	pid_t p2 <span style="color:#f92672">=</span> old_namespace_exec();
	<span style="color:#66d9ef">if</span> (p2 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
		errExit(<span style="color:#e6db74">&#34;fork&#34;</span>);
	waitpid(p1, NULL, <span style="color:#ae81ff">0</span>);
	waitpid(p2, NULL, <span style="color:#ae81ff">0</span>);
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<h4 id="go-语言描述">Go 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run ./src/go/01-namespace/01-mount/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
	<span style="color:#e6db74">&#34;time&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>

	<span style="color:#a6e22e">script</span> = <span style="color:#e6db74">&#34;ls data/binding/target &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; readlink /proc/self/ns/mnt &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; cat /proc/self/mounts | grep data/binding/target || true&#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; cat /proc/self/mountinfo | grep data/binding/target || true &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; cat /proc/self/mountstats | grep data/binding/target || true &#34;</span> <span style="color:#f92672">+</span>
		<span style="color:#e6db74">&#34;&amp;&amp; sleep 10&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#a6e22e">tip</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">tip</span>)
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;-cx&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 Mount Namespace（syscall.CLONE_NEWNS）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWNS</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceProccessFunc</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_SLAVE</span>|<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_REC</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 将 data/binding/source 挂载（绑定）到 data/binding/target
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 等价命令为：mount --bind data/binding/source data/binding/target
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man8/mount.8.html
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;data/binding/source&#34;</span>, <span style="color:#e6db74">&#34;data/binding/target&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_BIND</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== new mount namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">oldNamespaceProccess</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== old namespace process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 Mount Namespace 的进程
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceProccess</span>()
		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
		<span style="color:#75715e">// 3. 创建新的进程（不创建 Namespace），并执行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">oldNamespaceProccess</span>()
		<span style="color:#a6e22e">err1</span>, <span style="color:#a6e22e">err2</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r1</span>, <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r2</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err1</span>)
		}
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err2</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err2</span>)
		}
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
        <span style="color:#75715e">// 2. 该进程执行 newNamespaceProccessFunc，binding 文件系统，并执行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">newNamespaceProccessFunc</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div>
<h4 id="shell-描述">Shell 描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># sudo ./src/shell/01-namespace/01-mount/main.sh</span>

script<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ls data/binding/target  \
</span><span style="color:#e6db74">	&amp;&amp; readlink /proc/self/ns/mnt  \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mounts | grep data/binding/target || true \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mountinfo | grep data/binding/target || true  \
</span><span style="color:#e6db74">	&amp;&amp; cat /proc/self/mountstats | grep data/binding/target || true  \
</span><span style="color:#e6db74">	&amp;&amp; sleep 10&#34;</span>

<span style="color:#75715e"># 创建新进程，并为该进程创建一个 Mount Namespace（-m）</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html</span>

<span style="color:#75715e"># 注意 unshare 会自动取消进程的所有共享，因此不需要手动执行：mount --make-rprivate /</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html 的 --propagation 参数说明</span>

<span style="color:#75715e"># 将 data/binding/source 挂载（绑定）到 data/binding/target</span>
<span style="color:#75715e"># 因为在新的 Mount Namespace 中执行，所有其他进程的目录树不受影响</span>
<span style="color:#75715e"># 等价系统调用为：mount(&#34;data/binding/source&#34;, &#34;data/binding/target&#34;, NULL, MS_BIND, NULL);</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man8/mount.8.html</span>
unshare -m /bin/bash -c <span style="color:#e6db74">&#34;mount --bind data/binding/source data/binding/target \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;=== new mount namespace process ===&#39; &amp;&amp; set -x </span>$script<span style="color:#e6db74">&#34;</span> &amp;
pid1<span style="color:#f92672">=</span>$!

sleep <span style="color:#ae81ff">5</span>
<span style="color:#75715e"># 创建新的进程（不创建 Namespace），并执行测试命令</span>
/bin/bash -c <span style="color:#e6db74">&#34;echo &#39;=== old namespace process ===&#39; &amp;&amp; set -x </span>$script<span style="color:#e6db74">&#34;</span> &amp;
pid2<span style="color:#f92672">=</span>$!

wait $pid1
wait $pid2</code></pre></div>
<h3 id="输出及分析">输出及分析</h3>

<p>按照代码上方注释，编译并运行，输出形如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== new mount namespace process ===
+ ls data/binding/target
a
+ readlink /proc/self/ns/mnt
mnt:[4026532188]
+ grep data/binding/target
+ cat /proc/self/mounts
/dev/sda1 /home/rectcircle/container-core-tech-experiment/data/binding/target ext4 rw,relatime,errors=remount-ro 0 0
+ grep data/binding/target
+ cat /proc/self/mountinfo
231 210 8:1 /home/rectcircle/container-core-tech-experiment/data/binding/source /home/rectcircle/container-core-tech-experiment/data/binding/target rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
+ grep data/binding/target
+ cat /proc/self/mountstats
device /dev/sda1 mounted on /home/rectcircle/container-core-tech-experiment/data/binding/target with fstype ext4
+ sleep 10
=== old namespace process ===
+ ls data/binding/target
+ readlink /proc/self/ns/mnt
mnt:[4026531840]
+ grep data/binding/target
+ cat /proc/self/mounts
+ true
+ grep data/binding/target
+ cat /proc/self/mountinfo
+ true
+ grep data/binding/target
+ cat /proc/self/mountstats
+ true
+ sleep 10</pre></div>
<ul>
<li>前半部分输出为，具有新的 Mount Namespace 的进程打印的，以 <code>=== new mount namespace process ===</code> 开头</li>
<li>后半部分输出为，在旧的 Namespace 中进程打印的，以 <code>=== old namespace process ===</code> 开头</li>
<li>两半部分执行的测试命令是相同的

<ul>
<li>ls data/binding/target 输出，前半部分结果为 <code>a</code>，后半部分为空。证明了 Mount Namespace 隔离是有效的</li>
<li>后面的一系列对 <code>/proc</code> 关于 <code>mount</code> 的观察，前半部分有输出，后半部分没有输出。也证明了 Mount Namespace 隔离是有效的</li>
</ul></li>
</ul>

<h2 id="扩展实验-切换根文件系统">扩展实验：切换根文件系统</h2>

<p>最早，切换某个进程的根目录的系统调用为 <a href="https://man7.org/linux/man-pages/man2/chroot.2.html"><code>chroot(2)</code></a>，该能力最早出现在 1979 年的Unix V7 系统。chroot 仅仅是通过修改，进程的 task 结构体中 fs 结构体中的 root 字段实现的（<a href="https://huadeyu.tech/system/chroot-implement-detail.html">博客 1</a>）。存在很多越狱手段，参见：<a href="https://zhengyinyong.com/post/chroot-mechanism/#chroot-%E7%9A%84%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98">博客2</a>。</p>

<p>配合 Mount Namespace，<a href="https://man7.org/linux/man-pages/man2/pivot_root.2.html"><code>pivot_root(2) 系统调用</code></a>可以实现完全隔离的根目录。</p>

<h3 id="实验设计-1">实验设计</h3>

<p>为了验证 <a href="https://man7.org/linux/man-pages/man2/pivot_root.2.html"><code>pivot_root(2) 系统调用</code></a> 隔离根目录挂载点的能力。我们准备一个包含 <code>busybox</code> 的目录，用来充当新的根目录（下文称为 rootfs）。该目录位于 <code>data/busybox/rootfs</code>。准备命令为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p data/busybox/rootfs
cd data/busybox/rootfs
mkdir bin .oldrootfs
cd bin
wget https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
chmod +x busybox
<span style="color:#75715e"># ./busybox --install -s ./</span>
ln -s busybox sh
ln -s busybox ls
cd ..
mkdir .oldrootfs
touch README
touch .oldrootfs/README</code></pre></div>
<p>最终 <code>data/busybox/rootfs</code> 目录数结构为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">./data/busybox/rootfs/
├── bin
│   ├── busybox
│   ├── ls -&gt; busybox
│   └── sh -&gt; busybox
├── .oldrootfs
│   └── README
└── README</pre></div>
<p>本实验，启动具有新 Mount Namespace 进程，该进程会执行 pivot_root 将根目录切换到 <code>data/busybox/rootfs/</code>，并执行新的根目录的 <code>/bin/sh</code> （即 <code>data/busybox/rootfs/bin/sh</code>），执行 <code>ls /</code> 和 <code>ls /bin</code> 观察其输出。</p>

<blockquote>
<p>💡 busybox 是一个没有任何外部依赖（不依赖任何动态链接库，包括 glibc）的命令行工具合集，包含如 sh、ls 等常用命令。更多参见：<a href="https://busybox.net/">busybox 官网</a></p>
</blockquote>

<h3 id="源码-1">源码</h3>

<h4 id="c-语言描述-1">C 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#75715e">// gcc src/c/01-namespace/01-mount/pivot_root/main.c &amp;&amp; sudo ./a.out
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 本例参考了：https://man7.org/linux/man-pages/man2/pivot_root.2.html#EXAMPLES
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define _GNU_SOURCE    </span><span style="color:#75715e">// Required for enabling clone(2)
</span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/wait.h&gt;  // For waitpid(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mount.h&gt; // For mount(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/mman.h&gt;  // For mmap(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sched.h&gt;     // For clone(2)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;signal.h&gt;    // For SIGCHLD constant</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdio.h&gt;     // For perror(3), printf(3), perror(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;    // For execv(3), sleep(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;stdlib.h&gt;    // For exit(3), system(3)</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;limits.h&gt;    // For PATH_MAX</span><span style="color:#75715e">
</span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;sys/syscall.h&gt; // For  SYS_* constants</span><span style="color:#75715e">
</span><span style="color:#75715e"></span>
<span style="color:#75715e">#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
</span><span style="color:#75715e">                               } while (0)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">static</span> <span style="color:#66d9ef">int</span>
<span style="color:#a6e22e">pivot_root</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>new_root, <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>put_old)
{
    <span style="color:#66d9ef">return</span> syscall(SYS_pivot_root, new_root, put_old);
}

<span style="color:#75715e">#define STACK_SIZE (1024 * 1024)
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> child_args[] <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;/bin/sh&#34;</span>,
    <span style="color:#e6db74">&#34;-xc&#34;</span>,
    <span style="color:#e6db74">&#34;export PATH=/bin &amp;&amp; ls / &amp;&amp; ls /bin&#34;</span>,
    NULL};

<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> new_root <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;data/busybox/rootfs&#34;</span>;
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> put_old <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;data/busybox/rootfs/.oldrootfs&#34;</span>;
<span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">const</span> put_old_on_new_rootfs <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/.oldrootfs&#34;</span>;

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">new_namespace_func</span>(<span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>args)
{
    <span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>    <span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (mount(NULL, <span style="color:#e6db74">&#34;/&#34;</span>, NULL, MS_SLAVE <span style="color:#f92672">|</span> MS_REC, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        errExit(<span style="color:#e6db74">&#34;mount-MS_SLAVE&#34;</span>);
    <span style="color:#75715e">// 确保 new_root 是一个挂载点
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (mount(new_root, new_root, NULL, MS_BIND, NULL) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        errExit(<span style="color:#e6db74">&#34;mount-MS_BIND&#34;</span>);
    <span style="color:#75715e">// 切换根挂载目录，将 new_root 挂载到根目录，将旧的根目录挂载到 put_old 目录下
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// - new_root 和 put_old 必须是一个目录
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// - new_root 和 put_old 不能和当前根目录相同。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// - put_old 必须是 new_root 的子孙目录
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// - new_root 必须是挂载点的路径，但不能是根目录。如果不是的话，可以通过 mount bind 方式转换为一个挂载点（参见上一个命令）。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// - 旧的根目录必须是挂载点。
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 更多参见：https: // man7.org/linux/man-pages/man2/pivot_root.2.html
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 此外，可以通过 pivot_root(&#34;.&#34;, &#34;.&#34;) 来实现免除创建临时目录，参见： https://github.com/opencontainers/runc/commit/f8e6b5af5e120ab7599885bd13a932d970ccc748
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (pivot_root(new_root, put_old) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        errExit(<span style="color:#e6db74">&#34;pivot_root&#34;</span>);
    <span style="color:#75715e">// 根目录已经切换了，所以之前的工作目录已经不存在了，所以需要将 working directory 切换到根目录
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (chdir(<span style="color:#e6db74">&#34;/&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        errExit(<span style="color:#e6db74">&#34;chdir&#34;</span>);
    <span style="color:#75715e">// 取消挂载旧的根目录路径
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (umount2(put_old_on_new_rootfs, MNT_DETACH) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        perror(<span style="color:#e6db74">&#34;umount2&#34;</span>);
    printf(<span style="color:#e6db74">&#34;=== new mount namespace and pivot_root process ===</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
    execv(child_args[<span style="color:#ae81ff">0</span>], child_args);
    errExit(<span style="color:#e6db74">&#34;execv&#34;</span>);
}

<span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>()
{
    <span style="color:#75715e">// 为子进程提供申请函数栈
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">void</span> <span style="color:#f92672">*</span>child_stack <span style="color:#f92672">=</span> mmap(NULL, STACK_SIZE,
                             PROT_READ <span style="color:#f92672">|</span> PROT_WRITE,
                             MAP_PRIVATE <span style="color:#f92672">|</span> MAP_ANONYMOUS <span style="color:#f92672">|</span> MAP_STACK,
                             <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>);
    <span style="color:#66d9ef">if</span> (child_stack <span style="color:#f92672">==</span> MAP_FAILED)
        errExit(<span style="color:#e6db74">&#34;mmap&#34;</span>);
    <span style="color:#75715e">// 创建新进程，并为该进程创建一个 Mount Namespace（CLONE_NEWNS），并执行 new_namespace_func 函数
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// clone 库函数声明为：
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 		  /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>    pid_t p1 <span style="color:#f92672">=</span> clone(new_namespace_func, child_stack <span style="color:#f92672">+</span> STACK_SIZE, SIGCHLD <span style="color:#f92672">|</span> CLONE_NEWNS, NULL);
    <span style="color:#66d9ef">if</span> (p1 <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
        errExit(<span style="color:#e6db74">&#34;clone&#34;</span>);
    waitpid(p1, NULL, <span style="color:#ae81ff">0</span>);
    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
}
</code></pre></div>
<h4 id="go-语言描述-1">Go 语言描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:build linux
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// sudo go run ./src/go/01-namespace/01-mount/pivot_root/main.go
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>
	<span style="color:#e6db74">&#34;os/exec&#34;</span>
	<span style="color:#e6db74">&#34;syscall&#34;</span>
)

<span style="color:#66d9ef">const</span> (
	<span style="color:#a6e22e">sub</span> = <span style="color:#e6db74">&#34;sub&#34;</span>

	<span style="color:#a6e22e">newroot</span> = <span style="color:#e6db74">&#34;data/busybox/rootfs&#34;</span>

	<span style="color:#a6e22e">script</span> = <span style="color:#e6db74">&#34;export PATH=/bin &amp;&amp; ls / &amp;&amp; ls /bin&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#a6e22e">tip</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">tip</span>)
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;/bin/sh&#34;</span>, <span style="color:#e6db74">&#34;-cx&#34;</span>, <span style="color:#a6e22e">script</span>)
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNamespaceExec</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#a6e22e">cmd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>], <span style="color:#e6db74">&#34;sub&#34;</span>)
	<span style="color:#75715e">// 创建新进程，并为该进程创建一个 Mount Namespace（syscall.CLONE_NEWNS）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 更多参见：https://man7.org/linux/man-pages/man2/clone.2.html
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">SysProcAttr</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SysProcAttr</span>{
		<span style="color:#a6e22e">Cloneflags</span>: <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">CLONE_NEWNS</span>,
	}
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdin</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stdout</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>
	<span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Stderr</span> = <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>

	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">result</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">cmd</span>.<span style="color:#a6e22e">Run</span>()
	}()
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">pivotRootAndRun</span>() <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span> {
	<span style="color:#75715e">// 首先，需要阻止挂载事件传播到其他 Mount Namespace，参见：https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 如果不执行这个语句， cat /proc/self/mountinfo 所有行将会包含 shared，这样在这个子进程中执行 mount 其他进程也会受影响
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 关于 Shared subtrees 更多参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://segmentfault.com/a/1190000006899213
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 下面语句的含义是：重新递归挂（MS_REC）载 / ，并设置为不共享（MS_SLAVE 或 MS_PRIVATE）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 说明：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//   等价于执行：mount --make-rslave / 命令
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_SLAVE</span>|<span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_REC</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 确保 new_root 是一个挂载点
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">Mount</span>(<span style="color:#a6e22e">newroot</span>, <span style="color:#a6e22e">newroot</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">MS_BIND</span>, <span style="color:#e6db74">&#34;&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 切换根挂载目录，将 new_root 挂载到根目录，将旧的根目录挂载到 put_old 目录下
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 可以通过 pivot_root(&#34;.&#34;, &#34;.&#34;) 来实现免除创建临时目录，参见： https://github.com/opencontainers/runc/commit/f8e6b5af5e120ab7599885bd13a932d970ccc748
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// - new_root 和 put_old 必须是一个目录
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// - new_root 和 put_old 不能和当前根目录相同。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// - put_old 必须是 new_root 的子孙目录
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// - new_root 必须是挂载点的路径，但不能是根目录。如果不是的话，可以通过 mount bind 方式转换为一个挂载点（参见上一个命令）。
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// - 旧的根目录必须是挂载点。
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Chdir</span>(<span style="color:#a6e22e">newroot</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">PivotRoot</span>(<span style="color:#e6db74">&#34;.&#34;</span>, <span style="color:#e6db74">&#34;.&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#75715e">// 根目录已经切换了，所以之前的工作目录已经不存在了，所以需要将 working directory 切换到根目录
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Chdir</span>(<span style="color:#e6db74">&#34;/&#34;</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">runTestScript</span>(<span style="color:#e6db74">&#34;=== new mount namespace and pivot_root process ===&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#66d9ef">switch</span> len(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>) {
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>:
		<span style="color:#75715e">// 1. 执行 newNamespaceExec，启动一个具有新的 Mount Namespace 的进程
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">r1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">newNamespaceExec</span>()
		<span style="color:#a6e22e">err1</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">r1</span>
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err1</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			panic(<span style="color:#a6e22e">err1</span>)
		}
		<span style="color:#66d9ef">return</span>
	<span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span>:
		<span style="color:#75715e">// 2. 该进程执行 pivotRootAndRun，配置 Mount，调用 pivotRoot 并运行测试脚本
</span><span style="color:#75715e"></span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">sub</span> {
			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">pivotRootAndRun</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				panic(<span style="color:#a6e22e">err</span>)
			}
			<span style="color:#66d9ef">return</span>
		}
	}
	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;usage: %s [sub]&#34;</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">0</span>])
}</code></pre></div>
<h4 id="shell-描述-1">Shell 描述</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/usr/bin/env bash
</span><span style="color:#75715e"></span>
<span style="color:#75715e"># sudo ./src/shell/01-namespace/01-mount/pivot_root/main.sh</span>

new_root<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;data/busybox/rootfs&#34;</span>
script<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ls / &amp;&amp; ls /bin&#34;</span>

<span style="color:#75715e"># unshare -m: 创建新进程，并为该进程创建一个 Mount Namespace（-m）</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html\</span>
<span style="color:#75715e"># 注意 unshare 会自动取消进程的所有共享，因此不需要手动执行：mount --make-rprivate /</span>
<span style="color:#75715e"># 更多参见：https://man7.org/linux/man-pages/man1/unshare.1.html 的 --propagation 参数说明</span>

<span style="color:#75715e"># mount --bind: 确保 new_root 是一个挂载点</span>
<span style="color:#75715e"># cd $new_root: 确保 working directory 是新的 rootfs</span>
<span style="color:#75715e"># pivot_root: 切换 rootfs</span>
<span style="color:#75715e"># cd /: 根目录已经切换了，所以之前的工作目录已经不存在了，所以需要将 working directory 切换到根目录</span>
unshare -m /bin/bash -c <span style="color:#e6db74">&#34;mount --bind </span>$new_root<span style="color:#e6db74"> </span>$new_root<span style="color:#e6db74"> \
</span><span style="color:#e6db74">	&amp;&amp; cd </span>$new_root<span style="color:#e6db74"> \
</span><span style="color:#e6db74">	&amp;&amp; pivot_root . . \
</span><span style="color:#e6db74">	&amp;&amp; cd / \
</span><span style="color:#e6db74">	&amp;&amp; echo &#39;=== new mount namespace and pivot_root process ===&#39; \
</span><span style="color:#e6db74">	&amp;&amp; /bin/sh -xc \&#34;</span>$script<span style="color:#e6db74">\&#34;&#34;</span> &amp;
pid1<span style="color:#f92672">=</span>$!

wait $pid1</code></pre></div>
<h3 id="输出及分析-1">输出及分析</h3>

<p>按照代码上方注释，编译并运行，输出形如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">=== new mount namespace and pivot_root process ===
+ ls /
README  bin
+ ls /bin
busybox  ls       sh</pre></div>
<p>可以看出根目录已经切换了。</p>
]]></description></item><item><title>容器核心技术（二） Namespace 概述</title><link>https://www.rectcircle.cn/posts/container-core-tech-2-namespace-overview/</link><pubDate>Thu, 10 Mar 2022 22:30:01 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-2-namespace-overview/</guid><description type="html"><![CDATA[

<h2 id="全局系统资源">全局系统资源</h2>

<p>操作系统的一个主要作用就是将硬件抽象成一个个可以操作的资源给上层应用使用。这些资源可以简单分为两类：</p>

<ul>
<li>独占资源：如页表、内存空间（堆、栈）、寄存器、CPU 时间片等，这些资源的是按照进程隔离，在进程看来这些资源都是自己独占的。</li>
<li>全局资源：如网络、文件系统、设备等，这些资源的特性是在进程间共享的，不同进程的操作会影响到其他进程。</li>
</ul>

<p>全局系统资源给进程带来相互通讯协调的能力，但是也带来一些问题，即进程间相互影响。</p>

<h2 id="namespace-列表">Namespace 列表</h2>

<p>而 Namespace 就是 Linux 提供的一种对全局系统资源进程分组隔离的机制，即：同一个 Namespace 的进程看到的全局系统资源是共享的，而不同 Namespace 的进程全局系统资源是隔离的。截止到 Linux Kernel 5.6，Linux 提供了 8 种<a href="https://man7.org/linux/man-pages/man7/namespaces.7.html#DESCRIPTION">全局资源的 Namespace</a> ：</p>

<table>
<thead>
<tr>
<th>Namespace</th>
<th>Flag</th>
<th>man 手册</th>
<th>内核版本</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>Mount</td>
<td>CLONE_NEWNS</td>
<td><a href="https://man7.org/linux/man-pages/man7/mount_namespaces.7.html">mount_namespaces(7)</a></td>
<td>Kernel 2.4.19, 2002</td>
<td>挂载命名空间（mount namespaces），隔离挂载点等信息，子挂载命名空间的挂载不会向上传递到父挂载命名空间，是 Linux 内核历史上第一个命名空间的概念。</td>
</tr>

<tr>
<td>UTS</td>
<td>CLONE_NEWUTS</td>
<td><a href="https://man7.org/linux/man-pages/man7/uts_namespaces.7.html">uts_namespaces(7)</a></td>
<td>Kernel 2.6.19, 2006</td>
<td>Unix 主机命名空间（UTS namespaces, UNIX Time-Sharing），隔离主机名与域名等信息，不同的 UTS 命名空间可以拥有不同的主机名，在网络上呈现为多个主机。</td>
</tr>

<tr>
<td>IPC</td>
<td>CLONE_NEWIPC</td>
<td><a href="https://man7.org/linux/man-pages/man7/ipc_namespaces.7.html">ipc_namespaces(7)</a></td>
<td>Kernel 2.6.19, 2006</td>
<td>进程间通信命名空间（IPC namespaces, Inter-Process Communication），隔离 System V IPC，不同 IPC 命名空间中的进程不能使用传统的 System V 风格的进程间通信方式，如共享内存（SHM）等。</td>
</tr>

<tr>
<td>PID</td>
<td>CLONE_NEWNET</td>
<td><a href="https://man7.org/linux/man-pages/man7/pid_namespaces.7.html">pid_namespaces(7)</a></td>
<td>Kernel 2.6.24, 2008</td>
<td>进程 ID 命名空间（PID namespaces），隔离进程的 PID 空间，不同的 PID 命名空间中的 PID 可以重复，互不影响。</td>
</tr>

<tr>
<td>Network</td>
<td>CLONE_NEWNET</td>
<td><a href="https://man7.org/linux/man-pages/man7/network_namespaces.7.html">network_namespaces(7)</a></td>
<td>Kernel 2.6.29, 2009</td>
<td>网络命名空间（network namespaces），虚拟化一个完整的网络栈，每个网络栈拥有一套完整的网络资源，包括网络设备（interfaces）、路由表与防火墙等。与其他命名空间不同，网络命名空间没有层次结构，所有的网络命名空间互相独立，每个进程只能属于一个网络命名空间，并且网络命名空间在没有进程属于它的时候不会自动消失。</td>
</tr>

<tr>
<td>User</td>
<td>CLONE_NEWUSER</td>
<td><a href="https://man7.org/linux/man-pages/man7/user_namespaces.7.html">user_namespaces(7)</a></td>
<td>Kernel 3.8, 2013</td>
<td>用户命名空间（user namespaces），隔离用户与组信息，子用户命名空间中的每个用户和组（UID / GID）均映射到父用户命名空间中的一个用户和组，提供一种更好的权限隔离方式。通过将容器中的 root 用户映射到主机上的一个非特权用户，可以提升容器的安全性，这也是 LXC / LXD 实现「非特权容器」的方法。</td>
</tr>

<tr>
<td>Cgroup</td>
<td>CLONE_NEWCGROUP</td>
<td><a href="https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html">cgroup_namespaces(7)</a></td>
<td>Kernel 4.6, 2016</td>
<td>Cgroup 命名空间，类似 chroot，隔离 cgroup 层次结构，子命名空间看到的根 cgroup 结构实际上是父命名空间的一个子树。</td>
</tr>

<tr>
<td>Time</td>
<td>CLONE_NEWTIME</td>
<td><a href="https://man7.org/linux/man-pages/man7/time_namespaces.7.html">time_namespaces(7)</a></td>
<td>Kernel 5.6, 2020</td>
<td>系统时间命名空间，与 UTS 命名空间类似，允许不同的进程看到不同的系统时间。</td>
</tr>
</tbody>
</table>

<h2 id="linux-创建进程">Linux 创建进程</h2>

<p>在 Linux 中，创建进程众所周知的就是 <code>fork</code> 函数。实际上，创建进程的库函数有：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/fork.2.html"><code>fork</code> 函数</a>：通过复制当前进程的方式，创建一个新进程，返回新进程的进程 ID，父进程返回 0。<a href="https://man7.org/linux/man-pages/man2/fork.2.html#NOTES">注意</a>：

<ul>
<li>页表会进行全量复制，内存写时复制。</li>
<li>Linux kernel 2.3.3 之前，fork 是一个系统调用包装</li>
<li>Linux kernel 2.3.3 之后，fork 只是一个 glibc 的库函数，最终调用 <code>clone</code> 系统调用（使用 <code>SIGCHLD</code> 标志）</li>
</ul></li>
<li><a href="https://man7.org/linux/man-pages/man2/vfork.2.html"><code>vfork</code> 函数</a>：类似于 <code>fork</code> 性能略优于 <code>fork</code>，不会复制页表。编写跨 Unix 平台程序时，不建议使用。<a href="https://man7.org/linux/man-pages/man2/vfork.2.html">注意</a>：

<ul>
<li>不会复制页表，因此新进程不应该修改内存而是直接调用 <code>exec</code> 相关函数</li>
<li>在 Linux 中 <code>vfork</code> 不是一个系统调用，只是一个 glibc 的库函数，最终调用 <code>clone</code> 系统调用（使用 <code>CLONE_VM | CLONE_VFORK | SIGCHLD</code> 标志）</li>
</ul></li>
<li><a href="https://man7.org/linux/man-pages/man2/clone.2.html"><code>clone</code> 函数</a>：创建一个新进程（线程），与 <code>fork</code> 和 <code>vfork</code> 相比：

<ul>
<li>可以更精确的控制，哪些执行上下文在之间共享，可以做到 <code>fork</code>、<code>vfork</code>、<code>pthread_create</code> 类似的效果</li>
<li>可以控制进程的<strong>Namespace</strong>（容器的核心技术）
需要注意的是：</li>
<li><a href="https://man7.org/linux/man-pages/man2/clone.2.html"><code>clone</code> 函数</a> 是一个 glibc 函数，其也是 <code>clone</code> 系统调用的封装。</li>
<li><code>clone</code> 系统调用本身并不接受一个函数指针作为参数，其真实声明类似于，<code>long clone(unsigned long flags, void *child_stack, void *ptid, void *ctid, struct pt_regs *regs);</code>，参见：<a href="https://stackoverflow.com/a/18904917">stackoverflow</a>。</li>
<li>在 Linux 5.3 之后，<a href="https://man7.org/linux/man-pages/man2/clone.2.html#VERSIONS"><code>clone</code> 函数</a> 开始使用 clone3 系统调用</li>
<li>编写跨 Unix 平台程序时，不建议使用。</li>
</ul></li>
</ul>

<p>Namespace 实际上就是 Linux 在进程层面提供的一系列对全局系统资源进行隔离的机制。</p>

<h2 id="系统调用和命令">系统调用和命令</h2>

<p>Namespace 在 Linux 中是进程的属性和进程组紧密相关：一个进程的 Namespace 默认是和其父进程保持一致的。Linux 提供了几个系统调用，来创建、加入观察 Namespace：</p>

<ul>
<li>创建：通过 <a href="https://man7.org/linux/man-pages/man2/clone.2.html"><code>clone(2) 系统调用</code></a>的 flag 来为<strong>新创建的进程</strong>创建新的 Namespace</li>
<li>加入：通过 <a href="https://man7.org/linux/man-pages/man2/setns.2.html"><code>setns(2) 系统调用</code></a>将<strong>当前线程</strong>（注意当前进程不允许有多个线程）加入某个其他进程的 Namespace，<code>docker exec</code> 就是通过这个系统调用实现的（PID Namespace 是个例外，参见后续文章）</li>
<li>创建：通过 <a href="https://man7.org/linux/man-pages/man2/unshare.2.html"><code>unshare(2) 系统调用</code></a>为<strong>当前进程</strong>创建新的 Namespace（PID Namespace 是个例外，参见后续文章）</li>
<li>查看：通过 <a href="https://man7.org/linux/man-pages/man2/ioctl_ns.2.html"><code>ioctl_ns(2) 系统调用</code></a>来查看命名空间的关系（主要是 user namespace 和 pid namespace）</li>
</ul>

<p>除了系统调用外，Linux 也提供了相应的命令来创建、加入 Namespace：</p>

<ul>
<li>创建：通过 <a href="https://man7.org/linux/man-pages/man1/unshare.1.html"><code>unshare(1) 命令</code></a>启动一个进程，然后再为该进程，创建新的 Namespace（PID Namespace 是个例外，参见后续文章），该命令的实现为：先调用 <a href="https://man7.org/linux/man-pages/man2/unshare.2.html"><code>unshare(2) 系统调用</code></a>，然后 <strong><code>exec</code></strong> 执行命令</li>
<li>加入：通过 <a href="https://man7.org/linux/man-pages/man1/nsenter.1.html"><code>nsenter(1) 命令</code></a>启动一个进程，然后再将该进程，加入一个 Namespace（PID Namespace 是个例外，参见后续文章），该命令的实现为：先调用 <a href="https://man7.org/linux/man-pages/man2/unshare.2.html"><code>setns(2) 系统调用</code></a>，然后 <strong><code>fork-exec</code></strong> 执行命令</li>
</ul>

<h2 id="官方手册">官方手册</h2>

<p>关于 Namespace 的描述，Linux 手册非常详细的手册说明：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man7/namespaces.7.html">namespaces(7)</a> - 整体描述</li>
<li><a href="https://man7.org/linux/man-pages/man7/mount_namespaces.7.html">mount_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/uts_namespaces.7.html">uts_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/ipc_namespaces.7.html">ipc_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/pid_namespaces.7.html">pid_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/network_namespaces.7.html">network_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/user_namespaces.7.html">user_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html">cgroup_namespaces(7)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/time_namespaces.7.html">time_namespaces(7)</a></li>
</ul>

<h2 id="实验说明">实验说明</h2>

<p>后续文章，将以 Go 语言、 C 语言、Shell 命令三种形式，来介绍这些 Namespace。实验环境说明参见：<a href="/posts/container-core-tech-1-experiment-preparation-and-linux-base">容器核心技术（一） 实验环境准备 &amp; Linux 基础知识</a></p>
]]></description></item><item><title>Maven Repositories 和 Mirrors</title><link>https://www.rectcircle.cn/posts/maven-repositories-and-mirrors/</link><pubDate>Wed, 02 Mar 2022 20:25:37 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/maven-repositories-and-mirrors/</guid><description type="html"><![CDATA[

<h2 id="下载流程">下载流程</h2>

<p>Maven 下载依赖的过程中，有两个配置项：Repositories 和 Mirrors。Repositories 是可以在项目粒度的 <code>pom.xml</code> 中配置的，而 Mirrors 只能在 <code>settings.xml</code> 中配置。理解 Maven 下载流程，这两个配置的作用就不言自明了。</p>

<ul>
<li>Maven 收集项目 POM 中的 Dependency，发起下载 Dependency 的流程，并递归这个过程直至所有 Dependency 都下载完成</li>
<li>下载一个 Dependency 的首先根据配置和项目 POM，准备好两个列表：

<ul>
<li>Repositories 列表（最后总会添加默认的 id 为 central 的仓库 <code>https://repo.maven.apache.org/maven2</code>）</li>
<li>Mirrors 列表</li>
</ul></li>
<li>遍历 Repositories 列表中的

<ul>
<li>针对每一项 Repository

<ul>
<li>遍历 Mirrors 中的每一项，并匹配 MirrorOf 属性（Repository ID 匹配 MirrorOf），返回第一个匹配的 Mirror</li>
<li>如果存在匹配的 Mirror，则向 Mirror 配置的 URL 发起请求</li>
<li>如果不存在匹配的 Mirror，则向 Repository 配置的 URL 发起请求</li>
</ul></li>
<li>如果 URL 下载成功，则完成 Dependency 的下载</li>
<li>否则，继续遍历下一个 Repository</li>
</ul></li>
<li>如果遍历所有的 Repositories 列表都没有下载成功，则直接失败</li>
</ul>

<p>代码解析参见： <a href="https://www.cnblogs.com/ctxsdhy/p/8482725.html">https://www.cnblogs.com/ctxsdhy/p/8482725.html</a></p>

<h2 id="http-block-问题">http block 问题</h2>

<p>自 Maven 3.8.1 起，maven 禁止了对 Repository URL 为 http 协议的 Repository 进行下载。</p>

<p>禁止的原理就是利用 Mirror 特性实现的，Maven 3.8.1 起，其默认配置文件（<code>$MAVEN_HOME/conf/settings.xml</code>）添加如下配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">  <span style="color:#f92672">&lt;mirrors&gt;</span>
    <span style="color:#f92672">&lt;mirror&gt;</span>
      <span style="color:#f92672">&lt;id&gt;</span>maven-default-http-blocker<span style="color:#f92672">&lt;/id&gt;</span>
      <span style="color:#f92672">&lt;mirrorOf&gt;</span>external:http:*<span style="color:#f92672">&lt;/mirrorOf&gt;</span>
      <span style="color:#f92672">&lt;name&gt;</span>Pseudo repository to mirror external repositories initially using HTTP.<span style="color:#f92672">&lt;/name&gt;</span>
      <span style="color:#f92672">&lt;url&gt;</span>http://0.0.0.0/<span style="color:#f92672">&lt;/url&gt;</span>
      <span style="color:#f92672">&lt;blocked&gt;</span>true<span style="color:#f92672">&lt;/blocked&gt;</span>
    <span style="color:#f92672">&lt;/mirror&gt;</span>
  <span style="color:#f92672">&lt;/mirrors&gt;</span></code></pre></div>
<p>根据上文提到的下载流程，可以看出，所有 http Repository 都会匹配到该 mirror，然而当前 mirror 又配置了 blocked 为 true，则直接下载失败。</p>

<p>解决的办法为：</p>

<ol>
<li>不使用 http，全部切换为 https</li>

<li><p>每添加一个 http 的 Repository，都需要在用户配置（<code>~/.m2/settings.xml</code> 该文件优先级高于默认配置文件） 中添加配置：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;mirrors&gt;</span>
<span style="color:#f92672">&lt;mirror&gt;</span>
  <span style="color:#f92672">&lt;id&gt;</span>xxx-mirror<span style="color:#f92672">&lt;/id&gt;</span>
  <span style="color:#f92672">&lt;mirrorOf&gt;</span>xxx-repo<span style="color:#f92672">&lt;/mirrorOf&gt;</span> <span style="color:#75715e">&lt;!-- mirrorOf 需要匹配 --&gt;</span>
  <span style="color:#f92672">&lt;url&gt;</span>http://maven.aliyun.com/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
  <span style="color:#75715e">&lt;!-- &lt;url&gt;https://repo.maven.apache.org/maven2&lt;/url&gt; --&gt;</span>
<span style="color:#f92672">&lt;/mirror&gt;</span>
<span style="color:#f92672">&lt;/mirrors&gt;</span></code></pre></div></li>
</ol>

<h2 id="关于-mirrorof">关于 <code>mirrorOf</code></h2>

<p>结合上文下载流程。常见的配置为：</p>

<ul>
<li><code>*</code> 永远匹配，不建议使用</li>
<li><code>central</code> maven 中心仓库 <code>https://repo.maven.apache.org/maven2</code> （不包含各种安卓相关的依赖）</li>
</ul>

<h2 id="最佳配置">最佳配置</h2>

<p>不建议使用 <code>mirror</code> ，<code>mirror</code> 很容易出问题。</p>

<h3 id="场景-1-仅包含开源依赖">场景 1：仅包含开源依赖</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;settings&gt;</span>
    <span style="color:#f92672">&lt;profiles&gt;</span>
        <span style="color:#f92672">&lt;profile&gt;</span>
            <span style="color:#f92672">&lt;id&gt;</span>personal-repos<span style="color:#f92672">&lt;/id&gt;</span>
            <span style="color:#f92672">&lt;repositories&gt;</span>
                <span style="color:#f92672">&lt;repository&gt;</span>
                    <span style="color:#f92672">&lt;id&gt;</span>aliyun-public<span style="color:#f92672">&lt;/id&gt;</span>
                    <span style="color:#f92672">&lt;url&gt;</span>https://maven.aliyun.com/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
                <span style="color:#f92672">&lt;/repository&gt;</span>
            <span style="color:#f92672">&lt;/repositories&gt;</span>
            <span style="color:#75715e">&lt;!--  插件拉取地址  --&gt;</span>
            <span style="color:#f92672">&lt;pluginRepositories&gt;</span>
                <span style="color:#f92672">&lt;pluginRepository&gt;</span>
                    <span style="color:#f92672">&lt;id&gt;</span>aliyun-plugin<span style="color:#f92672">&lt;/id&gt;</span>
                    <span style="color:#f92672">&lt;url&gt;</span>https://maven.aliyun.com/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
                <span style="color:#f92672">&lt;/pluginRepository&gt;</span>
            <span style="color:#f92672">&lt;/pluginRepositories&gt;</span>
        <span style="color:#f92672">&lt;/profile&gt;</span>
    <span style="color:#f92672">&lt;/profiles&gt;</span>

    <span style="color:#f92672">&lt;activeProfiles&gt;</span>
        <span style="color:#f92672">&lt;activeProfile&gt;</span>personal-repos<span style="color:#f92672">&lt;/activeProfile&gt;</span>
    <span style="color:#f92672">&lt;/activeProfiles&gt;</span>
<span style="color:#f92672">&lt;/settings&gt;</span></code></pre></div>
<h3 id="场景-2-内网私有仓库">场景 2：内网私有仓库</h3>

<p>假设某公司：</p>

<ul>
<li>部署了一个有私有的 Maven 仓库，该私有仓库只包含公司内部的依赖，不包含公网开源的依赖。</li>

<li><p>主要研发人员位于国内</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;settings&gt;</span>
<span style="color:#f92672">&lt;profiles&gt;</span>
    <span style="color:#f92672">&lt;profile&gt;</span>
        <span style="color:#f92672">&lt;id&gt;</span>xxx-repos<span style="color:#f92672">&lt;/id&gt;</span>
        <span style="color:#f92672">&lt;repositories&gt;</span>
            <span style="color:#f92672">&lt;repository&gt;</span>
                <span style="color:#f92672">&lt;id&gt;</span>xxx-public<span style="color:#f92672">&lt;/id&gt;</span>
                <span style="color:#f92672">&lt;url&gt;</span>https://xxx/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
                <span style="color:#75715e">&lt;!-- 如果是 http，则需要在配置 mirror --&gt;</span>
                <span style="color:#75715e">&lt;!-- &lt;url&gt;http://xxx/repository/public&lt;/url&gt; --&gt;</span>
            <span style="color:#f92672">&lt;/repository&gt;</span>
            <span style="color:#f92672">&lt;repository&gt;</span>
                <span style="color:#f92672">&lt;id&gt;</span>aliyun-public<span style="color:#f92672">&lt;/id&gt;</span>
                <span style="color:#f92672">&lt;url&gt;</span>https://maven.aliyun.com/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
            <span style="color:#f92672">&lt;/repository&gt;</span>
        <span style="color:#f92672">&lt;/repositories&gt;</span>
        <span style="color:#75715e">&lt;!--  插件拉取地址  --&gt;</span>
        <span style="color:#f92672">&lt;pluginRepositories&gt;</span>
            <span style="color:#f92672">&lt;pluginRepository&gt;</span>
                <span style="color:#f92672">&lt;id&gt;</span>xxx-plugin<span style="color:#f92672">&lt;/id&gt;</span>
                <span style="color:#f92672">&lt;url&gt;</span>https://xxx/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
            <span style="color:#f92672">&lt;/pluginRepository&gt;</span>
            <span style="color:#f92672">&lt;pluginRepository&gt;</span>
                <span style="color:#f92672">&lt;id&gt;</span>aliyun-plugin<span style="color:#f92672">&lt;/id&gt;</span>
                <span style="color:#f92672">&lt;url&gt;</span>https://maven.aliyun.com/repository/public<span style="color:#f92672">&lt;/url&gt;</span>
            <span style="color:#f92672">&lt;/pluginRepository&gt;</span>
        <span style="color:#f92672">&lt;/pluginRepositories&gt;</span>
    <span style="color:#f92672">&lt;/profile&gt;</span>
<span style="color:#f92672">&lt;/profiles&gt;</span>

<span style="color:#f92672">&lt;activeProfiles&gt;</span>
    <span style="color:#f92672">&lt;activeProfile&gt;</span>xxx-repos<span style="color:#f92672">&lt;/activeProfile&gt;</span>
<span style="color:#f92672">&lt;/activeProfiles&gt;</span>

<span style="color:#f92672">&lt;mirrors&gt;</span>
    <span style="color:#75715e">&lt;!-- 如果内网的 URL 只有 http 协议的话，需要添加这个配置 --&gt;</span>
    <span style="color:#75715e">&lt;!-- 其中 mirrorOf 为对应的 Repository 的 ID --&gt;</span>
    <span style="color:#75715e">&lt;!-- &lt;mirror&gt;
</span><span style="color:#75715e">        &lt;id&gt;xxx-mirror&lt;/id&gt;
</span><span style="color:#75715e">        &lt;mirrorOf&gt;xxx-public&lt;/mirrorOf&gt;
</span><span style="color:#75715e">        &lt;url&gt;http://xxx/repository/public&lt;/url&gt;
</span><span style="color:#75715e">    &lt;/mirror&gt; --&gt;</span>
<span style="color:#f92672">&lt;/mirrors&gt;</span>
<span style="color:#f92672">&lt;/settings&gt;</span></code></pre></div></li>
</ul>
]]></description></item><item><title>容器核心技术（一）概述 &amp; 实验环境准备 &amp; 基础概念</title><link>https://www.rectcircle.cn/posts/container-core-tech-1-experiment-preparation-and-linux-base/</link><pubDate>Fri, 11 Feb 2022 20:17:40 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/container-core-tech-1-experiment-preparation-and-linux-base/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>本系列主要介绍的是：容器化技术涉及的 Linux 系统调用，如 Namespace、网络设备、cgroup 等。</p>

<p>主要参考： <a href="https://man7.org/">man7.org</a> 站点、<a href="https://weread.qq.com/web/bookDetail/a8932240721e42b5a89f479">《自己动手写 Docker》</a>、<a href="http://www.apuebook.com/">《UNIX环境高级编程（第3版）》</a> 以及部分 Docker 文档和源码。</p>

<h2 id="实验环境准备">实验环境准备</h2>

<p>为了更好的实验容器核心技术，通过虚拟机（VirtualBox）安装一个纯净的 Linux 系统环境（Debian 11）。</p>

<ul>
<li>下载安装 <a href="https://www.virtualbox.org/">VirtualBox</a>，（Mac 安装或更新后，需打开系统偏好设置 -&gt; 安全 -&gt; 安全性与隐私，解锁并重启）</li>
<li>下载 <a href="https://www.debian.org/">Debian 11</a> ISO 镜像</li>
<li>VirtualBox 最小化安装 Debian 11

<ul>
<li>点击新建（名称：Debian11-base，类型：Linux，版本：Debian），一路 Next 即可</li>
<li>配置网络，让宿主机可以访问虚拟机

<ul>
<li>工具右侧图标 -&gt; 网络 -&gt; 创建 -&gt; 启用 DHCP -&gt; 引用</li>
<li>Debian11-base -&gt; 设置 -&gt; 网络 -&gt; 网卡 2 -&gt; 启用（连接方式：Host Only，界面名称：上一步创建的） -&gt; OK</li>
</ul></li>
<li>点击设置 -&gt; 存储 -&gt; 没有盘片 -&gt; 分配光驱旁边的光盘 -&gt; 选择虚拟盘，选择上一步下载好的 Debian 11 -&gt; OK</li>
<li>点击启动，进入<a href="https://www.debian.org/releases/stable/amd64/ch06s03.zh-cn.html">系统安装</a>，注意：

<ul>
<li>语言时区等可以选择中文</li>
<li>配置网络：选择 <code>enpo0s3</code> 作为主网络</li>
<li>磁盘默认即可（最后一步确认，选择是）</li>
<li>apt 源可以选择华为云</li>
<li>选择并安装软件下载步骤可能很慢，要花费 30 分钟左右（解决办法可以参考：<a href="https://www.bilibili.com/video/av74615315/">B 站视频</a>）</li>
<li>选择和安装软件：桌面环境不需要安装，只需选择，SSH Server 和 标准系统工具</li>
</ul></li>
</ul></li>

<li><p>配置虚拟机系统内部网络</p>

<ul>
<li>登录到 root 账号</li>

<li><p>执行如下命令</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo <span style="color:#e6db74">&#39;auto enpo0s8&#39;</span> &gt;&gt; /etc/network/interfaces
echo <span style="color:#e6db74">&#39;iface enpo0s8 inet dhcp&#39;</span> &gt;&gt; /etc/network/interfaces
systemctl restart networking
ip a <span style="color:#75715e"># 查看 IP 是 192.168.56.xxx</span></code></pre></div></li>
</ul></li>

<li><p>宿主机执行 <code>ssh 普通用户名@192.168.56.xxx</code> 输入密码登录</p></li>

<li><p>安装必备环境（常见命令以及 C 和 Golang 开发环境）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">su root
apt install sudo wget curl vim psmisc
echo &#39;普通用户名 ALL=(ALL) NOPASSWD:ALL&#39; /etc/sudoers.d/myuser
apt install build-essential gdb
wget https://go.dev/dl/go1.17.7.linux-amd64.tar.gz
tar -zxvf go1.17.7.linux-amd64.tar.gz -C /usr/local/
echo &#39;export GOROOT=/usr/local/go&#39; &gt;&gt; /etc/profile
echo &#39;export PATH=/usr/local/go/bin:$PATH&#39; &gt;&gt; /etc/profile</pre></div></li>

<li><p>查看 系统版本（内核、gcc、glibc、go）</p>

<ul>
<li>内核版本 <code>uname -r</code>： <code>5.10.0-11-amd64</code></li>
<li>gcc 版本 <code>gcc -v</code>： <code>gcc version 10.2.1 20210110 (Debian 10.2.1-6)</code></li>
<li>glibc 版本 <code>ldd --version</code>：<code>ldd (Debian GLIBC 2.31-13+deb11u2) 2.31</code></li>
</ul></li>

<li><p>停止虚拟机 Debian11-base，复制一份 <code>Debian11-exp01</code> 来做实验，防止把环境搞坏了还要重新从头安装</p></li>
</ul>

<h2 id="实验代码库">实验代码库</h2>

<p>本系列实验代码库位于：<a href="https://github.com/rectcircle/container-core-tech-experiment">rectcircle/container-core-tech-experiment</a></p>

<h2 id="系统调用-库函数和工具">系统调用、库函数和工具</h2>

<p>学习 Linux 可以分两个比较独立的两个层面，其一 Linux 内核层，其二 Linux 应用层。本系列仅涉及 Linux 应用层相关内容。</p>

<p>Linux 是一个操作系统平台，其在应用层提供了多种能力，从底层到上层分别为：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/dir_section_2.html">系统调用</a>（有限数量，约 500 左右）。这些系统调用太过于底层，调用时，需要使用汇编设置寄存器的方式调用。在 Linux 的 <a href="https://man7.org/linux/man-pages/index.html">man 文档</a>中，其描述方式也是通过 glibc 的包装函数描述的</li>
<li>库函数（各种编程语言不同，同一种编程语言也有不同的实现）

<ul>
<li>其中 <a href="https://man7.org/linux/man-pages/dir_section_3.html">C 语言的库函数</a> 是最常见的，C 语言的库函数称为 <a href="https://man7.org/linux/man-pages/man7/libc.7.html">libc</a> （standard C library），Linux 上的 libc 实现有多种，主流的有：

<ul>
<li><a href="http://www.gnu.org/software/libc/">glibc</a> （文件名为：<code>libc.so.6</code>） 最广泛使用的大而全的 libc 实现， 主流的生产环境 Linux 发行版多数使用该版本，如 Debian 系、Red Hat 系。主要包含如下 API

<ul>
<li>Linux 系统调用的包装</li>
<li>常见 Unix API：BSD， OS-specific</li>
<li>各种 C 语言国际标准的实现：<a href="https://en.wikipedia.org/wiki/C11_(C_standard_revision)">ISO C11</a>, <a href="https://en.wikipedia.org/wiki/POSIX">POSIX.1-2008（可移植操作系统接口）</a>, <a href="https://en.wikipedia.org/wiki/IEEE_754-2008_revision">IEEE 754-2008（浮点数）</a></li>
</ul></li>
<li><a href="http://musl.libc.org/">musl libc</a> 轻量级库，在容器环境广泛使用，使用该 libc 的发行版有 <a href="https://zh.wikipedia.org/wiki/Alpine_Linux">Alpine Linux</a></li>
</ul></li>
<li>其他高级语言的标准库一般也会有对操作系统系统调用进行封装，但高级语言更在乎抽象和跨平台能力，一般只会封装多个操作系统都存在的部分，而不是只针对 Linux。</li>
</ul></li>
<li><a href="https://man7.org/linux/man-pages/dir_section_1.html">命令</a>，<code>*nix</code> 类操作系统，一般都会提供常用的命令行工具。这些命令行工具，可以由任意编程语言编写，一般都会通过对库函数的调用来实现某些特殊功能。</li>
</ul>

<p>以上这些，系统调用是最稳定的，是 Linux 的本质。而库函数是经过封装的，且不同编程语言的封装方式，用法还不一样。因此作为 Linux 应用层的学习者，最应该重视应该是系统调用。</p>

<p>作为 Linux 应用层学习者，应该自发的将学习的知识划分到以上三种层次的一个层次中，然后联系相关其他层的知识，构建一张完备的知识网络。</p>

<p>下面有个关于 Linux 创建一个新进程并绑定 <a href="https://man7.org/linux/man-pages/man7/namespaces.7.html">Namespace</a> 相关：</p>

<ul>
<li><a href="https://man7.org/linux/man-pages/man2/clone.2.html">clone 系统调用</a></li>
<li>Go 语言的 <a href="https://pkg.go.dev/os/exec#Command">exec.Command</a> 和 <a href="https://pkg.go.dev/syscall#SysProcAttr">syscall.SysProAttr 的 Cloneflags</a> 相关库函数 （glibc 库函数 clone 和系统调用 clone 是同一篇文档，不便于展示区别，因此使用 Go 语言）</li>
<li><a href="https://man7.org/linux/man-pages/man1/unshare.1.html">unshare 命令</a></li>
</ul>

<p>Linux 的文档非常丰富，且组织良好。通过 <a href="https://man7.org/linux/man-pages/index.html">man 站点</a>可以查看最权威详实的文档。</p>
]]></description></item><item><title>数据库枚举类型应该选什么数据类型呢？</title><link>https://www.rectcircle.cn/posts/db-enum-storage/</link><pubDate>Fri, 14 Jan 2022 22:44:30 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/db-enum-storage/</guid><description type="html"><![CDATA[

<h2 id="背景">背景</h2>

<p>在某个 Go 后端项目中，一些枚举字段在程序中使用了 int8 类型定义。数据库中存储使用 tinyint。</p>

<p>但是随着时间的推移，读代码时通过常量名还能很容易的理解其含义。</p>

<p>但是查看数据库时，想要了解这些枚举值数字的含义，每次都需要去查阅代码中的常量定义。仔细分析，需要进行如下脑力消耗和操作：</p>

<ul>
<li>努力回忆这个数字的含义，一般需要消耗 3 ~ 5 秒时间，发现记不起来：</li>
<li>打开 ID，打开项目，等待 IDE 启动，一般要消耗 10 秒左右时间</li>
<li>一层一层的从众多代码文件中查找枚举的定义位置，一般要消耗 10 秒左右时间</li>
<li>一个一个数 <code>iota</code> 数到枚举值所定义的常量名，一般要消耗 1~5 秒钟时间</li>
</ul>

<p>这一个过程，浪费的毫无意义的时间暂且不提，对精力的消耗以及打断思路是非常让人恼火的。</p>

<p>除此之外，为了沟通方便，后端一般还要将数字类型转换为字符串类型返回给前端，这又带来了很多没有必要的转换相关的代码的开发和维护。</p>

<p>因此，我就萌生了，直接在项目的所有地方都使用字符串枚举类型的想法（主要在数据库）。</p>

<p>入行以来一直以来枚举使用数字类型似乎是一种无需辩驳的 “常识”，这个 “常识” 是对的吗？ 反过来想，“常识” 既然形成了一定是有其原因的，如果不加论证，为了解决一些问题突破 “常识”，可能带来的问题反而会更大。</p>

<p>因此，本文的目的是，通过搜索引擎和一些论证，验证字符串枚举类型在大多数场景下：其优点明显大于缺点 且 明显优于数字枚举类型。</p>

<p>注：</p>

<ol>
<li>本文仅讨论数据库枚举类型存储方案的两种方式：字符串和数字类型的优劣，不讨论什么时候需要将枚举抽象成表再辅以外键的方案，也不讨论使用 MySQL 原生 enum 类型。</li>
<li>作者虽然会努力的从客观的角度来论证，但是可以看出本文是预设了答案，然后搜寻证据的，所以难免有不客观的地方。请以自己理解为准，也欢迎讨论。</li>
<li>string 和 int 真得重要吗？我认为涉及到研发人员的工作效率（体现在可读性、可运维性）的内容，都是关系到研发人员切身利益的事情，不可谓不重要。</li>
</ol>

<h2 id="性能分析">性能分析</h2>

<p>在由 <a href="https://link.juejin.cn/?target=http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FDonald_Knuth">Donald Knuth</a> 在 1974 写的著名的 <a href="https://dl.acm.org/doi/10.1145/356635.356640">Structured Programming with go to Statements</a> 一文中提到了 “过早优化” 的问题：</p>

<blockquote>
<p>性能瓶颈（这个概念）被滥用已是不争事实。 开发者们浪费了大量的时间去思考它、担心它，（例如）非关键代码上的运行速度。这些对效率的苛求，给调试与维护造成了很大的负面影响。我们理应忽略那小部分的效率，就拿达到 97% 而言，过早的优化是万恶之源。</p>

<p>虽说我们不应放弃优化那 3%，一个好的程序员不会因为这个比例小就放弃（译：指在开发意识上，对于效率的高度追求），而是会明智地观察和识别哪些是关键的代码。</p>
</blockquote>

<p>使用数字类型而不是字符串类型的一个主要原因是：数字类型的性能好、字符串类型的性能差。本部分论证的是：</p>

<ul>
<li>数字类型的性能好、字符串类型的性能差，真得有那么显著吗？针对这一点，分别从时间和空间的角度分析。</li>
<li>另一方面，我们需要认识到，即使 string 的性能差一点，但是这真的会是系统的核心瓶颈吗？针对这下文会给出分析。</li>
</ul>

<h3 id="时间">时间</h3>

<p>在网络上可以搜到一些对 MySQL 的 int、char、varchar 的字段的时间效率上进行分析的报告，这里引用一下 <a href="https://segmentfault.com/a/1190000016408843">《MySQL中int、char、varchar的性能浅谈》</a> 这篇文章的结论：</p>

<ul>
<li>无索引：全表扫描不会因为数据较小就变快，而是整体速度相同，int/bigint作为原生类型稍快12%。</li>
<li>有索引：char与varchar性能差不多，int速度稍快18%</li>
</ul>

<p>由此可以看出，数字类型在时间上优势并不明显。</p>

<h3 id="空间">空间</h3>

<p>如果使用 tinyint 类型，则只占 1 个字节，而字符串类型则会占用空间是定义的枚举变量的字符串长度，一般 2~10 个字符即 2~10 字节，因此存储空间上：字符串类型空间占用是 tinyint 的 2~10 倍。</p>

<p>看起来快达到一个数量级了。但是，从如下两个角度来看，数字类型的优势并不明显</p>

<ul>
<li>枚举字段在一张表中的占比一般很小，如果一个表每 10 个字段有 1 个枚举字段，字符串类型增加的存储空间，在整张表中，也不会增加 10%。</li>
<li>在所有资源类型中，磁盘价格是最廉价的，在这个大数据时代，OLTP 系统所占磁盘的成本，在系统的所有成本中，占比基本可以忽略不计。而人力又是最贵的，为此浪费时间，简直是得不偿失。</li>
</ul>

<h3 id="性能核心瓶颈">性能核心瓶颈</h3>

<p>换个角度来说，即使在时间和空间上，字符串的性能都比数字类型若1个数量级。但是，这真得是系统的核心瓶颈吗？</p>

<p>这很难直接给出明确答案。但是，我们在优化系统性能的过程中，真得有考虑过，通过改把表字段的类型从字符串改为数字解决的吗（系统严重设计缺陷除外）？</p>

<h2 id="可维护性">可维护性</h2>

<p>总的来说，字符串能提供人类可读的信息，而数字本事是无意义的。</p>

<h3 id="数据库可读性">数据库可读性</h3>

<p>代码可读性的重要性，这是所有开发人员都能达成的共识。但是数据库是否需要可读性呢？我认为同样重要，甚至更重要。</p>

<p>软件工程理论学科已经给出了一个结论：软件维护阶段的成本在软件生命周期中是最高的。因此在设计可维护性是系统设计的重中之重，而数据库在软件维护阶段是比源代码更频繁打交道的对象。因此数据库可读性的从这个角度来看，比代码可读性还要重要。</p>

<p>因此，在数据库中使用数字类型的枚举，在数据库可读性上来说，简直是一场灾难。背景中已经详细描述了相关说明。</p>

<h3 id="源代码可读性">源代码可读性</h3>

<p>在 Go 语言中，不管使用字符串还是数字类型枚举，都会定义常量，源代码可读性两者没有区别。</p>

<h3 id="沟通学习成本">沟通学习成本</h3>

<p>使用 数字 类型，沟通成本会异常的高，主要原因是：对于需要理解这些枚举的人来说，都要记录/记忆数字和真实含义的对应关系，这样的学习成本和理解成本是累计起来是不可小觑的。</p>

<p>而对于 字符串 类型，其含义是不言自定的，只要单词准确，人的学习和沟通成本会降低很多。</p>

<h2 id="可扩展性">可扩展性</h2>

<p>枚举类型的可扩展性</p>

<h3 id="调整顺序">调整顺序</h3>

<p>数字类型枚举在信息熵提供额外的没必要的顺序性，很有可能会错误的依赖了这个顺序性，这极大的破坏扩展性。</p>

<p>随着业务的发展，可能需要需要在枚举类型中某值中间添加一个值。</p>

<p>如果我们使用的是数字类型，且枚举类型在业务上是有顺序的，这样我们就会得到一个数字顺序和业务顺序不一致的情况，这有如下两个问题：</p>

<ul>
<li>逼死强迫症</li>
<li>如果代码中有隐式依赖数字顺序的场景，比如  <code>enum &lt; 某这个值</code>，这样就要仔细的修改代码</li>
</ul>

<p>而字符串类型没有提供而外的顺序信息</p>

<h3 id="枚举值语义扩展-收缩和重定义">枚举值语义扩展、收缩和重定义</h3>

<p>枚举值使用数字一个可能的好处是，数字是无意义的，随着业务的发展，枚举值的意义可能发生变化。</p>

<p>具体分析下，数字类型可能出下如几种情况：</p>

<ul>
<li>语义扩展，该值比之前更多的含义，使用数字类型只需重命名常量命名即可</li>
<li>语义收缩，该值的含义比之前减少，使用数字类型只需重命名常量命名即可</li>
<li>重定义，该值的含义和之前的完全不一样，使用数字类型只需重命名常量命名即可</li>
</ul>

<p>确实，如果使用字符串类型，枚举值的语义已经被字符串的值绑定了，因此就没办法修改这个值的含义了，确实是限制了可扩展性。</p>

<p>但是，换个角度来向，更改原有的值的语义真得的是一个好的设计吗，好的设计应该符合不可变原则的。该类型是可以通过，废弃和新增枚举值来实现，只是这种方式开发成本可能会高一点，但是也更容易测试，发生事故的可能性会低一些。</p>

<h2 id="最终结论">最终结论</h2>

<p>从上文可以看出，采用字符串类型枚举，其优点明显大于缺点 且 明显优于数字枚举类型。</p>

<p>但是，还是分场景给结论。</p>

<h3 id="特殊场景">特殊场景</h3>

<p>如下特殊场景还需需要使用数字类型：</p>

<ul>
<li>高流量的 C 端业务（用户量至少千万）</li>
<li>业务极具变化，极度不稳定，不重视设计</li>
</ul>

<h3 id="多数场景">多数场景</h3>

<p>其他场景，建议直接使用 字符串 类型枚举。比如：ToB（企业） 或者 ToD（研发） 的系统。</p>

<h2 id="引用">引用</h2>

<ul>
<li><a href="https://segmentfault.com/q/1010000003709270">REST 设计困惑？Mysql 是直接存字符串好还是数字好？</a> （本文中的所有观点大多有所涉及，建议阅读）</li>
<li><a href="https://open.vanillaforums.com/discussion/26846/strings-or-integers-for-storing-types-in-database">Strings or Integers for Storing Types in Database</a></li>
<li><a href="https://stackoverflow.com/questions/25718121/varchar10-or-int-for-status-column-in-sql">Varchar(10) or int for Status Column in SQL</a> （阐述数字类型的性能优势）</li>
<li><a href="https://stackoverflow.com/questions/6335868/is-it-better-to-use-an-integer-or-a-string-to-for-a-data-status-indicator/6335909">Is it better to use an integer or a string to for a data status indicator?</a> （建议采用专门的枚举表）</li>
<li><a href="https://www.indiehackers.com/post/string-vs-integer-for-enum-columns-14e7a0c5a5">String vs Integer for enum columns</a> （目前 9 人投票，67% 选 string）</li>
<li><a href="https://stackoverflow.com/questions/1612319/best-way-to-store-enum-values-in-database-string-or-int">Best way to store enum values in database - String or Int</a> （客观评价的被采纳的 22 个顶，直接建议采用字符串的但未被采纳的确有 28 个顶）</li>
<li><a href="https://softwareengineering.stackexchange.com/questions/284530/why-store-flags-enums-in-a-database-as-strings-instead-of-integers">Why store flags/enums in a database as strings instead of integers?</a> （预设答案的提问，和本文表达的意思一致）</li>
<li><a href="https://stackoverflow.com/questions/229856/ways-to-save-enums-in-database">Ways to save enums in database</a> （被采纳的答案建议使用 string，并提到了顺序问题）</li>
<li><a href="https://juejin.cn/post/6844903573298364423">MySQL 枚举类型的“八宗罪”</a> （本文未讨论的部分）</li>
<li><a href="https://dl.acm.org/doi/10.1145/356635.356640">Structured Programming with go to Statements</a> （提到过早优化问题）</li>
<li><a href="https://segmentfault.com/a/1190000016408843">MySQL中int、char、varchar的性能浅谈</a></li>
<li><a href="https://stackoverflow.com/questions/14893005/using-integer-vs-string-for-a-type-value-database-and-class-design">Using Integer vs String for a &ldquo;type&rdquo; value (Database and class design)</a></li>
</ul>
]]></description></item><item><title>OCI 镜像格式规范</title><link>https://www.rectcircle.cn/posts/oci-image-spec/</link><pubDate>Sun, 02 Jan 2022 01:35:34 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/oci-image-spec/</guid><description type="html"><![CDATA[

<h2 id="概览">概览</h2>

<p><a href="https://github.com/opencontainers/image-spec">Image Format</a> 定义了容器镜像的格式，平时讲的 Docker 镜像就是基于该标准定义打包的。该标准的具体形式表现为，镜像的文件和目录结构。目前版本为 <a href="https://opencontainers.org/release-notices/v1-0-2-image-spec/">v1.0.2</a>。</p>

<h2 id="理解-oci-image-规范">理解 OCI Image 规范</h2>

<p>OCI Image 规范原文追求的是严密无歧义，但对于读者来确是不易理解。本部分以符合人类对新知识认知的角度来概述 OCI Image 规范。部分内容为作者个人理解，如有错误欢迎指正。</p>

<h3 id="观察-nginx-镜像">观察 Nginx 镜像</h3>

<p>通过例子入门一项新知识是比较好的方式。因此先观察一个符合 OCI 镜像标准的镜像。这里以 <code>nginx:1.21.6</code> 镜像为例。</p>

<h4 id="使用-skopeo-导出镜像">使用 skopeo 导出镜像</h4>

<p>skopeo 是一个镜像处理工具，可以将镜像导出到符合 OCI 镜像规范的目录中。</p>

<p>通过如下方式编译安装 skopeo（更多参见：<a href="https://github.com/containers/skopeo/blob/main/install.md">官方安装文档</a>）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt update
sudo apt install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config
git clone https://github.com/containers/skopeo $GOPATH/src/github.com/containers/skopeo
cd $GOPATH/src/github.com/containers/skopeo <span style="color:#f92672">&amp;&amp;</span> make bin/skopeo
make bin/skopeo
sudo cp ./bin/skopeo /usr/local/bin</code></pre></div>
<p>从公开的 docker hub 下载镜像，并以 OCI 标准镜像的格式保存到 <code>nginx-oci</code> 目录下。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">skopeo --insecure-policy copy docker://nginx:1.21.6 oci:<span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/nginx-oci:test1</code></pre></div>
<h4 id="观察镜像布局">观察镜像布局</h4>

<p><code>cd nginx-oci</code>，观察镜像的目录结构。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── blobs
│   └── sha256
│       ├── 091c283c6a66ad0edd2ab84cb10edacc00a1a7bc5277f5365c0d5c5457a75aff
│       ├── 1ae07ab881bd848493ad54c2ba32017f94d1d8dbfd0ba41b618f17e80f834a0f
│       ├── 3c1ab086329527de39b56d3ad05b2a5305217de87394aaecb1e2d54e76a76171
│       ├── 55de5851019b8f65ed6e28120c6300e35e556689d021e4b3411c7f4e90a9704b
│       ├── 5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803
│       ├── 78091884b7bea0fa918527207924e9993bcc21bf7f1c9687da40042ceca31ac9
│       ├── b559bad762bec166fd028483dd2a03f086d363ee827d8c98b7268112c508665a
│       └── f785f4dcb172012149aabfe31ac2bab3dce8e1e9b12b97883a02765e6e3be77a
├── index.json
└── oci-layout</pre></div>
<h4 id="blobs-目录"><code>blobs</code> 目录</h4>

<p>该目录存储的是镜像 manifest 文件、config 文件以及文件系统层文件。</p>

<ul>
<li>该目录下的文件路径规则为：<code>${hash算法}/${该文件使用该算法的校验和}</code>，在工业界使用的一般是 <code>sha256/&lt;sha256&gt;</code>。</li>
<li><code>index.json</code>、manifest 文件、config 文件中的 <code>digest</code> 字段是一个引用标识符，其指向的内容就是 <code>blobs</code> 目录下和 <code>digest</code> 字段相对应的文件。</li>
</ul>

<h4 id="oci-layout-文件"><code>oci-layout</code> 文件</h4>

<p><code>cat oci-layout</code> 镜像布局版本号，目前为 <code>1.0.0</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{&#34;imageLayoutVersion&#34;: &#34;1.0.0&#34;}</pre></div>
<h4 id="index-json-文件"><code>index.json</code> 文件</h4>

<p><code>cat index.json</code> ，<code>index.json</code> 即下文原文翻译中的 <a href="#镜像索引">镜像索引</a> 文件，主要包含了一个指向 Manifest 文件的引用的列表，格式化后内容为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
    <span style="color:#f92672">&#34;manifests&#34;</span>: [
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:f785f4dcb172012149aabfe31ac2bab3dce8e1e9b12b97883a02765e6e3be77a&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">1183</span>,
            <span style="color:#f92672">&#34;annotations&#34;</span>: {
                 <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">注意这个就是，在执行</span> <span style="color:#960050;background-color:#1e0010">skopeo</span> <span style="color:#960050;background-color:#1e0010">时，添加的</span> <span style="color:#960050;background-color:#1e0010">tag</span>
                 <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">oci:$(pwd)/nginx-oci:test1</span>
                <span style="color:#f92672">&#34;org.opencontainers.image.ref.name&#34;</span>: <span style="color:#e6db74">&#34;test1&#34;</span>
            }
        }
    ]
}</code></pre></div>
<h4 id="manifest-文件"><code>manifest</code> 文件</h4>

<p>从 <code>index.json</code> 文件，可以看到 <code>manifests</code> 的 <code>digest</code> 为 <code>sha256:f785f4dcb172012149aabfe31ac2bab3dce8e1e9b12b97883a02765e6e3be77a</code>，因此 <code>cat blobs/sha256/f785f4dcb172012149aabfe31ac2bab3dce8e1e9b12b97883a02765e6e3be77a</code> 即可看到 manifest 的内容。</p>

<p><a href="#镜像-manifest">镜像 Manifest</a> 文件，包含两个部分</p>

<ol>
<li>指向 <a href="#镜像配置">镜像配置</a> 文件的引用</li>
<li>指向 <a href="#镜像层文件系统变更集">文件系统层</a> 文件的引用</li>
</ol>

<p>格式化后内容为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
    <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
    <span style="color:#f92672">&#34;config&#34;</span>: {
        <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.config.v1+json&#34;</span>,
        <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:3c1ab086329527de39b56d3ad05b2a5305217de87394aaecb1e2d54e76a76171&#34;</span>,
        <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">6567</span>
    },
    <span style="color:#f92672">&#34;layers&#34;</span>: [
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">31366257</span>
        },
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:1ae07ab881bd848493ad54c2ba32017f94d1d8dbfd0ba41b618f17e80f834a0f&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">25352768</span>
        },
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:78091884b7bea0fa918527207924e9993bcc21bf7f1c9687da40042ceca31ac9&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">601</span>
        },
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:091c283c6a66ad0edd2ab84cb10edacc00a1a7bc5277f5365c0d5c5457a75aff&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">893</span>
        },
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:55de5851019b8f65ed6e28120c6300e35e556689d021e4b3411c7f4e90a9704b&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">666</span>
        },
        {
            <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
            <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:b559bad762bec166fd028483dd2a03f086d363ee827d8c98b7268112c508665a&#34;</span>,
            <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">1394</span>
        }
    ]
}</code></pre></div>
<h4 id="镜像配置文件">镜像配置文件</h4>

<p>从 <code>manifest</code> 内容，可以看到 <code>config</code> 的 <code>digest</code> 为 <code>sha256:3c1ab086329527de39b56d3ad05b2a5305217de87394aaecb1e2d54e76a76171</code>，因此 <code>cat blobs/sha256/3c1ab086329527de39b56d3ad05b2a5305217de87394aaecb1e2d54e76a76171</code> 即可看到 <code>config</code> 文件的内容。</p>

<p><a href="#镜像配置">镜像配置</a> 文件，该文件的字段很容易理解，一般都可以和 Dockerfile 中的某一些字段对应。</p>

<p>格式化后内容为：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:35.041664322Z&#34;</span>,
    <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>,
    <span style="color:#f92672">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>,
    <span style="color:#f92672">&#34;config&#34;</span>: {
        <span style="color:#f92672">&#34;ExposedPorts&#34;</span>: {
            <span style="color:#f92672">&#34;80/tcp&#34;</span>: {}
        },
        <span style="color:#f92672">&#34;Env&#34;</span>: [
            <span style="color:#e6db74">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span>,
            <span style="color:#e6db74">&#34;NGINX_VERSION=1.21.6&#34;</span>,
            <span style="color:#e6db74">&#34;NJS_VERSION=0.7.2&#34;</span>,
            <span style="color:#e6db74">&#34;PKG_RELEASE=1~bullseye&#34;</span>
        ],
        <span style="color:#f92672">&#34;Entrypoint&#34;</span>: [
            <span style="color:#e6db74">&#34;/docker-entrypoint.sh&#34;</span>
        ],
        <span style="color:#f92672">&#34;Cmd&#34;</span>: [
            <span style="color:#e6db74">&#34;nginx&#34;</span>,
            <span style="color:#e6db74">&#34;-g&#34;</span>,
            <span style="color:#e6db74">&#34;daemon off;&#34;</span>
        ],
        <span style="color:#f92672">&#34;Labels&#34;</span>: {
            <span style="color:#f92672">&#34;maintainer&#34;</span>: <span style="color:#e6db74">&#34;NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e&#34;</span>
        },
        <span style="color:#f92672">&#34;StopSignal&#34;</span>: <span style="color:#e6db74">&#34;SIGQUIT&#34;</span>
    },
    <span style="color:#f92672">&#34;rootfs&#34;</span>: {
        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;layers&#34;</span>,
        <span style="color:#f92672">&#34;diff_ids&#34;</span>: [
            <span style="color:#e6db74">&#34;sha256:7d0ebbe3f5d26c1b5ec4d5dbb6fe3205d7061f9735080b0162d550530328abd6&#34;</span>,
            <span style="color:#e6db74">&#34;sha256:9a3a6af98e18f06f2a233aa2b2374a5d83d3812e2784b0ab8db949f34cd1a7d6&#34;</span>,
            <span style="color:#e6db74">&#34;sha256:9a94c4a55fe4c8a5cfea7fbac1dde94c38973dbdab17a6314f0c8b35b68aba95&#34;</span>,
            <span style="color:#e6db74">&#34;sha256:6173b6fa63db8be9be756acf32a7beea0e8115f4e932d7de50b6071e7c55ee50&#34;</span>,
            <span style="color:#e6db74">&#34;sha256:235e04e3592ae74b04d0f29af65312be4c50c259b23b74698e35d42b2a4430ab&#34;</span>,
            <span style="color:#e6db74">&#34;sha256:762b147902c09d1860cccdaf4c5b28f5dea3760cb35c213c60ba2315950cbdaa&#34;</span>
        ]
    },
    <span style="color:#f92672">&#34;history&#34;</span>: [
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T01:40:35.769668496Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) ADD file:90495c24c897ec47982e200f732f8be3109fcd791691ddffae0756898f91024f in / &#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T01:40:36.265271157Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  CMD [\&#34;bash\&#34;]&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:57:35.353797681Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  LABEL maintainer=NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:57:35.609113093Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  ENV NGINX_VERSION=1.21.6&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:57:35.827389248Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  ENV NJS_VERSION=0.7.2&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:57:36.065482015Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  ENV PKG_RELEASE=1~bullseye&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:32.922897871Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c set -x     \u0026\u0026 addgroup --system --gid 101 nginx     \u0026\u0026 adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos \&#34;nginx user\&#34; --shell /bin/false --uid 101 nginx     \u0026\u0026 apt-get update     \u0026\u0026 apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates     \u0026\u0026     NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62;     found=&#39;&#39;;     for server in         hkp://keyserver.ubuntu.com:80         pgp.mit.edu     ; do         echo \&#34;Fetching GPG key $NGINX_GPGKEY from $server\&#34;;         apt-key adv --keyserver \&#34;$server\&#34; --keyserver-options timeout=10 --recv-keys \&#34;$NGINX_GPGKEY\&#34; \u0026\u0026 found=yes \u0026\u0026 break;     done;     test -z \&#34;$found\&#34; \u0026\u0026 echo \u003e\u00262 \&#34;error: failed to fetch GPG key $NGINX_GPGKEY\&#34; \u0026\u0026 exit 1;     apt-get remove --purge --auto-remove -y gnupg1 \u0026\u0026 rm -rf /var/lib/apt/lists/*     \u0026\u0026 dpkgArch=\&#34;$(dpkg --print-architecture)\&#34;     \u0026\u0026 nginxPackages=\&#34;         nginx=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE}     \&#34;     \u0026\u0026 case \&#34;$dpkgArch\&#34; in         amd64|arm64)             echo \&#34;deb https://nginx.org/packages/mainline/debian/ bullseye nginx\&#34; \u003e\u003e /etc/apt/sources.list.d/nginx.list             \u0026\u0026 apt-get update             ;;         *)             echo \&#34;deb-src https://nginx.org/packages/mainline/debian/ bullseye nginx\&#34; \u003e\u003e /etc/apt/sources.list.d/nginx.list                         \u0026\u0026 tempDir=\&#34;$(mktemp -d)\&#34;             \u0026\u0026 chmod 777 \&#34;$tempDir\&#34;                         \u0026\u0026 savedAptMark=\&#34;$(apt-mark showmanual)\&#34;                         \u0026\u0026 apt-get update             \u0026\u0026 apt-get build-dep -y $nginxPackages             \u0026\u0026 (                 cd \&#34;$tempDir\&#34;                 \u0026\u0026 DEB_BUILD_OPTIONS=\&#34;nocheck parallel=$(nproc)\&#34;                     apt-get source --compile $nginxPackages             )                         \u0026\u0026 apt-mark showmanual | xargs apt-mark auto \u003e /dev/null             \u0026\u0026 { [ -z \&#34;$savedAptMark\&#34; ] || apt-mark manual $savedAptMark; }                         \u0026\u0026 ls -lAFh \&#34;$tempDir\&#34;             \u0026\u0026 ( cd \&#34;$tempDir\&#34; \u0026\u0026 dpkg-scanpackages . \u003e Packages )             \u0026\u0026 grep &#39;^Package: &#39; \&#34;$tempDir/Packages\&#34;             \u0026\u0026 echo \&#34;deb [ trusted=yes ] file://$tempDir ./\&#34; \u003e /etc/apt/sources.list.d/temp.list             \u0026\u0026 apt-get -o Acquire::GzipIndexes=false update             ;;     esac         \u0026\u0026 apt-get install --no-install-recommends --no-install-suggests -y                         $nginxPackages                         gettext-base                         curl     \u0026\u0026 apt-get remove --purge --auto-remove -y \u0026\u0026 rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list         \u0026\u0026 if [ -n \&#34;$tempDir\&#34; ]; then         apt-get purge -y --auto-remove         \u0026\u0026 rm -rf \&#34;$tempDir\&#34; /etc/apt/sources.list.d/temp.list;     fi     \u0026\u0026 ln -sf /dev/stdout /var/log/nginx/access.log     \u0026\u0026 ln -sf /dev/stderr /var/log/nginx/error.log     \u0026\u0026 mkdir /docker-entrypoint.d&#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:33.350372757Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) COPY file:65504f71f5855ca017fb64d502ce873a31b2e0decd75297a8fb0a287f97acf92 in / &#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:33.610126307Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b03c4e6c8c513ae014f691fb05d530257dfffd07035c1b75da in /docker-entrypoint.d &#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:33.859413094Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7de297435e32af634f29f7132ed0550d342cad9fd20158258 in /docker-entrypoint.d &#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:34.141005346Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) COPY file:09a214a3e07c919af2fb2d7c749ccbc446b8c10eb217366e5a65640ee9edcc25 in /docker-entrypoint.d &#34;</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:34.342239735Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  ENTRYPOINT [\&#34;/docker-entrypoint.sh\&#34;]&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:34.562322806Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  EXPOSE 80&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:34.813995669Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  STOPSIGNAL SIGQUIT&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        },
        {
            <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T08:58:35.041664322Z&#34;</span>,
            <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop)  CMD [\&#34;nginx\&#34; \&#34;-g\&#34; \&#34;daemon off;\&#34;]&#34;</span>,
            <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
        }
    ]
}</code></pre></div>
<h4 id="文件系统层文件">文件系统层文件</h4>

<p>从 <code>manifest</code> 内容，可以看到 <code>layers</code> 字段是个数组包含多个 <code>layer</code>，观察下第一个 <code>layer</code>，可以看出：</p>

<ul>
<li>其 <code>digest</code> 为 <code>sha256:5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803</code>，所以对应的文件位置为 <code>blobs/sha256/5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803</code></li>
<li>其 <code>mediaType</code> 为 <code>application/vnd.oci.image.layer.v1.tar+gzip</code> 看出该文件的格式为 <code>tar.gz</code></li>
</ul>

<p>解压此文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir layer0
tar -xzvf blobs/sha256/5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803 -C layer0</code></pre></div>
<p>执行 <code>tree -L 1 layer0</code> 观察下内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">layer0
├── bin
├── boot
├── dev
├── etc
├── home
├── lib
├── lib64
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var</pre></div>
<h3 id="image-规范解决什么了问题">Image 规范解决什么了问题</h3>

<p>镜像作为云原生生态的核心底层基座技术，标准化以解决兼容问题。</p>

<p>镜像规范需要需要解决如下：</p>

<ul>
<li>如何支持各种平台（操作系统和硬件架构）？

<ul>
<li>为每种平台构建专门的镜像，并通过<a href="#镜像索引">镜像索引</a>文件存储</li>
</ul></li>
<li>镜像的内容如何组织如何存储，做到共享镜像的相同部分以少空间占用？

<ul>
<li><a href="#镜像层文件系统变更集">文件系统层</a>以及文件系统层可以做到</li>
</ul></li>
<li>镜像的内容和配置的标识符如何生成，如何寻址？

<ul>
<li>通过<a href="#内容描述符">可寻址存储机制</a>（即对存储的内容进行摘要得到的 hash 值）</li>
</ul></li>
<li>支持给镜像打标签，以标识镜像的版本等信息？

<ul>
<li>通过<a href="#镜像索引">镜像索引</a>文件的 <code>manifests[].annotations[&quot;org.opencontainers.image.ref.name&quot;]</code> <a href="#注解">注解</a>字段实现</li>
</ul></li>
<li>镜像有关运行时的配置信息（如 Cmd、Entrypoint）如何存储？

<ul>
<li>通过<a href="#镜像配置">镜像配置</a>文件存储实现</li>
</ul></li>
<li>某个特定平台的一个镜像的配置和内容如何描述？

<ul>
<li>通过<a href="#镜像-Manifest">镜像 Manifest</a> 文件解决</li>
</ul></li>
<li>一套镜像（一个或多个支持多个平台的某个镜像，逻辑上属于一个镜像，但是不同的平台内容可能是不同的）的各个组成部分中如何组织？

<ul>
<li>通过定义一套<a href="#镜像布局">镜像布局</a>规范解决。</li>
</ul></li>
<li>镜像配置如何转换为容器运行时的配置？

<ul>
<li>通过定义<a href="#转换到-oci-运行时配置">转换到 OCI 运行时配置</a> 规范解决</li>
</ul></li>
</ul>

<p>镜像规范不涉及的问题：</p>

<ul>
<li>镜像如何分发（DockerHub）？

<ul>
<li>由 <a href="https://github.com/opencontainers/distribution-spec">OCI Distribution Spec</a> 专门来解决此问题，镜像规范的<a href="#媒体类型">媒体类型</a>的定义以及<a href="#内容描述符">可寻址存储机制</a>与此也有一定关系。</li>
</ul></li>
<li>容器的文件系统和配置的具体是怎样的？

<ul>
<li>由 <a href="https://github.com/opencontainers/runtime-spec">OCI Runtime Spec</a> 专门来解决此问题，镜像规范的<a href="#转换到-oci-运行时配置">转换到 OCI 运行时配置</a>与此有一定关系。</li>
</ul></li>
<li>如何在本地存储管理镜像？

<ul>
<li>该部分有各个中实现决定</li>
</ul></li>
</ul>

<h3 id="image-组成部分">Image 组成部分</h3>

<p>镜像的入口是：一个叫做 <a href="#镜像-manifest">镜像 Manifest</a> 的JSON 格式文件，包含指向元数据（配置）和文件系统（内容）的引用（或者叫描述符/标识符）。</p>

<p><img src="/image/oci/media-types.png" alt="image" /></p>

<p>注意：如果某个镜像需要支持多种不同的平台（操作系统 &amp; 指令集），则在 <a href="#镜像-manifest">镜像 Manifest</a> 之上还有一个 <a href="#镜像索引">镜像索引</a> 的JSON 格式文件，该文件其包含该镜像的支持所有平台的 <a href="#镜像-manifest">镜像 Manifest</a> 列表。</p>

<h4 id="元数据-配置">元数据（配置）</h4>

<p><a href="#镜像配置">镜像配置</a> （JSON 格式文件）：</p>

<ul>
<li>描述性信息，如：创建时间、作者、架构、操作系统等</li>
<li>运行时配置，如：运行的用户、工作目录等</li>
<li>根文件系统各层的对应的 tar 包（未压缩）的 hash 值（或者称为：标识符/描述符/校验和），用于校验文件系统。</li>
<li>镜像构建的历史</li>
</ul>

<h4 id="文件系统-内容">文件系统（内容）</h4>

<p><a href="#镜像层文件系统变更集">文件系统层</a></p>

<p>一个完整的文件系统可以理解为操作系统的根目录即 <code>/</code>，因此又被称为根文件系统。</p>

<p>因此对文件系统存储，最简单的做法就是：将整个文件系统（根目录 <code>/</code>）打成一个 tar 包。这么做存在浪费存储空间的问题：</p>

<ul>
<li>镜像和镜像之间一般都是存在继承关系的。如果直接存储，会存在大量的冗余</li>
<li>直接使用 tar 不压缩的话会占用大量存储空间。</li>
</ul>

<p>因此本规范对文件系统存储做了如下改进：</p>

<ul>
<li>将文件系统进行分层存储，每个层称为文件系统层（对应 Dockerfile 的大多数语句都会产生一个层）。</li>
<li>每一层仍使用 tar 进行打包，并可以通过 gzip 等压缩工具进行压缩。</li>
</ul>

<p>这样，文件系统由多个文件系统层组成，每个层的内容的就仅仅是当前层文件系统和上一层文件系统的 diff 的内容。具体参见：<a href="#镜像层文件系统变更集">文件系统层</a></p>

<h3 id="文件定位和类型">文件定位和类型</h3>

<p>从上文可以看出，在本规范中，不管是镜像的内容还是配置都以文件的方式存在的。</p>

<p>因此镜像规范对这些文件的类型进行了定义，称为<a href="#媒体类型">媒体类型</a> （MIME 格式）。</p>

<p>此外从上文可以看出，这些文件是存在相互引用的，如 <a href="#镜像-manifest">镜像 Manifest</a> 引用了 <a href="#镜像配置">镜像配置</a> 和 <a href="#镜像层文件系统变更集">文件系统层</a>。</p>

<p>因此在镜像规范中，通过<a href="#内容描述符">内容描述符</a>来表示这些内容。内容描述符简单来说就是文件的摘要值（如 SHA256 算法），利用摘要值的特性（同样的文件摘要值相同），有如下好处：</p>

<ul>
<li>同样的文件只需要存储一份</li>
<li>文件内容可以重新导出摘要</li>
</ul>

<p>内容摘要做为内容的标识符时，需要保证文件的内容是是稳定，这里的稳定指的是，两个不同的文件想表示的内容是一致的情况下，这两个文件逐字节比较应该是相等的（完全一致）。而从上文可以看出，一个镜像由多个文件组成，文件的内容有如下可能：</p>

<ul>
<li>tar</li>
<li>tar.gz</li>
<li>json</li>
</ul>

<p>因此这就要求：</p>

<ul>
<li>tar: 打包相同的文件时，结构内容应该是相同的（按照一定的排序规则、避免在文件中存储时间当前相关的内容），规范推荐使用 <a href="https://github.com/vbatts/tar-split">vbatts/tar-split</a></li>
<li>tar.gz: 要求压缩过程总不记录时间相关信息，规范没有提及相关工具</li>
<li>json: 字段按照一定的规则排序，不添加空白字符，规范推荐 <a href="https://github.com/docker/go/">github.com/docker/go</a>，实现的 <a href="http://wiki.laptop.org/go/Canonical_JSON">规范 JSON</a>。</li>
</ul>

<h3 id="rootfs-diff-ids-vs-layers">rootfs.diff_ids vs layers</h3>

<p>manifest layer 的 digest 和 config 的 diff_ids 有可能不一样。比如这里的 layer 文件格式是 <code>tar+gzip</code>，那么这里的 sha256 就是 <code>tar+gzip</code> 包的 <code>sha256</code>，而 <code>diff_ids</code> 是 <code>tar+gzip</code> 解压后 <code>tar</code> 文件的 sha256。</p>

<p>此外，layers 和 diff_ids 长度相等，一一对应，且数组的顺序和层应用的顺序一致（作者猜测）。</p>

<p>看起来有些重复，diff_ids 存在的目的（作者猜测）：试想将一个 layers 构建成 rootfs 文件系统的过程：首先会解压这些层为一个个目录，这些目录的目录名按照规范来说，就是目录的 sha256。但是 layer 的 sha256 是压缩文件（<code>tar.gz</code>），如果作为目录名就不合适。而一个目录的 sha256 可以理解为未压缩的归档文件 (<code>tar</code>) 的 sha256。此时就可以用到 diff_ids 中的 sha256 了，避免了一次多余的 sha256。另外，在比较严格场景下，可以用 <code>diff_ids</code> 对 layers 进行校验。</p>

<p>验证 manifest layer 的 digest，以 <code>sha256:5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803</code> 为例。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ shasum -a <span style="color:#ae81ff">256</span> blobs/sha256/5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803
5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803  blobs/sha256/5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803</code></pre></div>
<p>验证 manifest layer 对应的 diff_ids 是一致的 digest，例子为：
* layer <code>sha256:5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803</code>
* diff_ids <code>sha256:7d0ebbe3f5d26c1b5ec4d5dbb6fe3205d7061f9735080b0162d550530328abd6</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ gzip -d &lt; blobs/sha256/5eb5b503b37671af16371272f9c5313a3e82f1d0756e14506704489ad9900803 &gt; test.tar
$ shasum -a <span style="color:#ae81ff">256</span> test.tar

7d0ebbe3f5d26c1b5ec4d5dbb6fe3205d7061f9735080b0162d550530328abd6  test.tar</code></pre></div>
<h3 id="oci-image-和-docker-存储驱动关系">OCI Image 和 Docker 存储驱动关系</h3>

<p>OCI Image 规范虽然定义了<a href="#镜像布局">镜像布局</a>，但是这个镜像布局应该仅仅用于单个或者一组镜像的迁移（作者猜测）。Docker、Podman 等实现，可以自己决定如何组织镜像以及存储镜像的内容。</p>

<p>在 Docker 中，镜像通过 storage drivers 来统一存储镜像和容器。更多参见：<a href="https://docs.docker.com/storage/storagedriver/">Docker Storage Driver</a> （通过 <code>docker info | grep 'Storage Driver'</code> 可以查看当前 docker 使用的驱动）</p>

<p>CoW 类的存储引擎存储在如下问题：第一次读写镜像层的文件比本机文件<a href="https://docs.docker.com/storage/storagedriver/overlayfs-driver/#modifying-files-or-directories">慢</a>（比如递归 <code>chmod</code> 包含众多小文件的目录会非常慢，因为涉及到对层次文件系统镜像层的搜索和复制），目前 Docker 的驱动是：Overlay2，更多参见： <a href="https://arkingc.github.io/2017/05/05/2017-05-05-docker-filesystem-overlay/">Docker存储驱动—Overlay/Overlay2「译」</a>）</p>

<h2 id="实战">实战</h2>

<h3 id="基于一个镜像手动构建一个新镜像">基于一个镜像手动构建一个新镜像</h3>

<p>通过手动更改 OCI 镜像目录文件的方式，给 OCI 镜像的末尾添加新的一层。目标是给 debian:10 镜像根目录添加一个文件 <code>/test</code>，内容为 <code>test</code>。</p>

<h4 id="操作">操作</h4>

<p>概述：</p>

<ul>
<li>使用 skopeo 以 OCI 布局的方式下载 debian:10 到一个目录</li>
<li>创建 test 文件，并使用

<ul>
<li>tar 命令构建一个 tar 包</li>
<li>gzip 命令创建一个 tar.gz 包</li>
<li>shasum 计算 tar 和 tar.gz 的 sha256</li>
<li>ws 统计 tar.gz 的字节数</li>
</ul></li>
<li>拷贝 blobs

<ul>
<li>将 tar.gz 拷贝到 <code>./blobs/sha256/</code> 目录中文件名为其 sha256</li>
</ul></li>
<li>更改并重命名 Config 文件

<ul>
<li>更改 Config 文件，在 rootfs.diff_ids 字段末尾添加 tar 包的 sha256</li>
<li>更改 Config 文件，在 history 字段末尾添加说明</li>
<li>shasum 重新计算 Config 文件 sha256 并重命名，ws 重新统计 Config 的字节数</li>
</ul></li>
<li>更改并重命名 Manifest 文件

<ul>
<li>更改 Manifest 文件，在 layers 字段末尾添加该层</li>
<li>更改 Manifest 文件的 config.digest 和 config.size 为上一步得到的内容</li>
<li>shasum 重新计算 Manifest 文件 sha256 并重命名，ws 重新统计 Manifest 的字节数</li>
</ul></li>
<li>更改 index.json 中 Manifest 的 digest 和 size</li>
</ul>

<p>即</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">skopeo --insecure-policy copy docker://debian:10 oci:<span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/debian:10
skopeo --insecure-policy inspect oci:<span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/debian

echo <span style="color:#e6db74">&#39;test&#39;</span> &gt; test <span style="color:#f92672">&amp;&amp;</span> tar -cvf test.tar test <span style="color:#f92672">&amp;&amp;</span> rm -rf test
<span style="color:#75715e"># tar 包</span>
shasum -a <span style="color:#ae81ff">256</span> test.tar
<span style="color:#75715e"># 输出 79bc992e2a3522971739b49f7447c5c2bd3e3e0bf3aaf4d1665a061d21fae227  test.tar</span>

<span style="color:#75715e"># tar.gz 包</span>
gzip -c &lt; test.tar &gt; test.tar.gz
shasum -a <span style="color:#ae81ff">256</span> test.tar.gz
<span style="color:#75715e"># 输出 ca76799e31911cd42039323215265e5542c5921777837ca08ae625d5f629d45b  test.tar.gz</span>
wc -c test.tar.gz
<span style="color:#75715e"># 输出 125 test.tar.gz</span>

<span style="color:#75715e"># tar.gz 拷贝到 blobs 中</span>
cp test.tar.gz debian/blobs/sha256/ca76799e31911cd42039323215265e5542c5921777837ca08ae625d5f629d45b

<span style="color:#75715e"># 修改 Manifest，的 layers 添加 ,{&#34;mediaType&#34;:&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;,&#34;digest&#34;:&#34;sha256:ca76799e31911cd42039323215265e5542c5921777837ca08ae625d5f629d45b&#34;,&#34;size&#34;:125}</span>
vim debian/blobs/sha256/96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7

<span style="color:#75715e"># 修改 Config，rootfs.diff_ids 添加 ,&#34;sha256:79bc992e2a3522971739b49f7447c5c2bd3e3e0bf3aaf4d1665a061d21fae227&#34;</span>
<span style="color:#75715e"># 修改 Config，history 添加 ,{&#34;created&#34;:&#34;2022-02-05T12:24:47.914021193Z&#34;,&#34;created_by&#34;:&#34;/bin/sh -c #(nop)  manual add /test file&#34;,&#34;empty_layer&#34;:false}</span>
vim debian/blobs/sha256/7a66498b7b706ee180f1d3e2c55c5c0ffbe94aa1a9676784d956d4f2bbed4708
<span style="color:#75715e"># 计算 Config 文件 sha256</span>
shasum -a <span style="color:#ae81ff">256</span> debian/blobs/sha256/7a66498b7b706ee180f1d3e2c55c5c0ffbe94aa1a9676784d956d4f2bbed4708
<span style="color:#75715e"># 输出 a8690a78868d28aa9c8aea0fa5a6737df7f741da6b00da743d4c29b07ac36a3f  debian/blobs/sha256/7a66498b7b706ee180f1d3e2c55c5c0ffbe94aa1a9676784d956d4f2bbed4708</span>
<span style="color:#75715e"># 重命名</span>
mv debian/blobs/sha256/7a66498b7b706ee180f1d3e2c55c5c0ffbe94aa1a9676784d956d4f2bbed4708 debian/blobs/sha256/a8690a78868d28aa9c8aea0fa5a6737df7f741da6b00da743d4c29b07ac36a3f
<span style="color:#75715e"># 计算尺寸</span>
wc -c debian/blobs/sha256/a8690a78868d28aa9c8aea0fa5a6737df7f741da6b00da743d4c29b07ac36a3f
<span style="color:#75715e"># 输出 775 debian/blobs/sha256/a8690a78868d28aa9c8aea0fa5a6737df7f741da6b00da743d4c29b07ac36a3f</span>

<span style="color:#75715e"># 修改 Manifest 的 layers 添加 ,{&#34;mediaType&#34;:&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;,&#34;digest&#34;:&#34;sha256:ca76799e31911cd42039323215265e5542c5921777837ca08ae625d5f629d45b&#34;,&#34;size&#34;:125}</span>
<span style="color:#75715e"># 修改 Manifest 的 config.digest 为 sha256:a8690a78868d28aa9c8aea0fa5a6737df7f741da6b00da743d4c29b07ac36a3f</span>
<span style="color:#75715e"># 修改 Manifest 的 config.size 为 775</span>
vim debian/blobs/sha256/96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7
<span style="color:#75715e"># 计算 Manifest 文件 sha256</span>
shasum -a <span style="color:#ae81ff">256</span> debian/blobs/sha256/96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7
<span style="color:#75715e"># 输出 a167596582b29afeaecf45bdc43881c7cb659cbdc15ffdc201ab7850c5568d99  debian/blobs/sha256/96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7</span>
<span style="color:#75715e"># 重命名</span>
mv debian/blobs/sha256/96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7 debian/blobs/sha256/a167596582b29afeaecf45bdc43881c7cb659cbdc15ffdc201ab7850c5568d99
<span style="color:#75715e"># 计算 Manifest 文件尺寸</span>
wc -c debian/blobs/sha256/a167596582b29afeaecf45bdc43881c7cb659cbdc15ffdc201ab7850c5568d99
<span style="color:#75715e"># 输出 561 debian/blobs/sha256/a167596582b29afeaecf45bdc43881c7cb659cbdc15ffdc201ab7850c5568d99</span>

<span style="color:#75715e"># 更改 index.json 中 Manifest 的 digest 和 size 分别为 sha256:a167596582b29afeaecf45bdc43881c7cb659cbdc15ffdc201ab7850c5568d99 和 561</span>
vim debian/index.json</code></pre></div>
<h4 id="验证">验证</h4>

<p>使用 skopeo 检查修改后的镜像信息（<code>skopeoskopeo --insecure-policy inspect oci:$(pwd)/debian</code>），输出为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;Digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:96452e7eda6806d94705a8886614f77e594226850339f01f078e61b1cb193aa7&#34;</span>,
    <span style="color:#f92672">&#34;RepoTags&#34;</span>: [],
    <span style="color:#f92672">&#34;Created&#34;</span>: <span style="color:#e6db74">&#34;2022-01-26T01:40:47.914021193Z&#34;</span>,
    <span style="color:#f92672">&#34;DockerVersion&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#f92672">&#34;Labels&#34;</span>: <span style="color:#66d9ef">null</span>,
    <span style="color:#f92672">&#34;Architecture&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>,
    <span style="color:#f92672">&#34;Os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>,
    <span style="color:#f92672">&#34;Layers&#34;</span>: [
        <span style="color:#e6db74">&#34;sha256:a024302f8a017855dd20a107ace079dd543c4bdfa8e7c11472771babbe298d2b&#34;</span>
    ],
    <span style="color:#f92672">&#34;Env&#34;</span>: [
        <span style="color:#e6db74">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span>
    ]
}</code></pre></div>
<p>导入 docker 中</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">skopeo  --insecure-policy copy oci:<span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>/debian docker-daemon:debian-add-test:latest</code></pre></div>
<p>查看导入的镜像 <code>docker images debian-add-test</code>，输出如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
debian-add-test     latest              a8690a78868d        10 days ago         114MB</pre></div>
<p>运行该镜像，查看 /test 文件存在</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ docker run -u root -it --entrypoint bash debian-add-test:latest  
root@a26dcc05ba35:/# cat test 
test</code></pre></div>
<p>观察这两个镜像的层，手动改造的镜像共享了 <code>debian:10</code> 这一层</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ docker image inspect --format <span style="color:#e6db74">&#34;{{json .RootFS.Layers}}&#34;</span> debian:10 debian-add-test:latest
<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;sha256:b14cb48b3aebbc58396d0b3c2d0880fd9c002c56bb7453af3ddfe6e119c06df2&#34;</span><span style="color:#f92672">]</span>
<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;sha256:b14cb48b3aebbc58396d0b3c2d0880fd9c002c56bb7453af3ddfe6e119c06df2&#34;</span>,<span style="color:#e6db74">&#34;sha256:79bc992e2a3522971739b49f7447c5c2bd3e3e0bf3aaf4d1665a061d21fae227&#34;</span><span style="color:#f92672">]</span></code></pre></div>
<h3 id="尝试利用分层文件系统提高时间和空间效率">尝试利用分层文件系统提高时间和空间效率</h3>

<p>假设我们有很多个镜像，都依赖同一个软件包（文件内容完全相同）层，检验什么时候会共享？</p>

<ul>
<li>这个软件包层构成的层，在 OCI 规范层面是否是同样一个东西

<ul>
<li>结论：是<br /></li>
</ul></li>
<li>这个软件包层构成的层，在 DockerHub 存储上存储几份

<ul>
<li>结论：存一份</li>
</ul></li>
<li>Docker Pull 这样的多个镜像到本地后，这个软件包是存储一份还是多份，还需要下载吗？结论分情况讨论：

<ul>
<li>这个软件包层构成的层以及之前的层完全一样，则只存一份</li>
<li>否则，仍然会存多份</li>
</ul></li>
</ul>

<h4 id="操作-1">操作</h4>

<p>假设有两个 Dockerfile，都是基于 <code>debian:10</code>，需要构建两个镜像，</p>

<ul>
<li>镜像 debian-test-1，基于 <code>debian:10</code> 按顺序添加两层，分别为

<ul>
<li>添加 <code>/a</code> 文件内容为 <code>a</code></li>
<li>添加 <code>/b</code> 文件内容为 <code>b</code></li>
</ul></li>
<li>镜像 debian-test-2，基于 <code>debian:10</code> 按顺序添加三层，分别为

<ul>
<li>添加 <code>/c</code> 文件内容为 <code>c</code></li>
<li>添加 <code>/b</code> 文件内容为 <code>b</code></li>
<li>添加 <code>/a</code> 文件内容为 <code>a</code></li>
</ul></li>
</ul>

<p>构建两个镜像时的 a、b 文件需保证修改时间一致，使用 <a href="https://yeasy.gitbook.io/docker_practice/image/dockerfile/copy">COPY</a> 命令添加文件（可以保证文件的修改时间保留）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">echo a &gt; a
echo b &gt; b
echo c &gt; c
echo <span style="color:#e6db74">&#39;FROM debian:10&#39;</span> &gt; debian-test-1.Dockerfile
echo <span style="color:#e6db74">&#39;COPY ./a /a&#39;</span> &gt;&gt; debian-test-1.Dockerfile
echo <span style="color:#e6db74">&#39;COPY ./b /b&#39;</span> &gt;&gt; debian-test-1.Dockerfile

echo <span style="color:#e6db74">&#39;FROM debian:10&#39;</span> &gt; debian-test-2.Dockerfile
echo <span style="color:#e6db74">&#39;COPY ./c /c&#39;</span> &gt;&gt; debian-test-2.Dockerfile
echo <span style="color:#e6db74">&#39;COPY ./b /b&#39;</span> &gt;&gt; debian-test-2.Dockerfile
echo <span style="color:#e6db74">&#39;COPY ./a /a&#39;</span> &gt;&gt; debian-test-2.Dockerfile</code></pre></div>
<p>构建镜像</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ docker build . -t debian-test-1  -f debian-test-1.Dockerfile
Sending build context to Docker daemon  <span style="color:#ae81ff">6</span>.144kB
Step <span style="color:#ae81ff">1</span>/3 : FROM debian:10
 ---&gt; f66b71803fa0
Step <span style="color:#ae81ff">2</span>/3 : COPY ./a /a
 ---&gt; ba52439e84d5
Step <span style="color:#ae81ff">3</span>/3 : COPY ./b /b
 ---&gt; c1ec247c1970
Successfully built c1ec247c1970
Successfully tagged debian-test-1:latest

$ docker build . -t debian-test-2  -f debian-test-2.Dockerfile
Sending build context to Docker daemon  <span style="color:#ae81ff">6</span>.144kB
Step <span style="color:#ae81ff">1</span>/4 : FROM debian:10
 ---&gt; f66b71803fa0
Step <span style="color:#ae81ff">2</span>/4 : COPY ./c /c
 ---&gt; 031e90de101e
Step <span style="color:#ae81ff">3</span>/4 : COPY ./b /b
 ---&gt; bd87fa42a36a
Step <span style="color:#ae81ff">4</span>/4 : COPY ./a /a
 ---&gt; 7960f0dbc171
Successfully built 7960f0dbc171
Successfully tagged debian-test-2:latest</code></pre></div>
<h4 id="观察镜像层">观察镜像层</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ docker image inspect --format <span style="color:#e6db74">&#34;{{json .RootFS.Layers}}&#34;</span> debian-test-1:latest debian-test-2:latest
<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;sha256:b14cb48b3aebbc58396d0b3c2d0880fd9c002c56bb7453af3ddfe6e119c06df2&#34;</span>,<span style="color:#e6db74">&#34;sha256:19840e8fc4aaf4dda2dee6222b4d898580a8bcfcb0d3d1b56bfabe15e069aa7f&#34;</span>,<span style="color:#e6db74">&#34;sha256:87e2618117301e71d0b159d190ade4d4b1c17054e02d925629f902de210ae3fe&#34;</span><span style="color:#f92672">]</span>
<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;sha256:b14cb48b3aebbc58396d0b3c2d0880fd9c002c56bb7453af3ddfe6e119c06df2&#34;</span>,<span style="color:#e6db74">&#34;sha256:a9ad1c3056bda459dde5bdd84b0493579801fdd06923701e9a9ec6956e5adb05&#34;</span>,<span style="color:#e6db74">&#34;sha256:87e2618117301e71d0b159d190ade4d4b1c17054e02d925629f902de210ae3fe&#34;</span>,<span style="color:#e6db74">&#34;sha256:19840e8fc4aaf4dda2dee6222b4d898580a8bcfcb0d3d1b56bfabe15e069aa7f&#34;</span><span style="color:#f92672">]</span></code></pre></div>
<p>可以看出，两个镜像，添加 copy a 文件以及 copy b 文件的层的标识符都为：</p>

<ul>
<li><code>sha256:19840e8fc4aaf4dda2dee6222b4d898580a8bcfcb0d3d1b56bfabe15e069aa7f</code></li>
<li><code>sha256:87e2618117301e71d0b159d190ade4d4b1c17054e02d925629f902de210ae3fe</code></li>
</ul>

<p>这两个层在两个镜像中，进行了共享。</p>

<h4 id="观察-docker-镜像存储图">观察 Docker 镜像存储图</h4>

<p>（存储驱动为：overlay2）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker image inspect --format <span style="color:#e6db74">&#34;{{json .GraphDriver.Data}}&#34;</span> debian-test-1:latest debian-test-2:latest
<span style="color:#f92672">{</span><span style="color:#e6db74">&#34;LowerDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/6ae962c93e0d8835ec15c6655b8b2df7e903d3db888c5e43a0ceb02b59e30fe0/diff:/data00/docker/overlay2/4fe096c15e0b13963a5ca43f0a9ec876379e4ffd73ae851710ef20f5b294bdef/diff&#34;</span>,<span style="color:#e6db74">&#34;MergedDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/9454cb3ce328b9dca22398d1092a60f5b23f6a29a6971a4e8c55d5f6aeade351/merged&#34;</span>,<span style="color:#e6db74">&#34;UpperDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/9454cb3ce328b9dca22398d1092a60f5b23f6a29a6971a4e8c55d5f6aeade351/diff&#34;</span>,<span style="color:#e6db74">&#34;WorkDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/9454cb3ce328b9dca22398d1092a60f5b23f6a29a6971a4e8c55d5f6aeade351/work&#34;</span><span style="color:#f92672">}</span>
<span style="color:#f92672">{</span><span style="color:#e6db74">&#34;LowerDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/b790f8dfa4a8a1fe607c3e27f0448117d81618a72d8fae2742ac55d749ab4818/diff:/data00/docker/overlay2/0da97c5204dbaf1616d25183f2eaf6cc4d294e50a15aeca455addb4c39d64cac/diff:/data00/docker/overlay2/4fe096c15e0b13963a5ca43f0a9ec876379e4ffd73ae851710ef20f5b294bdef/diff&#34;</span>,<span style="color:#e6db74">&#34;MergedDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/0c65b2ec627a8a35819103cd0237fc644ee1f7d4f7dba6051011314cd828813c/merged&#34;</span>,<span style="color:#e6db74">&#34;UpperDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/0c65b2ec627a8a35819103cd0237fc644ee1f7d4f7dba6051011314cd828813c/diff&#34;</span>,<span style="color:#e6db74">&#34;WorkDir&#34;</span>:<span style="color:#e6db74">&#34;/data00/docker/overlay2/0c65b2ec627a8a35819103cd0237fc644ee1f7d4f7dba6051011314cd828813c/work&#34;</span><span style="color:#f92672">}</span></code></pre></div>
<p>可以发现，都不相同</p>

<h4 id="观察是否可以免于下载">观察是否可以免于下载</h4>

<p>（存储驱动为：overlay2）</p>

<ul>
<li><a href="https://docs.docker.com/registry/deploying/#run-a-local-registry">搭建一个本地私有镜像仓库</a></li>
<li>将两个镜像上传到镜像仓库中</li>
<li>彻底清理本地镜像</li>
<li>先 pull debian-test-2</li>
<li>再 pull debian-test-1，观察是否有下载过程</li>

<li><p><a href="https://docs.docker.com/registry/deploying/#stop-a-local-registry">清理私有镜像仓库</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 搭建一个本地私有镜像仓库</span>
docker run -d -p <span style="color:#ae81ff">5000</span>:5000 --restart<span style="color:#f92672">=</span>always --name registry registry:2

<span style="color:#75715e"># 将两个镜像上传到镜像仓库中</span>
docker tag debian-test-2 localhost:5000/debian-test-2
docker push localhost:5000/debian-test-2
docker tag debian-test-1 localhost:5000/debian-test-1
docker push localhost:5000/debian-test-1 <span style="color:#75715e"># 可以发现没有上传过程了，因为所有层，在 debian-test-2 中都存在了</span>

<span style="color:#75715e"># 彻底清理本地镜像</span>
docker image remove debian-test-2 debian-test-1 debian:10 localhost:5000/debian-test-2 localhost:5000/debian-test-1

<span style="color:#75715e"># pull debian-test-2</span>
docker pull localhost:5000/debian-test-2 &gt; test2.log <span style="color:#f92672">&amp;&amp;</span> cat test2.log
<span style="color:#75715e"># 输出为：</span>
<span style="color:#75715e"># Using default tag: latest</span>
<span style="color:#75715e"># latest: Pulling from debian-test-2</span>
<span style="color:#75715e"># a024302f8a01: Already exists</span>
<span style="color:#75715e"># 50c71f18192a: Pulling fs layer</span>
<span style="color:#75715e"># 44bed4909bf5: Pulling fs layer</span>
<span style="color:#75715e"># 100a67ecf9c3: Pulling fs layer</span>
<span style="color:#75715e"># 50c71f18192a: Verifying Checksum</span>
<span style="color:#75715e"># 50c71f18192a: Download complete</span>
<span style="color:#75715e"># 44bed4909bf5: Verifying Checksum</span>
<span style="color:#75715e"># 44bed4909bf5: Download complete</span>
<span style="color:#75715e"># 100a67ecf9c3: Download complete</span>
<span style="color:#75715e"># 50c71f18192a: Pull complete</span>
<span style="color:#75715e"># 44bed4909bf5: Pull complete</span>
<span style="color:#75715e"># 100a67ecf9c3: Pull complete</span>
<span style="color:#75715e"># Digest: sha256:3a024c871ac137c92e18faf10a5aa3115f71cd3987855b2560b40c807bd74d6c</span>
<span style="color:#75715e"># Status: Downloaded newer image for localhost:5000/debian-test-2:latest</span>
<span style="color:#75715e"># localhost:5000/debian-test-2:latest</span>

docker pull localhost:5000/debian-test-1 &gt; test1.log <span style="color:#f92672">&amp;&amp;</span> cat test1.log
<span style="color:#75715e"># 输出为：</span>
<span style="color:#75715e"># Using default tag: latest</span>
<span style="color:#75715e"># latest: Pulling from debian-test-1</span>
<span style="color:#75715e"># a024302f8a01: Already exists</span>
<span style="color:#75715e"># 100a67ecf9c3: Pulling fs layer</span>
<span style="color:#75715e"># 44bed4909bf5: Pulling fs layer</span>
<span style="color:#75715e"># 100a67ecf9c3: Verifying Checksum</span>
<span style="color:#75715e"># 100a67ecf9c3: Download complete</span>
<span style="color:#75715e"># 44bed4909bf5: Verifying Checksum</span>
<span style="color:#75715e"># 44bed4909bf5: Download complete</span>
<span style="color:#75715e"># 100a67ecf9c3: Pull complete</span>
<span style="color:#75715e"># 44bed4909bf5: Pull complete</span>
<span style="color:#75715e"># Digest: sha256:4b0d097b5c51309e06a02fd506f6e2ef0f456106cc12846b7e87a706e39af0ee</span>
<span style="color:#75715e"># Status: Downloaded newer image for localhost:5000/debian-test-1:latest</span>
<span style="color:#75715e"># localhost:5000/debian-test-1:latest</span>

<span style="color:#75715e"># 清理私有镜像仓库</span>
docker container stop registry <span style="color:#f92672">&amp;&amp;</span> docker container rm -v registry</code></pre></div></li>
</ul>

<p>可以看出，两个镜像仅仅共享了 debian:10 这一层。对于其他层，虽然层的内容以及标识符都是相同的，但是还是需要需要重新下载的。</p>

<h4 id="结论">结论</h4>

<ul>
<li>OCI 镜像规范的文件系统层本质上是一个图（整体来看可以有环，单个镜像来看是个链表），因此在 DockerHub 层面，可以只存储 debian:10、a、b、c 这四层</li>
<li>Docker 文件系统是一个树状结构，因此需要存储：

<ul>
<li>debian:10 （镜像 1、2 共享）</li>
<li>a -&gt; debian:10 （镜像 1）</li>
<li>b -&gt; a （镜像 1）</li>
<li>c -&gt; debian:10 （镜像 2）</li>
<li>b -&gt; a （镜像 2）</li>
<li>c -&gt; b （镜像 2）</li>
</ul></li>
</ul>

<p><img src="/image/oci/oci-image-spec-combat2.png" alt="image" /></p>

<p>因此，想使用这种技巧，使用缓存加速镜像下载，并减少镜像空间占用，是不现实的。</p>

<p>关于 Docker 的详细存储原理，参见博客：<a href="https://blog.k8s.li/Exploring-container-image.html">深入浅出容器镜像的一生🤔</a></p>

<h2 id="原文翻译">原文翻译</h2>

<blockquote>
<p>原文参见：<a href="https://github.com/opencontainers/image-spec/tree/v1.0.2">Github</a></p>
</blockquote>

<h3 id="spec">Spec</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/spec.md">原文链接</a></p>

<p>译者注：该部分是整篇规范的目录和概述</p>
</blockquote>

<h4 id="介绍">介绍</h4>

<p>该规范定义了由一个 manifest、镜像索引（可选）、 镜像层文件系统变更集 和 配置 组成的 OCI 镜像。</p>

<p>本规范的目标是创建一个可互操作的，用于构建、传输和准备要运行的容器镜像的工具。</p>

<h4 id="符号约定">符号约定</h4>

<p>关键词 &ldquo;MUST&rdquo; （必须）, &ldquo;MUST NOT&rdquo; （禁止）, &ldquo;REQUIRED&rdquo; （必要的）, &ldquo;SHALL&rdquo; （没有对应词）, &ldquo;SHALL NOT&rdquo;（没有对应词）, &ldquo;SHOULD&rdquo;（应该）, &ldquo;SHOULD NOT&rdquo;（不应该）, &ldquo;RECOMMENDED&rdquo; （建议）, &ldquo;NOT RECOMMENDED&rdquo; （不建议）, &ldquo;MAY&rdquo; （可能）, &ldquo;OPTIONAL&rdquo; （可选的） 将按照 <a href="http://tools.ietf.org/html/rfc2119">RFC 2119</a> 中的描述进行解释。（参见：<a href="http://www.ruanyifeng.com/blog/2007/03/rfc2119.html">RFC2119：表示要求的动词</a>）</p>

<h4 id="概览-1">概览</h4>

<p>站在高层级来看。镜像 Manifest 包含镜像内容和依赖的元数据，这些元数据主要包括一个或多个指向 filesystem layer 变更集的归档文件（其将被解包以构成最终可运行的文件系统）的可寻址标识符 （译者注：以及一个指向 Image 配置 的可寻址标识符）。Image 配置 包括应用参数、环境变量等信息。镜像索引 （译者注：可选的）是一个更高级别的 manifest，它主要包含一个，指向 manifest 的描述符的列表。通常情况下，镜像索引 可以提供的是操作系统或者硬件架构不同导致的镜像的不同实现（译者注：即为不同的平台定义不同的镜像）</p>

<blockquote>
<p>译者注：
* 可寻址表示符和描述符在本文中是同一事物，表示可以定位到内容的唯一标识符，这个标识符由内容本身的 hash 决定。
* 这一段看不懂实属正常，可以先看下文，回头再来看这段总结。</p>
</blockquote>

<p><img src="/image/oci/build-diagram.png" alt="image" /></p>

<p>构建好 OCI 镜像后，就可以通过名称来发现、下载、通过哈希验证、通过签名信任,，并解压到 OCI 运行时包中。</p>

<p><img src="/image/oci/run-diagram.png" alt="image" /></p>

<h5 id="理解这个标准">理解这个标准</h5>

<p>OCI Image 媒体类型 文档是理解规范整体结构的起点。</p>

<p>该规范的顶层组件包括：</p>

<ul>
<li>镜像 Manifest - 描述构成容器镜像的组件</li>
<li>镜像索引 - 一个注解的 镜像 Manifest 的索引</li>
<li>Image Layout - 描述一个镜像在文件系统中的布局情况</li>
<li>Filesystem Layer - 描述容器文件系统的变更集</li>
<li>Image 配置 - 转换为运行时 bundle 的镜像的层排序和配置</li>
<li>Conversion - 转换应该如何发生</li>
<li>Descriptor - 被引用内容的类型、元数据和内容地址的引用</li>
</ul>

<p>本规范的未来版本可能包括以下可选功能：</p>

<ul>
<li>基于签名镜像内容地址的签名</li>
<li>基于 DNS 联合且可委托的命名</li>
</ul>

<h3 id="媒体类型">媒体类型</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/main/media-types.md">原文链接</a></p>

<p>译者注：媒体类型定义了一个组成一个镜像的各种文件的具体类型标识和文件格式</p>
</blockquote>

<p>以下 媒体类型 标识此处描述的格式及其参考文档的链接：</p>

<ul>
<li><code>application/vnd.oci.descriptor.v1+json</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/descriptor.md">内容描述符</a></li>
<li><code>application/vnd.oci.layout.header.v1+json</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/image-layout.md#oci-layout-file">OCI Layout file</a> 声明使用的规范版本</li>
<li><code>application/vnd.oci.image.index.v1+json</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/image-index.md">镜像索引</a></li>
<li><code>application/vnd.oci.image.manifest.v1+json</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/manifest.md#image-manifest">镜像 Manifest</a></li>
<li><code>application/vnd.oci.image.config.v1+json</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/config.md">Image config</a></li>
<li><code>application/vnd.oci.image.layer.v1.tar</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/layer.md">tar 归档格式的 &ldquo;Layer&rdquo;</a></li>
<li><code>application/vnd.oci.image.layer.v1.tar+gzip</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types">tar 归档格式的 &ldquo;Layer&rdquo;</a> 并使用 <a href="https://tools.ietf.org/html/rfc1952">gzip</a> 进行压缩</li>
<li><code>application/vnd.oci.image.layer.nondistributable.v1.tar</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/layer.md#non-distributable-layers">具有分发限制的 tar 归档的 &ldquo;Layer&rdquo;</a></li>
<li><code>application/vnd.oci.image.layer.nondistributable.v1.tar+gzip</code>: <a href="/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types">具有分发限制的 tar 归档的 &ldquo;Layer&rdquo;</a> 并使用 <a href="https://tools.ietf.org/html/rfc1952">gzip</a> 进行压缩</li>
</ul>

<h4 id="media-type-冲突">Media Type 冲突</h4>

<p>该部分，主要描述了如果 HTTP 返回的 <code>Content-Type</code> 和真正的返回值不一致或者缺失应该如何处理。具体参见：<a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/media-types.md#media-type-conflicts">原文</a></p>

<h4 id="兼容性-matrix">兼容性 Matrix</h4>

<p>该部分，主要描述了该规范和 Docker 实现的一些不同点。具体参见：<a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/media-types.md#compatibility-matrix">原文</a></p>

<h4 id="关系">关系</h4>

<p>下图显示了上述 媒体类型 如何相互引用：</p>

<p><img src="/image/oci/media-types.png" alt="image" /></p>

<p>所有引用的引用都是通过描述符方式实现的。镜像索引 是一个 &ldquo;fat manifest&rdquo; ，其引用了每个目标平台的 镜像 Manifest 列表。一个 镜像 Manifest 引用一个 配置，一个或多个 Layers。</p>

<h3 id="内容描述符">内容描述符</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/descriptor.md">原文链接</a></p>

<p>译者注：内容描述符定义了一个镜像中各个部分内容的标识符如何生成，如何引用，如何查找</p>
</blockquote>

<ul>
<li>OCI 镜像由几个不同的组件组成，这些组件组成一个<a href="https://en.wikipedia.org/wiki/Merkle_tree">有向无环图 (DAG)</a>。</li>
<li>图中组件之间的引用通过内容描述符表示。</li>
<li>内容描述符（或简称为描述符）描述了目标内容的位置。</li>
<li>内容描述符包括内容类型、内容标识符（Digest）和原始内容的字节大小。</li>
<li>描述符 SHOULD 嵌入到其他格式中以安全地引用外部内容。</li>
<li>其他格式应该使用描述符来安全地引用外部内容。</li>
</ul>

<p>本节定义了 <code>application/vnd.oci.descriptor.v1+json</code> <a href="#媒体类型">媒体类型</a>。</p>

<h4 id="描述符属性">描述符属性</h4>

<p>描述符由一组封装在键值字段中的属性组成。</p>

<p>以下字段包含构成描述符的主要属性：</p>

<table>
<thead>
<tr>
<th>字段名</th>
<th>数据类型</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>mediaType</td>
<td>string</td>
<td>此 REQUIRED 属性包含引用内容的媒体类型。值必须符合 <a href="https://tools.ietf.org/html/rfc6838">RFC 6838</a>，包括其<a href="https://tools.ietf.org/html/rfc6838#section-4.2">第 4.2 节</a>中的命名要求。本规范的定义的媒体类型参见：<a href="#媒体类型">上文</a>。</td>
</tr>

<tr>
<td>digest</td>
<td>string</td>
<td>此 REQUIRED 属性是目标内容的Digest，要求参见：<a href="#digest">下文</a>。当通过不受信任的来源消费时，应根据此Digest验证检索到的内容。</td>
</tr>

<tr>
<td>size</td>
<td>int64</td>
<td>此 REQUIRED 属性指定原始内容的大小（以字节为单位）。存在此属性，以便客户端在处理之前具有预期的内容大小。如果检索到的内容的长度与指定的长度不匹配，则不应信任该内容。</td>
</tr>

<tr>
<td>urls</td>
<td>array of strings</td>
<td>此 OPTIONAL 属性指定可从中下载此对象的 URI 列表。每项必须符合 <a href="https://tools.ietf.org/html/rfc3986">RFC 3986</a>。条目应该使用 <a href="https://tools.ietf.org/html/rfc7230#section-2.7">RFC 7230</a> 中所定义 http 和 https 方案</td>
</tr>

<tr>
<td>annotations</td>
<td>string-string map</td>
<td>此 OPTIONAL 属性包含此描述符的任意元数据。此可选属性必须使用：注释规则。</td>
</tr>
</tbody>
</table>

<p>以下字段键是保留的，MUST NOT 被其他规范使用。</p>

<ul>
<li><code>data</code> string 该键保留用于规范的未来版本。</li>
</ul>

<p>所有其他字段可能包含在其他 OCI 规范中。在其他 OCI 规范中提出的扩展描述符字段添加应首先考虑添加到本规范中。</p>

<h4 id="digest">digest</h4>

<p>描述符的 digest 属性扮演着内容标识符和内容寻址的角色。其通过对字节进行抗冲突散列来唯一标识内容。如果 digest 可以以安全的方式进行通信，则可以通过独立重新计算Digest来验证来自不安全来源的内容，确保内容未被修改。</p>

<p>digest 属性的值是一个由算法部分和编码部分组成的字符串。该算法指定用于 digest 的加密散列函数和编码；编码部分包含散列函数的编码结果。</p>

<p>digest 字符串必须符合以下语法：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">digest                ::= algorithm &#34;:&#34; encoded
algorithm             ::= algorithm-component (algorithm-separator algorithm-component)*
algorithm-component   ::= [a-z0-9]+
algorithm-separator   ::= [+._-]
encoded               ::= [a-zA-Z0-9=_-]+</pre></div>
<p>请注意：算法可以对编码部分的语法施加特定于算法的限制。另见下文：<a href="#已注册的算法">已注册的算法</a>。</p>

<p>一些 digest 字符串例子如下：</p>

<table>
<thead>
<tr>
<th>digest</th>
<th>算法</th>
<th>是否注册</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b</code></td>
<td><a href="#sha-256">SHA-256</a></td>
<td>Yes</td>
</tr>

<tr>
<td><code>sha512:401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b372742...</code></td>
<td><a href="#sha-512">SHA-512</a></td>
<td>Yes</td>
</tr>

<tr>
<td><code>multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8</code></td>
<td>Multihash</td>
<td>No</td>
</tr>

<tr>
<td><code>sha256+b64u:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564</code></td>
<td>SHA-256 with urlsafe base64</td>
<td>No</td>
</tr>
</tbody>
</table>

<p>有关已注册算法的列表，请参阅：<a href="#已注册的算法">已注册的算法</a>。</p>

<p>如果符合上述语法，实现 SHOULD 允许使用无法识别的算法的 digest 通过验证。虽然 sha256 将仅使用十六进制编码的 digest，但算法中的分隔符和编码中的字母数字都包含在内以允许扩展。例如，我们可以将编码和算法参数化为 <code>multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8</code>，这将被视为有效但未被本规范注册。</p>

<h5 id="校验">校验</h5>

<p>在消费来自不受信任来源的描述符所针对的内容之前，应该根据 digest 字符串验证字节内容。在计算 digest 之前，应该验证内容的大小以减少哈希冲突空间。应该避免在计算散列之前进行繁重的处理。实现可以使用底层内容的规范化来确保稳定的内容标识符。</p>

<h5 id="digest-计算">Digest 计算</h5>

<p>Digest 由以下伪代码计算，其中 <code>H</code> 是选定的哈希算法，由字符串 <code>&lt;alg&gt;</code> 标识：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">let ID(C) = Descriptor.digest
let C = &lt;bytes&gt;
let D = &#39;&lt;alg&gt;:&#39; + Encode(H(C))
let verified = ID(C) == D</pre></div>
<p>上面，我们将内容标识符定义为 <code>ID(C)</code>，从 <code>Descriptor.digest</code> 字段中提取。内容 <code>C</code> 是一串字节。函数 <code>H</code> 以字节为单位返回 <code>C</code> 的哈希值，并传递给函数 <code>Encode</code> 并以算法为前缀以获得Digest。如果 <code>ID(C)</code> 等于 <code>D</code>，则验证结果为真，确认 <code>C</code> 是 <code>D</code> 标识的内容。 验证后，以下为真：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">D == ID(C) == &#39;&lt;alg&gt;:&#39; + Encode(H(C))</pre></div>
<p>通过独立计算 Digest，将 Digest 确认为内容标识符。</p>

<h5 id="已注册的算法">已注册的算法</h5>

<p>虽然 Digest 字符串的算法组件允许使用各种加密算法，但兼容的实现应该使用 <a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/descriptor.md#sha-256">SHA-256</a>。</p>

<p>本规范目前定义了以下算法标识符：</p>

<table>
<thead>
<tr>
<th>算法标识符</th>
<th>算法</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>sha256</code></td>
<td><a href="#sha-256">SHA-256</a></td>
</tr>

<tr>
<td><code>sha512</code></td>
<td><a href="#sha-512">SHA-512</a></td>
</tr>
</tbody>
</table>

<p>如果上表中没有包含有用的算法，则应该提交到本规范进行注册。</p>

<h6 id="sha-256">SHA-256</h6>

<p><a href="https://tools.ietf.org/html/rfc4634#section-4.1">SHA-256</a> 是一种抗碰撞散列函数，选择它是因为它具有普遍性、合理的大小和安全特性。实现上 MUST 实现 SHA-256 Digest 来验证描述符。</p>

<p>当算法标识符为 sha256 时，编码部分必须匹配 <code>/[a-f0-9]{64}/</code>。请注意，此处不得使用 <code>[A-F]</code>。</p>

<h6 id="sha-512">SHA-512</h6>

<p><a href="https://tools.ietf.org/html/rfc4634#section-4.2">SHA-512</a> 是一种抗碰撞散列函数，在某些 CPU 上可能比 SHA-256 性能更好。实现上 MAY 实现 SHA-512 Digest 来验证描述符。</p>

<p>当算法标识符为 sha512 时，编码部分必须匹配 <code>/[a-f0-9]{128}/</code>。请注意，此处不得使用 <code>[A-F]</code>。</p>

<h4 id="例子">例子</h4>

<p>以下示例描述了一个内容标识符为 <code>&quot;sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270&quot;</code> 且大小为 7682 字节的 Manifest：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
  <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7682</span>,
  <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270&#34;</span>
}</code></pre></div>
<p>在以下示例中，描述符指示可从特定 URL 检索（下载）引用的 Manifest：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
  <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7682</span>,
  <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270&#34;</span>,
  <span style="color:#f92672">&#34;urls&#34;</span>: [
    <span style="color:#e6db74">&#34;https://example.com/example-manifest&#34;</span>
  ]
}</code></pre></div>
<h3 id="镜像布局">镜像布局</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md">原文链接</a></p>

<p>译者注：镜像布局定义的是一个 image 的标准目录结构。</p>
</blockquote>

<ul>
<li>OCI 镜像布局是 OCI 内容可寻址 blob 和位置可寻址引用 (refs) 的目录结构。</li>
<li>此布局 MAY 用于各种不同的传输机制：存档格式（例如 tar、zip）、共享文件系统环境（例如 nfs）或网络文件获取（例如 http、ftp、rsync）。</li>
</ul>

<p>给定镜像布局和参考，工具可以通过以下方式创建 <a href="https://github.com/opencontainers/runtime-spec/blob/v1.0.0/bundle.md">OCI 运行时规范 Bundle</a>：</p>

<ul>
<li>按照 ref 查找 <a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md#image-manifest">manifest</a>，也可能通过 <a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/image-index.md">镜像索引</a></li>
<li>按指定顺序<a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md#applying">应用镜像层文件系统变更集</a></li>
<li>将<a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/config.md">镜像配置</a>转换为 <a href="https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md">OCI 运行时规范 config.json</a></li>
</ul>

<p>镜像布局如下：</p>

<ul>
<li><code>blobs</code> 目录

<ul>
<li>包含内容可寻址的 blob</li>
<li>一个 blob 没有 Schema，SHOULD 被认为是不透明的</li>
<li>目录必须存在并且可以为空</li>
<li>更多参见 <a href="#blobs">blobs</a> 章节</li>
</ul></li>
<li><code>oci-layout</code> 文件

<ul>
<li>MUST 存在</li>
<li>内容 MUST 是 JSON 对象</li>
<li>MUST 包含 <code>imageLayoutVersion</code> 字段</li>
<li>更多参见 <a href="#oci-layout-文件">oci-layout 文件</a> 章节</li>
<li>MAY 包含其他字段</li>
</ul></li>
<li><code>index.json</code> file

<ul>
<li>MUST 存在</li>
<li>MUST 是一个 <a href="/opencontainers/image-spec/blob/main/image-index.md">image index</a> JSON 对象</li>
<li>更多参见 <a href="#indexjson-文件">index.json</a> 章节</li>
</ul></li>
</ul>

<h4 id="布局示例">布局示例</h4>

<p>这是一个示例镜像布局：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cd example.com/app/
$ find . -type f
./index.json
./oci-layout
./blobs/sha256/3588d02542238316759cbf24502f4344ffcc8a60c803870022f335d1390c13b4
./blobs/sha256/4b0bc1c4050b03c95ef2a8e36e25feac42fd31283e8c30b3ee5df6b043155d3c
./blobs/sha256/7968321274dc6b6171697c33df7815310468e694ac5be0ec03ff053bb135e768</code></pre></div>
<p>Blob 由它们的内容（的 Hash 值）命名：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ shasum -a <span style="color:#ae81ff">256</span> ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51
afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51</code></pre></div>
<h4 id="blobs">Blobs</h4>

<ul>
<li>blobs 子目录中的对象名称由每个哈希算法的目录组成，其子目录将包含实际内容。</li>
<li><code>blobs/&lt;alg&gt;/&lt;encoded&gt;</code> 的内容必须匹配摘要 <code>&lt;alg&gt;:&lt;encoded&gt;</code>（每个描述符引用）。例如，blobs/<code>sha256/da39a3ee5e6b4b0d3255bfef95601890afd80709</code> 的内容 MUST 与摘要 <code>sha256:da39a3ee5e6b4b0d3255bfef95601890afd80709</code> 匹配。</li>
<li><code>&lt;alg&gt;</code> 和 <code>&lt;encoded&gt;</code> 的条目名称的字符集必须匹配描述符中描述的相应语法元素。</li>
<li>blobs 目录 MAY 包含未被任何 <a href="#indexjson-文件">refs</a> 引用的 blob。</li>
<li>blobs 目录 MAY 缺少引用的 blob，在这种情况下，缺少的 blob 应该由外部 blob 存储来完成。</li>
</ul>

<h5 id="blobs-示例">Blobs 示例</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat ./blobs/sha256/9b97579de92b1c195b85bb42a11011378ee549b02d7fe9c17bf2a6b35d5cb079 | jq
<span style="color:#f92672">{</span>
  <span style="color:#e6db74">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#e6db74">&#34;manifests&#34;</span>: <span style="color:#f92672">[</span>
    <span style="color:#f92672">{</span>
      <span style="color:#e6db74">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
      <span style="color:#e6db74">&#34;size&#34;</span>: <span style="color:#ae81ff">7143</span>,
      <span style="color:#e6db74">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51&#34;</span>,
      <span style="color:#e6db74">&#34;platform&#34;</span>: <span style="color:#f92672">{</span>
        <span style="color:#e6db74">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;ppc64le&#34;</span>,
        <span style="color:#e6db74">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>
      <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>,
...</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 | jq
<span style="color:#f92672">{</span>
  <span style="color:#e6db74">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#e6db74">&#34;config&#34;</span>: <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.config.v1+json&#34;</span>,
    <span style="color:#e6db74">&#34;size&#34;</span>: <span style="color:#ae81ff">7023</span>,
    <span style="color:#e6db74">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270&#34;</span>
  <span style="color:#f92672">}</span>,
  <span style="color:#e6db74">&#34;layers&#34;</span>: <span style="color:#f92672">[</span>
    <span style="color:#f92672">{</span>
      <span style="color:#e6db74">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
      <span style="color:#e6db74">&#34;size&#34;</span>: <span style="color:#ae81ff">32654</span>,
      <span style="color:#e6db74">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0&#34;</span>
    <span style="color:#f92672">}</span>,
...</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat ./blobs/sha256/5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270 | jq
<span style="color:#f92672">{</span>
  <span style="color:#e6db74">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>,
  <span style="color:#e6db74">&#34;author&#34;</span>: <span style="color:#e6db74">&#34;Alyssa P. Hacker &lt;alyspdev@example.com&gt;&#34;</span>,
  <span style="color:#e6db74">&#34;config&#34;</span>: <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;Hostname&#34;</span>: <span style="color:#e6db74">&#34;8dfe43d80430&#34;</span>,
    <span style="color:#e6db74">&#34;Domainname&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#e6db74">&#34;User&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
    <span style="color:#e6db74">&#34;AttachStdin&#34;</span>: false,
    <span style="color:#e6db74">&#34;AttachStdout&#34;</span>: false,
    <span style="color:#e6db74">&#34;AttachStderr&#34;</span>: false,
    <span style="color:#e6db74">&#34;Tty&#34;</span>: false,
    <span style="color:#e6db74">&#34;OpenStdin&#34;</span>: false,
    <span style="color:#e6db74">&#34;StdinOnce&#34;</span>: false,
    <span style="color:#e6db74">&#34;Env&#34;</span>: <span style="color:#f92672">[</span>
      <span style="color:#e6db74">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span>
    <span style="color:#f92672">]</span>,
    <span style="color:#e6db74">&#34;Cmd&#34;</span>: null,
    <span style="color:#e6db74">&#34;Image&#34;</span>: <span style="color:#e6db74">&#34;sha256:6986ae504bbf843512d680cc959484452034965db15f75ee8bdd1b107f61500b&#34;</span>,
...</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat ./blobs/sha256/9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0
<span style="color:#f92672">[</span>gzipped tar stream<span style="color:#f92672">]</span></code></pre></div>
<h4 id="oci-layout-文件-1">oci-layout 文件</h4>

<p>此 JSON 对象用作 Open Container Image Layout 基础的标记，并提供正在使用的镜像布局版本。在对布局进行更改时，imageLayoutVersion 值将与 OCI 镜像规范版本保持一致，并将固定给定版本，直到需要对镜像布局进行更改。oci-layout 定义了为 <code>application/vnd.oci.layout.header.v1+json</code> 的<a href="#媒体类型">媒体类型</a>。</p>

<h4 id="index-json-文件-1">index.json 文件</h4>

<p>这个 REQUIRED 文件是镜像布局的引用和描述符的入口点。<a href="#镜像索引">镜像索引</a>是多描述符入口点。</p>

<p>该索引提供了一个已建立的路径 (/index.json) 以具有镜像布局的入口点并发现辅助描述符。</p>

<ul>
<li>描述符的 <code>&quot;org.opencontainers.image.ref.name&quot;</code> 注释没有语义限制。</li>
<li>一般来说，manifests 字段中每个<a href="#内容描述符">描述符</a>对象的 mediaType 将是 <code>application/vnd.oci.image.index.v1+json</code> 或 <code>application/vnd.oci.image.manifest.v1+json</code>。</li>
<li>该规范的未来版本 MAY 使用不同的媒体类型（即新的版本的格式）。</li>
<li>遇到的未知媒体类型 SHOULD 被安全地忽略。</li>
</ul>

<p>实施者注：带有 <code>&quot;org.opencontainers.image.ref.name&quot;</code> 注释的描述符的常见用例是表示容器镜像的 &ldquo;tag&rdquo;。例如，一个镜像可能具有不同版本或软件构建的 &ldquo;tag&rdquo;。举个例子，您经常会看到的 &ldquo;tag&rdquo; ，例如 <code>&quot;v1.0.0-vendor.0&quot;</code>、<code>&quot;2.0.0-debug&quot;</code> 等。这些 &ldquo;tag&rdquo; 通常会在具有会对应到 <code>&quot;org.opencontainers.image.ref.name&quot;</code> 注释的 <code>&quot;v1.0.0-vendor.0&quot;</code>、<code>&quot;2.0.0-debug&quot;</code> 等。</p>

<h5 id="镜像索引示例">镜像索引示例</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#f92672">&#34;manifests&#34;</span>: [
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.index.v1+json&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7143</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:0228f90e926ba6b96e4f39cf294b2586d38fbb5a1e385c05cd1ee40ea54fe7fd&#34;</span>,
      <span style="color:#f92672">&#34;annotations&#34;</span>: {
        <span style="color:#f92672">&#34;org.opencontainers.image.ref.name&#34;</span>: <span style="color:#e6db74">&#34;stable-release&#34;</span>
      }
    },
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7143</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f&#34;</span>,
      <span style="color:#f92672">&#34;platform&#34;</span>: {
        <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;ppc64le&#34;</span>,
        <span style="color:#f92672">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>
      },
      <span style="color:#f92672">&#34;annotations&#34;</span>: {
        <span style="color:#f92672">&#34;org.opencontainers.image.ref.name&#34;</span>: <span style="color:#e6db74">&#34;v1.0&#34;</span>
      }
    },
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/xml&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7143</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:b3d63d132d21c3ff4c35a061adf23cf43da8ae054247e32faa95494d904a007e&#34;</span>,
      <span style="color:#f92672">&#34;annotations&#34;</span>: {
        <span style="color:#f92672">&#34;org.freedesktop.specifications.metainfo.version&#34;</span>: <span style="color:#e6db74">&#34;1.0&#34;</span>,
        <span style="color:#f92672">&#34;org.freedesktop.specifications.metainfo.type&#34;</span>: <span style="color:#e6db74">&#34;AppStream&#34;</span>
      }
    }
  ],
  <span style="color:#f92672">&#34;annotations&#34;</span>: {
    <span style="color:#f92672">&#34;com.example.index.revision&#34;</span>: <span style="color:#e6db74">&#34;r124356&#34;</span>
  }
}</code></pre></div>
<p>这展示了一个索引，该索引为此 image 布局提供两个命名引用和一个辅助媒体类型。</p>

<p>第一个命名引用（<code>stable-release</code>）指向另一个索引，该索引可能包含具有不同平台和注释的多个引用。请注意，<a href="#注释"><code>org.opencontainers.image.ref.name</code> 注释</a> SHOULD 只在 index.json 上的描述符（<code>manifests</code> 字段）上被认为是有效的。</p>

<p>第二个命名引用 (v1.0) 指向特定于 linux/ppc64le 平台的 Manifest。</p>

<h3 id="镜像索引">镜像索引</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/image-index.md">原文链接</a></p>

<p>译者注：描述了一组镜像，主要为了不同操作系统的分发不同的镜像</p>
</blockquote>

<p>镜像索引是一个更高级别的清单，它指向特定的<a href="#镜像-manifest">镜像 Manifest</a>，非常适合一个或多个平台。虽然镜像索引的使用对于镜像提供者来说是可选的，但镜像消费者应该准备好处理它们。</p>

<p>镜像索引定义了为 <code>application/vnd.oci.image.index.v1+json</code> 的<a href="#媒体类型">媒体类型</a>。</p>

<p>有关本文档兼容的媒体类型，请参阅 <a href="#兼容性-matrix">兼容性 Matrix</a>。</p>

<h4 id="镜像索引属性">镜像索引属性</h4>

<ul>
<li><p><strong><code>schemaVersion</code></strong> <em>int</em></p>

<p>此 REQUIRED 属性指定镜像 Manifest Schema 版本。 对于这个版本的规范，这必须是 <code>2</code> 以确保与旧版本的 Docker 向后兼容。 该字段的值不会改变。 在规范的未来版本中，该字段可能会被删除。</p></li>

<li><p><strong><code>mediaType</code></strong> <em>string</em></p>

<p>这个属性 SHOULD 配合 <a href="#兼容性-matrix">兼容性 Matrix</a> 使用以支持旧版本规范以及以及其他类似的外部格式。使用时，该字段值 MUST 是媒体类型 <code>application/vnd.oci.image.index.v1+json</code>。 此字段的使用与<a href="#内容描述符">描述符</a>的 <code>mediaType</code> 是不同的.</p></li>

<li><p><strong><code>manifests</code></strong> <em>array of objects</em></p>

<p>此 REQUIRED 属性包含特定平台的 <a href="/opencontainers/image-spec/blob/main/manifest.md">manifests</a> 列表。虽然这个属性必须存在，但数组的大小可以为零。</p>

<p><code>manifests</code> 中的每个对象都包含一组<a href="#描述符属性">描述符属性</a>，并具有以下附加属性和限制：</p>

<ul>
<li><p><strong><code>mediaType</code></strong> <em>string</em>
<a href="#描述符属性">描述符属性</a> 除了对 <code>manifests</code> 的限制外.</p>

<ul>
<li>MUST 实现 <a href="#镜像-manifest"><code>application/vnd.oci.image.manifest.v1+json</code></a></li>
<li>SHOULD 支持 <code>application/vnd.oci.image.index.v1+json</code> （嵌套索引）（目前仍正式未发布）</li>
</ul>

<p>与可移植性有关的镜像索引应该使用上述媒体类型之一。该规范的未来版本可能使用不同的媒体类型（即新的版本的格式规范）。必须忽略实现未知的遇到的媒体类型。</p></li>

<li><p><strong><code>platform</code></strong> <em>object</em></p>

<p>此 OPTIONAL 属性描述了镜像的最低运行时要求。 如果它的目标是特定于平台的，那么这个属性应该存在。如果多个 Manifest 匹配客户端或运行时的要求，则应使用第一个匹配条目。</p>

<ul>
<li><p><strong><code>architecture</code></strong> <em>string</em></p>

<p>此 REQUIRED 属性指定 CPU 体系结构。 镜像索引应该使用，并且实现应该理解 Go 语言文档中列出的值 <a href="https://golang.org/doc/install/source#environment"><code>GOARCH</code></a></p></li>

<li><p><strong><code>os</code></strong> <em>string</em></p>

<p>此 REQUIRED 属性指定操作系统。 镜像索引应该使用，并且实现应该理解 Go 语言文档中列出的值 <a href="https://golang.org/doc/install/source#environment"><code>GOOS</code></a></p></li>

<li><p><strong><code>os.version</code></strong> <em>string</em></p>

<p>此 OPTIONAL 属性指定引用的 blob 所针对的操作系统的版本。 实现可以拒绝使用不知道 <code>os.version</code> 与主机操作系统版本一起工作的清单。 有效值是实现定义的。 例如 Windows 上的 <code>10.0.14393.1066</code></p></li>

<li><p><strong><code>os.features</code></strong> <em>array of strings</em></p>

<p>此 OPTIONAL 属性指定一个字符串数组，每个字符串指定一个强制性的操作系统功能。 当 <code>os</code> 是 <code>windows</code> 时，应该使用镜像索引，并且实现应该理解以下值：</p>

<ul>
<li><code>win32k</code>: 镜像需要主机上的“win32k.sys”（注意：Nano Server 上缺少“win32k.sys”）</li>
</ul>

<p>当 os 不是 windows 时，值是实现定义的，应该提交给这个规范进行标准化。</p></li>

<li><p><strong><code>variant</code></strong> <em>string</em></p>

<p>此 OPTIONAL 属性指定 CPU 的变体。 镜像索引应该使用并且实现应该理解 <a href="#platform-variants">Platform Variants</a> 表中列出的 <code>variant</code> 值。</p></li>

<li><p><strong><code>features</code></strong> <em>array of strings</em></p>

<p>此属性为规范的未来版本保留。</p></li>
</ul></li>
</ul></li>

<li><p><strong><code>annotations</code></strong> <em>string-string map</em></p>

<p>此 OPTIONAL 属性包含镜像索引的任意元数据。 此可选属性必须使用 <a href="#注释规则">注释规则</a>。</p>

<p>参见 <a href="#预定义的注释">Pre-Defined Annotation Keys</a>.</p></li>
</ul>

<h4 id="platform-variants">Platform Variants</h4>

<p>当 CPU 的变体未在表中列出时，值是实现定义的，应该提交给本规范进行标准化。</p>

<table>
<thead>
<tr>
<th>ISA/ABI</th>
<th><code>architecture</code></th>
<th><code>variant</code></th>
</tr>
</thead>

<tbody>
<tr>
<td>ARM 32-bit, v6</td>
<td><code>arm</code></td>
<td><code>v6</code></td>
</tr>

<tr>
<td>ARM 32-bit, v7</td>
<td><code>arm</code></td>
<td><code>v7</code></td>
</tr>

<tr>
<td>ARM 32-bit, v8</td>
<td><code>arm</code></td>
<td><code>v8</code></td>
</tr>

<tr>
<td>ARM 64-bit, v8</td>
<td><code>arm64</code></td>
<td><code>v8</code></td>
</tr>
</tbody>
</table>

<h4 id="镜像索引示例-1">镜像索引示例</h4>

<p>示例显示指向两个平台的镜像清单的简单镜像索引：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#f92672">&#34;manifests&#34;</span>: [
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7143</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f&#34;</span>,
      <span style="color:#f92672">&#34;platform&#34;</span>: {
        <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;ppc64le&#34;</span>,
        <span style="color:#f92672">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>
      }
    },
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.manifest.v1+json&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7682</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270&#34;</span>,
      <span style="color:#f92672">&#34;platform&#34;</span>: {
        <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>,
        <span style="color:#f92672">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>
      }
    }
  ],
  <span style="color:#f92672">&#34;annotations&#34;</span>: {
    <span style="color:#f92672">&#34;com.example.key1&#34;</span>: <span style="color:#e6db74">&#34;value1&#34;</span>,
    <span style="color:#f92672">&#34;com.example.key2&#34;</span>: <span style="color:#e6db74">&#34;value2&#34;</span>
  }
}</code></pre></div>
<h3 id="镜像-manifest">镜像 Manifest</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md">原文链接</a></p>

<p>译者注：镜像 Manifest，描述了一个镜像配置和数据。</p>
</blockquote>

<p>镜像 Manifest 规范有三个主要目标。第一个目标是内容可寻址镜像，通过支持镜像模型，该模型镜像的配置可以被 hash 以生成镜像及其组件的唯一 ID。第二个目标是通过 &ldquo;fat manifest&rdquo; 允许多架构镜像，该 &ldquo;fat manifest&rdquo; 引用特定平台版本的镜像 Manifest。在 OCI 中，这被定义在<a href="#镜像索引">镜像索引</a>中。第三个目标是可<a href="#转换到-oci-运行时配置">转换</a>为 <a href="https://github.com/opencontainers/runtime-spec">OCI 运行时规范</a>。</p>

<p>本节定义 <code>application/vnd.oci.image.manifest.v1+json</code> <a href="#媒体类型">媒体类型</a>。对于兼容的媒体类型，请参见 <a href="#兼容性-matrix">Matrix</a>。</p>

<h4 id="镜像-manifest-属性">镜像 Manifest 属性</h4>

<ul>
<li><strong><code>schemaVersion</code></strong> <em>int</em></li>
</ul>

<p>此 REQUIRED 属性指定镜像清单架构版本。对于这个版本的规范，这必须是 2 以确保向后兼容旧版本的 Docker。该字段的值不会改变。在规范的未来版本中，该字段可能会被删除。</p>

<ul>
<li><p><strong><code>mediaType</code></strong> <em>string</em></p>

<p>此 SHOULD 属性被使用并且与本规范的早期版本和其他类似的外部格式<a href="#兼容性-matrix">保持兼容</a>。使用时，此字段必须包含媒体类型 <code>application/vnd.oci.image.manifest.v1+json</code>。此字段的使用与 mediaType 的<a href="#内容描述符">描述符</a>使用不同。</p></li>

<li><p><strong><code>config</code></strong> <em><a href="#内容描述符">内容描述符</a></em></p>

<p>此 REQUIRED 属性通过摘要引用容器的配置对象。除了<a href="#描述符属性">描述符要求</a>之外，该值还有以下附加限制：</p>

<ul>
<li><p><strong><code>mediaType</code></strong> <em>string</em></p>

<p>config 字段对这个<a href="#描述符属性">描述符属性</a> 有额外的限制。实现必须至少支持以下媒体类型：</p>

<ul>
<li><a href="#镜像配置"><code>application/vnd.oci.image.config.v1+json</code></a></li>
</ul>

<p>与可移植性有关的 Manifest SHOULD 使用上述媒体类型之一。</p></li>
</ul></li>

<li><p><strong><code>layers</code></strong> <em>array of objects</em></p>

<p>数组中的每个项目必须是一个<a href="#内容描述符">描述符</a>。数组 MUST 在索引 0 处具有基础层。随后的层必须按照堆栈顺序（即从 <code>layers[0]</code> 到 <code>layers[len(layers)-1]</code>）。最终的文件系统布局必须与将层<a href="#应用变更集">应用</a>到空目录的结果相匹配。初始空目录的所有权、模式和其他属性未指定。</p>

<p>除了<a href="#描述符属性">描述符要求</a>之外，该值还有以下附加限制：</p>

<ul>
<li><p><strong><code>mediaType</code></strong> <em>string</em></p>

<p><code>layers[]</code> 对此<a href="#描述符属性">描述符属性</a>对有额外的限制。实现必须至少支持以下媒体类型：</p>

<ul>
<li><a href="#镜像层文件系统变更集"><code>application/vnd.oci.image.layer.v1.tar</code></a></li>
<li><a href="#gzip-媒体类型"><code>application/vnd.oci.image.layer.v1.tar+gzip</code></a></li>
<li><a href="#不可分发层"><code>application/vnd.oci.image.layer.nondistributable.v1.tar</code></a></li>
<li><a href="#gzip-媒体类型"><code>application/vnd.oci.image.layer.nondistributable.v1.tar+gzip</code></a></li>
</ul>

<p>与可移植性有关的镜像 Manifest 应该使用上述媒体类型之一。</p>

<p>此字段中的条目将经常使用 <code>+gzip</code> 类型。</p></li>
</ul></li>

<li><p><strong><code>annotations</code></strong> <em>string-string map</em></p>

<p>此 OPTIONAL 属性包含镜像 Manifest 的任意元数据。此可选属性必须使用<a href="#注释规则">注释规则</a>。</p>

<p>请参阅<a href="#预定义的注释">预定义的注释</a></p></li>
</ul>

<h4 id="镜像-manifest-示例">镜像 Manifest 示例</h4>

<p>展示的是镜像 Manifest 的示例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;schemaVersion&#34;</span>: <span style="color:#ae81ff">2</span>,
  <span style="color:#f92672">&#34;config&#34;</span>: {
    <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.config.v1+json&#34;</span>,
    <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">7023</span>,
    <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7&#34;</span>
  },
  <span style="color:#f92672">&#34;layers&#34;</span>: [
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">32654</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0&#34;</span>
    },
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">16724</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b&#34;</span>
    },
    {
      <span style="color:#f92672">&#34;mediaType&#34;</span>: <span style="color:#e6db74">&#34;application/vnd.oci.image.layer.v1.tar+gzip&#34;</span>,
      <span style="color:#f92672">&#34;size&#34;</span>: <span style="color:#ae81ff">73109</span>,
      <span style="color:#f92672">&#34;digest&#34;</span>: <span style="color:#e6db74">&#34;sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736&#34;</span>
    }
  ],
  <span style="color:#f92672">&#34;annotations&#34;</span>: {
    <span style="color:#f92672">&#34;com.example.key1&#34;</span>: <span style="color:#e6db74">&#34;value1&#34;</span>,
    <span style="color:#f92672">&#34;com.example.key2&#34;</span>: <span style="color:#e6db74">&#34;value2&#34;</span>
  }
}</code></pre></div>
<h3 id="镜像层文件系统变更集">镜像层文件系统变更集</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md">原文链接</a></p>
</blockquote>

<p>本文档（译者注：本小结）描述了如何将文件系统和文件系统更改（如删除的文件）序列化到称为层的 blob 中。一个或多个层被应用在彼此之上以创建一个完整的文件系统。本文档将使用一个具体示例来说明如何创建和使用这些文件系统层。</p>

<p>本小结定义了 <code>application/vnd.oci.image.layer.v1.tar</code>, <code>application/vnd.oci.image.layer.v1.tar+gzip</code>, <code>application/vnd.oci.image.layer.nondistributable.v1.tar</code>, and <code>application/vnd.oci.image.layer.nondistributable.v1.tar+gzip</code> <a href="#媒体类型">媒体类型</a></p>

<h4 id="gzip-媒体类型"><code>+gzip</code> 媒体类型</h4>

<ul>
<li>媒体类型 <code>application/vnd.oci.image.layer.v1.tar+gzip</code> 表示一个 <code>application/vnd.oci.image.layer.v1.tar</code> 被 <a href="https://tools.ietf.org/html/rfc1952">gzip</a> 压缩.</li>
<li>媒体类型 <code>application/vnd.oci.image.layer.nondistributable.v1.tar+gzip</code> 表示一个 <code>application/vnd.oci.image.layer.nondistributable.v1.tar</code> 被 <a href="https://tools.ietf.org/html/rfc1952">gzip</a> 压缩</li>
</ul>

<h4 id="可分发格式">可分发格式</h4>

<ul>
<li><a href="#媒体类型">媒体类型</a> <code>application/vnd.oci.image.layer.v1.tar</code> 的层变更集 MUST 打包在 <a href="https://en.wikipedia.org/wiki/Tar_(computing)">tar 存档</a>中。</li>
<li><a href="#媒体类型">媒体类型</a> <code>application/vnd.oci.image.layer.v1.tar</code> 的层变更集 MUST NOT 生成的 tar 存档中包含文件路径的重复条目。</li>
</ul>

<h4 id="变更类型">变更类型</h4>

<p>变更集中可能发生的变更类型有：</p>

<ul>
<li>Additions （新增）</li>
<li>Modifications （修改）</li>
<li>Removals （删除）</li>
</ul>

<p>添加和修改在变更集 tar 存档中的表示方式相同。</p>

<p>删除使用 <a href="#whiteout"><code>&quot;whiteout&quot;</code></a> 文件项表示（请参阅：<a href="#变更的表示">变更的表示</a>）。</p>

<h5 id="文件类型">文件类型</h5>

<p>在本文档部分中，&rdquo;文件&rdquo; 或 &ldquo;条目&rdquo; 一词的使用包括以下内容（如果支持）：</p>

<ul>
<li>regular files 普通文件</li>
<li>directories 目录</li>
<li>sockets sockets 文件</li>
<li>symbolic links 符号链接</li>
<li>block devices 块设备</li>
<li>character devices 字符设备</li>
<li>FIFOs 队列</li>
</ul>

<h5 id="文件属性">文件属性</h5>

<p>在支持的情况下，必须包括添加和修改的文件属性，包括：</p>

<ul>
<li>Modification Time (mtime) 修改时间</li>
<li>User ID (uid) 用户 id

<ul>
<li>User Name (uname) 相对于 uid 是次要的</li>
</ul></li>
<li>Group ID (gid) 组 id

<ul>
<li>Group Name (gname) 相对于 gid 是次要的</li>
</ul></li>
<li>Mode (mode) 模式</li>
<li>Extended Attributes (xattrs) 扩展属性</li>
<li>Symlink reference (linkname + symbolic link type) 符号链接引用</li>
<li>Hardlink reference (linkname) 硬链接引用</li>
</ul>

<p>SHOULD NOT 使用<a href="https://zh.wikipedia.org/wiki/%E7%A8%80%E7%96%8F%E6%96%87%E4%BB%B6">稀疏文件</a>，因为它们缺乏跨 tar 实现的一致支持。</p>

<blockquote>
<p>译者注：
* 支持文件属性受限于 <a href="https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format">tar 归档</a>归档文件格式（即 POSIX IEEE P1003.1 1988 UStar format 格式， Linux 相关参见： <a href="https://www.gnu.org/software/tar/manual/html_section/Formats.html">man tar Controlling the Archive Format</a> ）
* 实现上使用 <a href="https://github.com/vbatts/tar-split">vbatts/tar-split</a> 打包</p>
</blockquote>

<h6 id="hardlinks">Hardlinks</h6>

<ul>
<li>硬链接是一种 POSIX 概念，用于在同一设备上为同一文件提供一个或多个目录条目。</li>
<li>并非所有文件系统都支持硬链接（例如 <a href="https://en.wikipedia.org/wiki/File_Allocation_Table">FAT</a>）。</li>
<li>除了目录之外的所有<a href="#文件类型">文件类型</a>都可以使用硬链接。</li>
<li>当链接计数大于 1 时，非目录文件被视为 &ldquo;硬链接&rdquo;。</li>
<li>硬链接文件位于同一设备上（即比较主要：次要对）并且具有相同的 inode。</li>
<li>与 &gt; 1 链接计数共享链接的相应文件可能位于生成变更集的目录之外，在这种情况下，链接名称不会记录在变更集中。</li>
<li>根据 GNU Basic Tar Format 和 libarchive tar(5)，硬链接存储在类型为 1 char 的 tar 存档中。</li>

<li><p>虽然派生新的或更改的硬链接的方法可能会有所不同，但可能的方法是：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">SET LinkMap to map[&lt; Major:Minor String &gt;]map[&lt; inode integer &gt;]&lt; path string &gt;
SET LinkNames to map[&lt; src path string &gt;]&lt; dest path string &gt;
FOR each path in root path
IF path type is directory
CONTINUE
ENDIF
SET filestat to stat(path)
IF filestat num of links == 1
CONTINUE
ENDIF
IF LinkMap[filestat device][filestat inode] is not empty
SET LinkNames[path] to LinkMap[filestat device][filestat inode]
ELSE
SET LinkMap[filestat device][filestat inode] to path
ENDIF
END FOR</pre></div></li>
</ul>

<p>使用这种方法，可以将一个目录的链接映射和链接名称与另一个目录的链接名称进行比较，以得出对硬链接的添加和更改。</p>

<h6 id="特定于平台的属性">特定于平台的属性</h6>

<p>Windows 上的实现必须支持这些附加属性，在 <a href="https://github.com/libarchive/libarchive/wiki/ManPageTar5#pax-interchange-format">PAX 供应商扩展</a>中编码如下：</p>

<ul>
<li><a href="https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx">Windows file attributes</a> (<code>MSWINDOWS.fileattr</code>)</li>
<li><a href="https://msdn.microsoft.com/en-us/library/cc230366.aspx">Security descriptor</a> (<code>MSWINDOWS.rawsd</code>): base64-encoded self-relative binary security descriptor</li>
<li>Mount points (<code>MSWINDOWS.mountpoint</code>): if present on a directory symbolic link, then the link should be created as a <a href="https://en.wikipedia.org/wiki/NTFS_junction_point">directory junction</a></li>
<li>Creation time (<code>LIBARCHIVE.creationtime</code>)</li>
</ul>

<h4 id="创建">创建</h4>

<h5 id="初始根文件系统">初始根文件系统</h5>

<p>初始根文件系统是基础层或父层。</p>

<p>对于此示例，镜像根文件系统的初始状态为空目录。目录的名称与层本身无关，仅用于产生比较的目的。</p>

<p>这是变更集的初始空目录结构，具有唯一的目录名称 <code>rootfs-c9d-v1</code>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">rootfs-c9d-v1/</pre></div>
<h5 id="填充初始文件系统">填充初始文件系统</h5>

<p>然后创建文件和目录：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">rootfs-c9d-v1/
    etc/
        my-app-config
    bin/
        my-app-binary
        my-app-tool</pre></div>
<p>然后将 rootfs-c9d-v1 目录创建为具有 rootfs-c9d-v1 的相对路径的普通 <a href="https://en.wikipedia.org/wiki/Tar_(computing)">tar 存档</a>。以下文件的条目：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">./
./etc/
./etc/my-app-config
./bin/
./bin/my-app-binary
./bin/my-app-tools</pre></div>
<h5 id="填充比较文件系统">填充比较文件系统</h5>

<p>创建一个新目录并使用先前根文件系统的副本或快照对其进行初始化。可以保留文件属性以制作此副本的示例命令是：</p>

<ul>
<li><a href="http://linux.die.net/man/1/cp">cp(1)</a>: <code>cp -a rootfs-c9d-v1/ rootfs-c9d-v1.s1/</code></li>
<li><a href="http://linux.die.net/man/1/rsync">rsync(1)</a>: <code>rsync -aHAX rootfs-c9d-v1/ rootfs-c9d-v1.s1/</code></li>
<li><a href="http://linux.die.net/man/1/tar">tar(1)</a>: <code>mkdir rootfs-c9d-v1.s1 &amp;&amp; tar --acls --xattrs -C rootfs-c9d-v1/ -c . | tar -C rootfs-c9d-v1.s1/ --acls --xattrs -x (including --selinux where supported)</code></li>
</ul>

<p>对快照的任何更改都不得更改或影响其拷贝自的目录（译者注：意识应该是，对 <code>rootfs-c9d-v1.s1</code> 的变更都不应该影响 <code>rootfs-c9d-v1/</code> 目录）。</p>

<p>例如 <code>rootfs-c9d-v1.s1</code> 是 <code>rootfs-c9d-v1</code> 的相同快照。这样，<code>rootfs-c9d-v1.s1</code> 就为更新和更改做好了准备。</p>

<p>实施者注：写时复制或联合文件系统（copy-on-write or union filesystem）可以有效地制作目录快照：</p>

<p>快照的初始布局：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">rootfs-c9d-v1.s1/
    etc/
        my-app-config
    bin/
        my-app-binary
        my-app-tools</pre></div>
<p>有关变更的更多详细信息，请参阅<a href="#变更类型">变更类型</a>。</p>

<p>例如，在 <code>/etc/my-app.d</code> 中添加一个包含默认配置文件的目录，删除现有的配置文件。还对 <code>./bin/my-app-tools</code> 二进制文件进行更改（属性或文件内容）以处理配置布局更改。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">rootfs-c9d-v1.s1/
    etc/
        my-app.d/
            default.cfg
    bin/
        my-app-binary
        my-app-tools</pre></div>
<h5 id="确定变更">确定变更</h5>

<p>比较两个目录时，相对根目录是顶级目录。比较目录，查找<a href="#变更类型">已添加、修改或删除</a>的文件。</p>

<p>对这个例子，<code>rootfs-c9d-v1/</code> 和 <code>rootfs-c9d-v1.s1/</code> 作为相对根路径进行递归比较。</p>

<p>找到以下变更集：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Added:      /etc/my-app.d/
Added:      /etc/my-app.d/default.cfg
Modified:   /bin/my-app-tools
Deleted:    /etc/my-app-config</pre></div>
<p>这表示删除 <code>/etc/my-app-config</code> 并添加了 <code>/etc/my-app.d/default.cfg</code> 目录和文件。 <code>/bin/my-app-tools</code> 也已替换为更新版本。</p>

<h5 id="变更的表示">变更的表示</h5>

<p>然后创建一个仅包含此变更集的 <a href="https://en.wikipedia.org/wiki/Tar_(computing)">tar 存档</a>：</p>

<ul>
<li>添加和修改的文件和目录</li>
<li>已删除的文件或目录被标记为 <a href="#whiteout">whiteout 文件</a></li>
</ul>

<p>生成的 <code>rootfs-c9d-v1.s1</code> 的 tar 存档具有以下条目：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">./etc/my-app.d/
./etc/my-app.d/default.cfg
./bin/my-app-tools
./etc/.wh.my-app-config</pre></div>
<p>为了表示在应用变更集时必须删除资源 <code>./etc/my-app-config</code>，条目的基本名称以 <code>.wh.</code> 为前缀。</p>

<h4 id="应用变更集">应用变更集</h4>

<ul>
<li><a href="#媒体类型">媒体类型</a> <code>application/vnd.oci.image.layer.v1.tar</code> 的层变更集会被应用，而不是简单地提取 tar 归档。</li>
<li>应用层变更集需要特别考虑 <a href="#whiteout">whiteout 文件</a>。</li>
<li>在层变更集中没有任何 <a href="#whiteout">whiteout 文件</a> 的情况下，存档会像常规 tar 存档一样被提取。</li>
</ul>

<h5 id="变更集应用在已存在的文件">变更集应用在已存在的文件</h5>

<p>如果目标路径已存在，此部分指定应用层变更集中的条目。</p>

<p>如果条目和现有路径都是目录，则现有路径的属性必须由变更集中条目的属性替换。在所有其他情况下，实现必须执行以下语义等效：</p>

<ul>
<li>删除文件路径（例如 Linux 系统上的 <a href="http://linux.die.net/man/2/unlink">unlink(2)</a>）</li>
<li>根据变更集条目的内容和属性重新创建文件路径</li>
</ul>

<h4 id="whiteout">Whiteout</h4>

<ul>
<li>whiteout 文件是具有特殊文件名的空文件，表示应删除路径。</li>
<li>whiteout 文件的文件名（译者注：不包含路径前缀的名称）由前缀 <code>.wh.</code> 加上要删除的路径的基本名称（译者注：即不包含路径前缀的名称）。</li>
<li>作为以 <code>.wh.</code> 为前缀的文件。是特殊的 whiteout 标记，不可能创建一个文件系统，其文件或目录的名称以 <code>.wh.</code> 开头。</li>
<li>一旦应用了 whiteout，whiteout 本身也 MUST 被隐藏。</li>
<li>whiteout 文件 MUST 仅能应用于 lower/parent 层。</li>
<li>与 whiteout 文件位于同一层的文件只能被后续层中的 whiteout 文件隐藏。</li>
</ul>

<p>以下是具有多个资源的基础层：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">a/
a/b/
a/b/c/
a/b/c/bar</pre></div>
<p>创建下一层时，删除原来的 <code>a/b</code> 目录，用 <code>a/b/c/foo</code> 重新创建：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">a/
a/.wh..wh..opq
a/b/
a/b/c/
a/b/c/foo</pre></div>
<p>在处理第二层时，首先应用 <code>a/.wh..wh..opq</code>，然后再创建新版本的 <code>a/b</code>，而不管遇到 whiteout 文件的顺序如何。例如，下面的层等价于上面的层：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">a/
a/b/
a/b/c/
a/b/c/foo
a/.wh..wh..opq</pre></div>
<p>实现生成层时 SHOULD 让 without 文件位于同级目录的其他条目之前。</p>

<h5 id="opaque-whiteout">Opaque Whiteout</h5>

<ul>
<li>除了表示应该从较低层中删除单个条目之外，层还可以使用 Opaque Whiteout 来删除所有子项。</li>
<li>一个 Opaque Whiteout 是一个名为 <code>.wh..wh..opq</code> 的文件，表示所有兄弟姐妹都隐藏在较低层中。</li>
</ul>

<p>我们以下面的基础层为例：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">etc/
	my-app-config
bin/
	my-app-binary
	my-app-tools
	tools/
		my-app-tool-one</pre></div>
<p>如果 bin/ 的所有子级都被删除，则下一层将具有以下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">bin/
	.wh..wh..opq</pre></div>
<p>这称为 opaque whiteout 格式。一个 opaque whiteout 文件隐藏了 bin/ 的所有子目录，包括子目录和所有后代。如果使用显式 whiteout 文件，这将等效于以下内容：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">bin/
	.wh.my-app-binary
	.wh.my-app-tools
	.wh.tools</pre></div>
<p>在这种情况下，将为每个条目生成一个唯一的 without 文件 。如果基础层中有更多 bin/ 的子级，则每个子级都会有一个条目。请注意，此 opaque whiteout 将应用于所有子目录，包括子目录、其他资源和所有后代。</p>

<p>实现应该使用显式的 whiteout 文件生成层，但必须接受两者。</p>

<p>任何给定的镜像都是由几个镜像文件系统变更集 tar 档案几个组成。</p>

<h4 id="不可分发层">不可分发层</h4>

<p>由于法律要求，某些层可能无法定期分发。这种不可分发的层通常直接从分发者下载，但从不上传。</p>

<p>不可分发的层应该使用 <code>application/vnd.oci.image.layer.nondistributable.v1.tar</code> 的替代媒体类型进行标记。实现 SHOULD NOT 上传带有此媒体类型标签的图层；然而，这种媒体类型 SHOULD NOT 影响实现是否下载层。</p>

<p><a href="#内容描述符">描述符</a>不可分发层的描述符可能包含用于直接下载这些层的 <code>urls</code>；但是，不应该使用 <code>urls</code> 字段的是否存在来确定层是否是不可分发的。</p>

<h3 id="镜像配置">镜像配置</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/config.md">原文链接</a></p>
</blockquote>

<p>OCI 镜像是由 根文件系统更改的有序集合 以及 容器运行时中会使用的相应执行参数组成的。本规范使用 JSON 格式描述了用于运行时的镜像、执行工具以及它与文件系统变更集的关系（被描述在 <a href="#镜像层文件系统变更集">镜像层文件系统变更集</a> 文档）</p>

<p>本节定义 <code>application/vnd.oci.image.config.v1+json</code> <a href="#媒体类型">媒体类型</a>。</p>

<h4 id="术语">术语</h4>

<p>本部分使用以下术语：</p>

<h4 id="layer-镜像层文件系统变更集"><a href="#镜像层文件系统变更集">Layer</a></h4>

<ul>
<li>镜像文件系统由层组成。</li>
<li>每个层表示一组基于 tar 层格式的文件系统更改集合，记录其相对于其父层添加、更改或删除的文件。</li>
<li>层没有配置元数据，例如环境变量或默认参数——这些是镜像作为一个整体而不是任何特定层的属性。</li>
<li>使用基于层或联合文件系统（如 AUFS），或通过计算文件系统快照的差异，文件系统变更集可用于呈现一系列镜像层，就好像它们是一个内聚的文件系统一样。</li>
</ul>

<h4 id="image-json">Image JSON</h4>

<ul>
<li>每个镜像都有一个关联的 JSON 结构，该结构描述了有关镜像的一些基本信息，例如创建日期、作者以及执行/运行时配置，例如其入口点、默认参数、网络和卷。</li>
<li>JSON 结构还引用镜像使用的每一层的加密哈希，并提供这些层的历史信息。</li>
<li>此 JSON 被认为是不可变的，因为更改它会更改计算的 <a href="#ImageID">ImageID</a>。</li>
<li>更改它意味着创建一个新的派生镜像，而不是更改现有镜像。</li>
</ul>

<h4 id="layer-diffid">Layer DiffID</h4>

<p>层 DiffID 是层的未压缩 tar 存档上的摘要，并以描述符摘要格式序列化，例如 sha256:a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9。层应该可重复地打包和解包以避免更改层 DiffID，例如通过使用 <a href="https://github.com/vbatts/tar-split">tar-split</a> 来保存 tar 标头。</p>

<p>注意：不要将 DiffID 与层摘要混淆，层摘要通常在清单中引用，它们是压缩或未压缩内容的摘要。</p>

<h4 id="layer-chainid">Layer ChainID</h4>

<p>为方便起见，有时用单个标识符来指代一个层的堆栈（译者注：stack，表示一系列有顺序的层）很有用。层的 DiffID 标识单个变更集，而 ChainID 标识这些变更集的应用。这确保我们拥有引用层本身的句柄，也拥有指向一系列变更集应用结果的句柄。与 <code>rootfs.diff_ids</code> 结合使用，当应用层到根文件系统时，可以唯一地、安全地识别结果。</p>

<h5 id="定义">定义</h5>

<p>一组应用层的 ChainID 使用以下递归定义：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ChainID(L₀) =  DiffID(L₀)
ChainID(L₀|...|Lₙ₋₁|Lₙ) = Digest(ChainID(L₀|...|Lₙ₋₁) + &#34; &#34; + DiffID(Lₙ))</pre></div>
<p>为此，我们定义二进制 <code>|</code> 操作是将右操作数应用于左操作数的结果。例如，给定基础层 A 和变更集 B，我们将 B 应用于 A 的结果称为 <code>A|B</code>。</p>

<p>上面，我们将单层的 <code>ChainID(L₀)</code> 定义为等效于该层的 DiffID。而，一组层的应用 <code>(L₀|...|Lₙ₁|Lₙ)</code> 的 <code>ChainID</code> 定义为递归 <code>Digest(ChainID(L₀|...|Lₙ₁) + &quot; &quot; + DiffID(Lₙ))</code>。</p>

<h5 id="解释">解释</h5>

<p>假设我们有层 A、B、C，从下到上排序，其中 A 是底部，C 是顶部。定义 <code>|</code> 作为二进制应用程序运算符，根文件系统可能是 <code>A|B|C</code>。虽然暗示 <code>C</code> 仅在应用于 <code>A|B</code> 时才有用，但标识符 <code>C</code> 不足以识别此结果，因为等式 <code>C = A|B|C</code>，这是不正确的。</p>

<p>主要问题是当我们对 <code>C</code> 有两个定义时，<code>C = C</code> 和 <code>C = A|B|C</code>。如果这是真的（有些挥手），<code>C = x|C</code> 其中 <code>x = 任何应用程序</code>。这意味着如果攻击者可以定义 <code>x</code>，则依赖 <code>C</code> 并不能保证以任何顺序应用层。</p>

<p><code>ChainID</code> 通过定义为复合散列来解决这个问题。<strong>我们将变更集 <code>C</code> 与依赖于顺序的应用程序 <code>A|B|C</code> 区分开来，通过说生成的 <code>rootfs</code> 由 <code>ChainID(A|B|C)</code> 标识，可以通过 <code>ImageConfig.rootfs</code> 计算。</strong></p>

<p>让我们展开 <code>ChainID(A|B|C)</code> 的定义来探索它的内部结构：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ChainID(A) = DiffID(A)
ChainID(A|B) = Digest(ChainID(A) + &#34; &#34; + DiffID(B))
ChainID(A|B|C) = Digest(ChainID(A|B) + &#34; &#34; + DiffID(C))</pre></div>
<p>我们可以替换每个定义并简化为一个等式：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ChainID(A|B|C) = Digest(Digest(DiffID(A) + &#34; &#34; + DiffID(B)) + &#34; &#34; + DiffID(C))</pre></div>
<p>希望以上内容能够说明 <code>ChainID</code> 的实际内容。最重要的是，我们可以很容易地看到 <code>ChainID(C) != ChainID(A|B|C)</code>，否则作为基本情况的 <code>ChainID(C) = DiffID(C)</code> 不可能为真。</p>

<h4 id="imageid">ImageID</h4>

<p>每个镜像的 ID 由其<a href="#image-json">配置 JSON</a> 的 SHA256 Hash 给出。它表示为 256 位的十六进制编码，例如 <code>sha256:a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9</code>。由于获取 hash 的<a href="#image-json">配置 JSON</a> 引用镜像中每一层的 hash，因此 ImageID 的这种描述使镜像内容可寻址（译者注：意思是镜像配置 JSON 包含的 <code>rootfs.diff_ids</code> 是内容的 hash，所以可以寻址到进行内容）。</p>

<h4 id="镜像配置属性">镜像配置属性</h4>

<p>注意：任何 OPTIONAL 字段也可以设置为 null，相当于不存在。</p>

<ul>
<li><p><strong>created</strong> <em>string</em>, OPTIONAL</p>

<p>创建镜像的日期时间，格式为 <a href="https://tools.ietf.org/html/rfc3339#section-5.6">RFC 3339, section 5.6</a>.</p></li>

<li><p><strong>author</strong> <em>string</em>, OPTIONAL</p>

<p>提供创建并负责维护镜像的个人或实体的姓名 and/or 电子邮件地址。</p></li>

<li><p><strong>architecture</strong> <em>string</em>, REQUIRED</p>

<p>此镜像中的二进制文件是为在其上运行而构建的CPU架构。
配置 SHOULD 用Go语言文件中列出的值，而实现应理解这些值 <a href="https://golang.org/doc/install/source#environment"><code>GOARCH</code></a>.</p></li>

<li><p><strong>os</strong> <em>string</em>, REQUIRED</p>

<p>镜像运行的操作系统的名称。
配置 SHOULD 用Go语言文件中列出的值，而实现应理解这些值 <a href="https://golang.org/doc/install/source#environment"><code>GOOS</code></a>.</p></li>

<li><p><strong>config</strong> <em>object</em>, OPTIONAL</p>

<p>在使用该映像运行容器时应作为基础的执行参数。
这个字段可以是 <code>null</code>，在这种情况下，任何执行参数都应该在创建容器时指定。</p>

<ul>
<li><p><strong>User</strong> <em>string</em>, OPTIONAL</p>

<p>用户名或UID，这是一个平台特定的结构，允许具体控制进程以哪个用户身份运行。
当创建容器时没有指定该值时，这将作为一个默认值使用。
对于基于Linux的系统，以下所有的都是有效的。<code>user</code>, <code>uid</code>, <code>user:group</code>, <code>uid:gid</code>, <code>uid:group</code>, <code>user:gid</code>。
如果没有指定<code>group</code> / <code>gid</code>，将应用容器中 <code>/etc/passwd</code> 中给定的 <code>user</code> / <code>uid</code> 的默认组和补充组。</p></li>

<li><p><strong>ExposedPorts</strong> <em>object</em>, OPTIONAL</p>

<p>一组要从运行此镜像的容器中公开的端口。
它的键可以是以下格式。
<code>port/tcp</code>, <code>port/udp</code>, <code>port</code>，如果没有指定，默认协议为<code>tcp</code>。
这些值作为默认值，在创建容器时与任何指定的值合并。
<strong>注意：</strong>这个JSON结构值是不寻常的，因为它是Go类型 <code>map[string]struct{}</code> 的直接JSON序列化，在JSON中表示为一个将其键映射到一个空对象的对象。</p></li>

<li><p><strong>Env</strong> <em>array of strings</em>, OPTIONAL</p>

<p>条目格式为：`VARNAME=VARVALUE&rsquo;。
这些值作为默认值，在创建容器时与任何指定的值合并。</p></li>

<li><p><strong>Entrypoint</strong> <em>array of strings</em>, OPTIONAL</p>

<p>一个参数列表，用作容器启动时要执行的命令。
这些值作为默认值，可以由创建容器时指定的入口取代。</p></li>

<li><p><strong>Cmd</strong> <em>array of strings</em>, OPTIONAL</p>

<p>容器的 entrypoint 的默认参数。
这些值作为默认值，可以由创建容器时指定的任何值来代替。
如果没有指定 <code>Entrypoint</code> 值，那么 <code>Cmd</code> 数组的第一个条目就应该被解释为要运行的可执行文件。</p></li>

<li><p><strong>Volumes</strong> <em>object</em>, OPTIONAL</p>

<p>一组描述进程可能写入容器实例特定数据的目录。
<strong>注意：</strong>这个JSON结构值是不寻常的，因为它是Go类型<code>map[string]struct{}</code>的直接JSON序列化，并在JSON中表示为将其键映射到一个空对象。</p></li>

<li><p><strong>WorkingDir</strong> <em>string</em>, OPTIONAL</p>

<p>设置容器中入口进程的当前工作目录。
这个值作为默认值，可以由创建容器时指定的工作目录代替。</p></li>

<li><p><strong>Labels</strong> <em>object</em>, OPTIONAL</p>

<p>该字段包含容器的任意元数据。
这个属性必须使用<a href="#注释规则">注释规则</a></p></li>

<li><p><strong>StopSignal</strong> <em>string</em>, OPTIONAL
该字段包含将被发送到容器中退出的系统调用信号。该信号可以是一个格式为 <code>SIGNAME</code> 的信号名称，例如 <code>SIGKILL</code> 或 <code>SIGRTMIN+3</code>。</p></li>
</ul></li>

<li><p><strong>rootfs</strong> <em>object</em>, REQUIRED</p>

<p>rootfs 键引用镜像所使用的层内容地址。
这使得镜像配置的哈希值（译者注：即上文提到的 ImageID）依赖于文件系统的哈希值。</p>

<ul>
<li><p><strong>type</strong> <em>string</em>, REQUIRED</p>

<p>必须被设置为<code>layers</code>。
如果在验证或解压镜像时遇到一个未知的值，实现必须产生一个错误。</p></li>

<li><p><strong>diff_ids</strong> <em>array of strings</em>, REQUIRED</p>

<p>一个层内容哈希值（`DiffIDs&rsquo;）的数组，按从头到尾的顺序排列。</p></li>
</ul></li>

<li><p><strong>history</strong> <em>array of objects</em>, OPTIONAL</p>

<p>描述了每个层的历史。
数组从第一个到最后一个排序。
该对象有以下字段。</p>

<ul>
<li><p><strong>created</strong> <em>string</em>, OPTIONAL</p>

<p>创建的日期时间，格式为 <a href="https://tools.ietf.org/html/rfc3339#section-5.6">RFC 3339, section 5.6</a>.</p></li>

<li><p><strong>author</strong> <em>string</em>, OPTIONAL</p>

<p>构建点的作者。</p></li>

<li><p><strong>created_by</strong> <em>string</em>, OPTIONAL</p>

<p>创建该层的命令。</p></li>

<li><p><strong>comment</strong> <em>string</em>, OPTIONAL</p>

<p>创建层时设置的一个自定义信息。</p></li>

<li><p><strong>empty_layer</strong> <em>boolean</em>, OPTIONAL</p>

<p>这个字段用来标记历史项目是否创建了一个文件系统的差异。
如果这个历史项目不对应于rootfs部分的实际层（例如，Dockerfile的<a href="https://docs.docker.com/engine/reference/builder/#/env">ENV</a>命令导致文件系统没有变化），它被设置为true。</p></li>
</ul></li>
</ul>

<p>Image JSON结构中的任何额外字段都被认为是特定的实现，并且必须被任何无法解释它们的实现所忽略。</p>

<p>JSON 格式化（空白字符）是可选的，实现可以使用没有空白字符的紧凑JSON。</p>

<h4 id="镜像配置示例">镜像配置示例</h4>

<p>下面是一个镜像配置JSON文档的例子。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
    <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2015-10-31T22:22:56.015925234Z&#34;</span>,
    <span style="color:#f92672">&#34;author&#34;</span>: <span style="color:#e6db74">&#34;Alyssa P. Hacker &lt;alyspdev@example.com&gt;&#34;</span>,
    <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>,
    <span style="color:#f92672">&#34;os&#34;</span>: <span style="color:#e6db74">&#34;linux&#34;</span>,
    <span style="color:#f92672">&#34;config&#34;</span>: {
        <span style="color:#f92672">&#34;User&#34;</span>: <span style="color:#e6db74">&#34;alice&#34;</span>,
        <span style="color:#f92672">&#34;ExposedPorts&#34;</span>: {
            <span style="color:#f92672">&#34;8080/tcp&#34;</span>: {}
        },
        <span style="color:#f92672">&#34;Env&#34;</span>: [
            <span style="color:#e6db74">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span>,
            <span style="color:#e6db74">&#34;FOO=oci_is_a&#34;</span>,
            <span style="color:#e6db74">&#34;BAR=well_written_spec&#34;</span>
        ],
        <span style="color:#f92672">&#34;Entrypoint&#34;</span>: [
            <span style="color:#e6db74">&#34;/bin/my-app-binary&#34;</span>
        ],
        <span style="color:#f92672">&#34;Cmd&#34;</span>: [
            <span style="color:#e6db74">&#34;--foreground&#34;</span>,
            <span style="color:#e6db74">&#34;--config&#34;</span>,
            <span style="color:#e6db74">&#34;/etc/my-app.d/default.cfg&#34;</span>
        ],
        <span style="color:#f92672">&#34;Volumes&#34;</span>: {
            <span style="color:#f92672">&#34;/var/job-result-data&#34;</span>: {},
            <span style="color:#f92672">&#34;/var/log/my-app-logs&#34;</span>: {}
        },
        <span style="color:#f92672">&#34;WorkingDir&#34;</span>: <span style="color:#e6db74">&#34;/home/alice&#34;</span>,
        <span style="color:#f92672">&#34;Labels&#34;</span>: {
            <span style="color:#f92672">&#34;com.example.project.git.url&#34;</span>: <span style="color:#e6db74">&#34;https://example.com/project.git&#34;</span>,
            <span style="color:#f92672">&#34;com.example.project.git.commit&#34;</span>: <span style="color:#e6db74">&#34;45a939b2999782a3f005621a8d0f29aa387e1d6b&#34;</span>
        }
    },
    <span style="color:#f92672">&#34;rootfs&#34;</span>: {
      <span style="color:#f92672">&#34;diff_ids&#34;</span>: [
        <span style="color:#e6db74">&#34;sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1&#34;</span>,
        <span style="color:#e6db74">&#34;sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef&#34;</span>
      ],
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;layers&#34;</span>
    },
    <span style="color:#f92672">&#34;history&#34;</span>: [
      {
        <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2015-10-31T22:22:54.690851953Z&#34;</span>,
        <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /&#34;</span>
      },
      {
        <span style="color:#f92672">&#34;created&#34;</span>: <span style="color:#e6db74">&#34;2015-10-31T22:22:55.613815829Z&#34;</span>,
        <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -c #(nop) CMD [\&#34;sh\&#34;]&#34;</span>,
        <span style="color:#f92672">&#34;empty_layer&#34;</span>: <span style="color:#66d9ef">true</span>
      }
    ]
}</code></pre></div>
<h3 id="注解">注解</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/annotations.md">原文链接</a></p>
</blockquote>

<p>该规范的几个组件，如<a href="#镜像-manifest">镜像 Manifest</a> 和<a href="#内容描述符">描述符</a>，都具有可选的注释属性，其格式是通用的，并在本节中定义。</p>

<p>此属性包含任意元数据。</p>

<h4 id="注释规则">注释规则</h4>

<ul>
<li>注释 MUST 是一个 Map，其中 Key 和 Value 都必须是字符串。</li>
<li>虽然 Value MUST 存在，但它 MAY 是一个空字符串。</li>
<li>Key 在这个 Map 中 MUST 是唯一的，最佳做法是 Key 以名字空间方式命名。</li>
<li>Key SHOULD 使用反向域名符号来命名 - 例如：<code>com.example.myKey</code>。</li>
<li>前缀<code>org.opencontainers</code>是为开放容器倡议（OCI）规范中定义的保留的 Key，其他规范和扩展不得使用。</li>
<li>使用<code>org.opencontainers.image</code>命名空间的 Key 是保留给OCI镜像规范使用的，决不能被其他规范和扩展使用，包括其他OCI规范。</li>
<li>如果没有注释，那么这个属性 MUST 不存在或为空 Map。</li>
<li>如果消费者遇到一个未知的注解 Key，不得生成一个错误。</li>
</ul>

<h4 id="预定义的注释">预定义的注释</h4>

<p>本规范定义了以下注释 Key，用于但不限于<a href="#镜像索引">镜像索引</a>和<a href="#镜像-manifest">镜像 manifest</a>作者。</p>

<ul>
<li><strong>org.opencontainers.image.created</strong> 建立镜像的日期时间（字符串，由<a href="https://tools.ietf.org/html/rfc3339#section-5.6">RFC 3339</a>定义的日期-时间）。</li>
<li><strong>org.opencontainers.image.author</strong> 负责该镜像的人员或组织的详细联系方式（自由格式字符串）</li>
<li><strong>org.opencontainers.image.url</strong> 可以找到更多关于镜像的信息的URL（字符串）。</li>
<li><strong>org.opencontainers.image.document</strong> 获取镜像文档的URL（字符串）</li>
<li><strong>org.opencontainers.image.source</strong> 获取构建镜像的源代码的URL（字符串）</li>
<li><strong>org.opencontainers.image.version</strong> 打包软件的版本。

<ul>
<li>该版本 MAY 与源代码库中的 Tag 或 Label 相匹配</li>
<li>版本可能是<a href="http://semver.org/">Semantic versioning-compatible</a></li>
</ul></li>
<li><strong>org.opencontainers.image.revision</strong> 被打包软件的源代码控制修订标识符。</li>
<li><strong>org.opencontainers.image.vendor</strong>发行实体、组织或个人的名称。</li>
<li><strong>org.opencontainers.image.licenses</strong> 包含的软件作为[SPDX License Expression<a href="https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60">spdx-license-expression</a> 分发的许可证。</li>

<li><p><strong>org.opencontainers.image.ref.name</strong> 目标的参考名称（字符串）（译者注：即镜像 TAG）。</p>

<ul>
<li>SHOULD 只有在 <a href="#镜像布局">镜像布局</a> 内的 <code>index.json</code> 上的描述符时使用才应被视为有效。</li>
<li>该值的字符集应符合 <code>A-Za-z0-9</code> 的字母和 <code>-._:@/+</code> 的分隔符集。</li>

<li><p>引用必须符合以下<a href="#ebnf">语法</a>。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">ref       ::= component (&#34;/&#34; component)*
component ::= alphanum (separator alphanum)*
alphanum  ::= [A-Za-z0-9]+
separator ::= [-._:@+] | &#34;--&#34;</pre></div></li>
</ul></li>

<li><p><strong>org.opencontainers.image.title</strong> 人可读的镜像标题（字符串）。</p></li>

<li><p><strong>org.opencontainers.image.description</strong> 镜像中打包的软件的可读描述（字符串）。</p></li>
</ul>

<h4 id="与-label-schema-的向后兼容">与 Label Schema 的向后兼容</h4>

<p><a href="https://label-schema.org">Label Schema</a> 为容器镜像定义了许多常规标签，现在这些标签被带有以 <strong>org.opencontainers.image</strong> 开头的 Key 的注释所取代。</p>

<p>虽然鼓励用户使用 <strong>org.opencontainers.image</strong> 键，但工具可以选择使用 <strong>org.label-schema</strong> 前缀支持兼容注释，如下所示。</p>

<table>
<thead>
<tr>
<th><code>org.opencontainers.image</code> 前缀</th>
<th><code>org.label-schema</code> 前缀</th>
<th>兼容性说明</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>created</code></td>
<td><code>build-date</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>url</code></td>
<td><code>url</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>source</code></td>
<td><code>vcs-url</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>version</code></td>
<td><code>version</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>revision</code></td>
<td><code>vcs-ref</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>vendor</code></td>
<td><code>vendor</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>title</code></td>
<td><code>name</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>description</code></td>
<td><code>description</code></td>
<td>Compatible</td>
</tr>

<tr>
<td><code>documentation</code></td>
<td><code>usage</code></td>
<td>如果文档通过 URL 定位，则值是兼容的</td>
</tr>

<tr>
<td><code>authors</code></td>
<td></td>
<td>在 Label Schema 中没有等价实现</td>
</tr>

<tr>
<td><code>licenses</code></td>
<td></td>
<td>在 Label Schema 中没有等价实现</td>
</tr>

<tr>
<td><code>ref.name</code></td>
<td></td>
<td>在 Label Schema 中没有等价实现</td>
</tr>

<tr>
<td></td>
<td><code>schema-version</code></td>
<td>在 OCI Image Spec 中没有等价实现</td>
</tr>

<tr>
<td></td>
<td><code>docker.*</code>, <code>rkt.*</code></td>
<td>在 OCI Image Spec 中没有等价实现</td>
</tr>
</tbody>
</table>

<h3 id="转换到-oci-运行时配置">转换到 OCI 运行时配置</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/conversion.md#annotation-fields">原文链接</a></p>
</blockquote>

<p>将 OCI 镜像提取到 <a href="#https://github.com/opencontainers/runtime-spec/blob/v1.0.0/bundle.md">OCI 运行时 Bundle</a> 中时，关于提取在这两个正交组件中，是有相关性的：</p>

<ul>
<li>从 <a href="#镜像层文件系统变更集">文件系统层集合</a> 中提取出根文件系统。</li>
<li>将镜像配置 blob 转换为 OCI 运行时配置 blob。</li>
</ul>

<p>本节定义如何将 <code>application/vnd.oci.image.config.v1+json</code> blob 转换为 <a href="https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md">OCI 运行时配置 blob</a>（即提取后的一个组件）。提取前的组件是前面定义的<a href="#镜像层文件系统变更集">文件系统层集合</a>，其与 <a href="#https://github.com/opencontainers/runtime-spec/blob/v1.0.0/bundle.md">OCI 运行时 Bundle</a> 的配置是正交的。本文件没有规定的运行时配置属性的值，其是由实现定义的。</p>

<p>转换器 MUST 依赖 OCI 镜像配置来构建 OCI 运行时配置，如本文档所述；这将创建 “默认生成的运行时配置”。</p>

<p>“默认生成的运行时配置” 可以被覆盖或与来自调用者的外部提供的输入相结合。此外，转换器可以有自己的实现定义的默认值和扩展，可以与 “默认生成的运行时配置” 结合使用。本文档中的限制仅涉及将实现定义的默认值与 “默认生成的运行时配置” 相结合。外部提供的输入被认为是对 <code>application/vnd.oci.image.config.v1+json</code> 的修改，并且这种修改没有限制。</p>

<p>例如，外部提供的输入可能会导致添加、删除或更改环境变量。但是，实现定义的默认值不应导致环境变量被删除或更改。</p>

<h4 id="单个值字段">单个值字段</h4>

<p>某些镜像配置字段在运行时配置中具有相同的对应项。其中一些是纯粹注释的字段，在下文<a href="#注释字段">单独的小节</a>中讲述。兼容的配置转换器 MUST 提取以下每一个字段到生成的运行时配置中的相应字段：</p>

<table>
<thead>
<tr>
<th>镜像字段</th>
<th>运行时字段</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>Config.WorkingDir</code></td>
<td><code>process.cwd</code></td>
<td></td>
</tr>

<tr>
<td><code>Config.Env</code></td>
<td><code>process.env</code></td>
<td>1</td>
</tr>

<tr>
<td><code>Config.Entrypoint</code></td>
<td><code>process.args</code></td>
<td>2</td>
</tr>

<tr>
<td><code>Config.Cmd</code></td>
<td><code>process.args</code></td>
<td>2</td>
</tr>
</tbody>
</table>

<ol>
<li>转换器可以向 <code>process.env</code> 添加额外的条目，但不应该添加 <code>Config.Env</code> 中已经存在的变量名称的条目。</li>
<li>如果同时指定了 <code>Config.Entrypoint</code> 和 <code>Config.Cmd</code>，转换器必须将 <code>Config.Cmd</code> 的值附加到<code>Config.Entrypoint</code> 的值上，并将 <code>process.args</code> 设置为该合并值。</li>
</ol>

<h5 id="注释字段">注释字段</h5>

<p>These fields all affect the <code>annotations</code> of the runtime configuration, and are thus subject to <a href="#注释转换">precedence</a>.</p>

<table>
<thead>
<tr>
<th>Image Field</th>
<th>Runtime Field</th>
<th>Notes</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>author</code></td>
<td><code>annotations</code></td>
<td>1,2</td>
</tr>

<tr>
<td><code>created</code></td>
<td><code>annotations</code></td>
<td>1,3</td>
</tr>

<tr>
<td><code>Config.Labels</code></td>
<td><code>annotations</code></td>
<td></td>
</tr>

<tr>
<td><code>Config.StopSignal</code></td>
<td><code>annotations</code></td>
<td>1,4</td>
</tr>
</tbody>
</table>

<ol>
<li>如果用户用 <code>Config.Labels</code> 明确指定了这个注解，那么在这个字段中指定的值具有较低的<a href="#注释转换">优先级</a>，转换器必须使用<code>Config.Labels</code>的值。</li>
<li>这个字段的值必须被设置为在 <code>annotations</code> 中 <code>org.opencontainers.image.author</code> 的值。</li>
<li>这个字段的值必须被设置为在 <code>annotations</code> 中 <code>org.opencontainers.image.created</code>的值。</li>
<li>这个字段的值必须被设置为在 <code>annotations</code> 中<code>org.opencontainers.image.stopSignal</code> 的值。</li>
</ol>

<h4 id="解析的字段">解析的字段</h4>

<p>某些镜像配置字段具有必须首先翻译的对应项。
兼容的配置转换器应该解析所有这些字段并在生成的运行时配置中设置相应的字段：</p>

<table>
<thead>
<tr>
<th>Image Field</th>
<th>Runtime Field</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>Config.User</code></td>
<td><code>process.user.*</code></td>
</tr>
</tbody>
</table>

<p>解析上述镜像字段的方法将在以下章节中介绍。</p>

<h5 id="config-user"><code>Config.User</code></h5>

<p>如果 <code>Config.User</code> 中的<a href="#镜像配置属性"><code>user</code>或<code>group</code></a>的值是数字（<code>uid</code>或<code>gid</code>），那么这些值必须被逐字复制到 <code>process.user.uid</code> 和 <code>process.user.gid</code>。
如果 <code>Config.User</code> 中的<a href="#镜像配置属性"><code>user</code>或<code>group</code></a>的值不是数字（<code>user</code>或<code>group</code>），那么转换器应该使用适合容器上下文的方法来解决用户信息。
对于类 Unix 系统，这可能涉及到通过 NSS 或从提取的容器的根文件系统解析 <code>/etc/passwd</code> 来确定 <code>process.user.uid</code> 和 <code>process.user.gid</code> 的值。</p>

<p>此外，转换器应将 <code>process.user.extraGids</code> 的值设置为与容器上下文中由 <code>Config.User</code> 描述的用户相对应的值。
对于类似 Unix 的系统，这可能涉及到通过 NSS 或解析 <code>/etc/group</code> 并确定 <code>process.user.uid</code> 中指定的用户的组成员资格来解决。
如果 <code>Config.User</code> 中的 <a href="#镜像配置属性"><code>user</code></a> 的值是数字，转换器不应该修改<code>process.user.extraGids</code>。</p>

<p>如果没有定义 <code>Config.User</code>，则转换后的 <code>process.user</code> 值是实现定义的。
如果 <code>Config.User</code> 不对应于容器上下文中的用户，转换器必须返回一个错误。</p>

<h4 id="可选字段">可选字段</h4>

<p>某些镜像配置字段并不适用于所有转换用例，因此对于配置转换器实施是可选的。
兼容的配置转换器 SHOULD 为用户提供一种将这些字段提取到生成的运行时配置中的方法：</p>

<table>
<thead>
<tr>
<th>Image Field</th>
<th>Runtime Field</th>
<th>Notes</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>Config.ExposedPorts</code></td>
<td><code>annotations</code></td>
<td>1</td>
</tr>

<tr>
<td><code>Config.Volumes</code></td>
<td><code>mounts</code></td>
<td>2</td>
</tr>
</tbody>
</table>

<ol>
<li>运行时配置中没有这个镜像字段的对应字段。但是转换器 SHOULD 设置<a href="#config.exposedports"><code>org.opencontainers.image.exposedPorts</code> 注释</a>。</li>
<li>实现 SHOULD 为这些位置提供挂载，以便应用程序数据不被写入容器的根文件系统。如果转换器使用挂载点为这个字段实现转换，它 SHOULD 将挂载点的 <code>destination</code> 设置为 <code>Config.Volumes</code> 中指定的值。一个实施方案 MAY 会在同一位置用镜像中的数据对挂载的内容进行初始化（译者注：原文为：seed）。如果从基于此配置描述的镜像的容器中创建一个 <em>新</em> 镜像，这些路径中的数据不应包括在 <em>新</em> 镜像中。<code>mounts</code> 的其他的字段与平台和环境有关，因此是由实现定义的。请注意，<code>Config.Volumes</code> 的实现不需要使用mountpoints，因为它实际上是一个文件系统的 mask。</li>
</ol>

<h5 id="config-exposedports"><code>Config.ExposedPorts</code></h5>

<p>OCI 运行时配置不提供表达 “容器暴露端口” 概念的方法。
但是，转换器 SHOULD 设置 <strong>org.opencontainers.image.exposedPorts</strong> 注释，除非这样做会<a href="#注释转换">导致冲突</a>。</p>

<p><strong>org.opencontainers.image.exposedPorts</strong> 是对应于 <a href="#镜像配置">为 <code>Config.ExposedPorts</code> 定义的键</a> 的值列表（字符串，逗号分隔值）。</p>

<h4 id="注释转换">注释转换</h4>

<p>本规范中有三种注释 OCI 镜像的方法：</p>

<ol>
<li><a href="#镜像配置">镜像配置</a> 的 <code>Config.Labels</code></li>
<li><a href="#镜像-manifest">镜像 Manifest</a> 的 <code>annotations</code> .</li>
<li><a href="#镜像索引">镜像索引</a> <code>annotations</code>.</li>
</ol>

<p>此外，还有本节定义的隐式 annotations，这些 annotations 是由图像配置的值决定的。
转换器不应试图从 <a href="#镜像-manifest">镜像 Manifest</a> 或 <a href="#镜像索引">镜像索引</a> 中提取注释。
如果隐式注释（或 <a href="#镜像-manifest">镜像 Manifest</a> 或 <a href="#镜像索引">镜像索引</a> 中的注释）与 <code>Config.Labels</code> 中明确指定的注释之间存在冲突（Key 相同但值不同），必须以 <code>Config.Labels</code> 中指定的值为准。</p>

<p>转换器 MAY 添加注释，这些注释的键没有在镜像中指定。
转换器 MUST NOT 修改镜像中指定的注释的值。</p>

<blockquote>
<p>译者注：一句话来说就是以 <a href="#镜像配置">镜像配置</a> 的 <code>Config.Labels</code> 为准，忽略其他地方的注释。</p>
</blockquote>

<h3 id="设计考量">设计考量</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/considerations.md">原文链接</a></p>
</blockquote>

<h4 id="可扩展性">可扩展性</h4>

<p>正在读取/处理<a href="#镜像-manifest">镜像 Manifest</a> 或<a href="#镜像索引">镜像索引</a>的实现在遇到未知属性时不得产生错误。相反，他们 MUST 忽略未知属性。</p>

<h4 id="规范化">规范化</h4>

<ul>
<li>OCI 镜像是<a href="https://en.wikipedia.org/wiki/Content-addressable_storage">内容可寻址的</a>。有关更多信息，请参见<a href="#内容描述符">描述符</a>。</li>
<li>内容可寻址存储的一个好处是易于重复数据删除。</li>
<li>很多镜像可能同时依赖于某一个层，但存储中只会有一个 blob。</li>
<li>使用不同的序列化，相同的层将具有不同的 Hash，并且如果引用该层的两个版本，则将有两个具有相同语义内容的 blob。</li>
<li>为了实现高效的存储，对 blob 的内容进行序列化的实现 SHOULD 使用规范的序列化。</li>
<li>这增加了不同实现可以将相同语义内容推送到存储而不创建冗余 blob 的机会。（译者注：原文为 This increases the chance that different implementations can push the same semantic content to the store without creating redundant blobs.）</li>
</ul>

<h5 id="json">JSON</h5>

<p>JSON 内容应该被序列化为规范的 JSON。在 OCI 镜像格式规范媒体类型中，所有以 <code>+json</code> 结尾的类型都包含 JSON 内容。实现：</p>

<ul>
<li><a href="https://golang.org/">Go</a>：<a href="https://github.com/docker/go/">github.com/docker/go</a>，声称实现了除 Unicode 规范化之外的<a href="http://wiki.laptop.org/go/Canonical_JSON">规范 JSON</a>。</li>
</ul>

<h4 id="ebnf">EBNF</h4>

<p>对于本规范中描述的字段格式，我们使用 <a href="https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form">Extended Backus-Naur Form</a> 的有限子集，类似于 <a href="https://www.w3.org/TR/REC-xml/#sec-notation">XML 规范</a>使用的。 OCI 规范中的语法是正则的，可以转换为单个正则表达式。但是，避免使用正则表达式以限制正则表达式语法之间的歧义。通过定义此处使用的 EBNF 子集，可以避免因链接到更大的规范而出现变化、误解或歧义的可能性。</p>

<p>语法由以下形式的规则组成：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">symbol ::= expression</pre></div>
<p>如果输入与表达式匹配，我们可以说我们有符号标识的产生式。规则定义中完全忽略空格。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">literal ::= &#34;matchthis&#34;</pre></div>
<p>上面的表达式定义了一个符号 <code>literal</code>，它与 <code>&quot;matchthis&quot;</code> 的精确输入相匹配。字符类由方括号 (<code>[]</code>) 描述，描述一组、范围或多个字符范围：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">set := [abc]
range := [A-Z]</pre></div>
<p>上述符号 <code>&quot;set&quot;</code> 将匹配 <code>&quot;a&quot;</code>、<code>&quot;b&quot;</code> 或 <code>&quot;c&quot;</code> 中的一个字符。符号 &ldquo;范围&rdquo; 将匹配任何字符，包括 &ldquo;A&rdquo; 到 &ldquo;Z&rdquo;。目前，仅定义了 7 位 ascii 文字和字符类的匹配，因为这就是本规范所要求的全部。可以在单个字符类中指定多个字符范围和显式字符，如下所示：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">multipleranges := [a-zA-Z=-]</pre></div>
<p>以上匹配范围 <code>A</code> 到 <code>Z</code>、<code>a</code> 到 <code>z</code> 中的字符以及单个字符 <code>-</code> 和 <code>=</code>。</p>

<p>表达式可以由一个或多个表达式组成，其中一个必须跟在另一个之后。这称为隐式连接运算符。例如，要满足以下规则，必须同时匹配 <code>A</code> 和 <code>B</code> 才能满足规则：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">symbol ::= A B</pre></div>
<p>每个表达式必须匹配一次且只能匹配一次，<code>A</code> 后跟 <code>B</code>。为了支持重复和可选匹配条件的描述，定义了后缀运算符 <code>*</code> 和 <code>+</code>。 <code>*</code> 表示前面的表达式可以匹配零次或多次。 <code>+</code> 表示前面的表达式必须匹配一次或多次。它们以下列形式出现：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">zeroormore ::= expression*
oneormore ::= expression+</pre></div>
<p>括号用于将表达式分组为更大的表达式：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">group ::= (A B)</pre></div>
<p>与上面更简单的表达式一样，运算符也可以应用于组。为了允许替换，我们还定义了中缀运算符 |。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">oneof ::= A | B</pre></div>
<p>以上表示表达式应匹配表达式 <code>A</code> 或 <code>B</code> 之一。</p>

<h5 id="优先级">优先级</h5>

<p>运算符优先级按以下顺序排列：</p>

<ul>
<li>Terminals （文字和字符类）</li>
<li>Grouping <code>()</code></li>
<li>一元运算符 <code>+*</code></li>
<li>级联</li>
<li>或 <code>|</code></li>
</ul>

<p>使用分组显示等价物可以更好地描述优先级。连接的优先级高于交替，例如 <code>A B | C D</code> 等价于 <code>(A B) | (C D)</code>。一元运算符的优先级高于或和级联，例如 <code>A+ | B+</code> 等价于 <code>(A+) | (B+)</code>。</p>

<h5 id="示例">示例</h5>

<p>下面结合前面的定义来匹配一个简单的相对路径名，描述各个组件：</p>

<p><code>
path      ::= component (&quot;/&quot; component)*
component ::= [a-z]+
</code></p>

<p>产生式 &ldquo;component&rdquo; 是一个或多个小写字母。那么，<code>&quot;path&quot;</code> 是至少一个 component，可能后跟零个或多个 /-component 对。上面可以转换成下面的正则表达式：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[a-z]+(?:/[a-z]+)*</pre></div>
<h3 id="oci-image-实现">OCI Image 实现</h3>

<blockquote>
<p><a href="https://github.com/opencontainers/image-spec/blob/v1.0.2/implementations.md">原文链接</a></p>
</blockquote>

<p>目前采用 OCI 镜像规范的项目或公司</p>

<ul>
<li><a href="https://github.com/projectatomic/skopeo">projectatomic/skopeo</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-manifest-formats.html">Amazon Elastic Container Registry (ECR)</a> (<a href="https://aws.amazon.com/about-aws/whats-new/2017/01/amazon-ecr-supports-docker-image-manifest-v2-schema-2/">announcement</a>)</li>
<li><a href="https://github.com/openSUSE/umoci">openSUSE/umoci</a></li>
<li><a href="https://github.com/cloudfoundry/grootfs">cloudfoundry/grootfs</a> (<a href="https://github.com/cloudfoundry/grootfs/blob/c3da26e1e463b51be1add289032f3dca6698b335/fetcher/remote/docker_src.go">source</a>)</li>
<li><a href="https://issues.apache.org/jira/browse/MESOS-5011">Mesos plans</a> (<a href="https://docs.google.com/document/d/1Pus7D-inIBoLSIPyu3rl_apxvUhtp3rp0_b0Ttr2Xww/edit#heading=h.hrvk2wboog4p">design doc</a>)</li>
<li><a href="https://github.com/docker">Docker</a>

<ul>
<li><a href="https://github.com/docker/docker/pull/26369">docker/docker (<code>docker save/load</code> WIP)</a></li>
<li><a href="https://github.com/docker/distribution/pull/2076">docker/distribution (registry PR)</a></li>
</ul></li>
<li><a href="https://github.com/containerd/containerd">containerd/containerd</a></li>
<li><a href="https://github.com/containers/">Containers</a>

<ul>
<li><a href="https://github.com/containers/build">containers/build</a></li>
<li><a href="https://github.com/containers/image">containers/image</a></li>
</ul></li>
<li><a href="https://github.com/coreos/rkt">coreos/rkt</a></li>
<li><a href="https://github.com/box-builder/box">box-builder/box</a></li>
<li><a href="https://github.com/coolljt0725/docker2oci">coolljt0725/docker2oci</a></li>
</ul>

<p><em>(要添加您的项目，请打开 <a href="https://github.com/opencontainers/image-spec/pulls">pull-request</a>)</em></p>

<h2 id="相关技术">相关技术</h2>

<ul>
<li><a href="https://en.wikipedia.org/wiki/Tar_(computing)">tar (computing)</a></li>
<li><a href="https://en.wikipedia.org/wiki/SHA-2">SHA-2 (SHA256)</a> 和 <a href="https://en.wikipedia.org/wiki/Secure_Hash_Algorithms">安全散列算法</a></li>
<li><a href="https://en.wikipedia.org/wiki/Content-addressable_storage">内容可寻址存储</a></li>
<li><a href="http://wiki.laptop.org/go/Canonical_JSON">规范 JSON</a></li>
<li><a href="https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form">Extended Backus-Naur Form</a></li>
<li><a href="https://docs.docker.com/storage/storagedriver/">Docker Storage Driver</a></li>
</ul>
]]></description></item><item><title>OCI 概览</title><link>https://www.rectcircle.cn/posts/oci-override/</link><pubDate>Sun, 26 Dec 2021 17:44:58 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/oci-override/</guid><description type="html"><![CDATA[

<h2 id="概述">概述</h2>

<p>OCI 全称 <a href="https://www.opencontainers.org/">Open Container Initiative</a>，即开放容器倡议。是一个对容器技术进行标准化的组织。</p>

<p>该组织的使命是围绕容器技术推广一套通用的、最小的、开放的标准和规范。</p>

<p>该组织在 Linux 基金会的支持下成立，核心成员主要包括：Docker、Redhat、IBM、微软、Google 等。</p>

<p>特别说明：目前 OCI 的标准基本上都是来源于 Docker，也就是说 Docker 的核心组件是符合 OCI 标准的，是 OCI 的一个参考实现。</p>

<p>该组织的主要工作内容是对容器化核心技术进行标准化，标准化有助于：</p>

<ul>
<li>避免容器化技术被某一非中立实体掌控</li>
<li>统一容器化技术底座，提高兼容性，避免容器生态分裂</li>
<li>为不同场景的实现提供参考标准</li>
</ul>

<h2 id="标准和提案">标准和提案</h2>

<p>截止，2021-12-31， OCI 一共发布了如下几个标准：</p>

<ul>
<li><a href="https://github.com/opencontainers/image-spec">Image Format</a> <a href="https://opencontainers.org/release-notices/v1-0-2-image-spec/">v1.0.2</a> 定义了容器镜像的格式，平时讲的 Docker 镜像就是基于该标准定义打包的。该标准的具体形式表现为，镜像的文件和目录结构。</li>
<li><a href="https://github.com/opencontainers/runtime-spec">Runtime Specification</a> <a href="https://opencontainers.org/release-notices/v1-0-2-runtime-spec">v1.0.2</a> 定义了容器的配置、执行环境和生命周期，平时通过 <code>docker run</code> 运行的一个 Docker 容器就是该标准的一个实现。</li>
<li><a href="https://github.com/opencontainers/distribution-spec">Distribution Specification</a> <a href="https://opencontainers.org/release-notices/v1-0-1-distribution-spec/">v1.0.1</a> 定义了如何管理和分发，符合 <a href="https://github.com/opencontainers/image-spec">Image Format</a>  的镜像 和 <a href="https://github.com/opencontainers/artifacts">OCI Artifacts</a>。该标准的具体形式表现为，一套 Registry HTTP API 文档。</li>
</ul>

<p>其他相关提案可以参见： <a href="https://github.com/opencontainers/tob/tree/main/proposals">opencontainers/tob</a>。</p>

<p>更多内容可以前往： <a href="https://www.opencontainers.org/">官网</a> 和 <a href="https://github.com/opencontainers">github</a> 查看。</p>

<h2 id="标准实现">标准实现</h2>

<blockquote>
<p>参考：<a href="https://xuanwo.io/2019/08/06/oci-intro/#image-build">博客</a></p>
</blockquote>

<h3 id="image">Image</h3>

<table>
<thead>
<tr>
<th>项目</th>
<th>stars</th>
<th>简介</th>
<th>在 container 中运行</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://github.com/moby/buildkit">moby/buildkit</a></td>
<td><img src="https://img.shields.io/github/stars/moby/buildkit.svg" alt="stars" /></td>
<td>从 docker build 拆分出来的项目，支持自动 GC，多种输入和输出格式，并发依赖解析，分布式 Worker 和 Rootless</td>
<td>参见：<a href="https://blog.frognew.com/2021/06/relearning-container-13.html">博客</a> 和 <a href="https://github.com/moby/buildkit/blob/master/examples/kubernetes/README.md">官方例子</a>。注意：rootless 模式需要宿主机<a href="https://github.com/moby/buildkit/blob/master/docs/rootless.md">特殊配置</a></td>
</tr>

<tr>
<td><a href="https://github.com/genuinetools/img">genuinetools/img</a></td>
<td><img src="https://img.shields.io/github/stars/genuinetools/img.svg" alt="stars" /></td>
<td>对 buildkit 的一层封装，单独的二进制，没有 daemon，支持 Rootless 执行，会自动创建 SUBUID，比 buildkit 使用起来更加容易</td>
<td>参考 moby/buildkit 说明</td>
</tr>

<tr>
<td><a href="https://github.com/uber/makisu">uber/makisu</a></td>
<td><img src="https://img.shields.io/github/stars/uber/makisu.svg" alt="stars" /></td>
<td>uber 开源的内部镜像构建工具，目标是在 Mesos 或 Kubernetes 上进行 Rootless 构建，支持的 Dockerfile 有些许不兼容，在非容器环境下运行会有问题，比如 <a href="https://github.com/uber/makisu/issues/233">Image failed to build without modifyfs</a></td>
<td>项目已经归档，不建议使用</td>
</tr>

<tr>
<td><a href="https://github.com/GoogleContainerTools/kaniko">GoogleContainerTools/kaniko</a></td>
<td><img src="https://img.shields.io/github/stars/GoogleContainerTools/kaniko.svg" alt="stars" /></td>
<td>Google 出品，目标是 Daemon free build on Kubernetes，要求运行镜像 <code>gcr.io/kaniko-project/executor</code> 进行构建，直接在别的镜像中使用二进制可能会不工作，原生支持 rootless</td>
<td>原生支持，且只能在官方提供的 image 中使用</td>
</tr>

<tr>
<td><a href="https://github.com/containers/buildah">containers/buildah</a></td>
<td><img src="https://img.shields.io/github/stars/containers/buildah.svg" alt="stars" /></td>
<td>开源组织 <a href="https://github.com/containers">Containers</a> 推出的项目，目标是构建 OCI 容器镜像，Daemon free，支持 Rootless，与 <a href="https://xie.infoq.cn/article/a7254c5d64fcb3be8d6822415">podman</a> 生态紧密结合</td>
<td>参见：<a href="https://insujang.github.io/2020-11-09/building-container-image-inside-container-using-buildah/">文章</a> 。注意 rootless 需要二选一：a) 性能高，运行容器时需要添加 fuse 设备，<code>docker run --device /dev/fuse:rw ...</code>（<a href="https://zhuanlan.zhihu.com/p/83015668">k8s 方案</a>）；b) 性能差，每一层都需要复制全量数据，通过 <code>buildah --storage-driver=vfs</code> 命令实现</td>
</tr>
</tbody>
</table>

<p>由于 moby/buildkit 和 containers/buildah 对宿主机存在依赖。云原生场景，建议直接使用 GoogleContainerTools/kaniko。</p>

<h3 id="runtime">Runtime</h3>

<ul>
<li><a href="https://github.com/opencontainers/runc">opencontainers/runc</a>：是 OCI Runtime 的参考实现。</li>
<li><a href="https://github.com/kata-containers/runtime">kata-containers/runtime</a>：容器标准反攻虚拟机，前身是 <a href="https://github.com/clearcontainers/runtime">clearcontainers/runtime</a> 与 <a href="https://github.com/hyperhq/runv">hyperhq/runv</a>，通过 <a href="https://github.com/kata-containers/runtime/tree/master/virtcontainers">virtcontainers</a> 提供高性能 OCI 标准兼容的硬件虚拟化容器，Linux Only，且需要特定硬件。</li>
<li><a href="https://github.com/google/gvisor">google/gvisor</a>：gVisor 是一个 Go 实现的用户态内核，包含了一个 OCI 兼容的 Runtime 实现，目标是提供一个可运行非受信代码的容器运行时沙盒，目前是 Linux Only，其他架构可能会支持。</li>
</ul>

<h3 id="distribution">Distribution</h3>

<ul>
<li><a href="https://github.com/distribution/distribution">distribution/distribution</a>，OCI Distribution Specification 的参考 标准实现。</li>
<li><a href="https://github.com/goharbor/harbor">goharbor/harbor</a>，CNCF 旗下的兼容 OCI Distribution Specification 的实现。</li>
</ul>

<p>关于两者区别，参见博客：<a href="https://cloud.tencent.com/developer/article/1080444">为什么有了Docker registry还需要Harbor？</a></p>

<h2 id="辅助工具">辅助工具</h2>

<h3 id="镜像迁移工具">镜像迁移工具</h3>

<ul>
<li><a href="https://github.com/containers/skopeo">containers/skopeo</a></li>
</ul>

<h2 id="容器化解决方案">容器化解决方案</h2>

<h3 id="单机">单机</h3>

<ul>
<li>Docker 商业公司</li>
<li>Podman 完全开源</li>
<li>rkt</li>
</ul>

<h3 id="集群">集群</h3>

<ul>
<li>Kubernetes</li>
</ul>

<h2 id="reference">Reference</h2>

<ul>
<li><a href="https://opencontainers.org/release-notices/overview/">发布记录</a></li>
<li><a href="https://xuanwo.io/2019/08/06/oci-intro/">开放容器标准(OCI) 内部分享</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/415819214">OCI的全链路生态</a></li>
</ul>
]]></description></item><item><title>一文彻底理解 Python 环境</title><link>https://www.rectcircle.cn/posts/understand-the-python-environment/</link><pubDate>Sat, 18 Dec 2021 00:02:56 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/understand-the-python-environment/</guid><description type="html"><![CDATA[

<h2 id="python-编译产物分析">Python 编译产物分析</h2>

<h3 id="编译过程">编译过程</h3>

<blockquote>
<ul>
<li>实验平台 （Debian 9 x86_64）</li>
<li><a href="https://devguide.python.org/setup">官方文档</a></li>
</ul>
</blockquote>

<p>clone 代码</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/python/cpython.git
cd cpython
git checkout <span style="color:#ae81ff">3</span>.10</code></pre></div>
<p>安装编译依赖</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo apt-get update
sudo apt-get build-dep python3
sudo apt-get install pkg-config
sudo apt-get install build-essential gdb lcov pkg-config <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>      libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>      libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>      lzma lzma-dev tk-dev uuid-dev zlib1g-dev</code></pre></div>
<p>将 Python 编译并安装到指定目录</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 配置安装源码同级目录（仅测试不能用于生产）</span>
./configure --prefix<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>dirname <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">))</span>/python-release
make
<span style="color:#75715e"># make test # 由于网络原因可能失败可以忽略</span>
<span style="color:#75715e"># sudo make install</span>
make install <span style="color:#75715e"># 由于不是安装奥系统路径可以不用 sudo</span>
cd ../python-release</code></pre></div>
<h3 id="编译产物目录结构">编译产物目录结构</h3>

<p>（如果不手动编译，这些类似的文件，可以在 /usr/local 路径下找到）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── bin                            // 可执行文件目录
│   ├── 2to3 -&gt; 2to3-3.10          // 自动将 Python2 转换为 Python3 的迁移工具
│   ├── 2to3-3.10
│   ├── idle3 -&gt; idle3.10          // 自带的 IDE
│   ├── idle3.10
│   ├── pip3                       // 包管理器
│   ├── pip3.10
│   ├── pydoc3 -&gt; pydoc3.10        // 文档生成器
│   ├── pydoc3.10
│   ├── python3 -&gt; python3.10      // Python3 解释器
│   ├── python3.10
│   ├── python3.10-config          // C 语言嵌入 Python 解释器，参见文档 https://helpmanual.io/man1/python3-config/
│   └── python3-config -&gt; python3.10-config
├── include
│   └── python3.10                 // Python 头文件，C 语言可以使用，参见文档 https://docs.python.org/3/extending/index.html
├── lib
│   ├── libpython3.10.a            // 链接库
│   ├── pkgconfig                  // ??
│   └── python3.10                 // Python 包目录，根目录的为标准库，site-packages 为第三方库
└── share
    └── man</pre></div>
<p>Python 的编译产物是标准的 Unix 目录结构（bin include lib share）。在默认情况下（编译期间不指定 &ndash;prefix），这些编译产物会被 copy 到系统的 <code>/usr/local</code> 目录下。</p>

<h2 id="python-包搜索路径">Python 包搜索路径</h2>

<p>通过 <code>./bin/python3 -c 'import sys; print(sys.path)'</code>，可以看到当前解释器的包搜索路径，大概可以看到如下内容：</p>

<ul>
<li><code>''</code></li>
<li><code>'/path/to/python-release/lib/python310.zip'</code></li>
<li><code>'/path/to/python-release/lib/python3.10'</code></li>
<li><code>'/path/to/python-release/lib/python3.10/lib-dynload'</code></li>
<li><code>'$HOME/.local/lib/python3.10/site-packages'</code></li>
<li><code>'/path/to/python-release/lib/python3.10/site-packages'</code></li>
</ul>

<p>在官方，并没有找到 Python 包搜索路径是如何初始化的。通过检索，找到这两个文章：<a href="https://stackoverflow.com/a/38403654">stackoverflow</a> | <a href="https://cloud.tencent.com/developer/news/156311">sys.path源码分析</a>。</p>

<p>Python 的包搜索路径是有一套复杂的约定性的规则。这个规则非常复杂，作为 Python 开发者只有理解了这套机制，才能更好的处理各种 ImportError 的错误，才能更好的在工程上应用 Python。</p>

<p>经过实验和上文提到的文章，梳理了一下 Linux 下 Python 包搜索路径 <a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.path">sys.path</a> 初始化的过程：</p>

<ul>
<li>确认变量

<ul>
<li><a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.prefix"><code>sys.base_prefix</code></a>，存放与平台无关的文件，值为编译期间指定的 <code>--prefix</code>，默认为 <code>/usr/local</code> ，如果指定了 python home （PYTHONHOME 环境变量 或 <code>pyvenv.cfg</code> home 字段），则为 python home 值（since 3.3）。</li>
<li><a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.base_exec_prefix"><code>sys.base_exec_prefix</code></a>，存放与平台有关的文件，与平台有关的文件（如 <code>lib/pythonX.Y/config</code>，<code>lib/pythonX.Y/lib-dynload</code>），值为编译期间指定的 <code>--exec-prefix</code>，默认为 <code>--prefix</code> 或 <code>/usr/local</code>，如果指定了 python home（PYTHONHOME 环境变量 或 <code>pyvenv.cfg</code> home 字段），则为 python home 值（since 3.3）。</li>
<li><a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.executable"><code>sys.executable</code></a> 从操作系统读取，从操作系统获取当前运行的进程文件的绝对路径。</li>
<li><a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.prefix"><code>sys.prefix</code></a>，存放与平台无关的文件。

<ul>
<li>递归搜索 <code>sys.executable</code> 及其祖宗目录，是否包含 <code>lib/pythonx.y/os.py</code> 文件，如果包含，则 <code>sys.prefix</code> 为该目录</li>
<li>如果找不到则为 <code>sys.base_prefix</code></li>
</ul></li>
<li><a href="https://docs.python.org/zh-cn/3/library/sys.html#sys.exec_prefix"><code>sys.exec_prefix</code></a>，与平台有关的文件（如 <code>lib/pythonX.Y/config</code>，<code>lib/pythonX.Y/lib-dynload</code>）

<ul>
<li>递归搜索 <code>sys.executable</code> 及其祖宗目录，是否包含 <code>lib/pythonx.y/lib-dynload</code> 文件，如果包含，则 <code>sys.exec_prefix</code> 为该目录</li>
<li>找不到则为 <code>sys.base_exec_prefix</code></li>
</ul></li>
</ul></li>
<li>然后可以计算 <code>sys.path</code> （优先级由高到低）

<ul>
<li>site-packages 中 <code>*.pth</code> 文件指向的 egg 目录（可能是 <code>dist-packages</code>）</li>
<li>解释器运行所在目录个 <code>''</code> （Work Dir）</li>
<li>PYTHONPATH 环境变量配置的路径</li>
<li><code>'&lt;sys.base_prefix&gt;/lib/pythonxy.zip'</code></li>
<li><code>&lt;sys.base_prefix&gt;</code> 拼接 <code>lib/python3.10</code>、 <code>lib/pythonx.y/plat_linux2</code>、 <code>lib/pythonx.y/lib_tk</code>、 <code>lib/pythonx.y/lib_old</code> 之类的（由编译配置决定）</li>
<li><code>&lt;sys.base_exec_prefix&gt;</code> 拼接 <code>lib/pythonx.y/lib-dynload</code> 之类的（由编译配置决定）</li>
<li><code>$HOME/.local/lib/pythonx.y/site-packages</code>（还有可能是 <code>dist-packages</code>）</li>
<li>site-packages 路径： <code>&lt;sys.prefix&gt;</code> 、 <code>&lt;sys.exec_prefix&gt;</code> 分别拼接 <code>lib/pythonx.y/site-packages</code> 、<code>lib/site-packages</code>（可能是 <code>dist-packages</code>）</li>
<li>site-packages 中 <code>*.pth</code> 文件指向的非 egg 目录（可能是 <code>dist-packages</code>）</li>
</ul></li>
</ul>

<p>更多：</p>

<ul>
<li>关于 <code>site-packages</code> 参见：<a href="https://docs.python.org/zh-cn/3/library/site.html">官方文档</a></li>
<li>关于 <code>*.pth</code> 文件，只能在 <code>site-packages</code> 路径下才能生效，参见 <a href="https://stackoverflow.com/questions/15208615/using-pth-files">stackoverflow</a></li>
</ul>

<h2 id="python-包管理工具">Python 包管理工具</h2>

<p>Python 历史悠久，因此，Python 经过了很多的中方案，目前实时标准是 pip，但是仍然非常混乱。具体可以参见：</p>

<ul>
<li><a href="https://blog.zengrong.net/post/python_packaging/">Python 包管理工具解惑</a></li>
<li><a href="https://packaging.python.org/en/latest/">官方文档</a></li>
</ul>

<h2 id="python-环境定义和分类">Python 环境定义和分类</h2>

<p>Python 环境指的是一个 Python 解释器，以及解释器启动后可以搜索到的 Python 包（<code>sys.path</code>）的集合。一般可以分为如下几种场景。</p>

<ul>
<li>全局环境。通过操作系统默认包管理器安装的 Python 或者 操作系统默认安装的 Python 的环境叫做全局 Python 环境。在一般的 Linux 发行版 和 MacOS 中， Python 解释器一般都作默认命令预装到操作系统中，注意不同操作系统、不同的版本预装的 Python 版本可能不同。不建议使用全局 Python 环境进行开发，原因在于 Python 的兼容性不太好，而不同项目依赖的 Python 解释器的版本可能不同，如果直接使用全局 Python 环境开发，极有可能出现环境混乱，不同项目间依赖相互干扰，甚至破坏其他系统软件的依赖，导致系统软件无法运行。</li>
<li>虚拟环境。为了解决全局环境的问题，Python 开发者一般会为项目创建虚拟环境，这个环境与全局环境以及其他虚拟环境相互隔离，因此虚拟环境是项目粒度的。</li>
<li>Conda 环境。是指使用 conda 包管理器管理的 Python 环境（请参阅 <a href="https://conda.io/projects/conda/en/latest/user-guide/getting-started.html">conda 入门</a> (conda.io)），与虚拟环境不同，Conda 可以很好地创建具有相互关联的依赖项以及二进制包的环境。与虚拟环境不同，conda 环境是全局粒度的。因此可以在系统安装多个 conda 环境，然后为不同的项目选择不同的 conda 环境。</li>
</ul>

<h2 id="常见的-python-环境管理工具">常见的 Python 环境管理工具</h2>

<p>环境管理管理工具大概可以分为两类</p>

<ul>
<li>Python 版本管理，主要来管理 Python 解释器本身的版本，解决如何实现和管理在系统中安装多个版本的 Python。此类工具如：

<ul>
<li>pyenv</li>
</ul></li>
<li>Python 包隔离，主要来隔离不同 Python 项目的依赖。

<ul>
<li>venv (pyvenv)</li>
<li>virtualenv</li>
</ul></li>
</ul>

<p>关于常见的环境管理工具参见： <a href="https://stackoverflow.com/questions/41573587/what-is-the-difference-between-venv-pyvenv-pyenv-virtualenv-virtualenvwrappe">stackoverflow</a>。本文主要介绍 venv</p>

<h3 id="venv">venv</h3>

<blockquote>
<p><a href="https://docs.python.org/3/library/venv.html">官方文档</a></p>
</blockquote>

<p>python 3.3 引入的 官方环境管理工具。如果是 Python 3 的项目，建议使用该工具。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">./bin/python3 -m venv ../python-venv-1</code></pre></div>
<p>此时将看到 <code>../python-venv-1</code> 目录结构如下所示</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── bin
│   ├── activate                // 一系列 activate 文件，通过 source 激活
│   ├── activate.csh
│   ├── activate.fish
│   ├── Activate.ps1
│   ├── pip
│   ├── pip3
│   ├── pip3.10
│   ├── python -&gt; python3
│   ├── python3 -&gt; 真实的 Python 解析器
│   └── python3.10 -&gt; python3
├── include
├── lib
│   └── python3.10
│       └── site-packages
│           ├── _distutils_hack
│           ├── distutils-precedence.pth
│           ├── pip
│           ├── pip-21.2.4.dist-info
│           ├── pkg_resources
│           ├── setuptools
│           └── setuptools-58.1.0.dist-info
├── lib64 -&gt; lib
└── pyvenv.cfg</pre></div>
<p>可以重点关注 <code>pyvenv.cfg</code> 文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cfg" data-lang="cfg"><span style="color:#a6e22e">home</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/path/to/python-release/bin</span>
<span style="color:#a6e22e">include-system-site-packages</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">false</span>
<span style="color:#a6e22e">version</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">3.10.1</span></code></pre></div>
<p>通过 <code>../python-venv-1/bin/python3 -c 'import sys; print(sys.path)'</code> 可以看到如下输出</p>

<ul>
<li><code>''</code></li>
<li><code>'/path/to/python-release/lib/python310.zip'</code></li>
<li><code>'/path/to/python-release/lib/python3.10'</code></li>
<li><code>'/path/to/python-release/lib/python3.10/lib-dynload'</code></li>
<li><code>'/path/to/python-venv-1/lib/python3.10/site-packages'</code></li>
</ul>

<p><code>python3 -m venv -h</code> 常见选项</p>

<ul>
<li><code>--system-site-packages</code> 默认为 false，设置为 true 时，会将执行该命令的 <code>site-packages</code> 添加到 <code>sys.path</code> 中，在此例子中为 <code>'/path/to/python-release/lib/python3.10/site-packages'</code></li>
</ul>

<h3 id="virtualenv">virtualenv</h3>

<p>和 venv 基本一致，支持 Python2.7。只建议 Python2.7 的项目使用该工具。</p>

<p>更多参见：<a href="https://virtualenv.pypa.io/en/latest/installation.html#">官方文档</a></p>

<h3 id="conda">conda</h3>

<p>一般多为数据分析/人工智能领域使用。</p>

<p>更多参见：<a href="https://conda.io/docs/user-guide/tasks/manage-environments.html">官方文档</a></p>
]]></description></item><item><title>Go Swagger</title><link>https://www.rectcircle.cn/posts/go-swagger/</link><pubDate>Sun, 12 Dec 2021 20:33:35 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-swagger/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<blockquote>
<p><a href="https://goswagger.io/">官方文档</a></p>
</blockquote>

<p><a href="https://swagger.io/">swagger/OpenAPI</a>，是一套业界标准的 HTTP API 描述标准。</p>

<p><a href="https://goswagger.io/">go-swagger</a>，是 Swagger 2.0 的 Go 语言的实现，以 cli 或 library 的方式，提供了如下能力：</p>

<ul>
<li>从 swagger 规范文件生成服务器</li>
<li>从 swagger 规范文件生成客户端</li>
<li>从 swagger 规范文件（alpha 阶段）生成 CLI（命令行工具）</li>
<li>支持 jsonschema 和 swagger 提供的大多数功能，包括多态性</li>
<li>从带注释的 go 代码生成 swagger 规范文件</li>
<li>使用 swagger 规范文件的其他工具</li>
<li>强大的自定义功能，带有供应商扩展和可自定义的模板</li>
</ul>

<h2 id="安装">安装</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go install github.com/go-swagger/go-swagger/cmd/swagger@latest
swagger version</code></pre></div>
<p>其他安装方式，参见：<a href="https://goswagger.io/install.html">官方文档</a></p>

<h2 id="样例说明">样例说明</h2>

<blockquote>
<p><a href="https://github.com/rectcircle/go-swagger-learn">Github 代码库</a></p>
</blockquote>

<h3 id="新建项目">新建项目</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir go-swagger-learn
go mod init github.com/rectcircle/go-swagger-learn</code></pre></div>
<h3 id="场景">场景</h3>

<p>假设我们要开发一个根据 UserID 获取用户信息的接口</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#960050;background-color:#1e0010">GET /users/{id}
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"># response 200
</span><span style="color:#960050;background-color:#1e0010">{
</span><span style="color:#960050;background-color:#1e0010">    &#34;code&#34;: 0,
</span><span style="color:#960050;background-color:#1e0010">    &#34;message&#34;: &#34;success&#34;,
</span><span style="color:#960050;background-color:#1e0010">    &#34;data&#34;: {
</span><span style="color:#960050;background-color:#1e0010">        &#34;id&#34;: 1,
</span><span style="color:#960050;background-color:#1e0010">        &#34;name&#34;:&#34;abc&#34;
</span><span style="color:#960050;background-color:#1e0010">    }
</span><span style="color:#960050;background-color:#1e0010">}</span></code></pre></div>
<p>希望使用 go-swagger 实现如下功能。</p>

<ul>
<li>根据代码生成 swagger 规范文件</li>
<li>根据 swagger 规范文件生成 client 代码</li>
</ul>

<h2 id="从代码生成-swagger-规范文件">从代码生成 swagger 规范文件</h2>

<blockquote>
<p><a href="https://goswagger.io/generate/spec.html">官方文档</a> | <a href="https://www.zybuluo.com/daduizhang/note/1412629">博客 1</a></p>
</blockquote>

<h3 id="例子">例子</h3>

<h4 id="编写代码和注释">编写代码和注释</h4>

<p>首先定义 domain （简化起见，我们吧 dto 和 接口定义在了一起）</p>

<p><code>domain/user.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">domain</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
)

<span style="color:#75715e">// User 实体
</span><span style="color:#75715e">// swagger:model User
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">User</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// User ID
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ID</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;id&#34;`</span>
	<span style="color:#75715e">// 用户名
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Name</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;name&#34;`</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UserService</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#75715e">// swagger:route GET /users/{id} users GetOneUser
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 通过 ID 获取 User 信息
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 这是接口的详细描述
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     Responses:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//       200: GetUserResponse
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">id</span> <span style="color:#66d9ef">int</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">User</span>, <span style="color:#66d9ef">error</span>)
}</code></pre></div>
<p>定义 http 请求和返回的声明</p>

<p><code>domain/user_http.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">domain</span>

<span style="color:#75715e">// swagger:parameters GetOneUser
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GetUserRequest</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// User ID 参数
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// Required: true
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// in: path
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">ID</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;id&#34;`</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">BaseResponseBodyForSwagger</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 错误码
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Code</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;code&#34;`</span>
	<span style="color:#75715e">// 错误信息
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Message</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;message&#34;`</span>
}

<span style="color:#75715e">// swagger:response GetUserResponse
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GetUserResponse</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// in: body
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Body</span> <span style="color:#66d9ef">struct</span> {
		<span style="color:#a6e22e">BaseResponseBodyForSwagger</span>
		<span style="color:#a6e22e">Data</span> <span style="color:#a6e22e">User</span> <span style="color:#e6db74">`json:&#34;data&#34;`</span>
	}
	<span style="color:#75715e">// Body GetUserResponseBody
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>最后，添加 <code>swagger:meta</code> 注解</p>

<p><code>domain/doc.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// Package domian go swagger learn
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// 这是描述
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// Terms Of Service:
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// 这是服务条款
</span><span style="color:#75715e">//
</span><span style="color:#75715e">//      Host: localhost
</span><span style="color:#75715e">//      Version: 0.0.1
</span><span style="color:#75715e">//
</span><span style="color:#75715e">// swagger:meta
</span><span style="color:#75715e"></span><span style="color:#f92672">package</span> <span style="color:#a6e22e">domain</span></code></pre></div>
<h4 id="执行命令生成-swagger-规范文件">执行命令生成 swagger 规范文件</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">swagger generate spec -o ./api/swagger.json</code></pre></div>
<h4 id="生成文件内容">生成文件内容</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;swagger&#34;</span>: <span style="color:#e6db74">&#34;2.0&#34;</span>,
  <span style="color:#f92672">&#34;info&#34;</span>: {
    <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;这是描述&#34;</span>,
    <span style="color:#f92672">&#34;title&#34;</span>: <span style="color:#e6db74">&#34;go swagger learn&#34;</span>,
    <span style="color:#f92672">&#34;termsOfService&#34;</span>: <span style="color:#e6db74">&#34;这是服务条款&#34;</span>,
    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.0.1&#34;</span>
  },
  <span style="color:#f92672">&#34;host&#34;</span>: <span style="color:#e6db74">&#34;localhost&#34;</span>,
  <span style="color:#f92672">&#34;paths&#34;</span>: {
    <span style="color:#f92672">&#34;/users/{id}&#34;</span>: {
      <span style="color:#f92672">&#34;get&#34;</span>: {
        <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;这是接口的详细描述&#34;</span>,
        <span style="color:#f92672">&#34;tags&#34;</span>: [
          <span style="color:#e6db74">&#34;users&#34;</span>
        ],
        <span style="color:#f92672">&#34;summary&#34;</span>: <span style="color:#e6db74">&#34;通过 ID 获取 User 信息&#34;</span>,
        <span style="color:#f92672">&#34;operationId&#34;</span>: <span style="color:#e6db74">&#34;GetOneUser&#34;</span>,
        <span style="color:#f92672">&#34;parameters&#34;</span>: [
          {
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>,
            <span style="color:#f92672">&#34;format&#34;</span>: <span style="color:#e6db74">&#34;int64&#34;</span>,
            <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;ID&#34;</span>,
            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;User ID 参数&#34;</span>,
            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;id&#34;</span>,
            <span style="color:#f92672">&#34;in&#34;</span>: <span style="color:#e6db74">&#34;path&#34;</span>,
            <span style="color:#f92672">&#34;required&#34;</span>: <span style="color:#66d9ef">true</span>
          }
        ],
        <span style="color:#f92672">&#34;responses&#34;</span>: {
          <span style="color:#f92672">&#34;200&#34;</span>: {
            <span style="color:#f92672">&#34;$ref&#34;</span>: <span style="color:#e6db74">&#34;#/responses/GetUserResponse&#34;</span>
          }
        }
      }
    }
  },
  <span style="color:#f92672">&#34;definitions&#34;</span>: {
    <span style="color:#f92672">&#34;User&#34;</span>: {
      <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;User 实体&#34;</span>,
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;object&#34;</span>,
      <span style="color:#f92672">&#34;properties&#34;</span>: {
        <span style="color:#f92672">&#34;id&#34;</span>: {
          <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;User ID&#34;</span>,
          <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>,
          <span style="color:#f92672">&#34;format&#34;</span>: <span style="color:#e6db74">&#34;int64&#34;</span>,
          <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;ID&#34;</span>
        },
        <span style="color:#f92672">&#34;name&#34;</span>: {
          <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;用户名&#34;</span>,
          <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;string&#34;</span>,
          <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;Name&#34;</span>
        },
        <span style="color:#f92672">&#34;status&#34;</span>: {
          <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;状态\nactive StatusActive\ninactive StatusInactive&#34;</span>,
          <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;string&#34;</span>,
          <span style="color:#f92672">&#34;enum&#34;</span>: [
            <span style="color:#e6db74">&#34;active&#34;</span>,
            <span style="color:#e6db74">&#34;inactive&#34;</span>
          ],
          <span style="color:#f92672">&#34;x-go-enum-desc&#34;</span>: <span style="color:#e6db74">&#34;active StatusActive\ninactive StatusInactive&#34;</span>,
          <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;Status&#34;</span>
        }
      },
      <span style="color:#f92672">&#34;x-go-package&#34;</span>: <span style="color:#e6db74">&#34;github.com/rectcircle/go-swagger-learn/domain&#34;</span>
    }
  },
  <span style="color:#f92672">&#34;responses&#34;</span>: {
    <span style="color:#f92672">&#34;GetUserResponse&#34;</span>: {
      <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
      <span style="color:#f92672">&#34;schema&#34;</span>: {
        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;object&#34;</span>,
        <span style="color:#f92672">&#34;properties&#34;</span>: {
          <span style="color:#f92672">&#34;code&#34;</span>: {
            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;错误码&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>,
            <span style="color:#f92672">&#34;format&#34;</span>: <span style="color:#e6db74">&#34;int64&#34;</span>,
            <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;Code&#34;</span>
          },
          <span style="color:#f92672">&#34;data&#34;</span>: {
            <span style="color:#f92672">&#34;$ref&#34;</span>: <span style="color:#e6db74">&#34;#/definitions/User&#34;</span>
          },
          <span style="color:#f92672">&#34;message&#34;</span>: {
            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;错误信息&#34;</span>,
            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;string&#34;</span>,
            <span style="color:#f92672">&#34;x-go-name&#34;</span>: <span style="color:#e6db74">&#34;Message&#34;</span>
          }
        }
      }
    }
  }
}</code></pre></div>
<h4 id="启动-server-观察文档">启动 server 观察文档</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">swagger serve api/swagger.json</code></pre></div>
<h4 id="简单说明">简单说明</h4>

<ul>
<li>在代码中使用 <code>// swagger:</code> 表示接下来的注释是 swagger 的注释，会用来生成 swagger 规范文件。</li>
<li>一个 http 接口的声明包含三个部分，接口描述，接口请求描述，接口返回描述。</li>
<li>接口描述注释：<code>// swagger:route [method] [path pattern] [?tag1 tag2 tag3] [operation id]</code>，允许位于任何位置</li>
<li>接口请求注释：<code>// swagger:parameters [operationid1 operationid2]</code> 必须在一个结构体声明上方，在内部通过 <code>in: body</code> 声明哪个字段是 body，注意字段名需要通过 <code>json</code> 注解声明。</li>
<li>接口返回注释：<code>// swagger:response [?response name]</code> 必须在一个结构体声明上方，在内部通过 <code>in: body</code> 声明哪个字段是 body，需要在 <code>// swagger:route</code> 下方关联上 response name。</li>
<li>枚举类型注释：<code>// swagger:enum [Type]</code> 在 <code>type Xxx string</code> 或 <code>type Xxx int</code> 上方可以声明一个枚举类型</li>
<li>字符串格式声明注释：<code>// swagger:strfmt [name]</code>，参见：<a href="https://goswagger.io/use/spec/strfmt.html">官方文档</a></li>
</ul>

<h3 id="命令行使用">命令行使用</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">Usage:
  swagger [OPTIONS] generate spec [spec-OPTIONS]

generate a swagger spec document from a go application

Application Options:
  -q, --quiet                  silence logs
      --log-output=LOG-FILE    redirect logs to file

Help Options:
  -h, --help                   Show this help message

[spec command options]
      -w, --work-dir=          the base path to use (default: .)
      -t, --tags=              build tags
      -m, --scan-models        includes models that were annotated with &#39;swagger:model&#39;
          --compact            when present, doesn&#39;t prettify the json
      -o, --output=            the file to write to
      -i, --input=             an input swagger file with which to merge
      -c, --include=           include packages matching pattern
      -x, --exclude=           exclude packages matching pattern
          --include-tag=       include routes having specified tags (can be specified many times)
          --exclude-tag=       exclude routes having specified tags (can be specified many times)
          --exclude-deps       exclude all dependencies of project</pre></div>
<h3 id="注释规范">注释规范</h3>

<p>参考：<a href="https://goswagger.io/use/spec.html">官方文档</a></p>

<h3 id="常见场景">常见场景</h3>

<h4 id="上传文件">上传文件</h4>

<p>假设我们实现一个简单上传文件接口，该接口请求体包含两部分</p>

<ul>
<li>文件内容</li>

<li><p>额外的一些元数据</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">Xxx</span> { 
	<span style="color:#75715e">// swagger:route POST /uploads uploads Upload
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 上传文件
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     Consumes:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     - multipart/form-data
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     Security:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//       token:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//     Responses:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//       200: UploadResponse
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 实现的时候使用流式上传以节约内存
</span><span style="color:#75715e"></span><span style="color:#a6e22e">Upload</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">meta</span> <span style="color:#a6e22e">UploadMeta</span>, <span style="color:#a6e22e">data</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadCloser</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">FileInfo</span>, <span style="color:#66d9ef">error</span>)
}

<span style="color:#75715e">// UploadRequest 上传文件的请求
</span><span style="color:#75715e">// swagger:parameters Upload
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UploadRequest</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 上传文件请求中的元数据部分，JSON 解构
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 结构参见 [UploadMeta model](#model-UploadMeta) （Content-Type 为 application/json）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 特别说明：由于 Swagger/OpenAPI 2.0 multipart/form-data 的限制，这里只能定义为 string，且不支持指定每个部分的 Content-Type，参见：
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//  [Swagger/OpenAPI 3.0 multipart-content 说明](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md#special-considerations-for-multipart-content) |
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//  [stackoverflow](https://stackoverflow.com/questions/44323585/how-to-send-a-json-object-as-part-of-a-multipart-request-in-swagger-editor) |
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//  [Swagger/OpenAPI 2.0 upload file](https://swagger.io/docs/specification/2-0/file-upload/)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// Required: true
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// in: formData
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Data</span> <span style="color:#66d9ef">string</span>
	<span style="color:#75715e">// 上传的文件内容
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 参见：[github issue](https://github.com/go-swagger/go-swagger/issues/1887)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">//
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// Required: false
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// in: formData
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// swagger:file
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">File</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">BaseResponseBody</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 错误码
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Code</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;code&#34;`</span>
	<span style="color:#75715e">// 错误信息
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Message</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;message&#34;`</span>
}

<span style="color:#75715e">// UploadResponse 返回某个 UploadResponse
</span><span style="color:#75715e">// swagger:response UploadResponse
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UploadResponse</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// in: body
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Body</span> <span style="color:#66d9ef">struct</span> {
		<span style="color:#a6e22e">BaseResponseBody</span>
		<span style="color:#a6e22e">Data</span> <span style="color:#a6e22e">FileInfo</span> <span style="color:#e6db74">`json:&#34;data&#34;`</span>
	}
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UploadMeta</span> <span style="color:#66d9ef">struct</span> {
<span style="color:#75715e">//...
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FileInfo</span> <span style="color:#66d9ef">struct</span> {
<span style="color:#75715e">//...
</span><span style="color:#75715e"></span>}</code></pre></div></li>
</ul>

<h4 id="支持-string-类型">支持 <code>*string</code> 类型</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">XxxRequest</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 命名
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// Extensions:
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// ---
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// x-nullable: true
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">Name</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;name&#34;`</span>
}</code></pre></div>
<p>参见：</p>

<ul>
<li><a href="https://goswagger.io/use/spec.html#known-vendor-extensions">官方文档 Extensions</a></li>
<li><a href="https://github.com/go-swagger/go-swagger/issues/2166">Github Issue</a></li>
</ul>

<h2 id="从-swagger-规范文件生成-client-代码">从 swagger 规范文件生成 client 代码</h2>

<blockquote>
<p><a href="https://goswagger.io/generate/client.html">官方文档</a></p>
</blockquote>

<h3 id="例子-1">例子</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir -p client/internal
<span style="color:#75715e"># swagger generate client -f api/swagger.json   --client-package=client/internal/client --model-package=client/internal/domain</span>
swagger generate client -f api/swagger.json  --target<span style="color:#f92672">=</span>client/internal
go mod tidy</code></pre></div>
<p>注意，model 不能复用我们手写的 <code>domain</code> 下的代码文件，因为 go-swagger 生成代码依赖 model 实现序列化和反序列函数。手动写的的是不存在这些的。</p>

<p>生成到 client/internal 的原因是，某些场景希望再包装一层，提供更加优化的接口，再暴露给其他人使用（多数情况下，没有必要）。</p>

<h3 id="命令行使用-1">命令行使用</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="color:#a6e22e">Usage</span>:
  <span style="color:#a6e22e">swagger</span> [<span style="color:#a6e22e">OPTIONS</span>] <span style="color:#a6e22e">generate</span> <span style="color:#a6e22e">client</span> [<span style="color:#a6e22e">client</span><span style="color:#f92672">-</span><span style="color:#a6e22e">OPTIONS</span>]

<span style="color:#a6e22e">generate</span> <span style="color:#a6e22e">all</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">files</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">client</span> <span style="color:#a6e22e">library</span>

<span style="color:#a6e22e">Application</span> <span style="color:#a6e22e">Options</span>:
  <span style="color:#f92672">-</span><span style="color:#a6e22e">q</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">quiet</span>                                                                     <span style="color:#a6e22e">silence</span> <span style="color:#a6e22e">logs</span>
      <span style="color:#f92672">--</span><span style="color:#a6e22e">log</span><span style="color:#f92672">-</span><span style="color:#a6e22e">output</span>=<span style="color:#a6e22e">LOG</span><span style="color:#f92672">-</span><span style="color:#a6e22e">FILE</span>                                                       <span style="color:#a6e22e">redirect</span> <span style="color:#a6e22e">logs</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">file</span>

<span style="color:#a6e22e">Help</span> <span style="color:#a6e22e">Options</span>:
  <span style="color:#f92672">-</span><span style="color:#a6e22e">h</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">help</span>                                                                      <span style="color:#a6e22e">Show</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">help</span> <span style="color:#a6e22e">message</span>

[<span style="color:#a6e22e">client</span> <span style="color:#a6e22e">command</span> <span style="color:#a6e22e">options</span>]
      <span style="color:#f92672">-</span><span style="color:#a6e22e">c</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">client</span><span style="color:#f92672">-</span><span style="color:#f92672">package</span>=                                                       <span style="color:#a6e22e">the</span> <span style="color:#f92672">package</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">save</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">client</span> <span style="color:#a6e22e">specific</span> <span style="color:#a6e22e">code</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">client</span>)
      <span style="color:#f92672">-</span><span style="color:#a6e22e">P</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">principal</span>=                                                            <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">model</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">use</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">security</span> <span style="color:#a6e22e">principal</span>
          <span style="color:#f92672">--</span><span style="color:#66d9ef">default</span><span style="color:#f92672">-</span><span style="color:#a6e22e">scheme</span>=                                                       <span style="color:#a6e22e">the</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">scheme</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">API</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">http</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">principal</span><span style="color:#f92672">-</span><span style="color:#a6e22e">is</span><span style="color:#f92672">-</span><span style="color:#66d9ef">interface</span>                                                <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">security</span> <span style="color:#a6e22e">principal</span> <span style="color:#a6e22e">provided</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">an</span> <span style="color:#66d9ef">interface</span>, <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">a</span> <span style="color:#66d9ef">struct</span>
          <span style="color:#f92672">--</span><span style="color:#66d9ef">default</span><span style="color:#f92672">-</span><span style="color:#a6e22e">produces</span>=                                                     <span style="color:#a6e22e">the</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">mime</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">that</span> <span style="color:#a6e22e">API</span> <span style="color:#a6e22e">operations</span> <span style="color:#a6e22e">produce</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">application</span><span style="color:#f92672">/</span><span style="color:#a6e22e">json</span>)
          <span style="color:#f92672">--</span><span style="color:#66d9ef">default</span><span style="color:#f92672">-</span><span style="color:#a6e22e">consumes</span>=                                                     <span style="color:#a6e22e">the</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">mime</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">that</span> <span style="color:#a6e22e">API</span> <span style="color:#a6e22e">operations</span> <span style="color:#a6e22e">consume</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">application</span><span style="color:#f92672">/</span><span style="color:#a6e22e">json</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">skip</span><span style="color:#f92672">-</span><span style="color:#a6e22e">models</span>                                                           <span style="color:#a6e22e">no</span> <span style="color:#a6e22e">models</span> <span style="color:#a6e22e">will</span> <span style="color:#a6e22e">be</span> <span style="color:#a6e22e">generated</span> <span style="color:#a6e22e">when</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">flag</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">specified</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">skip</span><span style="color:#f92672">-</span><span style="color:#a6e22e">operations</span>                                                       <span style="color:#a6e22e">no</span> <span style="color:#a6e22e">operations</span> <span style="color:#a6e22e">will</span> <span style="color:#a6e22e">be</span> <span style="color:#a6e22e">generated</span> <span style="color:#a6e22e">when</span> <span style="color:#a6e22e">this</span> <span style="color:#a6e22e">flag</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">specified</span>
      <span style="color:#f92672">-</span><span style="color:#a6e22e">A</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">name</span>=                                                                 <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">name</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">application</span>, <span style="color:#a6e22e">defaults</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">mangled</span> <span style="color:#a6e22e">value</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">title</span>

    <span style="color:#a6e22e">Options</span> <span style="color:#a6e22e">common</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">all</span> <span style="color:#a6e22e">code</span> <span style="color:#a6e22e">generation</span> <span style="color:#a6e22e">commands</span>:
      <span style="color:#f92672">-</span><span style="color:#a6e22e">f</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">spec</span>=                                                                 <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">spec</span> <span style="color:#a6e22e">file</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">use</span> (<span style="color:#66d9ef">default</span> <span style="color:#a6e22e">swagger</span>.{<span style="color:#a6e22e">json</span>,<span style="color:#a6e22e">yml</span>,<span style="color:#a6e22e">yaml</span>})
      <span style="color:#f92672">-</span><span style="color:#a6e22e">t</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">target</span>=                                                               <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">base</span> <span style="color:#a6e22e">directory</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">generating</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">files</span> (<span style="color:#66d9ef">default</span>: .<span style="color:#f92672">/</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">template</span>=[<span style="color:#a6e22e">stratoscale</span>]                                                <span style="color:#a6e22e">load</span> <span style="color:#a6e22e">contributed</span> <span style="color:#a6e22e">templates</span>
      <span style="color:#f92672">-</span><span style="color:#a6e22e">T</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">template</span><span style="color:#f92672">-</span><span style="color:#a6e22e">dir</span>=                                                         <span style="color:#a6e22e">alternative</span> <span style="color:#a6e22e">template</span> <span style="color:#a6e22e">override</span> <span style="color:#a6e22e">directory</span>
      <span style="color:#f92672">-</span><span style="color:#a6e22e">C</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">config</span><span style="color:#f92672">-</span><span style="color:#a6e22e">file</span>=                                                          <span style="color:#a6e22e">configuration</span> <span style="color:#a6e22e">file</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">use</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">overriding</span> <span style="color:#a6e22e">template</span> <span style="color:#a6e22e">options</span>
      <span style="color:#f92672">-</span><span style="color:#a6e22e">r</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">copyright</span><span style="color:#f92672">-</span><span style="color:#a6e22e">file</span>=                                                       <span style="color:#a6e22e">copyright</span> <span style="color:#a6e22e">file</span> <span style="color:#a6e22e">used</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">add</span> <span style="color:#a6e22e">copyright</span> <span style="color:#a6e22e">header</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">additional</span><span style="color:#f92672">-</span><span style="color:#a6e22e">initialism</span>=                                                <span style="color:#a6e22e">consecutive</span> <span style="color:#a6e22e">capitals</span> <span style="color:#a6e22e">that</span> <span style="color:#a6e22e">should</span> <span style="color:#a6e22e">be</span> <span style="color:#a6e22e">considered</span> <span style="color:#a6e22e">intialisms</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">allow</span><span style="color:#f92672">-</span><span style="color:#a6e22e">template</span><span style="color:#f92672">-</span><span style="color:#a6e22e">override</span>                                               <span style="color:#a6e22e">allows</span> <span style="color:#a6e22e">overriding</span> <span style="color:#a6e22e">protected</span> <span style="color:#a6e22e">templates</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">skip</span><span style="color:#f92672">-</span><span style="color:#a6e22e">validation</span>                                                       <span style="color:#a6e22e">skips</span> <span style="color:#a6e22e">validation</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">spec</span> <span style="color:#a6e22e">prior</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">generation</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">dump</span><span style="color:#f92672">-</span><span style="color:#a6e22e">data</span>                                                             <span style="color:#a6e22e">when</span> <span style="color:#a6e22e">present</span> <span style="color:#a6e22e">dumps</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">json</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">template</span> <span style="color:#a6e22e">generator</span> <span style="color:#a6e22e">instead</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">generating</span> <span style="color:#a6e22e">files</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">strict</span><span style="color:#f92672">-</span><span style="color:#a6e22e">responders</span>                                                     <span style="color:#a6e22e">Use</span> <span style="color:#a6e22e">strict</span> <span style="color:#66d9ef">type</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">handler</span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">with</span><span style="color:#f92672">-</span><span style="color:#a6e22e">expand</span>                                                           <span style="color:#a6e22e">expands</span> <span style="color:#a6e22e">all</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">ref</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">s</span> <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">spec</span> <span style="color:#a6e22e">prior</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">generation</span> (<span style="color:#a6e22e">shorthand</span> <span style="color:#a6e22e">to</span> <span style="color:#f92672">--</span><span style="color:#a6e22e">with</span><span style="color:#f92672">-</span><span style="color:#a6e22e">flatten</span>=<span style="color:#a6e22e">expand</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">with</span><span style="color:#f92672">-</span><span style="color:#a6e22e">flatten</span>=[<span style="color:#a6e22e">minimal</span>|<span style="color:#a6e22e">full</span>|<span style="color:#a6e22e">expand</span>|<span style="color:#a6e22e">verbose</span>|<span style="color:#a6e22e">noverbose</span>|<span style="color:#a6e22e">remove</span><span style="color:#f92672">-</span><span style="color:#a6e22e">unused</span>]    <span style="color:#a6e22e">flattens</span> <span style="color:#a6e22e">all</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">ref</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">s</span> <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">spec</span> <span style="color:#a6e22e">prior</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">generation</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">minimal</span>, <span style="color:#a6e22e">verbose</span>)

    <span style="color:#a6e22e">Options</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">model</span> <span style="color:#a6e22e">generation</span>:
      <span style="color:#f92672">-</span><span style="color:#a6e22e">m</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">model</span><span style="color:#f92672">-</span><span style="color:#f92672">package</span>=                                                        <span style="color:#a6e22e">the</span> <span style="color:#f92672">package</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">save</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">models</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">models</span>)
      <span style="color:#f92672">-</span><span style="color:#a6e22e">M</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">model</span>=                                                                <span style="color:#a6e22e">specify</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">model</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">include</span> <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">generation</span>, <span style="color:#a6e22e">repeat</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">multiple</span> (<span style="color:#a6e22e">defaults</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">all</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">existing</span><span style="color:#f92672">-</span><span style="color:#a6e22e">models</span>=                                                      <span style="color:#a6e22e">use</span> <span style="color:#a6e22e">pre</span><span style="color:#f92672">-</span><span style="color:#a6e22e">generated</span> <span style="color:#a6e22e">models</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">g</span>. <span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#a6e22e">foobar</span><span style="color:#f92672">/</span><span style="color:#a6e22e">model</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">strict</span><span style="color:#f92672">-</span><span style="color:#a6e22e">additional</span><span style="color:#f92672">-</span><span style="color:#a6e22e">properties</span>                                          <span style="color:#a6e22e">disallow</span> <span style="color:#a6e22e">extra</span> <span style="color:#a6e22e">properties</span> <span style="color:#a6e22e">when</span> <span style="color:#a6e22e">additionalProperties</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">set</span> <span style="color:#a6e22e">to</span> <span style="color:#66d9ef">false</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">keep</span><span style="color:#f92672">-</span><span style="color:#a6e22e">spec</span><span style="color:#f92672">-</span><span style="color:#a6e22e">order</span>                                                       <span style="color:#a6e22e">keep</span> <span style="color:#a6e22e">schema</span> <span style="color:#a6e22e">properties</span> <span style="color:#a6e22e">order</span> <span style="color:#a6e22e">identical</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">spec</span> <span style="color:#a6e22e">file</span>
          <span style="color:#f92672">--</span><span style="color:#66d9ef">struct</span><span style="color:#f92672">-</span><span style="color:#a6e22e">tags</span>=                                                          <span style="color:#a6e22e">the</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">tags</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">generate</span>, <span style="color:#a6e22e">repeat</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">multiple</span> (<span style="color:#a6e22e">defaults</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">json</span>)

    <span style="color:#a6e22e">Options</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">operation</span> <span style="color:#a6e22e">generation</span>:
      <span style="color:#f92672">-</span><span style="color:#a6e22e">O</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">operation</span>=                                                            <span style="color:#a6e22e">specify</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">operation</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">include</span>, <span style="color:#a6e22e">repeat</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">multiple</span> (<span style="color:#a6e22e">defaults</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">all</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">tags</span>=                                                                 <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">tags</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">include</span>, <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">specified</span> <span style="color:#a6e22e">defaults</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">all</span>
      <span style="color:#f92672">-</span><span style="color:#a6e22e">a</span>, <span style="color:#f92672">--</span><span style="color:#a6e22e">api</span><span style="color:#f92672">-</span><span style="color:#f92672">package</span>=                                                          <span style="color:#a6e22e">the</span> <span style="color:#f92672">package</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">save</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">operations</span> (<span style="color:#66d9ef">default</span>: <span style="color:#a6e22e">operations</span>)
          <span style="color:#f92672">--</span><span style="color:#a6e22e">with</span><span style="color:#f92672">-</span><span style="color:#a6e22e">enum</span><span style="color:#f92672">-</span><span style="color:#a6e22e">ci</span>                                                          <span style="color:#a6e22e">allow</span> <span style="color:#66d9ef">case</span><span style="color:#f92672">-</span><span style="color:#a6e22e">insensitive</span> <span style="color:#a6e22e">enumerations</span>
          <span style="color:#f92672">--</span><span style="color:#a6e22e">skip</span><span style="color:#f92672">-</span><span style="color:#a6e22e">tag</span><span style="color:#f92672">-</span><span style="color:#a6e22e">packages</span>                                                     <span style="color:#a6e22e">skips</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">generation</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">tag</span><span style="color:#f92672">-</span><span style="color:#a6e22e">based</span> <span style="color:#a6e22e">operation</span> <span style="color:#a6e22e">packages</span>, <span style="color:#a6e22e">resulting</span> <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">flat</span> <span style="color:#a6e22e">generation</span></pre></div>
<h3 id="更多参见">更多参见</h3>

<p><a href="https://goswagger.io/generate/client.html">官方文档</a></p>
]]></description></item><item><title>Java JPA 更新丢失问题</title><link>https://www.rectcircle.cn/posts/java-jpa-update-lost/</link><pubDate>Wed, 01 Dec 2021 18:02:20 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/java-jpa-update-lost/</guid><description type="html"><![CDATA[

<h2 id="触发场景">触发场景</h2>

<ul>
<li>JPA (Hibernate) (Spring Boot 2)</li>
<li>MySQL 隔离级别不为 Serializable</li>
<li>实体没有使用 <code>@DynamicUpdate</code> 注解</li>
<li>至少存在两个接口频繁执行：先查询再更新同一个实体的操作（不管更新的字段是否相同）</li>
</ul>

<h2 id="问题描述">问题描述</h2>

<p>导致第二类更新丢失。关于第二类更新丢失参见：<a href="https://juejin.cn/post/6844903854857781262">该文</a></p>

<h2 id="解决办法">解决办法</h2>

<h3 id="场景一-如果两个接口不是更新同一个字段">场景一：如果两个接口不是更新同一个字段</h3>

<ul>
<li>将 <code>entity.setXxx</code> 改为编写 HQL 的调用，指定更新有改动的字段</li>
<li>添加 <code>@DynamicUpdate</code> 注解，参考：<a href="https://www.baeldung.com/spring-data-jpa-dynamicupdate">该文</a></li>
</ul>

<h3 id="场景二-如果两个接口同时更新同一个字段">场景二：如果两个接口同时更新同一个字段</h3>

<ul>
<li>悲观锁 <code>select for update</code></li>
<li>乐观锁 <code>update set xxx = ? where id = ? and xxx = select 到的数据</code></li>
</ul>
]]></description></item><item><title>深入理解 Go 类型系统和反射</title><link>https://www.rectcircle.cn/posts/go-type-and-reflect/</link><pubDate>Sat, 27 Nov 2021 23:55:39 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-type-and-reflect/</guid><description type="html"><![CDATA[

<blockquote>
<p>本文 Go 版本为 1.17</p>
</blockquote>

<h2 id="类型-值">类型、值</h2>

<p>在讨论一个 Go 语言的类型系统时一定要区分类型和值。</p>

<p>类型是通过 <code>type</code> 关键词创建的，是一种抽象性的描述，主要描述了内存布局、绑定的函数以及可访问性。</p>

<p>值是通过 <code>new</code>、<code>make</code>、<code>类型名{...}</code>、<code>类型名(...)</code> 创建的一种类型的具体实例，在运行时表现为一段连续的内存片段。</p>

<p>在纯静态语言中（如 C）、类型仅仅在编译时有效，编译后的运行时，所有类型信息均已丢失。</p>

<p>而在 Go 语言之类的有 Runtime / VM 的支持反射的语言中，类型信息在运行时也被保留了下来。在 Go 语言中，通过 <code>interface{}</code> 类型的值，可以获取到类型信息。</p>

<p>注意本文中的<strong>值</strong>的概念和传值传引用中的值不是一个概念，不要混淆。</p>

<h2 id="涉及的源码位置">涉及的源码位置</h2>

<p>在 Go 的源码中，涉及到类型系统的部分有如下几个地方</p>

<ul>
<li>反射包 <code>reflect</code></li>
<li>运行时包 <code>runtime</code></li>
</ul>

<h2 id="类型的数据结构">类型的数据结构</h2>

<p>Go 语言的类型的数据结构由如下几个部分构成（源码位于：<code>runtime/type.go</code> 和 <code>reflect/type.go</code>）</p>

<ol>
<li><code>runtime._type</code> 所有类型都共有的属性，包含，内存大小</li>
<li>Determined by <code>runtime._type.kind</code> 由类型的类别决定</li>
<li><code>runtime.uncommontype</code> 该类型的定义的包路径绑定的方法的指针和偏移量位置</li>
<li>func type of input and output, if runtime._type.kind is Func 当该类型类别为函数时，记录函数参数类型指针数组和返回类型指针数组</li>
<li><code>[]runtime.method</code> （具体逻辑参考 <code>reflect.method</code>）该类型绑定的所有方法

<ol>
<li>前面为 Exported 方法</li>
<li>后面为 unExported 方法</li>
</ol></li>
</ol>

<h2 id="类型的类别-type-kind">类型的类别 (<code>Type.Kind</code>)</h2>

<p>Go 语言中，所有的类型（包括自定义和内置类型）都可以划分为 26 种 Kind（在本文中翻译为类别）。定义在 <code>runtime/type.go</code> 和 <code>reflect/type.go</code>。</p>

<h2 id="值的数据结构">值的数据结构</h2>

<p>Go 语言的不同类型的类别(<code>Type.Kind</code>)的数据结构是不同的，可以在 <code>reflect/value.go</code> 和 <code>runtime</code> 看到相关实现。在此仅列出几个简单的数据结构</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// reflect/value.go
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">StringHeader</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">Data</span> <span style="color:#66d9ef">uintptr</span>
	<span style="color:#a6e22e">Len</span>  <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SliceHeader</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">Data</span> <span style="color:#66d9ef">uintptr</span>
	<span style="color:#a6e22e">Len</span>  <span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">Cap</span>  <span style="color:#66d9ef">int</span>
}</code></pre></div>
<h2 id="go-常见类型及其值的实现">Go 常见类型及其值的实现</h2>

<p>TODO <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUyNTUwOTI4Ng==&amp;action=getalbum&amp;album_id=1937329367336943619&amp;scene=173&amp;from_msgid=2247484360&amp;from_itemidx=1&amp;count=3&amp;nolastread=1#wechat_redirect">系列文章</a></p>

<h3 id="数值类型-以-int-为例">数值类型 —— 以 int 为例</h3>

<h3 id="数组类型">数组类型</h3>

<h3 id="字符串和切片类型">字符串和切片类型</h3>

<h3 id="map-类型">map 类型</h3>

<h3 id="结构体类型">结构体类型</h3>

<h3 id="指针类型">指针类型</h3>

<h3 id="函数类型">函数类型</h3>

<h3 id="接口类型">接口类型</h3>

<h2 id="reflect-api-详解">reflect API 详解</h2>

<h3 id="reflect-类型-和-值">reflect 类型 和 值</h3>

<h3 id="reflect-typeof-和-reflect-valueof-原理">reflect.TypeOf 和 reflect.ValueOf 原理</h3>

<h4 id="go-语言函数传参数传递">Go 语言函数传参数传递</h4>

<p>在 Go 语言中，参数传递发生一次赋值或者类型转换操作（相关规则参见文章：<a href="https://gfw.go101.org/article/value-conversions-assignments-and-comparisons.html">类型转换、赋值和值比较规则大全</a>）</p>

<h4 id="空接口值的数据结构">空接口值的数据结构</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// reflect/value.go
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">emptyInterface</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">typ</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">rtype</span>          <span style="color:#75715e">// 指向类型的数据结构
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">word</span> <span style="color:#a6e22e">unsafe</span>.<span style="color:#a6e22e">Pointer</span>  <span style="color:#75715e">// 指向值的数据结构
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">// // 或者定义在 runtime/runtime2.go
</span><span style="color:#75715e">// type eface struct {
</span><span style="color:#75715e">// 	_type *_type
</span><span style="color:#75715e">// 	data  unsafe.Pointer
</span><span style="color:#75715e"></span><span style="color:#f92672">//</span> }</code></pre></div>
<h4 id="reflect-typeof-分析">reflect.TypeOf 分析</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// reflect/type.go
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">i</span> <span style="color:#66d9ef">interface</span>{}) <span style="color:#a6e22e">Type</span> {
	<span style="color:#a6e22e">eface</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">*</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">emptyInterface</span>)(<span style="color:#a6e22e">unsafe</span>.<span style="color:#a6e22e">Pointer</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">i</span>))
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">toType</span>(<span style="color:#a6e22e">eface</span>.<span style="color:#a6e22e">typ</span>)
}</code></pre></div>
<ul>
<li>调用 TypeOf 时，会将发生一个赋值操作，编译器会插入生成一个空接口值的逻辑，即 <code>emptyInterface</code> 的实例</li>
<li>获取到 <code>emptyInterface.typ</code> 并返回</li>
</ul>

<h4 id="reflect-valueof-分析">reflect.ValueOf 分析</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// reflect/value.go
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Value</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">typ</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">rtype</span>
	<span style="color:#a6e22e">ptr</span> <span style="color:#a6e22e">unsafe</span>.<span style="color:#a6e22e">Pointer</span>
	<span style="color:#a6e22e">flag</span>
}
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">flag</span> <span style="color:#66d9ef">uintptr</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ValueOf</span>(<span style="color:#a6e22e">i</span> <span style="color:#66d9ef">interface</span>{}) <span style="color:#a6e22e">Value</span> {}</code></pre></div>
<ul>
<li>调用 ValueOf 时，会将发生一个赋值操作，编译器会插入生成一个空接口值的逻辑，即 <code>emptyInterface</code> 的实例</li>
<li>构造 Value 结构体，前两个参数 <code>emptyInterface</code> 都有，然后根据情况构造 flag 即可</li>
</ul>

<h4 id="接口赋值给空接口">接口赋值给空接口</h4>

<p>设想这样一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// https://play.studygolang.com/p/K3GkahbJ51E
</span><span style="color:#75715e"></span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
    <span style="color:#e6db74">&#34;reflect&#34;</span>
    <span style="color:#e6db74">&#34;fmt&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyError</span> <span style="color:#66d9ef">struct</span> {}
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">e</span> <span style="color:#a6e22e">MyError</span>) <span style="color:#a6e22e">Error</span>() <span style="color:#66d9ef">string</span> {<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;MyError&#34;</span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">e</span> <span style="color:#66d9ef">error</span> = <span style="color:#a6e22e">MyError</span>{}
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">e</span>)) 
    <span style="color:#75715e">// output main.MyError
</span><span style="color:#75715e"></span>}</code></pre></div>
<p>直觉上，应该返回 <code>error</code>。Go 的实现却是 <code>main.MyError</code>。也就是说 Go 的空接口指向的类型不可能是一个接口类型。</p>

<p>如果想获取到一个接口的反射类型或者值只能通过接口指针的方式获取：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">ep</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">error</span> = <span style="color:#f92672">...</span>
<span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">TypeOf</span>(<span style="color:#a6e22e">ep</span>)
<span style="color:#a6e22e">reflect</span>.<span style="color:#a6e22e">ValueOf</span>(<span style="color:#a6e22e">ep</span>)</code></pre></div>
<h3 id="类型-api">类型 API</h3>

<h3 id="值-api">值 API</h3>
]]></description></item><item><title>Intellij 平台插件开发 API</title><link>https://www.rectcircle.cn/posts/intellij-platform-plugin-dev-api/</link><pubDate>Sat, 06 Nov 2021 19:47:35 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/intellij-platform-plugin-dev-api/</guid><description type="html"><![CDATA[

<h2 id="相关资源">相关资源</h2>

<ul>
<li><a href="https://plugins.jetbrains.com/docs/intellij/basics.html">官方文档</a></li>
<li><a href="https://github.com/JetBrains/intellij-sdk-code-samples">官方 Demo</a></li>
<li><a href="https://plugins.jetbrains.com/intellij-platform-explorer">插件源码搜索浏览</a></li>
<li><a href="https://github.com/rectcircle/learn-intellij-platform-plugin">本文 Demo 库</a></li>
</ul>

<h2 id="相关术语">相关术语</h2>

<h3 id="fqn">FQN</h3>

<p>Fully qualified name, 完全限定名称。在本文语境中，可能是</p>

<ul>
<li>插件中各个贡献点（组件）的 ID</li>
<li>类的全名</li>
</ul>

<h2 id="插件运行时分析">插件运行时分析</h2>

<h3 id="类加载">类加载</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/plugin-class-loaders.html">官方文档</a></p>
</blockquote>

<p>IntelliJ 平台的 IDE 是通过 Java 编写的，因此其插件是以 Jar 包的被加载到 IDE 的 JVM 中的。</p>

<p>IntelliJ 平台的 IDE 和插件都是运行在同一个 JVM 中的，因此插件与插件、插件和 IDE 是运行在同一个进程中的。为了保证隔离性，对于每个插件创建一个独立的 ClassLoader 并用这个 ClassLoader 来加载这个插件 jar 包的类。这个 ClassLoader 为 <code>com.intellij.ide.plugins.cl.PluginClassLoader</code>。</p>

<p>由于该 ClassLoader 类实现了双亲委派机制。所以，针对 Jetbrains API 相关的单例对象来说，对于不同的插件来说就不是隔离的，因此需要小心。（比如 <code>JBCefApp</code>）</p>

<p>而，针对其他插件的依赖，因为不同的插件使用不同的类加载器，所以默认情况下是无法查找到类的。因此，需在在 <code>plugin.xml</code> 的声明 <code>&lt;depends&gt;</code>，这是，本插件的的类加载器就会尝试委托依赖的插件的类加载器来加载依赖的类。</p>

<h3 id="ui-线程与并发">UI、线程与并发</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/general-threading-rules.html">官方文档</a></p>
</blockquote>

<p>IntelliJ 平台的 IDE UI 是基于 Java Swing 技术实现的。不像 JavaScript 是单线程的，而 Java 是支持多线程的。</p>

<p>在 Swing 的设计中，为了简化并发带来的问题，所有的 UI 更新操作都应该在 <code>EDT</code> 线程，即 Event Dispatch Thread。（关于为什么 UI 框架都要使用单线程的讨论 <a href="https://stackoverflow.com/questions/5544447/why-are-most-ui-frameworks-single-threaded">stackoverflow</a>）</p>

<p>因此，如果在 IntelliJ 平台插件中，非 <code>EDT</code> 线程中想要触发 UI 更新操作，需按照如下写法，将相关逻辑放到 <code>EDT</code> 线程中执行</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#75715e">// Exception in thread &#34;AWT-AppKit&#34; com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments: EventQueue.isDispatchThread()=false Toolkit.getEventQueue()=com.intellij.ide.IdeEventQueue@68959cb5
</span><span style="color:#75715e">// https://intellij-support.jetbrains.com/hc/en-us/community/posts/206754235/comments/208642689
</span><span style="color:#75715e"></span>Application <span style="color:#a6e22e">application</span> <span style="color:#f92672">=</span> ApplicationManager.<span style="color:#a6e22e">getApplication</span>();
<span style="color:#66d9ef">if</span>(application.<span style="color:#a6e22e">isDispatchThread</span>()) {
    application.<span style="color:#a6e22e">runWriteAction</span>(runnable);
} <span style="color:#66d9ef">else</span> {
    application.<span style="color:#a6e22e">invokeLater</span>(()<span style="color:#f92672">-&gt;</span> application.<span style="color:#a6e22e">runWriteAction</span>(runnable));
}</code></pre></div>
<p>更多，关于多线程的设计和 API，参见<a href="https://plugins.jetbrains.com/docs/intellij/general-threading-rules.html">官方文档</a></p>

<h2 id="本地化">本地化</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/localization-guide.html">官方文档</a></p>
</blockquote>

<p>官方文档没怎么看懂，其他关于本地化的内容：</p>

<ul>
<li><a href="https://plugins.jetbrains.com/docs/intellij/basic-action-system.html#localizing-actions-and-groups">actions 本地化</a> （<a href="https://github.com/JetBrains/intellij-sdk-code-samples/tree/6a87dd7dd2106a124deac65ade0f642b240f4b62/action_basics">Demo</a>）</li>
<li><a href="https://plugins.jetbrains.com/docs/intellij/settings-guide.html#settings-declaration-attributes">配置项</a></li>
</ul>

<h2 id="service-依赖注入">Service 依赖注入</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/plugin-services.html">官方文档</a></p>
</blockquote>

<h3 id="应用粒度">应用粒度</h3>

<p>注册</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;!-- 注册一个应用级别的 service （全局实例化一个）--&gt;</span>
<span style="color:#f92672">&lt;applicationService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.services.MyApplicationService&#34;</span><span style="color:#f92672">/&gt;</span></code></pre></div>
<p>获取</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java">ApplicationManager.<span style="color:#a6e22e">getApplication</span>()
          .<span style="color:#a6e22e">getService</span>(MyApplicationService.<span style="color:#a6e22e">class</span>);</code></pre></div>
<h3 id="项目粒度">项目粒度</h3>

<p>注册</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">        <span style="color:#75715e">&lt;!-- 注册一个项目级别的 service（每个窗口实例化一个） --&gt;</span>
        <span style="color:#f92672">&lt;projectService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.services.MyProjectService&#34;</span><span style="color:#f92672">/&gt;</span></code></pre></div>
<p>获取</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java">project.<span style="color:#a6e22e">getService</span>(MyProjectService.<span style="color:#a6e22e">class</span>); <span style="color:#f92672">//</span> com.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span></code></pre></div>
<h2 id="版本-平台-插件依赖以及兼容性">版本、平台、插件依赖以及兼容性</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html">兼容性</a> | <a href="https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html">插件依赖</a></p>
</blockquote>

<h3 id="编译依赖声明">编译依赖声明</h3>

<p><code>build.gradle.kts</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kts" data-lang="kts">// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
intellij {
    // ...

    // https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
    // 平台版本如 IC / IU 等（社区版/专业版）
    type.set(properties(&#34;platformType&#34;))
    // 开发编译时使用的平台版如 2021.1.1
    version.set(properties(&#34;platformVersion&#34;))

    // ...

    // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
    // 本插件依赖的插件，如 org.jetbrains.plugins.go:211.6693.111
    plugins.set(properties(&#34;platformPlugins&#34;).split(&#39;,&#39;).map(String::trim).filter(String::isNotEmpty))
}</code></pre></div>
<h3 id="插件依赖声明">插件依赖声明</h3>

<p><code>src/main/resources/META-INF/plugin.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#75715e">&lt;!-- 依赖的内置插件. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html --&gt;</span>
    <span style="color:#f92672">&lt;depends&gt;</span>com.intellij.modules.platform<span style="color:#f92672">&lt;/depends&gt;</span>
    <span style="color:#75715e">&lt;!-- 可选依赖 （该调试特性仅支持 goland）
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
</span><span style="color:#75715e">    --&gt;</span>
    <span style="color:#f92672">&lt;depends</span> <span style="color:#a6e22e">optional=</span><span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#a6e22e">config-file=</span><span style="color:#e6db74">&#34;demo-golang.xml&#34;</span><span style="color:#f92672">&gt;</span>org.jetbrains.plugins.go<span style="color:#f92672">&lt;/depends&gt;</span></code></pre></div>
<h3 id="场景-平台特定功能">场景：平台特定功能</h3>

<p>假设提供一个插件，这个插件有一些功能是全平台都可以使用，某些功能只能在某些特定平台（如 GoLand），中使用。配置方式如下：</p>

<p>第一步，添加编译插件依赖 <code>gradle.properties</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-properties" data-lang="properties">platformPlugins = org.jetbrains.plugins.go:211.6693.111</code></pre></div>
<p>第二步，添加声明 <code>src/main/resources/META-INF/plugin.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#75715e">&lt;!-- 可选依赖 （该调试特性仅支持 goland）
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
</span><span style="color:#75715e">    --&gt;</span>
    <span style="color:#f92672">&lt;depends</span> <span style="color:#a6e22e">optional=</span><span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#a6e22e">config-file=</span><span style="color:#e6db74">&#34;demo-golang.xml&#34;</span><span style="color:#f92672">&gt;</span>org.jetbrains.plugins.go<span style="color:#f92672">&lt;/depends&gt;</span></code></pre></div>
<p>第三步，添加特定平台插件配置文件 <code>src/main/resources/META-INF/demo-golang.xml</code> ，语法 和 <code>src/main/resources/META-INF/plugin.xml</code> 完全一样</p>

<h2 id="状态持久化">状态持久化</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/persisting-state-of-components.html">官方文档</a></p>
</blockquote>

<p>插件开发需要持久化一些状态到磁盘中，以做到重启后状态恢复（如保存设置项）。Intellij 平台提供了 <code>com.intellij.openapi.components.PersistentStateComponent</code> 接口， <code>com.intellij.openapi.components.State</code> 注解，以及 <code>com.intellij.openapi.components.Storage</code> 注解实现该能力。</p>

<h3 id="实现-persistentstatecomponent">实现 PersistentStateComponent</h3>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/settings/AppSettingsState.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">settings</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">application</span>.<span style="color:#a6e22e">ApplicationManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">PersistentStateComponent</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">State</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">Storage</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">util</span>.<span style="color:#a6e22e">xmlb</span>.<span style="color:#a6e22e">XmlSerializerUtil</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">Nullable</span>;

<span style="color:#a6e22e">@State</span>(
        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;org.intellij.sdk.settings.AppSettingsState&#34;</span>, <span style="color:#75715e">// 该状态存储的唯一标识符，最终会作为 xml 文件的 tags
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// reloadable = false, // default true，如果序列化的文件被外部程序更改了，是否重新加载窗口
</span><span style="color:#75715e"></span>        storages <span style="color:#f92672">=</span> <span style="color:#a6e22e">@Storage</span>(<span style="color:#e6db74">&#34;SdkSettingsPlugin.xml&#34;</span>) <span style="color:#75715e">// 存储文件名，Project 级别状态，且不需要存储到代码仓库，需使用 StoragePathMacros.WORKSPACE_FILE
</span><span style="color:#75715e"></span>)
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> AppSettingsState <span style="color:#a6e22e">implements</span> PersistentStateComponent<span style="color:#f92672">&lt;</span>AppSettingsState<span style="color:#f92672">&gt;</span> {

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> userId <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;John Q. Public&#34;</span>;
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> ideaStatus <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> AppSettingsState <span style="color:#a6e22e">getInstance</span>() {
        <span style="color:#66d9ef">return</span> ApplicationManager.<span style="color:#a6e22e">getApplication</span>().<span style="color:#a6e22e">getService</span>(AppSettingsState.<span style="color:#a6e22e">class</span>);
    }

    <span style="color:#a6e22e">@Nullable</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">AppSettingsState</span> getState() {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>;
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> loadState(<span style="color:#a6e22e">@NotNull</span> AppSettingsState <span style="color:#a6e22e">state</span>) {
        XmlSerializerUtil.<span style="color:#a6e22e">copyBean</span>(state, <span style="color:#66d9ef">this</span>);
    }

}</code></pre></div>
<h3 id="persistentstatecomponent-生命周期">PersistentStateComponent 生命周期</h3>

<ul>
<li><code>loadState</code>

<ul>
<li>实例化该对象时，且对应位置存在序列化文件时，被调用。</li>
<li>磁盘上文件被修改了，此时开发者需要自己来处理状态变更，如果不处理，可以通过 <code>reloadable = true</code> 通过重载来解决。</li>
</ul></li>
<li><code>getState</code> 当用户在设置弹窗点击保存、关闭 IDE、IDE 停用时，被调用。调用是会比较是否和默认值一致，如果一致，则磁盘中不会有文件。否则将写入磁盘中。</li>
</ul>

<h3 id="在-plugin-xml-中注册">在 plugin.xml 中注册</h3>

<p>应用级别状态（所有窗口共享该状态）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#f92672">&lt;extensions</span> <span style="color:#a6e22e">defaultExtensionNs=</span><span style="color:#e6db74">&#34;com.intellij&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;applicationService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.settings.AppSettingsState&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/extensions&gt;</span></code></pre></div>
<p>项目级别状态（每个窗口一个）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#f92672">&lt;extensions</span> <span style="color:#a6e22e">defaultExtensionNs=</span><span style="color:#e6db74">&#34;com.intellij&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;projectService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.settings.AppSettingsState&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/extensions&gt;</span></code></pre></div>
<h3 id="存储位置">存储位置</h3>

<ul>
<li>Application 级别状态，存储在 <code>~/Library/ApplicationSupport/JetBrains/IntelliJIdea2021.2/options</code> 路径下</li>
<li>Project 级别状态，存储在 <code>~/.idea</code> 下

<ul>
<li>如果使用 <code>StoragePathMacros.WORKSPACE_FILE</code> 常量。则存储在

<ul>
<li><code>path/to/project/project.iws</code> - for file-based projects</li>
<li><code>path/to/project/.idea/workspace.xml</code> - for directory-based ones</li>
</ul></li>
<li><code>StoragePathMacros.WORKSPACE_FILE</code> 是有特殊的含义，表示该状态，不会同步到代码仓库中，是该用户特化的配置而不是团队共享的，参见 <a href="https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems">.idea gitignore 说明</a></li>
<li><code>StoragePathMacros.WORKSPACE_FILE</code> 只能在 项目级别使用，如果在 Application 级别使用，将报错</li>
</ul></li>
</ul>

<p>序列化后的一个例子如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;application&gt;</span>
  <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;org.intellij.sdk.settings.AppSettingsState&#34;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;option</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;ideaStatus&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#f92672">/&gt;</span>
  <span style="color:#f92672">&lt;/component&gt;</span>
<span style="color:#f92672">&lt;/application&gt;</span></code></pre></div>
<h3 id="场景-实现一个通用动态状态存储工具">场景：实现一个通用动态状态存储工具</h3>

<p>可以看出，Intellij 插件的状态存储是静态的，需要和类绑定。使用起来有些不方便。因此，想基于该 API，实现动态的状态存储 API。API 设计如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">enum</span> <span style="color:#a6e22e">StatueLevel</span> {
    Global,
    Workspace,
}

<span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">DynamicState</span> {
    String <span style="color:#a6e22e">get</span>(String <span style="color:#a6e22e">key</span>, StatueLevel <span style="color:#a6e22e">level</span>);
    <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">update</span>(String <span style="color:#a6e22e">key</span>, String <span style="color:#a6e22e">value</span>, StatueLevel <span style="color:#a6e22e">level</span>);
}</code></pre></div>
<p>实现思路：</p>

<ul>
<li>创建一个类 DynamicStateImpl 实现，PersistentStateComponent，只有一个字段 <code>Value</code>， 为序列化后的 JSON。</li>
<li>创建两个类继承 GlobalDynamicState、 WorkspaceGlobalDynamicState 继承 <code>DynamicStateImpl</code>

<ul>
<li>GlobalDynamicState 添加 <code>@State</code> 注解，存储在 <code>global.xml</code></li>
<li>WorkspaceGlobalDynamicState 添加 <code>@State</code> 注解，存储在 <code>StoragePathMacros.WORKSPACE_FILE</code></li>
</ul></li>
<li>创建类 DynamicStateService 依赖注入 GlobalDynamicState、 WorkspaceGlobalDynamicState，并实现 DynamicState 接口</li>
<li>GlobalDynamicState、 WorkspaceGlobalDynamicState、 DynamicStateService 分别注册为 applicationService、projectService、projectService</li>
</ul>

<h2 id="配置项">配置项</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/settings-tutorial.html">官方文档</a> | <a href="https://github.com/JetBrains/intellij-sdk-code-samples/tree/main/settings">官方 Demo</a></p>
</blockquote>

<p>配置项在 Intellij 是以 UI 窗口的方式来配置的，因此需要定一个 Swing 组件，存储则使用上文提到的<a href="#状态持久化">状态持久化</a>。再加上配置项入口类。这就是一个典型的 MVC 模型。</p>

<ul>
<li>Model - 状态持久化</li>
<li>Controller - plugin.xml 配置的入口类</li>
<li>View - Swing 组件</li>
</ul>

<p>本例中将创建一个全局配置。</p>

<h3 id="入口类-appsettingsconfigurable">入口类 <code>AppSettingsConfigurable</code></h3>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/settings/AppSettingsConfigurable.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">settings</span>;


<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">options</span>.<span style="color:#a6e22e">Configurable</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">Nls</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">Nullable</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;

<span style="color:#75715e">/**
</span><span style="color:#75715e"> * Controller，应用级别配置的实现，必须提供无参数构造函数。
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> AppSettingsConfigurable <span style="color:#a6e22e">implements</span> Configurable {
    <span style="color:#75715e">// 有一些标记接口，如 Configurable.NoScroll、Configurable.NoMargin，用来配置窗口的滚动和边框
</span><span style="color:#75715e"></span>
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">AppSettingsComponent</span> mySettingsComponent;

    <span style="color:#a6e22e">@Nls</span>(capitalization <span style="color:#f92672">=</span> Nls.<span style="color:#a6e22e">Capitalization</span>.<span style="color:#a6e22e">Title</span>)
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getDisplayName() {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;SDK: Application Settings Example&#34;</span>;
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JComponent</span> getPreferredFocusedComponent() {
        <span style="color:#66d9ef">return</span> mySettingsComponent.<span style="color:#a6e22e">getPreferredFocusedComponent</span>();
    }

    <span style="color:#75715e">// 创建一个 Swing 组件，打开设置该设置窗口，该函数会被调用
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Nullable</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JComponent</span> createComponent() {
        mySettingsComponent <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> AppSettingsComponent();
        <span style="color:#66d9ef">return</span> mySettingsComponent.<span style="color:#a6e22e">getPanel</span>();
    }

    <span style="color:#75715e">// 用于判断是否 enable apply 按钮
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> isModified() {
        AppSettingsState <span style="color:#a6e22e">settings</span> <span style="color:#f92672">=</span> AppSettingsState.<span style="color:#a6e22e">getInstance</span>();
        <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">modified</span> <span style="color:#f92672">=</span> <span style="color:#f92672">!</span>mySettingsComponent.<span style="color:#a6e22e">getUserNameText</span>().<span style="color:#a6e22e">equals</span>(settings.<span style="color:#a6e22e">userId</span>);
        modified <span style="color:#f92672">|=</span> mySettingsComponent.<span style="color:#a6e22e">getIdeaUserStatus</span>() <span style="color:#f92672">!=</span> settings.<span style="color:#a6e22e">ideaStatus</span>;
        <span style="color:#66d9ef">return</span> modified;
    }

    <span style="color:#75715e">// 点击 apply 触发
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> apply() {
        AppSettingsState <span style="color:#a6e22e">settings</span> <span style="color:#f92672">=</span> AppSettingsState.<span style="color:#a6e22e">getInstance</span>();
        settings.<span style="color:#a6e22e">userId</span> <span style="color:#f92672">=</span> mySettingsComponent.<span style="color:#a6e22e">getUserNameText</span>();
        settings.<span style="color:#a6e22e">ideaStatus</span> <span style="color:#f92672">=</span> mySettingsComponent.<span style="color:#a6e22e">getIdeaUserStatus</span>();
    }

    <span style="color:#75715e">// 在 Configurable.createComponent 后立即被调用，在此处初始化 UI 值
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> reset() {
        AppSettingsState <span style="color:#a6e22e">settings</span> <span style="color:#f92672">=</span> AppSettingsState.<span style="color:#a6e22e">getInstance</span>();
        mySettingsComponent.<span style="color:#a6e22e">setUserNameText</span>(settings.<span style="color:#a6e22e">userId</span>);
        mySettingsComponent.<span style="color:#a6e22e">setIdeaUserStatus</span>(settings.<span style="color:#a6e22e">ideaStatus</span>);
    }

    <span style="color:#75715e">// 用户点击 UI 上的确认或者取消，窗口销毁后会调用该函数
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> disposeUIResources() {
        mySettingsComponent <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
    }
}</code></pre></div>
<h3 id="在-plugin-xml-注册入口类">在 plugin.xml 注册入口类</h3>

<p><code>src/main/resources/META-INF/plugin.xml</code></p>

<p>分为两种配置级别，application 级别 和 project 级别。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">        <span style="color:#75715e">&lt;!-- 属性 applicationConfigurable 和 projectConfigurable 贡献点
</span><span style="color:#75715e">            parentId - 定义当前设置项在设置窗口中的位置，可选值为 https://plugins.jetbrains.com/docs/intellij/settings-guide.html#values-for-parent-id-attribute
</span><span style="color:#75715e">            Id - 唯一 ID，建议和类名一致
</span><span style="color:#75715e">            instance - Configurable 实现类的全名，和 provider 二选一
</span><span style="color:#75715e">            provider - ConfigurableProvider 实现类的全名，和 instance 二选一
</span><span style="color:#75715e">            nonDefaultProject - projectConfigurable 专属属性，是否允许用户配置默认配置 true - 该配置默认值写死的， false - 该配置默认值用户可以配置
</span><span style="color:#75715e">                nonDefaultProject = false 场景例子：编辑器字体，用户可以改变默认的字体，也可以专门为这个项目设置特定的配置
</span><span style="color:#75715e">            displayName - 展示名，不需要本地化场景
</span><span style="color:#75715e">            key 和 bundle - 需要本地化场景
</span><span style="color:#75715e">            groupWeight - 排序顺序，默认为 0 （权重最低）
</span><span style="color:#75715e">            dynamic - 设置项内容是否是动态的计算的，默认 false
</span><span style="color:#75715e">            childrenEPName - 如果配置项有多页，可以通过该字段组成树形结构？？
</span><span style="color:#75715e">        --&gt;</span>
        <span style="color:#75715e">&lt;!-- 应用级别配置贡献点 --&gt;</span>
        <span style="color:#75715e">&lt;!-- https://plugins.jetbrains.com/docs/intellij/settings-guide.html#settings-declaration-attributes --&gt;</span>
        <span style="color:#f92672">&lt;applicationConfigurable</span> <span style="color:#a6e22e">parentId=</span><span style="color:#e6db74">&#34;tools&#34;</span>
                                 <span style="color:#a6e22e">instance=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.settings.AppSettingsConfigurable&#34;</span>
                                 <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;org.intellij.sdk.settings.AppSettingsConfigurable&#34;</span>
                                 <span style="color:#a6e22e">displayName=</span><span style="color:#e6db74">&#34;SDK: Application Settings Example&#34;</span><span style="color:#f92672">/&gt;</span>
<span style="color:#75715e">&lt;!--        &lt;projectConfigurable parentId=&#34;tools&#34; instance=&#34;org.company.ProjectSettingsConfigurable&#34;--&gt;</span>
<span style="color:#75715e">&lt;!--                             id=&#34;org.company.ProjectSettingsConfigurable&#34; displayName=&#34;My Project Settings&#34;--&gt;</span>
<span style="color:#75715e">&lt;!--                             nonDefaultProject=&#34;true&#34;/&gt;--&gt;</span></code></pre></div>
<h3 id="状态持久化实现">状态持久化实现</h3>

<p>参见上文<a href="#状态持久化">状态持久化</a>，注意需注册到 plugin.xml</p>

<p><code>src/main/resources/META-INF/plugin.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">        <span style="color:#f92672">&lt;applicationService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.settings.AppSettingsState&#34;</span><span style="color:#f92672">/&gt;</span></code></pre></div>
<h3 id="配置-ui-appsettingscomponent">配置 UI <code>AppSettingsComponent</code></h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">settings</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">JBCheckBox</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">JBLabel</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">JBTextField</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">util</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">FormBuilder</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;

<span style="color:#75715e">/**
</span><span style="color:#75715e"> * 封装 Swing 组件
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> AppSettingsComponent {

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> JPanel <span style="color:#a6e22e">myMainPanel</span>;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> JBTextField <span style="color:#a6e22e">myUserNameText</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> JBTextField();
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> JBCheckBox <span style="color:#a6e22e">myIdeaUserStatus</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> JBCheckBox(<span style="color:#e6db74">&#34;Do you use IntelliJ IDEA? &#34;</span>);

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">AppSettingsComponent</span>() {
        myMainPanel <span style="color:#f92672">=</span> FormBuilder.<span style="color:#a6e22e">createFormBuilder</span>()
                .<span style="color:#a6e22e">addLabeledComponent</span>(<span style="color:#66d9ef">new</span> JBLabel(<span style="color:#e6db74">&#34;Enter user name: &#34;</span>), myUserNameText, 1, <span style="color:#66d9ef">false</span>)
                .<span style="color:#a6e22e">addComponent</span>(myIdeaUserStatus, 1)
                .<span style="color:#a6e22e">addComponentFillVertically</span>(<span style="color:#66d9ef">new</span> JPanel(), 0)
                .<span style="color:#a6e22e">getPanel</span>();
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JPanel</span> getPanel() {
        <span style="color:#66d9ef">return</span> myMainPanel;
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JComponent</span> getPreferredFocusedComponent() {
        <span style="color:#66d9ef">return</span> myUserNameText;
    }

    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getUserNameText() {
        <span style="color:#66d9ef">return</span> myUserNameText.<span style="color:#a6e22e">getText</span>();
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> setUserNameText(<span style="color:#a6e22e">@NotNull</span> String <span style="color:#a6e22e">newText</span>) {
        myUserNameText.<span style="color:#a6e22e">setText</span>(newText);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> getIdeaUserStatus() {
        <span style="color:#66d9ef">return</span> myIdeaUserStatus.<span style="color:#a6e22e">isSelected</span>();
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> setIdeaUserStatus(<span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">newStatus</span>) {
        myIdeaUserStatus.<span style="color:#a6e22e">setSelected</span>(newStatus);
    }

}</code></pre></div>
<h2 id="ui-组件-以工具窗口为例">UI 组件 - 以工具窗口为例</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/user-interface-components.html">UI 官方文档</a> | <a href="https://plugins.jetbrains.com/docs/intellij/tool-windows.html">toolwindow 官方文档</a> | <a href="https://github.com/JetBrains/intellij-sdk-code-samples/tree/main/tool_window">toolwindow 官方 Demo</a> |</p>
</blockquote>

<p>intellij 平台插件可以在 IDE 上的各个地方添加定制化的 UI。该小结将介绍工具窗口 （toolwindow）相关能力，设想有如下需求：</p>

<p>在 IDE 里面添加一个侧边栏，点击该侧边栏可以看到日期、时区和时间，以及刷新和隐藏按钮</p>

<h3 id="创建-swing-类和-form-配置">创建 Swing 类和 form 配置</h3>

<p>在 <code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow</code> 右击 <code>New -&gt; Swing UI Designer -&gt; GUI Form</code> 输入 Form 名 <code>MyToolWindow</code> 并勾选 Create bound class。</p>

<p>通过拖动布置 UI，并编写 <code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyToolWindow.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">toolwindow</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">wm</span>.<span style="color:#a6e22e">ToolWindow</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">java</span>.<span style="color:#a6e22e">util</span>.<span style="color:#a6e22e">Calendar</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> MyToolWindow {

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JButton</span> refreshToolWindowButton;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JButton</span> hideToolWindowButton;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JLabel</span> currentDate;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JLabel</span> currentTime;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JLabel</span> timeZone;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JPanel</span> myToolWindowContent;

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">MyToolWindow</span>(ToolWindow <span style="color:#a6e22e">toolWindow</span>) {
        <span style="color:#75715e">// 添加两个按钮的回调函数
</span><span style="color:#75715e"></span>        hideToolWindowButton.<span style="color:#a6e22e">addActionListener</span>(e <span style="color:#f92672">-&gt;</span> toolWindow.<span style="color:#a6e22e">hide</span>(<span style="color:#66d9ef">null</span>));
        refreshToolWindowButton.<span style="color:#a6e22e">addActionListener</span>(e <span style="color:#f92672">-&gt;</span> currentDateTime());

        <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentDateTime</span>();
    }

    <span style="color:#75715e">// 刷新 UI 组件状态
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> currentDateTime() {
        <span style="color:#75715e">// Get current date and time
</span><span style="color:#75715e"></span>        Calendar <span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> Calendar.<span style="color:#a6e22e">getInstance</span>();
        currentDate.<span style="color:#a6e22e">setText</span>(
                instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">DAY_OF_MONTH</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;/&#34;</span>
                        <span style="color:#f92672">+</span> (instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">MONTH</span>) <span style="color:#f92672">+</span> 1) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;/&#34;</span>
                        <span style="color:#f92672">+</span> instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">YEAR</span>)
        );
        currentDate.<span style="color:#a6e22e">setIcon</span>(<span style="color:#66d9ef">new</span> ImageIcon(getClass().<span style="color:#a6e22e">getResource</span>(<span style="color:#e6db74">&#34;/toolWindow/Calendar-icon.png&#34;</span>)));
        <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">min</span> <span style="color:#f92672">=</span> instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">MINUTE</span>);
        String <span style="color:#a6e22e">strMin</span> <span style="color:#f92672">=</span> min <span style="color:#f92672">&lt;</span> 10 <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">+</span> min <span style="color:#f92672">:</span> String.<span style="color:#a6e22e">valueOf</span>(min);
        currentTime.<span style="color:#a6e22e">setText</span>(instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">HOUR_OF_DAY</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;:&#34;</span> <span style="color:#f92672">+</span> strMin);
        currentTime.<span style="color:#a6e22e">setIcon</span>(<span style="color:#66d9ef">new</span> ImageIcon(getClass().<span style="color:#a6e22e">getResource</span>(<span style="color:#e6db74">&#34;/toolWindow/Time-icon.png&#34;</span>)));
        <span style="color:#75715e">// Get time zone
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">long</span> <span style="color:#a6e22e">gmt_Offset</span> <span style="color:#f92672">=</span> instance.<span style="color:#a6e22e">get</span>(Calendar.<span style="color:#a6e22e">ZONE_OFFSET</span>); <span style="color:#75715e">// offset from GMT in milliseconds
</span><span style="color:#75715e"></span>        String <span style="color:#a6e22e">str_gmt_Offset</span> <span style="color:#f92672">=</span> String.<span style="color:#a6e22e">valueOf</span>(gmt_Offset <span style="color:#f92672">/</span> 3600000);
        str_gmt_Offset <span style="color:#f92672">=</span> (gmt_Offset <span style="color:#f92672">&gt;</span> 0) <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;GMT + &#34;</span> <span style="color:#f92672">+</span> str_gmt_Offset <span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;GMT - &#34;</span> <span style="color:#f92672">+</span> str_gmt_Offset;
        timeZone.<span style="color:#a6e22e">setText</span>(str_gmt_Offset);
        timeZone.<span style="color:#a6e22e">setIcon</span>(<span style="color:#66d9ef">new</span> ImageIcon(getClass().<span style="color:#a6e22e">getResource</span>(<span style="color:#e6db74">&#34;/toolWindow/Time-zone-icon.png&#34;</span>)));
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JPanel</span> getContent() {
        <span style="color:#66d9ef">return</span> myToolWindowContent;
    }
}</code></pre></div>
<h3 id="编写窗口工厂类">编写窗口工厂类</h3>

<p>创建 <code>ToolWindowFactory</code> 接口的一个实现类，并实现 <code>createToolWindowContent</code> 方法，来创建一个 swing 窗口，其他必要有用过的接口为：</p>

<ul>
<li><code>isApplicable(Project)</code> 根据项目类型决定是否启用该工具窗口</li>
</ul>

<p>标记接口</p>

<ul>
<li><code>com.intellij.openapi.startup.StartupActivity.DumbAware</code></li>
</ul>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyToolWindowFactory.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">toolwindow</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">wm</span>.<span style="color:#a6e22e">ToolWindow</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">wm</span>.<span style="color:#a6e22e">ToolWindowFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">content</span>.<span style="color:#a6e22e">Content</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">content</span>.<span style="color:#a6e22e">ContentFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> MyToolWindowFactory <span style="color:#a6e22e">implements</span> ToolWindowFactory {

    <span style="color:#75715e">/**
</span><span style="color:#75715e">     * 创建一个 ToolWindows 窗口
</span><span style="color:#75715e">     *
</span><span style="color:#75715e">     * @param project    当前视图
</span><span style="color:#75715e">     * @param toolWindow 当前 tool window
</span><span style="color:#75715e">     */</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> createToolWindowContent(<span style="color:#a6e22e">@NotNull</span> Project <span style="color:#a6e22e">project</span>, <span style="color:#a6e22e">@NotNull</span> ToolWindow <span style="color:#a6e22e">toolWindow</span>) {
        MyToolWindow <span style="color:#a6e22e">myToolWindow</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> MyToolWindow(toolWindow);
        ContentFactory <span style="color:#a6e22e">contentFactory</span> <span style="color:#f92672">=</span> ContentFactory.<span style="color:#a6e22e">SERVICE</span>.<span style="color:#a6e22e">getInstance</span>();
        Content <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> contentFactory.<span style="color:#a6e22e">createContent</span>(myToolWindow.<span style="color:#a6e22e">getContent</span>(), <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">false</span>);
        toolWindow.<span style="color:#a6e22e">getContentManager</span>().<span style="color:#a6e22e">addContent</span>(content);
    }

}</code></pre></div>
<h3 id="在-plugin-xml-中注册-1">在 plugin.xml 中注册</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#f92672">&lt;extensions</span> <span style="color:#a6e22e">defaultExtensionNs=</span><span style="color:#e6db74">&#34;com.intellij&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#75715e">&lt;!-- 注册一个工具窗口按钮，在此配置工具栏按钮的信息，属性说明如下
</span><span style="color:#75715e">                id 工具窗口的id - 对应于工具窗口按钮上显示的文本。要提供一个本地化的文本，通过 `toolwindow.stripe.[id]` 方式给出（空格替换为 `_` ），本地化参见 https://plugins.jetbrains.com/docs/intellij/localization-guide.html
</span><span style="color:#75715e">                anchor 位置 &#34;left&#34; (default), &#34;right&#34; or &#34;bottom&#34;
</span><span style="color:#75715e">                secondary 指定工具窗口是否显示在次要组中（如果 anchor 为 左或右， 该字段为 true 则显示在下方）
</span><span style="color:#75715e">                icon 图标使用 13x13 像素，更多参见 https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html
</span><span style="color:#75715e">                factoryClass 工厂类
</span><span style="color:#75715e">        --&gt;</span>
        <span style="color:#f92672">&lt;toolWindow</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;Sample Calendar&#34;</span>
                    <span style="color:#a6e22e">secondary=</span><span style="color:#e6db74">&#34;true&#34;</span>
                    <span style="color:#a6e22e">icon=</span><span style="color:#e6db74">&#34;AllIcons.General.Modified&#34;</span>
                    <span style="color:#a6e22e">anchor=</span><span style="color:#e6db74">&#34;right&#34;</span>
                    <span style="color:#a6e22e">factoryClass=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.toolwindow.MyToolWindowFactory&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/extensions&gt;</span></code></pre></div>
<h3 id="通过代码注册窗口">通过代码注册窗口</h3>

<p>参见 <a href="https://upsource.jetbrains.com/idea-ce/file/idea-ce-8f0275fd7faaeafdb8900147eab3d256fe4221cb/platform/platform-api/src/com/intellij/openapi/wm/ToolWindowManager.kt?_ga=2.177404709.189575459.1636082800-220348679.1634563234">com.intellij.openapi.wm.ToolWindowManager.registerToolWindow()</a></p>

<h3 id="其他-ui-组件">其他 UI 组件</h3>

<p>参见 <a href="https://plugins.jetbrains.com/docs/intellij/user-interface-components.html">官方文档</a></p>

<h2 id="运行调试">运行调试</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/run-configurations.html">官方文档</a> | <a href="https://github.com/JetBrains/intellij-sdk-code-samples/tree/main/run_configuration">官方 Demo</a></p>
</blockquote>

<p>设想如下场景：</p>

<p>假设某些项目需要运行在特定的环境中，本地没法启动。如果直接使用 Jetbrains 原生提供的远程调试的能力，用户还需要手动编译，将编译产物同步到远端，并运行服务，然后再配置远程调试端口体验极差。此时假设已经有了一个 cli 工具和服务可以做到将编译产物同步到远端运行服务，并返回调试端口，此时希望有一个 Jetrains 插件可以让用户免配置的直接启动。</p>

<p>针对该场景，这个插件的逻辑大概如下：</p>

<ul>
<li>需要有一个自定义运行配置的配置页面，用户填写一些鉴权和远端服务的 ID 等信息</li>
<li>用户创建好改调试配置后，点击运行或调试按钮，将调起如下自定义逻辑

<ul>
<li>前置准备，编译，调用 cli 将编译产物同步到远端运行服务，获取调试端口，并将相关日志打印到 console 中</li>
<li>调用 Jetbrains 原生远程调试能力，连接到远端服务</li>
</ul></li>
<li>更进一步，甚至可以实现一个管理页面，用户可以免配置的从插件的自定义窗口中的 start 按钮，一键启动调试</li>
</ul>

<p>简单起见，简化为如下场景：</p>

<ul>
<li>用户配置一个脚本文件</li>
<li>执行（以 golang 调试为例）

<ul>
<li>先弹窗输入输出这个用户配置的内容</li>
<li>run 模式

<ul>
<li>调用 <code>go run ./</code></li>
</ul></li>
<li>debug 模式

<ul>
<li>调用 <code>dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient ./</code> 启动进程</li>
<li>检测到 dlv 启动成功后，调用 Jetbrains 原生远程调试能力，连接固定的 <code>localhost:2345</code> 调试端口</li>
</ul></li>
</ul></li>
<li>在 UI 组件中，添加一个 debug 按钮，可以唤起调试配置</li>
</ul>

<h3 id="运行配置管理">运行配置管理</h3>

<h4 id="代码实现">代码实现</h4>

<p>自定义一套运行配置需要分别实现如下接口</p>

<ul>
<li><code>com.intellij.execution.configurations.ConfigurationType</code> 运行配置类型：对应 IDE Run Configuration 的模板列表页中每一个顶级项目，负责声明关联的 <code>ConfigurationFactory</code> 并作为唯一标识符</li>
<li><code>com.intellij.execution.configurations.ConfigurationFactory</code> 运行配置工厂：对应 IDE Run Configuration 的模板列表页中每一个子项，负责创建 <code>RunConfiguration</code></li>
<li><code>com.intellij.execution.configurations.RunConfiguration</code> 运行配置：表示一个运行调试配置实例，会和一个运行配置项编辑器 UI、以及一个运行配置状态存储绑定</li>
<li><code>com.intellij.openapi.options.SettingsEditor</code> 运行配置项编辑器 UI：一种特殊的 UI，用来编辑展示运行配置项</li>
<li><code>com.intellij.openapi.components.BaseState</code> 运行配置项存储</li>
</ul>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoRunConfigurationType.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>;


<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">ConfigurationFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">ConfigurationType</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">icons</span>.<span style="color:#a6e22e">AllIcons</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;

<span style="color:#75715e">// 运行配置类型：对应 IDE Run Configuration 的模板列表页中每一个顶级项目，负责声明关联的 `ConfigurationFactory` 并作为唯一标识符
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoRunConfigurationType <span style="color:#a6e22e">implements</span> ConfigurationType {

    <span style="color:#75715e">// 配置类型 ID
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">String</span> ID <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;DemoRunConfiguration&#34;</span>;

    <span style="color:#75715e">// 展示名
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getDisplayName() {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;Demo&#34;</span>;
    }

    <span style="color:#75715e">// 描述
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getConfigurationTypeDescription() {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;Demo run configuration type&#34;</span>;
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Icon</span> getIcon() {
        <span style="color:#66d9ef">return</span> AllIcons.<span style="color:#a6e22e">General</span>.<span style="color:#a6e22e">Information</span>;
    }

    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getId() {
        <span style="color:#66d9ef">return</span> ID;
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">ConfigurationFactory</span>[] getConfigurationFactories() {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ConfigurationFactory[]{<span style="color:#66d9ef">new</span> DemoConfigurationFactory(<span style="color:#66d9ef">this</span>)};
    }

}</code></pre></div>
<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoConfigurationFactory.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>;


<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">ConfigurationFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">ConfigurationType</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">RunConfiguration</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">BaseState</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">Nullable</span>;

<span style="color:#75715e">// 运行配置工厂：对应 IDE Run Configuration 的模板列表页中每一个子项，负责创建 `RunConfiguration`
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoConfigurationFactory <span style="color:#a6e22e">extends</span> ConfigurationFactory {

    <span style="color:#75715e">// ConfigurationType 作为工厂的成员
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">DemoConfigurationFactory</span>(ConfigurationType <span style="color:#a6e22e">type</span>) {
        <span style="color:#66d9ef">super</span>(type);
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">@NotNull</span> String <span style="color:#a6e22e">getId</span>() {
        <span style="color:#66d9ef">return</span> DemoRunConfigurationType.<span style="color:#a6e22e">ID</span>;
    }

    <span style="color:#75715e">// 获取到一个模板配置
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">RunConfiguration</span> createTemplateConfiguration(<span style="color:#a6e22e">@NotNull</span> Project <span style="color:#a6e22e">project</span>) {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DemoRunConfiguration(project, <span style="color:#66d9ef">this</span>, <span style="color:#e6db74">&#34;Demo&#34;</span>);
    }

    <span style="color:#75715e">// 声明该配置的选项声明类是什么
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Nullable</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Class</span><span style="color:#f92672">&lt;?</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">BaseState</span><span style="color:#f92672">&gt;</span> getOptionsClass() {
        <span style="color:#66d9ef">return</span> DemoRunConfigurationOptions.<span style="color:#a6e22e">class</span>;
    }

}</code></pre></div>
<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoRunConfiguration.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>;


<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ExecutionException</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">Executor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#f92672">*</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">OSProcessHandler</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">ProcessHandler</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">ProcessHandlerFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">ProcessTerminatedListener</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">ExecutionEnvironment</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">options</span>.<span style="color:#a6e22e">SettingsEditor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">Nullable</span>;

<span style="color:#75715e">// 运行配置：表示一个运行调试配置实例，会和一个运行配置项编辑器 UI、以及一个运行配置状态存储绑定
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoRunConfiguration <span style="color:#a6e22e">extends</span> RunConfigurationBase<span style="color:#f92672">&lt;</span>DemoRunConfigurationOptions<span style="color:#f92672">&gt;</span> {

    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">DemoRunConfiguration</span>(Project <span style="color:#a6e22e">project</span>, ConfigurationFactory <span style="color:#a6e22e">factory</span>, String <span style="color:#a6e22e">name</span>) {
        <span style="color:#66d9ef">super</span>(project, factory, name);
    }

    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">DemoRunConfigurationOptions</span> getOptions() {
        <span style="color:#66d9ef">return</span> (DemoRunConfigurationOptions) <span style="color:#66d9ef">super</span>.<span style="color:#a6e22e">getOptions</span>();
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getScriptName() {
        <span style="color:#66d9ef">return</span> getOptions().<span style="color:#a6e22e">getScriptName</span>();
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> setScriptName(String <span style="color:#a6e22e">scriptName</span>) {
        getOptions().<span style="color:#a6e22e">setScriptName</span>(scriptName);
    }

    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">SettingsEditor</span><span style="color:#f92672">&lt;?</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">RunConfiguration</span><span style="color:#f92672">&gt;</span> getConfigurationEditor() {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> DemoSettingsEditor();
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> checkConfiguration() {
    }

    <span style="color:#75715e">// 核心入口，获取到 RunProfileState 实现
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Nullable</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">RunProfileState</span> getState(<span style="color:#a6e22e">@NotNull</span> Executor <span style="color:#a6e22e">executor</span>, <span style="color:#a6e22e">@NotNull</span> ExecutionEnvironment <span style="color:#a6e22e">executionEnvironment</span>) {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> CommandLineState(executionEnvironment) {
            <span style="color:#a6e22e">@NotNull</span>
            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">ProcessHandler</span> startProcess() <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
                <span style="color:#75715e">// 先简单的实现为直接通过命令行执行
</span><span style="color:#75715e"></span>                GeneralCommandLine <span style="color:#a6e22e">commandLine</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> GeneralCommandLine(getOptions().<span style="color:#a6e22e">getScriptName</span>());
                OSProcessHandler <span style="color:#a6e22e">processHandler</span> <span style="color:#f92672">=</span> ProcessHandlerFactory.<span style="color:#a6e22e">getInstance</span>().<span style="color:#a6e22e">createColoredProcessHandler</span>(commandLine);
                ProcessTerminatedListener.<span style="color:#a6e22e">attach</span>(processHandler);
                <span style="color:#66d9ef">return</span> processHandler;
            }
        };
    }
}</code></pre></div>
<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoRunConfigurationOptions.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">RunConfigurationOptions</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">components</span>.<span style="color:#a6e22e">StoredProperty</span>;

<span style="color:#75715e">// 运行配置项存储
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoRunConfigurationOptions <span style="color:#a6e22e">extends</span> RunConfigurationOptions {

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> StoredProperty<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">myScriptName</span> <span style="color:#f92672">=</span> string(<span style="color:#e6db74">&#34;&#34;</span>).<span style="color:#a6e22e">provideDelegate</span>(<span style="color:#66d9ef">this</span>, <span style="color:#e6db74">&#34;scriptName&#34;</span>);

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">String</span> getScriptName() {
        <span style="color:#66d9ef">return</span> myScriptName.<span style="color:#a6e22e">getValue</span>(<span style="color:#66d9ef">this</span>);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> setScriptName(String <span style="color:#a6e22e">scriptName</span>) {
        myScriptName.<span style="color:#a6e22e">setValue</span>(<span style="color:#66d9ef">this</span>, scriptName);
    }

}</code></pre></div>
<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoSettingsEditor.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>;


<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">options</span>.<span style="color:#a6e22e">SettingsEditor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">LabeledComponent</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">TextFieldWithBrowseButton</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;

<span style="color:#75715e">// 运行配置项编辑器 UI：一种特殊的 UI，用来编辑展示运行配置项
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoSettingsEditor <span style="color:#a6e22e">extends</span> SettingsEditor<span style="color:#f92672">&lt;</span>DemoRunConfiguration<span style="color:#f92672">&gt;</span> {

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">JPanel</span> myPanel;
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LabeledComponent</span><span style="color:#f92672">&lt;</span>TextFieldWithBrowseButton<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">myScriptName</span>;

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">void</span> resetEditorFrom(DemoRunConfiguration <span style="color:#a6e22e">demoRunConfiguration</span>) {
        myScriptName.<span style="color:#a6e22e">getComponent</span>().<span style="color:#a6e22e">setText</span>(demoRunConfiguration.<span style="color:#a6e22e">getScriptName</span>());
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">void</span> applyEditorTo(<span style="color:#a6e22e">@NotNull</span> DemoRunConfiguration <span style="color:#a6e22e">demoRunConfiguration</span>) {
        demoRunConfiguration.<span style="color:#a6e22e">setScriptName</span>(myScriptName.<span style="color:#a6e22e">getComponent</span>().<span style="color:#a6e22e">getText</span>());
    }

    <span style="color:#a6e22e">@NotNull</span>
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">JComponent</span> createEditor() {
        <span style="color:#66d9ef">return</span> myPanel;
    }

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">void</span> createUIComponents() {
        myScriptName <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> LabeledComponent<span style="color:#f92672">&lt;&gt;</span>();
        myScriptName.<span style="color:#a6e22e">setComponent</span>(<span style="color:#66d9ef">new</span> TextFieldWithBrowseButton());
    }

}</code></pre></div>
<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/configuration/DemoSettingsEditor.form</code> swing form 表单略</p>

<h4 id="在-plugin-xml-中注册-2">在 plugin.xml 中注册</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;configurationType</span> <span style="color:#a6e22e">implementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.runconfiguration.DemoRunConfigurationType&#34;</span><span style="color:#f92672">/&gt;</span></code></pre></div>
<h3 id="执行过程-programrunner">执行过程 ProgramRunner</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/run-configuration-execution.html">官方文档</a></p>
</blockquote>

<h4 id="添加插件依赖并注册-programrunner">添加插件依赖并注册 ProgramRunner</h4>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html">官方兼容性说明</a> | <a href="https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html">官方依赖说明</a></p>
</blockquote>

<p>由于这个场景需要调用 Goland 的调试器，所以需要添加依赖 Goland 插件。</p>

<p>第一步，添加构建依赖 <code>gradle.properties</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-properties" data-lang="properties">platformVersion = 2021.1.1
platformPlugins = org.jetbrains.plugins.go:211.6693.111</code></pre></div>
<p>第二步，添加插件声明 <code>src/main/resources/META-INF/plugin.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#75715e">&lt;!-- 可选依赖 （该调试特性仅支持 goland）
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
</span><span style="color:#75715e">        https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
</span><span style="color:#75715e">    --&gt;</span>
    <span style="color:#f92672">&lt;depends</span> <span style="color:#a6e22e">optional=</span><span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#a6e22e">config-file=</span><span style="color:#e6db74">&#34;demo-golang.xml&#34;</span><span style="color:#f92672">&gt;</span>org.jetbrains.plugins.go<span style="color:#f92672">&lt;/depends&gt;</span></code></pre></div>
<p>第三步，创建 <code>src/main/resources/META-INF/demo-golang.xml</code>，将运行配置类型移动到该文件，并添加 programRunner 贡献点</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#f92672">&lt;idea-plugin&gt;</span>
    <span style="color:#f92672">&lt;extensions</span> <span style="color:#a6e22e">defaultExtensionNs=</span><span style="color:#e6db74">&#34;com.intellij&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#75715e">&lt;!-- 调试运行配置的贡献点 --&gt;</span>
        <span style="color:#f92672">&lt;configurationType</span> <span style="color:#a6e22e">implementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.run.configuration.DemoRunConfigurationType&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#75715e">&lt;!-- 运行器贡献点 --&gt;</span>
        <span style="color:#f92672">&lt;programRunner</span> <span style="color:#a6e22e">implementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.run.execution.DemoProgramRunner&#34;</span> <span style="color:#a6e22e">order=</span><span style="color:#e6db74">&#34;first&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/extensions&gt;</span>
<span style="color:#f92672">&lt;/idea-plugin&gt;</span></code></pre></div>
<h4 id="programrunner-实现">ProgramRunner 实现</h4>

<ul>
<li>实现 <code>ProgramRunner</code> 接口

<ul>
<li><code>canRun</code> 根据运行类型（Debug、Run） 和 <code>RunProfile</code> 类型判断是否由该配置 Runner 运行</li>
<li><code>execute</code> 真正的执行函数，逻辑基本上为，<code>ExecutionManager.startRunProfile</code>

<ul>
<li>调用 <code>RunProfile</code> 的 <code>getState</code> 方法</li>
<li>执行回调函数

<ul>
<li>自定义的操作</li>
<li>调用 <code>RunProfileState.execute</code>，获得 <code>ExecutionResult</code></li>
<li>根据运行模式返回不同的 <code>RunContentDescriptor</code></li>
</ul></li>
</ul></li>
</ul></li>
</ul>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/execution/DemoProgramRunner.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">execution</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>.<span style="color:#a6e22e">DemoRunConfiguration</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">goide</span>.<span style="color:#a6e22e">dlv</span>.<span style="color:#a6e22e">DlvDebugProcess</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">goide</span>.<span style="color:#a6e22e">dlv</span>.<span style="color:#a6e22e">DlvDisconnectOption</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">goide</span>.<span style="color:#a6e22e">dlv</span>.<span style="color:#a6e22e">DlvRemoteVmConnection</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ExecutionException</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ExecutionManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ExecutionResult</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">RunProfile</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">RunnerSettings</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">ExecutionEnvironment</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">ProgramRunner</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">RunContentBuilder</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">RunContentDescriptor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">fileEditor</span>.<span style="color:#a6e22e">FileDocumentManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">xdebugger</span>.<span style="color:#a6e22e">XDebugProcess</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">xdebugger</span>.<span style="color:#a6e22e">XDebugProcessStarter</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">xdebugger</span>.<span style="color:#a6e22e">XDebugSession</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">xdebugger</span>.<span style="color:#a6e22e">XDebuggerManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NonNls</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">java</span>.<span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">InetSocketAddress</span>;

<span style="color:#75715e">// 一个 Runner：实现运行配置启动后的的行为
</span><span style="color:#75715e"></span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoProgramRunner <span style="color:#a6e22e">implements</span> ProgramRunner<span style="color:#f92672">&lt;</span>RunnerSettings<span style="color:#f92672">&gt;</span>{
    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">@NotNull</span> <span style="color:#a6e22e">@NonNls</span> String <span style="color:#a6e22e">getRunnerId</span>() {
        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;DemoProgramRunner&#34;</span>;
    }

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">DlvDebugProcess</span> process;

    <span style="color:#75715e">// 该此执行是否有该 Runner 负责，同时处理 Debug 和 Run 两种场景
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">boolean</span> canRun(<span style="color:#a6e22e">@NotNull</span> String <span style="color:#a6e22e">executorId</span>, <span style="color:#a6e22e">@NotNull</span> RunProfile <span style="color:#a6e22e">profile</span>) {
        <span style="color:#66d9ef">return</span> (ExecutionUtil.<span style="color:#a6e22e">isDebugMode</span>(executorId) <span style="color:#f92672">||</span> ExecutionUtil.<span style="color:#a6e22e">isRunMode</span>(executorId))
                <span style="color:#f92672">&amp;&amp;</span> profile <span style="color:#a6e22e">instanceof</span> DemoRunConfiguration;
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> execute(<span style="color:#a6e22e">@NotNull</span> ExecutionEnvironment <span style="color:#a6e22e">environment</span>) <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
        <span style="color:#75715e">// 启动 run 配置，调用 `RunProfile` 的 getState 方法
</span><span style="color:#75715e"></span>        ExecutionManager.<span style="color:#a6e22e">getInstance</span>(environment.<span style="color:#a6e22e">getProject</span>()).<span style="color:#a6e22e">startRunProfile</span>(environment, state <span style="color:#f92672">-&gt;</span> {
            <span style="color:#75715e">// 先保存所有未保存文件
</span><span style="color:#75715e"></span>            FileDocumentManager.<span style="color:#a6e22e">getInstance</span>().<span style="color:#a6e22e">saveAllDocuments</span>();
            <span style="color:#75715e">// state 为 DemoRunConfiguration.getState() 返回，即 DemoRunProfileState，调用 RunProfileState.execute
</span><span style="color:#75715e"></span>            ExecutionResult <span style="color:#a6e22e">executionResult</span> <span style="color:#f92672">=</span> state.<span style="color:#a6e22e">execute</span>(environment.<span style="color:#a6e22e">getExecutor</span>(), <span style="color:#66d9ef">this</span>);
            <span style="color:#66d9ef">if</span> (executionResult <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span>) {
                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>;
            }
            <span style="color:#75715e">// 获取到 RunContentDescriptor
</span><span style="color:#75715e"></span>            <span style="color:#66d9ef">if</span> (ExecutionUtil.<span style="color:#a6e22e">isDebugMode</span>(environment)) {
                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">debugModeRunContentDescriptor</span>(environment, executionResult);
            } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (ExecutionUtil.<span style="color:#a6e22e">isRunMode</span>(environment)) {
                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">runModeRunContentDescriptor</span>(environment, executionResult);
            }
            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ExecutionException(<span style="color:#e6db74">&#34;Not support&#34;</span>);
        });
    }

    <span style="color:#75715e">// 给 RunProfileState 用，控制连接到 dlv（因为进程启动了，不一定就可以立即连接了，需要 State 自己决定合适连接）
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> connectToDlv(String <span style="color:#a6e22e">host</span>, <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">port</span>) {
        process.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">new</span> InetSocketAddress(host, port));
    }

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">RunContentDescriptor</span> runModeRunContentDescriptor(<span style="color:#a6e22e">@NotNull</span> ExecutionEnvironment <span style="color:#a6e22e">environment</span>, <span style="color:#a6e22e">@NotNull</span> ExecutionResult <span style="color:#a6e22e">executionResult</span>) <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
        <span style="color:#75715e">// 简单返回一个 RunContentDescriptor
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> RunContentBuilder(executionResult, environment).<span style="color:#a6e22e">showRunContent</span>(environment.<span style="color:#a6e22e">getContentToReuse</span>());
    }

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">RunContentDescriptor</span> debugModeRunContentDescriptor(<span style="color:#a6e22e">@NotNull</span> ExecutionEnvironment <span style="color:#a6e22e">environment</span>, <span style="color:#a6e22e">@NotNull</span> ExecutionResult <span style="color:#a6e22e">executionResult</span>) <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
        <span style="color:#75715e">// Debug 调试器附加到进程中，然后一个 Bugger RunContentDescriptor
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span> XDebuggerManager.<span style="color:#a6e22e">getInstance</span>(environment.<span style="color:#a6e22e">getProject</span>()).<span style="color:#a6e22e">startSession</span>(environment, <span style="color:#66d9ef">new</span> XDebugProcessStarter() {
            <span style="color:#a6e22e">@Override</span>
            <span style="color:#a6e22e">@NotNull</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">XDebugProcess</span> start(<span style="color:#a6e22e">@NotNull</span> XDebugSession <span style="color:#a6e22e">session</span>) <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
                process <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> DlvDebugProcess(
                        session,
                        <span style="color:#66d9ef">new</span> DlvRemoteVmConnection(DlvDisconnectOption.<span style="color:#a6e22e">DETACH</span>),
                        executionResult,
                        <span style="color:#66d9ef">true</span>);
                <span style="color:#66d9ef">return</span> process;
            }
        }).<span style="color:#a6e22e">getRunContentDescriptor</span>();
    }
}</code></pre></div>
<h4 id="runprofilestate-实现">RunProfileState 实现</h4>

<p>RunProfileState 即某次执行的状态控制，一般负责</p>

<ul>
<li>确定启动的外部命令</li>
<li>添加进程状态的监听的回调（包括启动/停止/命令行输出）</li>
</ul>

<p>一般继承 <code>CommandLineState</code> 即可</p>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/execution/DemoRunProfileState.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">execution</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>.<span style="color:#a6e22e">DemoRunConfiguration</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ExecutionException</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">CommandLineState</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">GeneralCommandLine</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">process</span>.<span style="color:#f92672">*</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">ExecutionEnvironment</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">ConsoleView</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">ConsoleViewContentType</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">RunContentDescriptor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">RunContentManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">Messages</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">util</span>.<span style="color:#a6e22e">Key</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">xdebugger</span>.<span style="color:#a6e22e">XDebuggerManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoRunProfileState <span style="color:#a6e22e">extends</span> CommandLineState {

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DemoRunProfileState</span>(ExecutionEnvironment <span style="color:#a6e22e">environment</span>) {
        <span style="color:#66d9ef">super</span>(environment);
    }

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">@NotNull</span> ProcessHandler <span style="color:#a6e22e">startProcess</span>() <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">ExecutionException</span> {
        var <span style="color:#a6e22e">environment</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">getEnvironment</span>();
        var <span style="color:#a6e22e">demoRunConfiguration</span> <span style="color:#f92672">=</span> (DemoRunConfiguration)  environment.<span style="color:#a6e22e">getRunnerAndConfigurationSettings</span>().<span style="color:#a6e22e">getConfiguration</span>();

        Messages.<span style="color:#a6e22e">showInfoMessage</span>(demoRunConfiguration.<span style="color:#a6e22e">getScriptName</span>(), <span style="color:#e6db74">&#34;这是用户的配置&#34;</span>);

        <span style="color:#75715e">// 下面是创建一个外部命令行进程
</span><span style="color:#75715e"></span>        GeneralCommandLine <span style="color:#a6e22e">commandLine</span>;
        <span style="color:#66d9ef">if</span> (ExecutionUtil.<span style="color:#a6e22e">isRunMode</span>(environment)) { <span style="color:#75715e">// Run 模式
</span><span style="color:#75715e"></span>            commandLine <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> GeneralCommandLine(<span style="color:#e6db74">&#34;go&#34;</span>, <span style="color:#e6db74">&#34;run&#34;</span>, <span style="color:#e6db74">&#34;./&#34;</span>);
        } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (ExecutionUtil.<span style="color:#a6e22e">isDebugMode</span>(environment)) { <span style="color:#75715e">// Debug 模式
</span><span style="color:#75715e"></span>            commandLine <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> GeneralCommandLine(<span style="color:#e6db74">&#34;dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient ./&#34;</span>.<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">&#34; &#34;</span>));
        } <span style="color:#66d9ef">else</span> {
            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> ExecutionException(<span style="color:#e6db74">&#34;Not support&#34;</span>);
        }
        commandLine.<span style="color:#a6e22e">setWorkDirectory</span>(environment.<span style="color:#a6e22e">getProject</span>().<span style="color:#a6e22e">getBasePath</span>());
        OSProcessHandler <span style="color:#a6e22e">processHandler</span> <span style="color:#f92672">=</span> ProcessHandlerFactory.<span style="color:#a6e22e">getInstance</span>().<span style="color:#a6e22e">createColoredProcessHandler</span>(commandLine);
        ProcessTerminatedListener.<span style="color:#a6e22e">attach</span>(processHandler);

        <span style="color:#75715e">// 添加进程的事件监听
</span><span style="color:#75715e"></span>        processHandler.<span style="color:#a6e22e">addProcessListener</span>(<span style="color:#66d9ef">new</span> ProcessAdapter() {
            <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">boolean</span> connected <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;

            <span style="color:#75715e">// 进程终止的回调
</span><span style="color:#75715e"></span>            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> processTerminated(<span style="color:#a6e22e">@NotNull</span> ProcessEvent <span style="color:#a6e22e">event</span>) {
                getConsoleView(processHandler).<span style="color:#a6e22e">print</span>(<span style="color:#e6db74">&#34;进程结束停止了&#34;</span>, ConsoleViewContentType.<span style="color:#a6e22e">SYSTEM_OUTPUT</span>);
            }

            <span style="color:#75715e">// 检测到进程有输出时的回调
</span><span style="color:#75715e"></span>            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> onTextAvailable(<span style="color:#a6e22e">@NotNull</span> ProcessEvent <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">@NotNull</span> Key <span style="color:#a6e22e">outputType</span>) {
                <span style="color:#75715e">// 检测到关键词后，连接到 dlv
</span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>connected <span style="color:#f92672">&amp;&amp;</span> ExecutionUtil.<span style="color:#a6e22e">isDebugMode</span>(environment) <span style="color:#f92672">&amp;&amp;</span> event.<span style="color:#a6e22e">getText</span>().<span style="color:#a6e22e">contains</span>(<span style="color:#e6db74">&#34;API server listening at&#34;</span>)) {
                    getConsoleView(processHandler).<span style="color:#a6e22e">print</span>(<span style="color:#e6db74">&#34;即将连接到 dlv&#34;</span>, ConsoleViewContentType.<span style="color:#a6e22e">SYSTEM_OUTPUT</span>);
                    ((DemoProgramRunner) environment.<span style="color:#a6e22e">getRunner</span>()).<span style="color:#a6e22e">connectToDlv</span>(<span style="color:#e6db74">&#34;localhost&#34;</span>, 2345);
                    connected <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
                }
            }

        });
        <span style="color:#66d9ef">return</span> processHandler;
    }

    <span style="color:#75715e">// 获取到执行页面的 Console，可以打印一些自定义的内容
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ConsoleView</span> getConsoleView(ProcessHandler <span style="color:#a6e22e">processHandler</span>) {
        var <span style="color:#a6e22e">environment</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">getEnvironment</span>();
        var <span style="color:#a6e22e">project</span> <span style="color:#f92672">=</span> environment.<span style="color:#a6e22e">getProject</span>();
        <span style="color:#66d9ef">if</span> (ExecutionUtil.<span style="color:#a6e22e">isDebugMode</span>(environment)) {
            var <span style="color:#a6e22e">session</span> <span style="color:#f92672">=</span> XDebuggerManager.<span style="color:#a6e22e">getInstance</span>(project).<span style="color:#a6e22e">getCurrentSession</span>();
            <span style="color:#66d9ef">if</span> (session <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
                <span style="color:#66d9ef">return</span> session.<span style="color:#a6e22e">getConsoleView</span>();
            }
        }
        RunContentDescriptor <span style="color:#a6e22e">contentDescriptor</span> <span style="color:#f92672">=</span> RunContentManager
                .<span style="color:#a6e22e">getInstance</span>(project)
                .<span style="color:#a6e22e">findContentDescriptor</span>(environment.<span style="color:#a6e22e">getExecutor</span>(), processHandler);
        ConsoleView <span style="color:#a6e22e">console</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
        <span style="color:#66d9ef">if</span> (contentDescriptor <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> contentDescriptor.<span style="color:#a6e22e">getExecutionConsole</span>() <span style="color:#66d9ef">instanceof</span> ConsoleView) {
            console <span style="color:#f92672">=</span> (ConsoleView) contentDescriptor.<span style="color:#a6e22e">getExecutionConsole</span>();
        }
        <span style="color:#66d9ef">return</span> console;
    }
}</code></pre></div>
<h4 id="其他工具函数">其他工具函数</h4>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/execution/ExecutionUtil.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">executors</span>.<span style="color:#a6e22e">DefaultDebugExecutor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">executors</span>.<span style="color:#a6e22e">DefaultRunExecutor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">runners</span>.<span style="color:#a6e22e">ExecutionEnvironment</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> ExecutionUtil {

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isDebugMode</span>(String <span style="color:#a6e22e">executorId</span>) {
        <span style="color:#66d9ef">return</span> DefaultDebugExecutor.<span style="color:#a6e22e">EXECUTOR_ID</span>.<span style="color:#a6e22e">equals</span>(executorId);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isDebugMode</span>(ExecutionEnvironment <span style="color:#a6e22e">environment</span>) {
        <span style="color:#66d9ef">return</span> isDebugMode(environment.<span style="color:#a6e22e">getExecutor</span>().<span style="color:#a6e22e">getId</span>());
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isRunMode</span>(String <span style="color:#a6e22e">executorId</span>) {
        <span style="color:#66d9ef">return</span> DefaultRunExecutor.<span style="color:#a6e22e">EXECUTOR_ID</span>.<span style="color:#a6e22e">equals</span>(executorId);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">boolean</span> <span style="color:#a6e22e">isRunMode</span>(ExecutionEnvironment <span style="color:#a6e22e">environment</span>) {
        <span style="color:#66d9ef">return</span> isRunMode(environment.<span style="color:#a6e22e">getExecutor</span>().<span style="color:#a6e22e">getId</span>());
    }
}</code></pre></div>
<h3 id="通过代码启动调试">通过代码启动调试</h3>

<p>在 工具窗口 中添加一个按钮，来快速运行。</p>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyToolWindow.form</code> 添加一个 <code>runButton</code></p>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyToolWindow.java</code> 添加事件监听（注意修改调用方）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java">    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> Project <span style="color:#a6e22e">project</span>;

    <span style="color:#75715e">// 构造函数添加一个 project 属性，同步修改 src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyToolWindowFactory.java
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">MyToolWindow</span>(Project <span style="color:#a6e22e">project</span>, ToolWindow <span style="color:#a6e22e">toolWindow</span>) {
        <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">project</span> <span style="color:#f92672">=</span> project;
        <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>        runButton.<span style="color:#a6e22e">addActionListener</span>(e <span style="color:#f92672">-&gt;</span> {
            var <span style="color:#a6e22e">demoRunService</span> <span style="color:#f92672">=</span> project.<span style="color:#a6e22e">getService</span>(DemoRunService.<span style="color:#a6e22e">class</span>);
            <span style="color:#66d9ef">if</span> (demoRunService <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
                demoRunService.<span style="color:#a6e22e">run</span>();
            }
        });
        <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">JPanel</span> getContent() {
        <span style="color:#75715e">// 只有 Goland 场景才激活该按钮
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (project.<span style="color:#a6e22e">getService</span>(DemoRunService.<span style="color:#a6e22e">class</span>) <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span>) {
            runButton.<span style="color:#a6e22e">setEnabled</span>(<span style="color:#66d9ef">false</span>);
        }
        <span style="color:#66d9ef">return</span> myToolWindowContent;
    }</code></pre></div>
<p>编程启动调试器</p>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/run/DemoRunService.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>.<span style="color:#a6e22e">DemoRunConfiguration</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">configuration</span>.<span style="color:#a6e22e">DemoRunConfigurationType</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">ProgramRunnerUtil</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">RunManager</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">RunnerAndConfigurationSettings</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">execution</span>.<span style="color:#a6e22e">executors</span>.<span style="color:#a6e22e">DefaultRunExecutor</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DemoRunService {
    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">final</span> Project <span style="color:#a6e22e">project</span>;

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DemoRunService</span>(Project <span style="color:#a6e22e">project</span>) {
        <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">project</span> <span style="color:#f92672">=</span> project;
    }

    <span style="color:#75715e">// 程序调起调试器
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> run() {
        ProgramRunnerUtil.<span style="color:#a6e22e">executeConfiguration</span>(getSettings(), DefaultRunExecutor.<span style="color:#a6e22e">getRunExecutorInstance</span>());
    }

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">RunnerAndConfigurationSettings</span> getSettings() {
        var <span style="color:#a6e22e">runManager</span> <span style="color:#f92672">=</span> RunManager.<span style="color:#a6e22e">getInstance</span>(project);
        var <span style="color:#a6e22e">settingName</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Automatic generated&#34;</span>;
        <span style="color:#75715e">// var settings = runManager.findConfigurationByTypeAndName(new DemoRunConfigurationType(), settingName); // 可选的从已有配置中查找
</span><span style="color:#75715e"></span>        var <span style="color:#a6e22e">settings</span> <span style="color:#f92672">=</span> runManager.<span style="color:#a6e22e">createConfiguration</span>(<span style="color:#e6db74">&#34;Automatic generated&#34;</span>, DemoRunConfigurationType.<span style="color:#a6e22e">class</span>);
        <span style="color:#75715e">// runManager.addConfiguration(settings); // 可选的保存下来
</span><span style="color:#75715e"></span>        var <span style="color:#a6e22e">config</span> <span style="color:#f92672">=</span> (DemoRunConfiguration) settings.<span style="color:#a6e22e">getConfiguration</span>();
        config.<span style="color:#a6e22e">setScriptName</span>(<span style="color:#e6db74">&#34;test.sh&#34;</span>);
        <span style="color:#66d9ef">return</span> settings;
    }

}</code></pre></div>
<p>在 <code>src/main/resources/META-INF/demo-golang.xml</code> 中注册（只为 Goland 服务）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">        <span style="color:#f92672">&lt;projectService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.run.DemoRunService&#34;</span> <span style="color:#f92672">/&gt;</span></code></pre></div>
<h2 id="webview-jcef">Webview （JCEF）</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/jcef.html">官方文档</a></p>
</blockquote>

<p>IntelliJ 插件提供了利用 Web 前端技术栈开发插件的的能力，即 JCEF。（类似安卓开发的 Webview，参见 从 <a href="https://zhuanlan.zhihu.com/p/28184028">Blink 内核渲染架构演进看浏览器技术发展</a> ）</p>

<p>JCEF 即 Java 端的 <a href="https://bitbucket.org/chromiumembedded/cef/wiki/Home">CEF</a> 框架（CEF 即 Chromium Embedded Framework Chromium 嵌入框），其提供一种可以将 Chromium 嵌入到 Swing 的能力。</p>

<h3 id="示例代码">示例代码</h3>

<p>在 Toolwindows 加载一个 Webview</p>

<p><code>src/main/java/com/github/rectcircle/learnintellijplatformplugin/toolwindow/MyWebviewFactory.java</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#f92672">package</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">rectcircle</span>.<span style="color:#a6e22e">learnintellijplatformplugin</span>.<span style="color:#a6e22e">toolwindow</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Project</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">wm</span>.<span style="color:#a6e22e">ToolWindow</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">openapi</span>.<span style="color:#a6e22e">wm</span>.<span style="color:#a6e22e">ToolWindowFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">content</span>.<span style="color:#a6e22e">Content</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">content</span>.<span style="color:#a6e22e">ContentFactory</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">intellij</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">jcef</span>.<span style="color:#f92672">*</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">com</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">cef</span>.<span style="color:#a6e22e">JCefAppConfig</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">cef</span>.<span style="color:#a6e22e">browser</span>.<span style="color:#a6e22e">CefBrowser</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">cef</span>.<span style="color:#a6e22e">browser</span>.<span style="color:#a6e22e">CefFrame</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">cef</span>.<span style="color:#a6e22e">handler</span>.<span style="color:#f92672">*</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">org</span>.<span style="color:#a6e22e">jetbrains</span>.<span style="color:#a6e22e">annotations</span>.<span style="color:#a6e22e">NotNull</span>;

<span style="color:#f92672">import</span> <span style="color:#a6e22e">javax</span>.<span style="color:#a6e22e">swing</span>.<span style="color:#f92672">*</span>;
<span style="color:#f92672">import</span> <span style="color:#a6e22e">java</span>.<span style="color:#a6e22e">awt</span>.<span style="color:#f92672">*</span>;

<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> MyWebviewFactory <span style="color:#a6e22e">implements</span> ToolWindowFactory {

    <span style="color:#a6e22e">@Override</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> createToolWindowContent(<span style="color:#a6e22e">@NotNull</span> Project <span style="color:#a6e22e">project</span>, <span style="color:#a6e22e">@NotNull</span> ToolWindow <span style="color:#a6e22e">toolWindow</span>) {
        var <span style="color:#a6e22e">panel</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> JPanel();
        panel.<span style="color:#a6e22e">setLayout</span>(<span style="color:#66d9ef">new</span> BorderLayout());
        ContentFactory <span style="color:#a6e22e">contentFactory</span> <span style="color:#f92672">=</span> ContentFactory.<span style="color:#a6e22e">SERVICE</span>.<span style="color:#a6e22e">getInstance</span>();
        Content <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> contentFactory.<span style="color:#a6e22e">createContent</span>(panel, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">false</span>);
        toolWindow.<span style="color:#a6e22e">getContentManager</span>().<span style="color:#a6e22e">addContent</span>(content);

        <span style="color:#75715e">// API 1 - JBCefApp 判断是否支持
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>JBCefApp.<span style="color:#a6e22e">isSupported</span>()) {
            var <span style="color:#a6e22e">notSupportedLabel</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> JLabel();
            notSupportedLabel.<span style="color:#a6e22e">setText</span>(<span style="color:#e6db74">&#34;Not support webview: see https://plugins.jetbrains.com/docs/intellij/jcef.html#jbcefapp&#34;</span>);
            panel.<span style="color:#a6e22e">add</span>(notSupportedLabel);
            <span style="color:#66d9ef">return</span>;
        }
        <span style="color:#75715e">// API 2 - 对 JBCefApp 进行配置的单例类，需在 JBCefApp.getInstance() 调用前进行配置（如 new JBCefBrowser()）。
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// 不建议进行配置，因为所有插件共享一个
</span><span style="color:#75715e"></span>        System.<span style="color:#a6e22e">out</span>.<span style="color:#a6e22e">println</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">getClass</span>().<span style="color:#a6e22e">getClassLoader</span>().<span style="color:#a6e22e">toString</span>());
        System.<span style="color:#a6e22e">out</span>.<span style="color:#a6e22e">println</span>(JBCefApp.<span style="color:#a6e22e">class</span>.<span style="color:#a6e22e">getClassLoader</span>().<span style="color:#a6e22e">toString</span>());
        JCefAppConfig.<span style="color:#a6e22e">getInstance</span>().<span style="color:#a6e22e">getCefSettings</span>();
        <span style="color:#75715e">// API 3 - JBCefBrowser Jetbrains 对 Cef 的封装，包含 JBCefClient 和 CefBrowser
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// 给定 URL 或者 HTML 创建一个浏览器实例
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// API 3.1 - 指定 URL 从网络上加载
</span><span style="color:#75715e">//        var jbCefBrowser = new JBCefBrowser(&#34;https://rectcircle.cn&#34;);
</span><span style="color:#75715e"></span>        var <span style="color:#a6e22e">jbCefBrowser</span> <span style="color:#f92672">=</span>  <span style="color:#66d9ef">new</span> JBCefBrowser();

        <span style="color:#75715e">// API 3.2 - 指定 HTML 直接加载（打开开发者工具，刷新后就没了。看实现是，读取一次后就删掉了 JBCefFileSchemeHandlerFactory）
</span><span style="color:#75715e"></span>        jbCefBrowser.<span style="color:#a6e22e">loadHTML</span>(
                <span style="color:#e6db74">&#34;&lt;!DOCTYPE html&gt;&lt;html lang=\&#34;en\&#34;&gt;&lt;head&gt;&lt;title&gt;Test&lt;/title&gt;&lt;/head&gt;&lt;body&gt;拼成的HTML，不是从 URL 加载的&lt;/body&gt;&lt;/html&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;https://rectcircle.cn&#34;</span>  <span style="color:#75715e">// 可选的，最终浏览器访问的是 file:///jbcefbrowser/随机数#url=https://rectcircle.cn 走的文件协议，应该还是有跨域问题
</span><span style="color:#75715e"></span>        );
        jbCefBrowser.<span style="color:#a6e22e">getJBCefClient</span>().<span style="color:#a6e22e">addLoadHandler</span>(<span style="color:#66d9ef">new</span> CefLoadHandlerAdapter() { <span style="color:#75715e">// 解决刷新问题的方案（后果是浏览器上会产生无效的 history），更好的做法是不使用 JBCefApp 来创建 JBCefBrowser，而是使用自建的 CefApp 来创建 CefClient 和 CefBrowser
</span><span style="color:#75715e"></span>            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> onLoadError(CefBrowser <span style="color:#a6e22e">browser</span>, CefFrame <span style="color:#a6e22e">frame</span>, ErrorCode <span style="color:#a6e22e">errorCode</span>, String <span style="color:#a6e22e">errorText</span>, String <span style="color:#a6e22e">failedUrl</span>) {
                <span style="color:#66d9ef">if</span> (errorCode <span style="color:#f92672">==</span> ErrorCode.<span style="color:#a6e22e">ERR_FILE_NOT_FOUND</span> <span style="color:#f92672">&amp;&amp;</span> failedUrl.<span style="color:#a6e22e">startsWith</span>(<span style="color:#e6db74">&#34;file:///jbcefbrowser&#34;</span>)) {
                    jbCefBrowser.<span style="color:#a6e22e">loadHTML</span>(
                            <span style="color:#e6db74">&#34;&lt;!DOCTYPE html&gt;&lt;html lang=\&#34;en\&#34;&gt;&lt;head&gt;&lt;title&gt;Test&lt;/title&gt;&lt;/head&gt;&lt;body&gt;拼成的HTML，不是从 URL 加载的 2&lt;/body&gt;&lt;/html&gt;&#34;</span>,
                            <span style="color:#e6db74">&#34;https://rectcircle.cn&#34;</span>  <span style="color:#75715e">// 可选的，最终浏览器访问的是 file:///jbcefbrowser/随机数#url=https://rectcircle.cn 走的文件协议，应该还是有跨域问题
</span><span style="color:#75715e"></span>                    );
                }
            }
        }, jbCefBrowser.<span style="color:#a6e22e">getCefBrowser</span>());
        panel.<span style="color:#a6e22e">add</span>(jbCefBrowser.<span style="color:#a6e22e">getComponent</span>(), BorderLayout.<span style="color:#a6e22e">CENTER</span>);
        <span style="color:#75715e">// API 4 - JBCefClient 可以添加一些事件回调，拦截网络请求等
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// API 5 - CefBrowser 动态执行 JS 代码，获取 Dom 等
</span><span style="color:#75715e"></span>        jbCefBrowser.<span style="color:#a6e22e">getJBCefClient</span>().<span style="color:#a6e22e">addLoadHandler</span>(<span style="color:#66d9ef">new</span> CefLoadHandlerAdapter() {
            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> onLoadEnd(CefBrowser <span style="color:#a6e22e">browser</span>, CefFrame <span style="color:#a6e22e">frame</span>, <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">httpStatusCode</span>) {
                <span style="color:#75715e">// API 5.1 - 动态执行 JS 代码
</span><span style="color:#75715e"></span>                browser.<span style="color:#a6e22e">executeJavaScript</span>(
                        <span style="color:#e6db74">&#34;setInterval(()=&gt;{console.log(\&#34;Java 调用的\&#34;)}, 1000)&#34;</span>
                        , <span style="color:#e6db74">&#34;https://rectcircle.cn/js/main.js&#34;</span> <span style="color:#75715e">// 假装这个代码是从该 URL 中下载的
</span><span style="color:#75715e"></span>                        , 0);
            }
        }, jbCefBrowser.<span style="color:#a6e22e">getCefBrowser</span>());
        <span style="color:#75715e">// API 6 - JBCefJSQuery JS 调用 Java 回调函数
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">JBCefJSQuery</span> myJSQuery <span style="color:#f92672">=</span> JBCefJSQuery.<span style="color:#a6e22e">create</span>((JBCefBrowserBase) jbCefBrowser);
        myJSQuery.<span style="color:#a6e22e">addHandler</span>((args) <span style="color:#f92672">-&gt;</span> {
            System.<span style="color:#a6e22e">out</span>.<span style="color:#a6e22e">println</span>(<span style="color:#e6db74">&#34;JS 调用了 这个函数，参数是：&#34;</span> <span style="color:#f92672">+</span> args);
            <span style="color:#66d9ef">if</span> (<span style="color:#e6db74">&#34;null&#34;</span>.<span style="color:#a6e22e">equals</span>(args)) {
                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> JBCefJSQuery.<span style="color:#a6e22e">Response</span>(<span style="color:#66d9ef">null</span>, 1, <span style="color:#e6db74">&#34;不允许 null&#34;</span>);
            } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (<span style="color:#e6db74">&#34;undefined&#34;</span>.<span style="color:#a6e22e">equals</span>(args)) {
                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> JBCefJSQuery.<span style="color:#a6e22e">Response</span>(<span style="color:#66d9ef">null</span>); <span style="color:#75715e">// 这样 JS 侧，会掉 onFailure
</span><span style="color:#75715e"></span>            }
            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> JBCefJSQuery.<span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#34;Java 的返回值&#34;</span>);
        });
        jbCefBrowser.<span style="color:#a6e22e">getJBCefClient</span>().<span style="color:#a6e22e">addLoadHandler</span>(<span style="color:#66d9ef">new</span> CefLoadHandlerAdapter() {
            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">void</span> onLoadEnd(CefBrowser <span style="color:#a6e22e">browser</span>, CefFrame <span style="color:#a6e22e">frame</span>, <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">httpStatusCode</span>) {
                <span style="color:#75715e">// 将模块注入到浏览器中执行里面
</span><span style="color:#75715e"></span>                <span style="color:#75715e">/*
</span><span style="color:#75715e">                window.JavaPanelBridge = {
</span><span style="color:#75715e">                    callJava: function(arg) {
</span><span style="color:#75715e">                        window.cefQuery_762768232_1({
</span><span style="color:#75715e">                            request: &#39;&#39; + JSON.stringify(arg),
</span><span style="color:#75715e">                            onSuccess: response=&gt;console.log(&#39;callJava 成功&#39;, response),
</span><span style="color:#75715e">                            onFailure: (error_code,error_message)=&gt;console.log(&#39;callJava 失败&#39;, error_code, error_message)
</span><span style="color:#75715e">                        });
</span><span style="color:#75715e">                    }
</span><span style="color:#75715e">                };
</span><span style="color:#75715e">                */</span>
                browser.<span style="color:#a6e22e">executeJavaScript</span>(
                        <span style="color:#e6db74">&#34;window.JavaPanelBridge = {&#34;</span> <span style="color:#f92672">+</span>
                                <span style="color:#e6db74">&#34;callJava : function(arg) {&#34;</span> <span style="color:#f92672">+</span>
                                myJSQuery.<span style="color:#a6e22e">inject</span>(
                                        <span style="color:#e6db74">&#34;JSON.stringify(arg)&#34;</span>,
                                        <span style="color:#e6db74">&#34;response =&gt; console.log(&#39;callJava 成功&#39;, response)&#34;</span>,
                                        <span style="color:#e6db74">&#34;(error_code, error_message) =&gt; console.log(&#39;callJava 失败&#39;, error_code, error_message)&#34;</span>
                                    ) <span style="color:#f92672">+</span>
                                <span style="color:#e6db74">&#34;}&#34;</span> <span style="color:#f92672">+</span>
                            <span style="color:#e6db74">&#34;};&#34;</span> <span style="color:#f92672">+</span>
                            <span style="color:#e6db74">&#34;setInterval(()=&gt;{JavaPanelBridge.callJava(); JavaPanelBridge.callJava(null); JavaPanelBridge.callJava({a:1}); JavaPanelBridge.callJava(\&#34;这是参数\&#34;);}, 5000)&#34;</span>,
                        <span style="color:#e6db74">&#34;https://rectcircle.cn/js/js-bridge.js&#34;</span>, 0);
            }
        }, jbCefBrowser.<span style="color:#a6e22e">getCefBrowser</span>());

    }
}</code></pre></div>
<p>注册 <code>src/main/resources/META-INF/plugin.xml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">        <span style="color:#f92672">&lt;toolWindow</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;Webview&#34;</span>
                    <span style="color:#a6e22e">secondary=</span><span style="color:#e6db74">&#34;true&#34;</span>
                    <span style="color:#a6e22e">icon=</span><span style="color:#e6db74">&#34;AllIcons.General.Modified&#34;</span>
                    <span style="color:#a6e22e">anchor=</span><span style="color:#e6db74">&#34;right&#34;</span>
                    <span style="color:#a6e22e">factoryClass=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.toolwindow.MyWebviewFactory&#34;</span><span style="color:#f92672">/&gt;</span></code></pre></div>
<p>运行后，打开该 webview ，右键可以打开开发者工具</p>

<h3 id="cef-api">Cef API</h3>

<blockquote>
<p><a href="https://github.com/fanfeilong/cefutil/blob/master/doc/CEF%20General%20Usage-zh-cn.md">文章</a></p>
</blockquote>

<ul>
<li>CefApp - 表示一组共享配置的进程组，提供了进程粒度的一些回调函数，可以理解为同样配置的 Chrome 窗口的集合</li>
<li>CefClient - 提供访问 Browser 实例的回调接口。一个 CefClient 实现可以在任意数量的 Browser 进程中共享</li>
<li>CefBrowser 和 CefFrame - 可以给浏览器发送命令和获取浏览器的各种信息，可以理解为 Chrome 浏览器的一个标签页和其顶层 frame（如果包含 iframe 则会有多个 CefFrame）</li>
</ul>

<h3 id="jb-封装的-api">JB 封装的 API</h3>

<h4 id="jbcefapp">JBCefApp</h4>

<p>对 org.cef.CefApp 进行封装的单例类，包含几个常用的静态方法</p>

<ul>
<li><code>isSupported</code> 当前环境是否支持</li>
<li><code>getInstance</code> 获取 <code>JBCefApp</code> （如果不存在将创建） 的实例，配置内容 <code>JCefAppConfig</code> 来确认</li>
</ul>

<h4 id="jcefappconfig">JCefAppConfig</h4>

<p>对 JBCefApp 进行配置的单例类，需在 JBCefApp.getInstance() 调用前进行配置（如 new JBCefBrowser()）。</p>

<p>不建议进行配置，因为所有插件共享一个，更多参见上文 《类加载》</p>

<h4 id="jbcefbrowser">JBCefBrowser</h4>

<p>对 CefBrowserClient 和 CefBrowser 的封装。可以返回一个 swing 组件，直接用在各种 UI 上（如 ToolWindow）。</p>

<p>创建方式有几种</p>

<ul>
<li>不指定 CefBrowserClient 和 CefBrowser ，将通过 JBCefApp 来创建 <code>public JBCefBrowser()</code> 和 <code>public JBCefBrowser(@NotNull String url)</code></li>
<li>复用已经创建的 JBCefClient 和 CefBrowser <code>public JBCefBrowser(@NotNull CefBrowser cefBrowser, @NotNull JBCefClient client)</code> 和 <code>public JBCefBrowser(@NotNull JBCefClient client, @Nullable String url)</code></li>
</ul>

<h4 id="jbcefclient">JBCefClient</h4>

<p>添加或删除各种事件处理函数。比如，页面加载、生命周期事件等等</p>

<h4 id="cefbrowser">CefBrowser</h4>

<p>Cef 原生浏览器对象，对应一个浏览器页面。可以获取 URL、dom，<strong>执行 JS</strong> 等，利用该特性可以实现 js-bridge 发送事件给浏览器的能力</p>

<h4 id="jbcefjsquery">JBCefJSQuery</h4>

<p>创建一个 JS 回调处理函数，可以做到在 JS 调用 Java 函数的特点，利用该特性可以实现 js-bridge 调用原生 API 的能力</p>
]]></description></item><item><title>Intellij 平台插件开发概览</title><link>https://www.rectcircle.cn/posts/intellij-platform-plugin-dev-overview/</link><pubDate>Sat, 06 Nov 2021 10:55:14 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/intellij-platform-plugin-dev-overview/</guid><description type="html"><![CDATA[

<h2 id="相关资源">相关资源</h2>

<ul>
<li><a href="https://plugins.jetbrains.com/docs/intellij/basics.html">官方文档</a></li>
<li><a href="https://github.com/JetBrains/intellij-sdk-code-samples">官方 Demo</a></li>
<li><a href="https://plugins.jetbrains.com/intellij-platform-explorer">插件源码搜索浏览</a></li>
<li><a href="https://github.com/rectcircle/learn-intellij-platform-plugin">本文 Demo 库</a></li>
</ul>

<h2 id="插件能力">插件能力</h2>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/types-of-plugins.html">原文</a></p>
</blockquote>

<p>最常见的插件能力可以分为如下几类：</p>

<ul>
<li>UI Themes - 定制图标、颜色、边框、编辑器方案、背景图片（<a href="https://plugins.jetbrains.com/search?headline=164-theme&amp;tags=Theme">主题市场</a>）</li>
<li>自定义语言支持 - 文件类型识别、词法分析、语法高亮、格式化、代码洞察和代码完成、检查和快速修复、Intention actions（小灯泡）（<a href="https://plugins.jetbrains.com/docs/intellij/custom-language-support-tutorial.html">官方手册</a>）</li>
<li>框架集成 - 特定的代码洞察、直接访问特定于框架的功能（<a href="https://github.com/JetBrains/intellij-plugins/tree/master/struts2">Struct2 例子</a> | <a href="https://plugins.jetbrains.com/search?orderBy=update%20date&amp;shouldHaveSource=true&amp;tags=Framework%20integration">插件市场</a>）</li>
<li>工具集成 - 附加行为的实现、UI 组件、访问外部资源（<a href="https://plugins.jetbrains.com/plugin/7272?pr=idea">gerrit 例子</a>）</li>
<li>添加自定义 UI 页面 - 不仅仅可以修改标准的 UI 页面，还支持添加自定义的 UI 界面</li>
</ul>

<h2 id="项目初步体验">项目初步体验</h2>

<blockquote>
<p><a href="https://github.com/rectcircle/learn-intellij-platform-plugin">Demo 库</a></p>
</blockquote>

<h3 id="项目创建">项目创建</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/github-template.html">官方文档</a> | <a href="https://github.com/JetBrains/intellij-platform-plugin-template">github template</a></p>
</blockquote>

<p>根据模板创建 github 仓库</p>

<p>打开 <a href="https://github.com/JetBrains/intellij-platform-plugin-template">github template</a>，点击 <code>Use this template</code></p>

<p>clone 到本地</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://github.com/rectcircle/learn-intellij-platform-plugin.git</code></pre></div>
<p>并使用 Intellej IDEA 打开 Clone 的项目（确保已安装 Java 11）</p>

<h3 id="gradle-项目配置">Gradle 项目配置</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/gradle-prerequisites.html">官方文档</a> | <a href="https://www.jianshu.com/p/4bcdf07d4579">Gradle 博客 1</a> | <a href="https://developer.aliyun.com/article/25589">Gradle 多模块 2</a> | <a href="https://www.jianshu.com/p/fabfb23274e6">Gradle 多模块 3</a></p>
</blockquote>

<p><code>settings.gradle.kts</code> Kotlin 脚本描述的 gradle Module 配置文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kts" data-lang="kts">// 根项目名，在 gradle 中，默认对应 maven 中的 ArtifactId 概念
rootProject.name = &#34;learn-intellij-platform-plugin&#34;</code></pre></div>
<p><code>gradle.properties</code> 项目属性配置文件，如插件唯一标识，需要构建的版本，平台版本，依赖的插件，Java 版本，gradle 版本等</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-properties" data-lang="properties"># 插件唯一标识
pluginGroup = cn.rectcircle.learnintellijplatformplugin
pluginName = learn-intellij-platform-plugin
pluginVersion = 0.0.1

# 其他略</code></pre></div>
<p><code>build.gradle.kts</code> Kotlin 脚本描述的 gradle Build 配置文件，会读取 <code>gradle.properties</code>，进行配置</p>

<h3 id="项目结构">项目结构</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── .github/                Github 配置
├── .run/                   预定义的 Run/Debug 配置
├── gradle
│   └── wrapper/            Gradle Wrapper
├── build/                  编译输出目录
├── src                     插件源代码
│   └── main
│       ├── kotlin/         Kotlin 源码
│       ├── java/           Java 源码（需手动新建）
│       └── resources/      Resources - plugin.xml, 图标, 消息
│   └── test
│       ├── kotlin/         Kotlin 测试
│       ├── java/           Java 测试
│       └── testData/       测试数据
├── .gitignore              Git ignoring
├── build.gradle.kts        Gradle 构建配置
├── CHANGELOG.md            Full change history
├── gradle.properties       Gradle 配置属性
├── gradlew                 *nix Gradle Wrapper binary
├── gradlew.bat             Windows Gradle Wrapper binary
├── LICENSE                 License, MIT by default
├── qodana.yml              Qodana configuration file
├── README.md               README
└── settings.gradle.kts     Gradle 项目配置</pre></div>
<h3 id="插件配置文件">插件配置文件</h3>

<p><code>src/main/resources/META-INF/plugin.xml</code> 插件贡献点/依赖注入配置文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;!-- 插件配置文件. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html --&gt;</span>
<span style="color:#f92672">&lt;idea-plugin&gt;</span>
    <span style="color:#75715e">&lt;!-- 插件唯一标示符 --&gt;</span>
    <span style="color:#f92672">&lt;id&gt;</span>cn.rectcircle.learnintellijplatformplugin<span style="color:#f92672">&lt;/id&gt;</span>
    <span style="color:#75715e">&lt;!-- 插件版本 --&gt;</span>
    <span style="color:#f92672">&lt;version&gt;</span>0.0.1<span style="color:#f92672">&lt;/version&gt;</span>

    <span style="color:#75715e">&lt;!-- 插件展示名 --&gt;</span>
    <span style="color:#f92672">&lt;name&gt;</span>Demo 插件<span style="color:#f92672">&lt;/name&gt;</span>
    <span style="color:#75715e">&lt;!-- 插件描述 --&gt;</span>
    <span style="color:#f92672">&lt;description&gt;</span><span style="color:#75715e">&lt;![CDATA[
</span><span style="color:#75715e">        &lt;p&gt;这是插件描述这是插件描述这是插件描述这是插件描述这是插件描述这是插件描述&lt;/p&gt;
</span><span style="color:#75715e">        &lt;p&gt;这是插件描述这是插件描述这是插件描述这是插件描述这是插件描述这是插件描述&lt;/p&gt;
</span><span style="color:#75715e">    ]]&gt;</span><span style="color:#f92672">&lt;/description&gt;</span>
    <span style="color:#75715e">&lt;!-- 供应商 / 作者 --&gt;</span>
    <span style="color:#f92672">&lt;vendor&gt;</span>rectcircle<span style="color:#f92672">&lt;/vendor&gt;</span>

    <span style="color:#75715e">&lt;!-- 依赖的内置插件. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html --&gt;</span>
    <span style="color:#f92672">&lt;depends&gt;</span>com.intellij.modules.platform<span style="color:#f92672">&lt;/depends&gt;</span>

    <span style="color:#75715e">&lt;!-- 插件扩展声明. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html --&gt;</span>
<span style="color:#75715e">&lt;!--    &lt;extensionPoints&gt;&lt;/extensionPoints&gt;--&gt;</span>

    <span style="color:#75715e">&lt;!-- 自定义扩展声明. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html#declaring-extensions --&gt;</span>
    <span style="color:#75715e">&lt;!-- 配置贡献点 --&gt;</span>
    <span style="color:#f92672">&lt;extensions</span> <span style="color:#a6e22e">defaultExtensionNs=</span><span style="color:#e6db74">&#34;com.intellij&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#75715e">&lt;!-- https://plugins.jetbrains.com/docs/intellij/plugin-services.html#declaring-a-service --&gt;</span>
        <span style="color:#75715e">&lt;!-- 注册一个应用级别的 service （全局实例化一个）--&gt;</span>
        <span style="color:#f92672">&lt;applicationService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.services.MyApplicationService&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#75715e">&lt;!-- 注册一个项目级别的 service（每个窗口实例化一个） --&gt;</span>
        <span style="color:#f92672">&lt;projectService</span> <span style="color:#a6e22e">serviceImplementation=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.services.MyProjectService&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/extensions&gt;</span>

    <span style="color:#75715e">&lt;!-- 注册应用级别监听器. see: https://plugins.jetbrains.com/docs/intellij/plugin-listeners.html#defining-application-level-listeners --&gt;</span>
    <span style="color:#f92672">&lt;applicationListeners&gt;</span>
        <span style="color:#f92672">&lt;listener</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;com.github.rectcircle.learnintellijplatformplugin.listeners.MyProjectManagerListener&#34;</span>
                  <span style="color:#a6e22e">topic=</span><span style="color:#e6db74">&#34;com.intellij.openapi.project.ProjectManagerListener&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;/applicationListeners&gt;</span>
<span style="color:#f92672">&lt;/idea-plugin&gt;</span></code></pre></div>
<h3 id="源码概览">源码概览</h3>

<p>整体来看，Jetbrains 插件是一个 Gradle 驱动的 Java  / Kotlin 项目，UI 方面主要使用了 Swing 技术。</p>

<p><code>src/main/kotlin/com/github/rectcircle/learnintellijplatformplugin</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">.
├── MyBundle.kt                         提供对资源消息的访问的捆绑类（用于外部字符串）
├── listeners
│   └── MyProjectManagerListener.kt     项目粒度监听器
└── services
    ├── MyApplicationService.kt         应用粒度 Service
    └── MyProjectService.kt             项目粒度 Service</pre></div>
<h3 id="测试">测试</h3>

<p>参见：<a href="https://github.com/JetBrains/intellij-platform-plugin-template#testing">此处</a></p>

<h3 id="调试运行插件">调试运行插件</h3>

<p>该模板已经默认配置了调试配置，选择 <code>Run Plugin</code> 即可。</p>

<p>此时，调试 IDE 的数据目录将存储在 <code>$buildDir/idea-sandbox/config</code> （<code>build/idea-sandbox/config</code>）下</p>

<h2 id="调试运行最佳实践">调试运行最佳实践</h2>

<p>如果插件需要运行在不同的平台（IDEA、GoLand）之中，且需要在不同的平台进行调试。设想如下场景</p>

<p>假设使用 IDEA 进行 插件开发，插件最终运行在 IDEA、Goland、PyCharm 之中。且开发者都是在 Mac 设备上开发，IDEA 通过 Toolbox 安装的。</p>

<p>此时可以按照如下方式进行配置：</p>

<p>编写 <code>build.gradle.kts</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kts" data-lang="kts">// 通过 macos 的 defaults 命令获取 app 的路径
fun parseIdeDir(appName: String): String {
    val byteOut = ByteArrayOutputStream()
    val cmd = &#34;defaults read &#39;&#34; + System.getProperties().getProperty(&#34;user.home&#34;)+ &#34;/Applications/JetBrains Toolbox/&#34;+ appName +&#34;.app/Contents/Info&#39; JetBrainsToolboxApp&#34;
    project.exec {
        commandLine(&#34;bash&#34;, &#34;-c&#34;, cmd)
        standardOutput = byteOut
    }
    val ideDir = String(byteOut.toByteArray()).trim() + &#34;/Contents&#34;
    println(&#34;IdeDir is $ideDir&#34;)
    return ideDir
}

tasks {
    runIde {
        // 根据环境变量来启动不同的平台
        val runOn =System.getenv()[&#34;RUN_ON&#34;]
        if (runOn != null &amp;&amp; &#34;&#34; != runOn) {
            // https://github.com/JetBrains/gradle-intellij-plugin/issues/772
            systemProperty(&#34;idea.platform.prefix&#34;, runOn)
            // https://github.com/JetBrains/gradle-intellij-plugin/blob/master/README.md#running-dsl
            ideDir.set(file(parseIdeDir(runOn)))
        }
    }
}</code></pre></div>
<p>添加 GoLand 的配置 <code>Run -&gt; Edit Configuration</code> 选择 <code>Run Plugin</code> Copy 一份。修改表单</p>

<ul>
<li>Name 为 Run Plugin On Goland</li>
<li>添加环境变量 <code>RUN_ON=GoLand</code></li>
<li>勾选 <code>Store as project file</code></li>
</ul>

<h2 id="插件包目录结构分析">插件包目录结构分析</h2>

<h3 id="构建插件">构建插件</h3>

<blockquote>
<p><a href="https://plugins.jetbrains.com/docs/intellij/deployment.html#building-distribution">官方文档</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">./gradlew buildPlugin</pre></div>
<p>产物位于 <code>build/distributions</code> 目录下，为 zip 压缩包。里面包含 <code>lib</code> 目录，内部为一系列 jar 包</p>

<h3 id="手动安装">手动安装</h3>

<p><code>command + ,</code>，搜索 plugins，点击插件管理页的齿轮，点击从磁盘安装，选择上一步的 zip 包</p>

<h3 id="安装目录">安装目录</h3>

<p><code>~/Library/ApplicationSupport/JetBrains/IntelliJIdea2021.2/plugins/</code>，即直接将 zip 解压到此目录下</p>

<h2 id="将外部资源打包到插件并访问">将外部资源打包到插件并访问</h2>

<blockquote>
<p>参考：<a href="https://intellij-support.jetbrains.com/hc/en-us/community/posts/360010217720-Looking-for-a-way-to-configure-the-gradle-to-add-bin-my-binary-exe-inside-the-plugin-zip-file">问答</a></p>
</blockquote>

<p>设想一个场景，插件的功能依托于外部 cli 调用。因此，需要做到两件事</p>

<ul>
<li>将这个 cli 打包到插件中</li>
<li>插件代码可以获取到安装后的插件路径</li>
</ul>

<p>因此第一步，修改 <code>build.gradle.kts</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kts" data-lang="kts">    // 在 prepareSandbox 最后一步进行资源拷贝
    prepareSandbox {
        doLast{
            copy {
                from(file(&#34;$projectDir/README.md&#34;))
                into(file(&#34;$buildDir/idea-sandbox/plugins/${rootProject.name}/bin&#34;))
            }
        }
    }</code></pre></div>
<p>第二步，在代码里获取到插件所在路径 <code>src/main/kotlin/com/github/rectcircle/learnintellijplatformplugin/services/MyProjectService.kt</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kt" data-lang="kt">    init {
        <span style="color:#75715e">// 获取到插件路径，即可读取到外部资源路径
</span><span style="color:#75715e"></span>        println(PluginManagerCore.getPlugin(PluginManager.getPluginByClassName(<span style="color:#66d9ef">this</span>.javaClass.name))<span style="color:#f92672">?.</span>pluginPath)
        println(MyBundle.message(<span style="color:#e6db74">&#34;projectService&#34;</span>, project.name))
    }</code></pre></div>
<h2 id="发布">发布</h2>

<h3 id="发布到官方市场">发布到官方市场</h3>

<p>参考：<a href="https://plugins.jetbrains.com/docs/intellij/deployment.html">官方文档</a></p>

<h3 id="发布私有插件市场">发布私有插件市场</h3>

<p>参考：<a href="https://plugins.jetbrains.com/docs/intellij/update-plugins-format.html">官方文档</a></p>
]]></description></item><item><title>digpro 设计</title><link>https://www.rectcircle.cn/posts/digpro/</link><pubDate>Sun, 17 Oct 2021 18:53:24 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/digpro/</guid><description type="html"><![CDATA[

<h2 id="库信息">库信息</h2>

<ul>
<li><a href="https://github.com/rectcircle/digpro/">github</a></li>
<li><a href="https://github.com/rectcircle/digpro/blob/master/README_zh-CN.md">中文文档</a></li>
<li><a href="https://pkg.go.dev/github.com/rectcircle/digpro">GoDoc</a></li>
</ul>

<h2 id="缘由">缘由</h2>

<p>在 <a href="/posts/go-dependency-injection/">Go 依赖注入</a> 调研过程中，发现主流的 go 依赖注入库，功能均有所缺失。</p>

<p>经过调研发现，<a href="fx-github">uber-go/fx</a> 也是基于 <a href="https://github.com/uber-go/dig">uber-go/dig</a>，通过仔细阅读器文档阅读和源码。可以看出 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 是一个比较优质的开源库，且有较好的扩展性。</p>

<p>因此，决定对 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 项目进行的封装，这就有了 <a href="https://github.com/rectcircle/digpro">digpro</a> 项目</p>

<h2 id="渐进使用">渐进使用</h2>

<p><a href="https://github.com/rectcircle/digpro">digpro</a> 基于 <a href="https://github.com/uber-go/dig">uber-go/dig</a>。因此希望用户可以直接在已经使用 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 的项目零成本的使用 <a href="https://github.com/rectcircle/digpro">digpro</a> 提供的能力。</p>

<p>这就要求，<a href="https://github.com/rectcircle/digpro">digpro</a> 仅仅把 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 作为一个依赖库，而 fork 魔改</p>

<p>但是 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 作为一个基础库，很多高级的用法没法进行兼容，因此如果用户想更好的使用 <a href="https://github.com/rectcircle/digpro">digpro</a>，需要使用 <a href="https://github.com/rectcircle/digpro">digpro</a> 导出的类型。</p>

<p>另外 <a href="https://github.com/rectcircle/digpro">digpro</a> 导出的类型，需嵌入 dig 的对象，这样就可以继承 dig 的全部能力了。</p>

<p>基于以上分析， <a href="https://github.com/rectcircle/digpro">digpro</a> 在处理和 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 的关系上做了如下决定</p>

<ul>
<li><a href="https://github.com/uber-go/dig">uber-go/dig</a> 作为一个依赖库，而 fork 魔改</li>
<li>导出两套 API

<ul>
<li>Lower Level API 可以直接用于 <a href="https://github.com/uber-go/dig">uber-go/dig</a>，可获得 <a href="https://github.com/rectcircle/digpro">digpro</a> 部分能力</li>
<li>High Level API 是对 <a href="https://github.com/uber-go/dig">uber-go/dig</a> 的一个 wrap 类型，可获得 <a href="https://github.com/rectcircle/digpro">digpro</a> 全部能力</li>
</ul></li>
</ul>

<h2 id="dig-api-分析">dig API 分析</h2>

<blockquote>
<p>v1.13.0</p>
</blockquote>

<h3 id="new">New</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Option</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Container</span></code></pre></div>
<p>创建一个 dig.Container 结构体，dig 依赖注入的功能基于该能力实现。</p>

<p>opts 有如下几种</p>

<ul>
<li><code>dig.DeferAcyclicVerification()</code> 配置延后循环依赖检查，默认在执行 <code>Provide</code> 就进行检查</li>
<li><code>dig.DryRun(dry bool)</code> 默认为 false。当为 true 时，在调用 <code>Invoke</code> 时，将不会报缺少依赖错误，而是创建一个零值直接调用</li>
</ul>

<h3 id="container-provide">Container.Provide</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#f92672">*</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Container</span>).<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">constructor</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span></code></pre></div>
<p>向容器里面注册一个构造函数 constructor。constructor 是一个函数类型，且其返回值必须包含一个非 error 的返回值。</p>

<p>调用该函数，只是注册该 constructor，并解析出其输入输出，并不会实际执行构造函数。</p>

<p>opts 有如下几种</p>

<ul>
<li><code>dig.Name(name string)</code> 给 constructor 返回值添加一个名字，在 Provide 多个返回相同类型的 constructor 很有用。</li>
<li><code>dig.Group(group string)</code> 将 constructor 返回值构造一个切片。注意，该选项不能和 <code>Name</code> 和 <code>As</code> 同时使用。</li>
<li><code>dig.FillProvideInfo(info *ProvideInfo)</code> 将 constructor 的输入输出信息写入 info 中。</li>
<li><code>dig.As(i ...interface{})</code> 将 constructor 转换为 i 类型，i 必须为一个接口指针类型如 <code>new(io.Reader)</code></li>
<li><code>dig.LocationForPC(pc uintptr)</code> 添加调用信息，如果 constructor 是通过反射构造出来的函数，可以使用该配置优化错误输出</li>
</ul>

<h3 id="container-invoke">Container.Invoke</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#f92672">*</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Container</span>).<span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">function</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">InvokeOption</span>) <span style="color:#66d9ef">error</span></code></pre></div>
<p>分析 function 的依赖，并构造出这些依赖，然后执行 function 函数。</p>

<p>调用该函数时，如果会递归的一层一层的调用 Provide 注册的 constructor，如果 constructor 已经没调用过了，将直接使用环境。</p>

<p>该函数目前还没 opts</p>

<h3 id="dig-in-和-dig-out-类型">dig.In 和 dig.Out 类型</h3>

<ul>
<li><code>dig.In</code> 参见 <a href="https://pkg.go.dev/go.uber.org/dig@v1.13.0#hdr-Parameter_Objects">参数对象</a></li>
<li><code>dig.Out</code> 参见 <a href="https://pkg.go.dev/go.uber.org/dig@v1.13.0#hdr-Result_Objects">结果对象</a></li>
</ul>

<h2 id="功能设计">功能设计</h2>

<table>
<thead>
<tr>
<th>特性</th>
<th>状态</th>
<th>Lower Level API</th>
<th>High Level API</th>
</tr>
</thead>

<tbody>
<tr>
<td>值 Provider</td>
<td>发布</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>属性依赖注入</td>
<td>发布</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>从容器里提取对象</td>
<td>发布</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>Override 已存在的 Provider</td>
<td>发布</td>
<td>❌</td>
<td>✅</td>
</tr>

<tr>
<td>循环引用</td>
<td>TODO</td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody>
</table>

<h3 id="值-provider">值 Provider</h3>

<p>如果想直接注册一个创建好的对象，使用 dig，需要如下的写法。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">string</span> {<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;abc&#34;</span>})</code></pre></div>
<p>可以看出，这种写法相对比较繁琐。因此，如果有如下的 API 会简化不少</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#e6db74">&#34;abc&#34;</span>)</code></pre></div>
<p>这样，其声明为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>).<span style="color:#a6e22e">Supply</span>(<span style="color:#a6e22e">value</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span></code></pre></div>
<p>利用 <code>dig.Provide</code> 方法 和反射的 <code>reflect.ValueOf</code>、 <code>reflect.FuncOf</code>、<code>reflect.MakeFunc</code> 即可轻松实现该特性，伪代码为（忽略一些细节）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Supply</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">value</span> <span style="color:#a6e22e">T</span>) <span style="color:#66d9ef">func</span>() <span style="color:#a6e22e">T</span> {
    <span style="color:#75715e">// 假设支持泛型的伪代码（可以使用反射实现类似效果）
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>() <span style="color:#a6e22e">T</span> {<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">T</span>}
}
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Supply</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">value</span> <span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">Supply</span>(<span style="color:#a6e22e">value</span>), <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}</code></pre></div>
<h3 id="属性依赖注入">属性依赖注入</h3>

<p>如果有一个结构体</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo</span> <span style="color:#66d9ef">struct</span> {
    <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`name:&#34;a&#34;`</span>
    <span style="color:#a6e22e">B</span> <span style="color:#66d9ef">int</span>
    <span style="color:#a6e22e">c</span> <span style="color:#66d9ef">bool</span> <span style="color:#e6db74">`digpro:&#34;ignore&#34;`</span>
}</code></pre></div>
<p>用户希望可以只提供该结构体的类型信息或者一个模板，依赖注入器即可自动根据该类型创建该对象，并能将根据其字段类型和 tag，向依赖注入容器中查找合适的对象，并设置到该对象的字段中。用类似于</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;a&#34;</span>))
<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#ae81ff">1</span>)
<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">Foo</span>{<span style="color:#a6e22e">c</span>: <span style="color:#66d9ef">true</span>}) 
<span style="color:#f92672">//</span> <span style="color:#a6e22e">这样我们就能重容器中拿到</span> <span style="color:#a6e22e">Foo</span> {<span style="color:#a6e22e">A</span>: <span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#a6e22e">B</span>: <span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">c</span>: <span style="color:#66d9ef">true</span>} <span style="color:#a6e22e">对象</span></code></pre></div>
<p>这样其声明为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">structOrStructPtr</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span></code></pre></div>
<p>利用 <code>dig.Provide</code>、<code>dig.In</code> 类型，方法和反射的 <code>reflect.ValueOf</code>、 <code>reflect.FuncOf</code>、<code>reflect.MakeFunc</code>、<code>reflect.MakeFunc</code>、<code>reflect.StructField</code>、<code>reflect.StructOf</code> 即可轻松实现该特性，伪代码为（忽略一些细节）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Struct</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">structOrStructPtr</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span> {
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">in</span> <span style="color:#66d9ef">struct</span> {
        <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span>
        <span style="color:#75715e">// structOrStructPtr 的所有字段不包含 `digpro:&#34;ignore&#34;` 标志的字段，未导出的字段转化为导出对象
</span><span style="color:#75715e"></span>    }) <span style="color:#a6e22e">T</span> {
        <span style="color:#75715e">// 遍历 in 的所有字段，并赋值给给 structOrStructPtr 相应的字段
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span>  <span style="color:#a6e22e">T</span>
    }
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Struct</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">structOrStructPtr</span> <span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span> {
    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">value</span>), <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}</code></pre></div>
<h3 id="从容器里提取对象">从容器里提取对象</h3>

<p>仍以 <a href="#属性依赖注入">属性依赖注入</a> 为例，假设用户想从容器中提取 Foo 对象出来，大概用法大致如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">foo</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">Foo</span>{})
<span style="color:#f92672">//</span> <span style="color:#a6e22e">此时</span> <span style="color:#a6e22e">foo</span>.(<span style="color:#a6e22e">Foo</span>) <span style="color:#a6e22e">为</span> <span style="color:#a6e22e">Foo</span> {<span style="color:#a6e22e">A</span>: <span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#a6e22e">B</span>: <span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">c</span>: <span style="color:#66d9ef">true</span>}</code></pre></div>
<p>这样其声明为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">typ</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ExtractOption</span>) (<span style="color:#66d9ef">interface</span>{}, <span style="color:#66d9ef">error</span>) {
}</code></pre></div>
<p>opts 和 Provide 类似，可以配置 Name、Group 字段来提取指定的对象</p>

<p>利用 <code>dig.Invoke</code>、<code>dig.In</code>  类型，方法和反射的 <code>reflect.ValueOf</code>、 <code>reflect.FuncOf</code>、<code>reflect.MakeFunc</code> 即可轻松实现该特性，伪代码为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">MakeExtractFunc</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ExtractOption</span>) <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">T</span>) {
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">in</span> <span style="color:#66d9ef">struct</span>{
        <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span>
        <span style="color:#a6e22e">Value</span> <span style="color:#a6e22e">T</span> <span style="color:#e6db74">`name:&#34;xxx&#34;`</span> <span style="color:#75715e">// 或者 `group:&#34;xxx&#34;`
</span><span style="color:#75715e"></span>    }) {
        <span style="color:#f92672">*</span><span style="color:#a6e22e">t</span> = <span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">Value</span> 
    }
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Extract</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">typ</span> <span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ExtractOption</span>) (<span style="color:#a6e22e">T</span>, <span style="color:#66d9ef">error</span>) {
    <span style="color:#a6e22e">t</span> <span style="color:#f92672">:=</span> new(<span style="color:#a6e22e">T</span>)
    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}
<span style="color:#75715e">// 如果底层类型是接口类型，函数声明应该为
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Extract</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">typ</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ExtractOption</span>) (<span style="color:#a6e22e">T</span>, <span style="color:#66d9ef">error</span>) {
    <span style="color:#a6e22e">t</span> <span style="color:#f92672">:=</span> new(<span style="color:#a6e22e">T</span>)
    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}</code></pre></div>
<h3 id="override-已存在的-provider">Override 已存在的 Provider</h3>

<p>针对 Provider 类型的 API</p>

<ul>
<li><code>Provide</code></li>
<li><code>Supply</code></li>
<li><code>Struct</code></li>
</ul>

<p>希望可以覆盖已经注册的一个 constructor，用户可能按照如下方法使用</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#ae81ff">1</span>)
<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#ae81ff">2</span>, <span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">Override</span>()) <span style="color:#75715e">// 不报错
</span><span style="color:#75715e"></span><span style="color:#f92672">//</span> <span style="color:#a6e22e">此时提取</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">类型将获取</span> <span style="color:#ae81ff">2</span></code></pre></div>
<p>伪代码如下（忽略一些细节）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ContainerWrapper</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Container</span>
	<span style="color:#a6e22e">provideInfos</span> []<span style="color:#a6e22e">internal</span>.<span style="color:#a6e22e">ProvideInfosWrapper</span>
}

<span style="color:#75715e">// 所有 Provider 类型的 API 最终均通过 `Provide` 函数实现
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Supply</span>(<span style="color:#f92672">...</span>) {
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#f92672">...</span>)
}
<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Struct</span>(<span style="color:#f92672">...</span>) {
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#f92672">...</span>)
}

<span style="color:#75715e">// 代理对 Provide 的调用
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">constructor</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span> {
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">hasOverrideOpt</span> {
        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Container</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#f92672">...</span>)
    }

    <span style="color:#75715e">// 获取该构造函数的 dig.ProvideInfo 信息
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">info</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideInfo</span>{}
    <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>().<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">constructor</span>, <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">FillProvideInfo</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">info</span>))
    <span style="color:#75715e">// 遍历 c.provideInfos 检查是否已经注册过了
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#75715e">// 如果已经注册过了，先通过反射删除掉 c.Container 中的数据，然后
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Container</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#f92672">...</span>)
    <span style="color:#75715e">// 否则没有找到
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">error</span>
}</code></pre></div>
<ul>
<li>在 <code>digpro.Provide</code> 将代理对 <code>dig.Provide</code> 的调用，做如下事情

<ul>
<li>将 <code>digpro.Provide(...)</code> 的调用转换为 <code>digpro.Provide(..., )</code></li>
</ul></li>
</ul>

<h3 id="循环引用">循环引用</h3>

<p>对于 <code>digpro.Struct(...)</code> API，如果参数是一个指针，且字段也是一个指针或者接口，则允许出现循环引用的情况。</p>

<p>假设存在如下两个结构体</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">D1</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">D2</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">D2</span>
}
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">D2</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">D1</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">D1</span>
}</code></pre></div>
<p>希望用户按照如下的写法也不会报错</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Struct</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">D1</span>{})
<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Struct</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">D2</span>{})
<span style="color:#f92672">//</span> <span style="color:#a6e22e">提取</span> <span style="color:#a6e22e">D2</span> <span style="color:#a6e22e">时</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">或者调用</span> <span style="color:#a6e22e">Invoke</span> <span style="color:#a6e22e">是可以拿到正确的结果</span></code></pre></div>
<p>为了实现这个需求，需要对多个函数进行改造</p>

<h4 id="containerwrapper-变化">ContainerWrapper 变化</h4>

<p>添加一个 Provider 结果类型</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ContainerWrapper</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#75715e">// 添加一个记录需要注入属性的字段的 map
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">propertyInjects</span> <span style="color:#66d9ef">map</span>[<span style="color:#a6e22e">ProvideOutput</span>]<span style="color:#a6e22e">PropertyInfo</span>
}</code></pre></div>
<h4 id="struct-变化">Struct 变化</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Struct</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">structOrStructPtr</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span> {
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">in</span> <span style="color:#66d9ef">struct</span> {
        <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span>
        <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// T 如果 T 是指针，那么其的指针/接口类型字段将被忽略（如果存在忽略的字段，则说明需要属性依赖注入）
</span><span style="color:#75715e"></span>    }) <span style="color:#a6e22e">T</span> {
        <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span>  <span style="color:#a6e22e">T</span>
    }
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Struct</span>[<span style="color:#a6e22e">T</span>](<span style="color:#a6e22e">structOrStructPtr</span> <span style="color:#a6e22e">T</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">ProvideOption</span>) <span style="color:#66d9ef">error</span> {
    <span style="color:#75715e">// 如果需要需要属性依赖注入，则记录到 c.propertyInjects 中
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">value</span>), <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}</code></pre></div>
<h4 id="extract-使用代理-invoke">Extract 使用代理 Invoke</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">digpro</span>.<span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">typ</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ExtractOption</span>) (<span style="color:#66d9ef">interface</span>{}, <span style="color:#66d9ef">error</span>) {
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>    <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#f92672">...</span>)
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<h4 id="代理-invoke">代理 Invoke</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">proxyInvokeFunction</span>[<span style="color:#a6e22e">A</span>](<span style="color:#a6e22e">function</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">A</span>)) <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">A</span>) {
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">a</span> <span style="color:#a6e22e">A</span>) {
        <span style="color:#75715e">// 注意 in 类型需要遍历内部
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">在</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">propertyInjects</span> <span style="color:#a6e22e">中</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">没有执行过属性注入</span> {
            <span style="color:#a6e22e">将</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">标记为已经执行过属性注入</span>
            <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">的每个依赖项</span> <span style="color:#a6e22e">dep</span> {
                <span style="color:#a6e22e">dep</span> = <span style="color:#a6e22e">执行</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">dep</span>) 
                <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">dep</span> = <span style="color:#a6e22e">dep</span>
            }
            <span style="color:#a6e22e">如果存在错误</span>
            <span style="color:#a6e22e">将</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">恢复为未执行过属性注入</span>
        }
        <span style="color:#a6e22e">function</span>(<span style="color:#a6e22e">a</span>)
    }
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ContainerWrapper</span>) <span style="color:#a6e22e">Invoke</span>[<span style="color:#a6e22e">A</span>](<span style="color:#a6e22e">function</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">A</span>), <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">InvokeOption</span>) <span style="color:#66d9ef">error</span> {
    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">proxyInvokeFunction</span>(<span style="color:#a6e22e">function</span>), <span style="color:#a6e22e">opts</span><span style="color:#f92672">...</span>)
}</code></pre></div>]]></description></item><item><title>Go 依赖注入库调研与体验</title><link>https://www.rectcircle.cn/posts/go-dependency-injection/</link><pubDate>Sun, 03 Oct 2021 16:11:34 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/go-dependency-injection/</guid><description type="html"><![CDATA[

<h2 id="介绍">介绍</h2>

<p>参考 <a href="/posts/dependency-injection/">依赖注入</a> 一文</p>

<h2 id="调研范围">调研范围</h2>

<p>本文将调研如下几个比较主流的 Go 依赖注入库和框架。</p>

<table>
<thead>
<tr>
<th>仓库</th>
<th>stars（截止 2021-10-03）</th>
</tr>
</thead>

<tbody>
<tr>
<td><a href="https://github.com/google/wire">google/wire</a></td>
<td>6.6k</td>
</tr>

<tr>
<td><a href="https://github.com/uber-go/dig">uber-go/dig</a></td>
<td>2.1K</td>
</tr>

<tr>
<td><a href="https://github.com/uber-go/fx">uber-go/fx</a></td>
<td>2.3k</td>
</tr>

<tr>
<td><a href="https://github.com/go-spring/go-spring">go-spring/go-spring</a></td>
<td>930</td>
</tr>
</tbody>
</table>

<h2 id="实验项目">实验项目</h2>

<h3 id="创建">创建</h3>

<p>创建一个实现项目 <code>go-dependency-injection-learn</code> 对以上库进行体验（go 1.17）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mkdir go-dependency-injection-learn
cd go-dependency-injection-learn
go mod tidy
go mod init github.com/rectcircle/go-dependency-injection-learn</code></pre></div>
<h3 id="简单场景-bean">简单场景 Bean</h3>

<p>Bean 结构如下：</p>

<ul>
<li>A 包含一个 string 类型字段</li>
<li>B 包含一个 int 类型字段</li>
<li>C 包含 A 和 B</li>
</ul>

<p>现在需要将 A 和 B 构造出来并注入到 B 中。</p>

<p>代码如下，创建包目录 <code>mkdir -p bean/sample</code>，并创建文件 <code>bean/sample/sample.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">sample</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#75715e">// 类型 A 和 构造器
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewA</span>(<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">A</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">A</span>{
		<span style="color:#a6e22e">aField</span>: <span style="color:#a6e22e">aField</span>,
	}
}

<span style="color:#75715e">// 类型 B 和 构造器
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">B</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewB</span>(<span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">B</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">B</span>{
		<span style="color:#a6e22e">bField</span>: <span style="color:#a6e22e">bField</span>,
	}
}

<span style="color:#75715e">// 类型 B 和 构造器
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">C</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">A</span>
	<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">B</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">C</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;I am C and c.a is %s, c.b is %d&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">aField</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">bField</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewC</span>(<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">A</span>, <span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">B</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">C</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">C</span>{
		<span style="color:#a6e22e">a</span>: <span style="color:#a6e22e">a</span>,
		<span style="color:#a6e22e">b</span>: <span style="color:#a6e22e">b</span>,
	}
}

<span style="color:#75715e">// 假设最终要构造一个 C，写法如下
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ManualInitialize</span>(<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">C</span> {
	<span style="color:#a6e22e">a</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewA</span>(<span style="color:#a6e22e">aField</span>)
	<span style="color:#a6e22e">b</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewB</span>(<span style="color:#a6e22e">bField</span>)
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewC</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>
}</code></pre></div>
<h3 id="仓库地址">仓库地址</h3>

<p><a href="https://github.com/rectcircle/go-dependency-injection-learn">https://github.com/rectcircle/go-dependency-injection-learn</a></p>

<h2 id="wire">wire</h2>

<blockquote>
<ul>
<li>version: v0.5.0</li>
<li><a href="https://juejin.cn/post/6844903901469097998">博客</a></li>
<li><a href="https://github.com/google/wire">repo</a></li>
</ul>
</blockquote>

<h3 id="安装并添加依赖">安装并添加依赖</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go get github.com/google/wire/cmd/wire</code></pre></div>
<p>创建包目录 <code>mkdir wire</code></p>

<h3 id="简单场景例子">简单场景例子</h3>

<h4 id="编写代码">编写代码</h4>

<p>编写声明文件 <code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//go:generate wire
</span><span style="color:#75715e">//go:build wireinject
</span><span style="color:#75715e">// +build wireinject
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;github.com/google/wire&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/go-dependency-injection-learn/bean/sample&#34;</span>
)

<span style="color:#75715e">// 初始化声明
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeSample</span>(<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>))
}</code></pre></div>
<p>编写调用者函数 <code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeSample: %s\n&#34;</span>, <span style="color:#a6e22e">InitializeSample</span>(<span style="color:#e6db74">&#34;test&#34;</span>, <span style="color:#ae81ff">1</span>))
}</code></pre></div>
<h4 id="执行代码生成">执行代码生成</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 代码生成（使用 wireinject tag 让编译器去扫描该代码文件）</span>
go generate --tags wireinject -v ./... <span style="color:#75715e"># 或者 wire ./wire</span> </code></pre></div>
<p>将生成一个 <code>wire/wire_gen.go</code> 文件，文件内容为</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// Code generated by Wire. DO NOT EDIT.
</span><span style="color:#75715e"></span>
<span style="color:#75715e">//go:generate go run github.com/google/wire/cmd/wire
</span><span style="color:#75715e">//+build !wireinject
</span><span style="color:#75715e"></span>
<span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;github.com/rectcircle/go-dependency-injection-learn/bean/sample&#34;</span>
)

<span style="color:#75715e">// Injectors from wire.go:
</span><span style="color:#75715e"></span>
<span style="color:#75715e">// 依赖注入声明
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeSample</span>(<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span> {
	<span style="color:#a6e22e">a</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>(<span style="color:#a6e22e">aField</span>)
	<span style="color:#a6e22e">b</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>(<span style="color:#a6e22e">bField</span>)
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>
}</code></pre></div>
<h4 id="wire-wire-go-说明"><code>wire/wire.go</code> 说明</h4>

<ul>
<li><code>//go:generate wire</code> 第一行声明一个生成器。这样，开发者只需在项目根目录执行 <code>go generate --tags wireinject -v ./...</code> 即可快速更新声明代码。</li>
<li><code>//go:build wireinject</code> 第二行为 Go 1.17 及以上版本新的条件编译注释，表示只有包含 <code>--tags wireinject</code> 时该文件才参与编译。条件编译是 wire 利用的核心能力，在进行代码生成的时候，go 编译器会解析 <code>wire/wire.go</code> 文件，生成一个 <code>wire/wire_gen.go</code>。在编译阶段，编译器将只认识 <code>wire/wire_gen.go</code>，而忽略 <code>wire/wire.go</code>，从而将生成的代码编译到产物中。</li>
<li><code>// +build wireinject</code> 第三行为 Go1.16 即之前版本的条件编译声明，目的和与第二个相同</li>
<li>函数（<code>Injector</code>），wire 会扫描该文件中的所有函数，并根据函数声明类型，返回值类型，以及 <code>wire.Build</code> 传递的构造函数（<code>Provider</code>），构建一个依赖关系树，并将该函数的函数体，生成到 <code>wire/wire_gen.go</code> 文件中。</li>
</ul>

<h4 id="wire-wire-gen-go-说明"><code>wire/wire_gen.go</code> 说明</h4>

<ul>
<li>第一行为，告诉 IDE 该文件是由程序自动生成的，以禁止用户修改</li>
<li><code>//go:generate</code> 为声明代码生成器，以支持 <code>go generate</code> 命令可以更新生成的代码</li>
<li><code>//+build !wireinject</code> 为条件编译，表示不包含 <code>wireinject</code> 标签时，识别该代码文件</li>
<li>函数 (<code>Injector</code>)，根据 <code>wire/wire</code> 中函数生成的</li>
</ul>

<h3 id="核心概念">核心概念</h3>

<blockquote>
<p><a href="https://go.dev/blog/wire#how-does-it-work">官方博客</a></p>
</blockquote>

<h4 id="provider">Provider</h4>

<p><code>Provider</code> 一个普通的 Go 函数，可以认为是一个构造函数（不过还存在其他形式），下面有几个例子</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewUserStore</span>(<span style="color:#a6e22e">cfg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Config</span>, <span style="color:#a6e22e">db</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">mysql</span>.<span style="color:#a6e22e">DB</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">UserStore</span>, <span style="color:#66d9ef">error</span>) {<span style="color:#f92672">...</span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewDefaultConfig</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Config</span> {<span style="color:#f92672">...</span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewDB</span>(<span style="color:#a6e22e">info</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ConnectionInfo</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">mysql</span>.<span style="color:#a6e22e">DB</span>, <span style="color:#66d9ef">error</span>) {<span style="color:#f92672">...</span>}</code></pre></div>
<p>一组有树状调用关系的 Providers 可以组成 ProviderSet，并将会作为 wire.Build 的参数列表之一，wire 在生成代码的时候，会分析这些 Provider 的树状调用关系，并生成代码。</p>

<p>如想使用 wire，则需要先声明一系列 Provider</p>

<h4 id="injector">Injector</h4>

<p><code>Injector</code> 是按依赖顺序调用 Provider 的生成函数。开发者只需要编写 Injector 签名，包括任何需要的输入作为参数，并插入对 wire.Build 的调用，其参数为 provider 列表或 ProviderSet 表，例子如下：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">initUserStore</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">UserStore</span>, <span style="color:#66d9ef">error</span>) {
    <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">UserStoreSet</span>, <span style="color:#a6e22e">NewDB</span>)
    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<h3 id="原理">原理</h3>

<p>编译时，构建一颗调用链树，数的根节点是 Injector 的返回值，叶子节点是 Injector 的参数和无参Provider，非叶子节点是有参 Provider。</p>

<p>另外 wire 构建树的边是根据<strong>类型而非名称</strong>进行匹配的，因此就要保证：</p>

<ul>
<li>同一个 Provider 不能有相同的类型的参数</li>
<li>Injector 的参数不能有相同类型的参数</li>
</ul>

<p>最终 wire 将这棵树转换为 go 代码写入 <code>*_gen.go</code> 文件。</p>

<h3 id="vscode-配置">VSCode 配置</h3>

<p>由于 wire 依赖 go 条件编译，因此如果想要对 Injector 声明文件添加智能提示，需要添加如下配置</p>

<p><code>.vscode/settings.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
	<span style="color:#f92672">&#34;gopls&#34;</span>: {
		<span style="color:#f92672">&#34;buildFlags&#34;</span>: [<span style="color:#e6db74">&#34;-tags=wireinject&#34;</span>]
	},
}</code></pre></div>
<p>另附上 VSCode 调试配置</p>

<p><code>.vscode/tasks.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">See</span> <span style="color:#960050;background-color:#1e0010">https://go.microsoft.com/fwlink/?LinkId=733558</span>
	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">for</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">documentation</span> <span style="color:#960050;background-color:#1e0010">about</span> <span style="color:#960050;background-color:#1e0010">the</span> <span style="color:#960050;background-color:#1e0010">tasks.json</span> <span style="color:#960050;background-color:#1e0010">format</span>
	<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.0.0&#34;</span>,
	<span style="color:#f92672">&#34;tasks&#34;</span>: [
		{
			<span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;mirePreLaunchTask&#34;</span>,
			<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
			<span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;go generate --tags wireinject -v ./...&#34;</span>,
		}
	]
}</code></pre></div>
<p><code>.vscode/launch.json</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">使用</span> <span style="color:#960050;background-color:#1e0010">IntelliSense</span> <span style="color:#960050;background-color:#1e0010">了解相关属性。</span> 
	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">悬停以查看现有属性的描述。</span>
	<span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">欲了解更多信息，请访问:</span> <span style="color:#960050;background-color:#1e0010">https://go.microsoft.com/fwlink/?linkid=830387</span>
	<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.2.0&#34;</span>,
	<span style="color:#f92672">&#34;configurations&#34;</span>: [
		{
			<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch wire&#34;</span>,
			<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
			<span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
			<span style="color:#f92672">&#34;preLaunchTask&#34;</span>: <span style="color:#e6db74">&#34;mirePreLaunchTask&#34;</span>,
			<span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;auto&#34;</span>,
			<span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}/wire&#34;</span>
		}
	]
}</code></pre></div>
<h3 id="特性">特性</h3>

<h4 id="使用流程">使用流程</h4>

<ul>
<li>声明 / 定义 Provider</li>
<li>声明 Injector</li>
<li>调用 wire 命令生成代码</li>
<li>编译运行</li>
</ul>

<h4 id="provider-set">Provider Set</h4>

<p>当一些provider通常是一起使用的时候，可以使用 provider set 将它们组织起来。</p>

<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">//不能声明在 Injector 里面
</span><span style="color:#75715e"></span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sampleSet</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeSample2</span>(<span style="color:#a6e22e">aField</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">bField</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">c</span> <span style="color:#66d9ef">bool</span> <span style="color:#75715e">/*无用输入不会报错*/</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">sampleSet</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeSample2: %s\n&#34;</span>, <span style="color:#a6e22e">InitializeSample2</span>(<span style="color:#e6db74">&#34;test2&#34;</span>, <span style="color:#ae81ff">2</span>, <span style="color:#66d9ef">true</span>))</code></pre></div>
<h4 id="接口绑定">接口绑定</h4>

<p>将接口类型和结构体类型绑定，使得调用树可以搭建</p>

<p><code>wire/interface_bind.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/google/wire&#34;</span>

<span style="color:#75715e">// Fooer - 接口
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Fooer</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">Foo</span>() <span style="color:#66d9ef">string</span>
}

<span style="color:#75715e">// MyFooer - 接口 Fooer 的实现
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyFooer</span> <span style="color:#66d9ef">string</span>

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">MyFooer</span>) <span style="color:#a6e22e">Foo</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> string(<span style="color:#f92672">*</span><span style="color:#a6e22e">b</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newMyFooer</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">MyFooer</span> {
	<span style="color:#a6e22e">foo</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">MyFooer</span>(<span style="color:#e6db74">&#34;Hello, World!&#34;</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">foo</span>
}

<span style="color:#75715e">// 结构体 Bar
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Bar</span> <span style="color:#66d9ef">string</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newBar</span>(<span style="color:#a6e22e">f</span> <span style="color:#a6e22e">Fooer</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Foo</span>()
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">InterfaceSet</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(
	<span style="color:#a6e22e">newMyFooer</span>,
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Bind</span>(new(<span style="color:#a6e22e">Fooer</span>), new(<span style="color:#f92672">*</span><span style="color:#a6e22e">MyFooer</span>)), <span style="color:#75715e">// 将结构体类型和接口绑定
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">newBar</span>)</code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 3. 结构体绑定
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeWithInterfaceBind</span>() <span style="color:#66d9ef">string</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">InterfaceSet</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeWithInterfaceBind: %s\n&#34;</span>, <span style="color:#a6e22e">InitializeWithInterfaceBind</span>())</code></pre></div>
<h4 id="属性注入-provider">属性注入 Provider</h4>

<p><code>wire/attribute_injection_provider.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/google/wire&#34;</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo2</span> <span style="color:#66d9ef">int</span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Bar2</span> <span style="color:#66d9ef">int</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newFoo2</span>() <span style="color:#a6e22e">Foo2</span> { <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> }

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newBar2</span>() <span style="color:#a6e22e">Bar2</span> { <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">2</span> }

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FooBar2</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">MyFoo2</span>   <span style="color:#a6e22e">Foo2</span>
	<span style="color:#a6e22e">MyBar2</span>   <span style="color:#a6e22e">Bar2</span>
	<span style="color:#a6e22e">MyBar2_2</span> <span style="color:#a6e22e">Bar2</span> <span style="color:#e6db74">`wire:&#34;-&#34;`</span> <span style="color:#75715e">// 忽略该字段
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">StructProviderSet</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(
	<span style="color:#a6e22e">newFoo2</span>,
	<span style="color:#a6e22e">newBar2</span>,
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Struct</span>(new(<span style="color:#a6e22e">FooBar2</span>), <span style="color:#e6db74">&#34;MyFoo2&#34;</span>)) <span style="color:#75715e">// 只绑定一个参数
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">StructProviderSet2</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(
	<span style="color:#a6e22e">newFoo2</span>,
	<span style="color:#a6e22e">newBar2</span>,
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Struct</span>(new(<span style="color:#a6e22e">FooBar2</span>), <span style="color:#e6db74">&#34;*&#34;</span>)) <span style="color:#f92672">//</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">号</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#a6e22e">表示注入全部字段</span><span style="color:#960050;background-color:#1e0010">，</span><span style="color:#e6db74">`wire:&#34;-&#34;`</span> <span style="color:#a6e22e">的除外</span></code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 4. 结构体 Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeStructProvider</span>() <span style="color:#a6e22e">FooBar2</span> { <span style="color:#75715e">// 返回结构体
</span><span style="color:#75715e"></span>	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">StructProviderSet</span>))
}
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeStructProvider2</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">FooBar2</span> { <span style="color:#75715e">// 返回结构体指针
</span><span style="color:#75715e"></span>	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">StructProviderSet2</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeStructProvider: %#v\n&#34;</span>, <span style="color:#a6e22e">InitializeStructProvider</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeStructProvider2: %#v\n&#34;</span>, <span style="color:#a6e22e">InitializeStructProvider2</span>())</code></pre></div>
<h4 id="值-provider">值 Provider</h4>

<p><code>wire/value_privider.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>

	<span style="color:#e6db74">&#34;github.com/google/wire&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo3</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">X</span> <span style="color:#66d9ef">int</span>
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">BindValueSet1</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Value</span>(<span style="color:#a6e22e">Foo3</span>{<span style="color:#a6e22e">X</span>: <span style="color:#ae81ff">42</span>}))
<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">BindValueSet2</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">InterfaceValue</span>(new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>), <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>))</code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 5. 绑定值
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeValue1</span>() <span style="color:#a6e22e">Foo3</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">BindValueSet1</span>))
}
<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeValue2</span>() <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">BindValueSet2</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeValue1: %#v\n&#34;</span>, <span style="color:#a6e22e">InitializeValue1</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeValue2: %#v\n&#34;</span>, <span style="color:#a6e22e">InitializeValue2</span>())</code></pre></div>
<h4 id="结构体字段-provider">结构体字段 Provider</h4>

<p><code>wire/struct_field_provider.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/google/wire&#34;</span>

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo4</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">S</span> <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">N</span> <span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">F</span> <span style="color:#66d9ef">float64</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewFoo4</span>() <span style="color:#a6e22e">Foo4</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Foo4</span>{<span style="color:#a6e22e">S</span>: <span style="color:#e6db74">&#34;Hello, World!&#34;</span>, <span style="color:#a6e22e">N</span>: <span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">F</span>: <span style="color:#ae81ff">3.14</span>}
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">StructFieldProviderSet</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(
	<span style="color:#a6e22e">NewFoo4</span>,
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">FieldsOf</span>(new(<span style="color:#a6e22e">Foo4</span>), <span style="color:#e6db74">&#34;S&#34;</span>))</code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 6. 结构体字段 Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeStructField</span>() <span style="color:#66d9ef">string</span> {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">StructFieldProviderSet</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeStructField: %s\n&#34;</span>, <span style="color:#a6e22e">InitializeStructField</span>())</code></pre></div>
<h4 id="返回错误">返回错误</h4>

<p><code>wire/error.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;errors&#34;</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newStringOrError</span>(<span style="color:#a6e22e">isErr</span> <span style="color:#66d9ef">bool</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">isErr</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;模拟构造函数执行抛出异常&#34;</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;hello World&#34;</span>, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 7. 返回错误
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeTestError</span>(<span style="color:#a6e22e">isErr</span> <span style="color:#66d9ef">bool</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">newStringOrError</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">InitializeTestError</span>(<span style="color:#66d9ef">true</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeTestError(true): %s, %v\n&#34;</span>, <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span>)
	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span> = <span style="color:#a6e22e">InitializeTestError</span>(<span style="color:#66d9ef">false</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeTestError(false): %s, %v\n&#34;</span>, <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span>)</code></pre></div>
<h4 id="清理函数">清理函数</h4>

<p>该清理函数指的是构建成功的清理函数。</p>

<p><code>wire/cleanup.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newStringOrCleanup</span>(<span style="color:#a6e22e">isErr</span> <span style="color:#66d9ef">bool</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">func</span>(), <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">isErr</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;模拟构造函数执行抛出异常&#34;</span>)
	}
	<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;hello World&#34;</span>, <span style="color:#66d9ef">func</span>() {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;调用了 cleanup 函数&#34;</span>)
	}, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<p><code>wire/wire.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#75715e">// 8. Cleanup
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeTestCleanup</span>(<span style="color:#a6e22e">isErr</span> <span style="color:#66d9ef">bool</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">func</span>(), <span style="color:#66d9ef">error</span>) {
	panic(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">newStringOrCleanup</span>))
}</code></pre></div>
<p><code>wire/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">cleanup</span>, <span style="color:#a6e22e">e</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">InitializeTestCleanup</span>(<span style="color:#66d9ef">true</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeTestCleanup(true): %s, %v\n&#34;</span>, <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cleanup</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">cleanup</span>()
	}
	<span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">cleanup</span>, <span style="color:#a6e22e">e</span> = <span style="color:#a6e22e">InitializeTestCleanup</span>(<span style="color:#66d9ef">false</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;call InitializeTestCleanup(false): %s, %v\n&#34;</span>, <span style="color:#a6e22e">s</span>, <span style="color:#a6e22e">e</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cleanup</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">cleanup</span>()
	}</code></pre></div>
<h3 id="最佳实践">最佳实践</h3>

<blockquote>
<p><a href="https://juejin.cn/post/6844903901469097998#heading-11">掘金</a> | <a href="https://github.com/google/wire/blob/main/docs/best-practices.md">官方</a></p>
</blockquote>

<h4 id="区分类型">区分类型</h4>

<p>由于injector的函数中，不允许出现重复的参数类型，否则wire将无法区分这些相同的参数类型，比如：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FooBar</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">foo</span> <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">bar</span> <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewFooBar</span>(<span style="color:#a6e22e">foo</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">bar</span> <span style="color:#66d9ef">string</span>) <span style="color:#a6e22e">FooBar</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">FooBar</span>{
	    <span style="color:#a6e22e">foo</span>: <span style="color:#a6e22e">foo</span>,  
	    <span style="color:#a6e22e">bar</span>: <span style="color:#a6e22e">bar</span>,
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeFooBar</span>(<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">string</span>) <span style="color:#a6e22e">FooBar</span> {
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">NewFooBar</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">FooBar</span>{}
}</code></pre></div>
<p>生成代码，将报错：<code>provider has multiple parameters of type string</code></p>

<p>因此需要，对 String 进行类型定义</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo</span> <span style="color:#66d9ef">string</span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Bar</span> <span style="color:#66d9ef">string</span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FooBar</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">foo</span> <span style="color:#a6e22e">Foo</span>
	<span style="color:#a6e22e">bar</span> <span style="color:#a6e22e">Bar</span>
}
<span style="color:#f92672">//</span> <span style="color:#f92672">...</span></code></pre></div>
<h4 id="option-struct">Option Struct</h4>

<p>如果一个 provider （构造函数）包含了许多依赖，可以将这些依赖放在一个options结构体中，从而避免构造函数的参数太多：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Message</span> <span style="color:#66d9ef">string</span>

<span style="color:#75715e">// Options
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Options</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">Messages</span> []<span style="color:#a6e22e">Message</span>
	<span style="color:#a6e22e">Writer</span>   <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>
	<span style="color:#a6e22e">Reader</span>   <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>
}
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Greeter</span> <span style="color:#66d9ef">struct</span> {
}

<span style="color:#75715e">// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
</span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewGreeter</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Options</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Greeter</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
}
<span style="color:#75715e">// GreeterSet 使用wire.Struct设置Options为provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">GreeterSet</span> = <span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">NewSet</span>(<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Struct</span>(new(<span style="color:#a6e22e">Options</span>), <span style="color:#e6db74">&#34;*&#34;</span>), <span style="color:#a6e22e">NewGreeter</span>)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">InitializeGreeter</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">msg</span> []<span style="color:#a6e22e">Message</span>, <span style="color:#a6e22e">w</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>, <span style="color:#a6e22e">r</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Greeter</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">wire</span>.<span style="color:#a6e22e">Build</span>(<span style="color:#a6e22e">GreeterSet</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
}</code></pre></div>
<h4 id="provider-sets-在-libraries-中声明的注意事项">Provider Sets 在 Libraries 中声明的注意事项</h4>

<p>Provider Sets 声明到库里面时。在迭代过程中，不应该破坏Provider Set的兼容性。</p>

<p>如下更改不会破坏兼容性</p>

<ul>
<li>删除Provider无用的输入的声明（修改某个 Provider）</li>
<li>添加新的输出，且该类型是新建的不会和用户附加的 Provider 产生冲突</li>
</ul>

<h4 id="mock">Mock</h4>

<p>参见：<a href="https://github.com/google/wire/blob/main/docs/best-practices.md#mocking">官方</a></p>

<h3 id="缺点">缺点</h3>

<ul>
<li>由于其实现限制，类型不能相同，需要定义许多额外的基础类型别名</li>
<li><a href="https://juejin.cn/post/6844903901469097998#heading-14">mock支持暂时不够友好</a></li>
</ul>

<h2 id="dig">dig</h2>

<blockquote>
<ul>
<li>version: v1.13.0</li>
<li><a href="https://pkg.go.dev/go.uber.org/dig#pkg-overview">官方 docs</a></li>
<li><a href="https://juejin.cn/post/6898514836100120590">文章 1</a></li>
</ul>
</blockquote>

<h3 id="添加依赖">添加依赖</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go get <span style="color:#e6db74">&#39;go.uber.org/dig@v1&#39;</span></code></pre></div>
<h3 id="简单场景例子-1">简单场景例子</h3>

<p>创建包目录 <code>mkdir dig</code>，并编写代码 <code>dig/sample.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#e6db74">&#34;github.com/rectcircle/go-dependency-injection-learn/bean/sample&#34;</span>
	<span style="color:#e6db74">&#34;go.uber.org/dig&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunSample</span>(<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#75715e">// 注册构造函数需要的参数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">int</span>) { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> }),
		<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#75715e">// 调用函数，并将 bean 注入函数参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span>) {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalln</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;print dot graph&#34;</span>)
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Visualize</span>(<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()
}</code></pre></div>
<p><code>dig/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">RunSample</span>(<span style="color:#e6db74">&#34;string&#34;</span>, <span style="color:#ae81ff">1</span>)
}</code></pre></div>
<p><code>go run ./dig</code> 输出</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">nodes: {
	 *sample.B -&gt; deps: [int], ctor: func(int) *sample.B
	 *sample.C -&gt; deps: [*sample.A *sample.B], ctor: func(*sample.A, *sample.B) *sample.C
	 string -&gt; deps: [], ctor: func() (string, int)
	 int -&gt; deps: [], ctor: func() (string, int)
	 *sample.A -&gt; deps: [string], ctor: func(string) *sample.A
}
values: {
	 *sample.C =&gt; I am C and c.a is string, c.b is 1
	 string =&gt; string
	 int =&gt; 1
	 *sample.A =&gt; &amp;{string}
	 *sample.B =&gt; &amp;{1}
}</pre></div>
<h3 id="核心-api">核心 API</h3>

<ul>
<li><code>dig.New</code> 创建一个依赖注入容器</li>
<li><code>func (*dig.Container).Provide(constructor interface{}, opts ...dig.ProvideOption) error</code> 注册一个构造函数到容器里面。该动作不会触发依赖图完整性检查，也不会执行该函数，仅仅执行注册并构建一个依赖图。如下情况下才会返回 error

<ul>
<li>constructor 为 nil，或者不是一个函数</li>
<li>constructor 没有返回大于等于一个非 error 返回值</li>
<li>constructor 返回值的类型已经存在，且没有命名</li>
<li>opts 是非法的</li>
</ul></li>
<li><code>func (*dig.Container).Invoke(function interface{}, opts ...dig.InvokeOption) error</code> 解析 function，对照其参数类型，去容器里面去找或者构建相关对象。另外只有调用 <code>Invoke</code>，Provider 注册的构造函数才会被执行，且最多执行一次。也就是说 dig 采用单例模式。如下情况下才会返回 error：

<ul>
<li>function 为 nil，或者不是一个函数</li>
<li>function 的参数依赖图不完整，无法构建</li>
<li>function 的依赖路径中存在环（循环依赖）</li>
<li>function 最后一个返回参数是 error，将原样返回</li>
</ul></li>
<li><code>func (*dig.Container).String() string</code> 返回已经注册构造函数和依赖关系，以及已经构造出来的对象，可用于 debug。</li>
<li><code>dig.Visualize(c, os.Stdout)</code> 打印依赖图的 dot graph，可用于测试和观察层次关系</li>
</ul>

<h3 id="原理-1">原理</h3>

<p>运行时，利用反射分析 构造函数 关系，并构建对象。</p>

<h3 id="vscode-配置-1">VSCode 配置</h3>

<p><code>.vscode/launch.json</code> 的 configurations 数组添加如下</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">		{
			<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Launch dig&#34;</span>,
			<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;go&#34;</span>,
			<span style="color:#f92672">&#34;request&#34;</span>: <span style="color:#e6db74">&#34;launch&#34;</span>,
			<span style="color:#f92672">&#34;mode&#34;</span>: <span style="color:#e6db74">&#34;auto&#34;</span>,
			<span style="color:#f92672">&#34;program&#34;</span>: <span style="color:#e6db74">&#34;${workspaceFolder}/dig&#34;</span>
		}<span style="color:#960050;background-color:#1e0010">,</span></code></pre></div>
<h3 id="特性-1">特性</h3>

<h4 id="接口绑定-as">接口绑定 - as</h4>

<p>说明</p>

<ul>
<li>如果构造函数返回多个值，<code>dig.As</code> 将报错 v</li>
<li><code>dig.As</code> 可以和 <code>dig.Name</code> 一起使用 v</li>
<li><code>dig.As</code> 不可以和 <code>dig.Group</code> 一起使用</li>
<li><code>dig.As</code> 的参数只允许是接口指针类型</li>
<li>如果构造函数返回的值实现了多个接口，则 <code>dig.As</code> 可以指定多个</li>
</ul>

<p><code>dig/as.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;bytes&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#e6db74">&#34;go.uber.org/dig&#34;</span>
)

<span style="color:#75715e">// Fooer - 接口
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Fooer</span> <span style="color:#66d9ef">interface</span> {
	<span style="color:#a6e22e">Foo</span>() <span style="color:#66d9ef">string</span>
}

<span style="color:#75715e">// MyFooer - 接口 Fooer 的实现
</span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MyFooer</span> <span style="color:#66d9ef">string</span>

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">MyFooer</span>) <span style="color:#a6e22e">Foo</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> string(<span style="color:#f92672">*</span><span style="color:#a6e22e">b</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newMyFooer</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">MyFooer</span> {
	<span style="color:#a6e22e">foo</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">MyFooer</span>(<span style="color:#e6db74">&#34;Hello, World!&#34;</span>)
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">foo</span>
}

<span style="color:#75715e">// 结构体 Bar
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Bar</span> <span style="color:#66d9ef">string</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newBar</span>(<span style="color:#a6e22e">f</span> <span style="color:#a6e22e">Fooer</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Foo</span>()
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithInterfaceError1</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()

	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>, <span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>) {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
	}, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>), new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>)))
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;错误场景 1 - 构造函数不支持返回多个值: %s\n&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithMultipleInterfaceAndName</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()

	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	}, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>), new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>)), <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;buffer&#34;</span>)) <span style="color:#75715e">// 通过 as 将结构体和接口进行绑定
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunWithMultipleInterfaceAndName: %s&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">String</span>())
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithInterfaceMultiple</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()

	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>() (<span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>, <span style="color:#f92672">*</span><span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>) {
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>
	}, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>), new(<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>))) <span style="color:#75715e">// 通过 as 将结构体和接口进行绑定
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;返回多个不同接口的值 RunWithInterfaceMultiple: %s&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">String</span>())
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithInterfaceError2</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newMyFooer</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">Fooer</span>)), <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Group</span>(<span style="color:#e6db74">&#34;test&#34;</span>)) <span style="color:#75715e">// 通过 as 将结构体和接口进行绑定
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;错误场景 2 - `dig.As` 不可以和 `dig.Group` 一起使用: %s\n&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithInterfaceError3</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newMyFooer</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">MyFooer</span>))) <span style="color:#75715e">// 通过 as 将结构体和接口进行绑定
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;错误场景 3 - `dig.As` 的参数只允许是接口指针类型: %s\n&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunWithInterface</span>() {

	<span style="color:#a6e22e">RunWithInterfaceError1</span>()
	<span style="color:#a6e22e">RunWithMultipleInterfaceAndName</span>()
	<span style="color:#a6e22e">RunWithInterfaceError2</span>()
	<span style="color:#a6e22e">RunWithInterfaceError3</span>()

	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newMyFooer</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">As</span>(new(<span style="color:#a6e22e">Fooer</span>))), <span style="color:#75715e">// 通过 as 将结构体和接口进行绑定
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newBar</span>),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#75715e">// 调用函数，并将 bean 注入函数参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">result</span> <span style="color:#66d9ef">string</span>) {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunWithInterface: %s\n&#34;</span>, <span style="color:#a6e22e">result</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalln</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">String</span>())
}</code></pre></div>
<p><code>dig/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">RunWithInterface</span>()</code></pre></div>
<h4 id="参数对象-结果对象和可选依赖">参数对象、结果对象和可选依赖</h4>

<ul>
<li>参数对象，用在 Provide 和 Invoke 的函数参数的参数上，用来简化代码，参数对象为包含</li>
<li>结果对象，当 Provide 传入的 constructor 返回结果过多时，可以考虑使用结果对象，以简化代码</li>
<li>注意：参数对象和结果对象不允许包含私有（未导出）字段（就算添加 optional 也不行）</li>
</ul>

<p><code>dig/parameter_result_objects_and_optional.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#e6db74">&#34;go.uber.org/dig&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ABEIn</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span> <span style="color:#75715e">// 参数化对象，需嵌入该结构体作为表示，可以用在 Provide 和 Invoke 第一个参数的参数中
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">A</span>      <span style="color:#66d9ef">int</span>
	<span style="color:#a6e22e">B</span>      <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">E</span>      <span style="color:#66d9ef">int16</span> <span style="color:#e6db74">`optional:&#34;true&#34;`</span> <span style="color:#75715e">// 可选依赖，如果不存在，则为 0 值或者 nil
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">c</span>      <span style="color:#66d9ef">bool</span>  <span style="color:#e6db74">`optional:&#34;true&#34;`</span> <span style="color:#75715e">// dig.In 不允许有私有（未导出）字段，optional 也不行
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">BCOut</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Out</span> <span style="color:#75715e">// 结果对象，需嵌入该结构体作为表示，可以用在 Provide 的第一个参数的返回值中使用
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">B</span>       <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">C</span>       <span style="color:#66d9ef">bool</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newBC</span>() <span style="color:#a6e22e">BCOut</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">BCOut</span>{
		<span style="color:#a6e22e">B</span>: <span style="color:#e6db74">&#34;b&#34;</span>,
		<span style="color:#a6e22e">C</span>: <span style="color:#66d9ef">true</span>,
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newA</span>() <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newD</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">ABEIn</span>) <span style="color:#66d9ef">int8</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">2</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunParameterResultObjects</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#75715e">// 调用 newBC 的返回值 BCOut，dig 会将 BCOut 的每个字段作为 value 作为放到容器中，而不是将 BCOut 放入容器中
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newBC</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newA</span>),
		<span style="color:#75715e">// dig 会从容器中查询 ABIn 的每个字段，并构建 ABIn 结构体，然后再调用该函数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newD</span>),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#75715e">// 调用函数，并将 bean 注入函数参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">abe</span> <span style="color:#a6e22e">ABEIn</span>, <span style="color:#a6e22e">c</span> <span style="color:#66d9ef">bool</span>, <span style="color:#a6e22e">d</span> <span style="color:#66d9ef">int8</span>) {
		<span style="color:#75715e">// dig 会从容器中查询 ABIn 的每个字段，并构建 ABIn 结构体，然后再调用该函数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunParameterResultObjects: a=%d, b=%s, c=%t, d=%d, e=%d\n&#34;</span>, <span style="color:#a6e22e">abe</span>.<span style="color:#a6e22e">A</span>, <span style="color:#a6e22e">abe</span>.<span style="color:#a6e22e">B</span>, <span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">d</span>, <span style="color:#a6e22e">abe</span>.<span style="color:#a6e22e">E</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalln</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
}</code></pre></div>
<p><code>dig/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">RunParameterResultObjects</span>()</code></pre></div>
<h4 id="命名值和组">命名值和组</h4>

<ul>
<li>命名值，注册的构造函数的返回值存在相同类型时，dig 不知到该注入哪一个。因此 dig 提供命名值的机制来对同类型的值进行命名。使用命名值的注意事项如下：

<ul>
<li>不支持在同一个函数里返回同一类型的多个值，比如 <code>func newInt0And1() (int, int)</code></li>
<li>命名值不能注入到非命令名参数中，比如 <code>c.Provide(newInt2, dig.Name(&quot;int2&quot;))</code> 然后 <code>c.Invoke(func(int_ int){})</code> 将报错</li>
<li>不允许同一类型存在多个相同的命名</li>
<li>针对同一类型，允许存在 0 个或 1 个未命名值，多个命名不同的命名值</li>
<li><code>c.Provide(constructor, dig.Name(&quot;xxx&quot;))</code> 如果 constructor 存在多个返回值，则所有的值都将命名为 xxx，也就是说 name 是类型下的一个属性，不要求全局唯一</li>
<li><code>c.Provide(newIntAndString, dig.Name(&quot;a&quot;), dig.Name(&quot;b&quot;))</code> 若存在多个 name option，以最后一个为准</li>
<li>结构体 tag <code>name</code> 可以和 <code>optional</code> 共同使用，如果没有 <code>optional</code>，且容器中没有将报错</li>
</ul></li>
<li>组，注册的构造函数的返回值存在相同类型时，可以将这些组织成一个 List。使用组的注意事项如下：

<ul>
<li>同一个函数里返回同一类型的多个值，可以用 group</li>
<li>同一个函数里返回不同类型的多个值，也可以使用 group</li>
<li>在 group 在注入过程中，如果没有相关 Provider，则注入一个 nil 类型</li>
<li>注入过程中，不保证 group 有序</li>
<li>在结果对象中，使用 <code>group</code> 标签时，可以使用 <code>flatten</code> 将一个切片展平</li>
</ul></li>
<li>注意事项

<ul>
<li>针对同一个值，命名值和组只能使用一种</li>
<li>命名值和组是 value 类型下面的一个属性，不要求全局唯一，换句话来说，一个 value 在容器中的 key 为 type + ?name + ?group。</li>
<li>命名值和组，在 <code>Invoke</code> 中只能通过 参数对象 和 结构体 tag （<code>name</code>、<code>group</code>） 的方式使用，无法直接使用</li>
</ul></li>
</ul>

<p><code>dig/name_and_group.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#e6db74">&#34;go.uber.org/dig&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newInt</span>() <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newInt0And1</span>() (<span style="color:#66d9ef">int</span>, <span style="color:#66d9ef">int</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newIntAndString</span>() (<span style="color:#66d9ef">int</span>, <span style="color:#66d9ef">string</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#34;a&#34;</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newInt2</span>() <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">2</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newInt3</span>() <span style="color:#66d9ef">int</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">3</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newItemA</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;a&#34;</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Int3AndItemBOut</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Out</span>
	<span style="color:#a6e22e">Int4</span>      <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int4&#34;`</span>
	<span style="color:#a6e22e">ItemB</span>     <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`group:&#34;list&#34;`</span>
	<span style="color:#a6e22e">ItemOther</span> []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`group:&#34;list,flatten&#34;`</span> <span style="color:#75715e">// 注意展平
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newItemCD</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">string</span>) {
	<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;c&#34;</span>, <span style="color:#e6db74">&#34;d&#34;</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newInt4AndItemBOut</span>() <span style="color:#a6e22e">Int3AndItemBOut</span> {
	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Int3AndItemBOut</span>{
		<span style="color:#a6e22e">Int4</span>:      <span style="color:#ae81ff">4</span>,
		<span style="color:#a6e22e">ItemB</span>:     <span style="color:#e6db74">&#34;b&#34;</span>,
		<span style="color:#a6e22e">ItemOther</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;e&#34;</span>, <span style="color:#e6db74">&#34;f&#34;</span>, <span style="color:#e6db74">&#34;g&#34;</span>},
	}
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">NameAndGroupIn</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span>
	<span style="color:#a6e22e">Int0</span> <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int0&#34; optional:&#34;true&#34;`</span>
	<span style="color:#a6e22e">Int1</span> <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int1&#34; optional:&#34;true&#34;`</span>
	<span style="color:#a6e22e">Int2</span> <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int2&#34;`</span>
	<span style="color:#a6e22e">Int3</span> <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int3&#34;`</span>
	<span style="color:#a6e22e">Int4</span> <span style="color:#66d9ef">int</span>      <span style="color:#e6db74">`name:&#34;int4&#34;`</span>
	<span style="color:#a6e22e">List</span> []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`group:&#34;list&#34;`</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IntStringGroup</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">In</span>
	<span style="color:#a6e22e">ListInt</span>        []<span style="color:#66d9ef">int</span>    <span style="color:#e6db74">`group:&#34;list_int&#34;`</span>
	<span style="color:#a6e22e">ListIntString1</span> []<span style="color:#66d9ef">int</span>    <span style="color:#e6db74">`group:&#34;list_int_string&#34;`</span>
	<span style="color:#a6e22e">ListIntString2</span> []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`group:&#34;list_int_string&#34;`</span>
	<span style="color:#a6e22e">ListString</span>     []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`group:&#34;list_string&#34;`</span> <span style="color:#75715e">// 允许不存在
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunNameAndGroupError1</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt0And1</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt0And1</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int0&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt0And1</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int0&#34;</span>), <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int0&#34;</span>)),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;错误场景1[%d] - 不支持在同一个函数里返回同一类型的多个值: %s\n&#34;</span>, <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">err</span>)
		}
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunNameAndGroupError2</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt2</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int2&#34;</span>)),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">int_</span> <span style="color:#66d9ef">int</span>) {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;int: %d\n&#34;</span>, <span style="color:#a6e22e">int_</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;错误场景2 - 命名值不能注入到非命令名参数中:&#34;</span>, <span style="color:#a6e22e">err</span>)
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunNameAndGroupError3</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt2</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int2&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt2</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int2&#34;</span>)),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;错误场景3 - 不允许同一类型存在多个相同的命名 %s\n&#34;</span>, <span style="color:#a6e22e">err</span>)
		}
	}
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunNameReturnMultipleAndNameMultiple</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newIntAndString</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;a&#34;</span>), <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;b&#34;</span>))
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;RunNameReturnMultipleAndNameMultiple: int 和 string 每个值都会被命名成 b&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunGroupReturnTypeGroup</span>() {
	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()

	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt0And1</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Group</span>(<span style="color:#e6db74">&#34;list_int&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newIntAndString</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Group</span>(<span style="color:#e6db74">&#34;list_int_string&#34;</span>)),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">IntGroup</span> <span style="color:#a6e22e">IntStringGroup</span>) {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunGroupReturnSameTypeGroup: list_int=%#v\n&#34;</span>, <span style="color:#a6e22e">IntGroup</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalln</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;RunGroupReturnSameTypeGroup: 构造函数返回想同类型可以使用 group 进行聚合&#34;</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunNameAndGroup</span>() {

	<span style="color:#a6e22e">RunNameAndGroupError1</span>()
	<span style="color:#a6e22e">RunNameAndGroupError2</span>()
	<span style="color:#a6e22e">RunNameAndGroupError3</span>()
	<span style="color:#a6e22e">RunNameReturnMultipleAndNameMultiple</span>()
	<span style="color:#a6e22e">RunGroupReturnTypeGroup</span>()

	<span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">New</span>()

	<span style="color:#75715e">// 注册构造函数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">errs</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">error</span>{
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt2</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int2&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt3</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Name</span>(<span style="color:#e6db74">&#34;int3&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newInt4AndItemBOut</span>),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newItemA</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Group</span>(<span style="color:#e6db74">&#34;list&#34;</span>)),
		<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">newItemCD</span>, <span style="color:#a6e22e">dig</span>.<span style="color:#a6e22e">Group</span>(<span style="color:#e6db74">&#34;list&#34;</span>)),
	}
	<span style="color:#75715e">// 错误处理
</span><span style="color:#75715e"></span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">errs</span> {
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
		}
	}
	<span style="color:#75715e">// 调用函数，并将 bean 注入函数参数
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">_int</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">NameAndGroupIn</span>) {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunNameAndGroup: int=%d, in=%#v\n&#34;</span>, <span style="color:#a6e22e">_int</span>, <span style="color:#a6e22e">in</span>)
	})
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>)
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalln</span>(<span style="color:#a6e22e">err</span>)
	}
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">String</span>())
}</code></pre></div>
<p><code>dig/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">	<span style="color:#a6e22e">RunNameAndGroup</span>()</code></pre></div>
<h3 id="缺点-1">缺点</h3>

<ul>
<li>运行进行分析和注入，不利于静态分析，不利于尽早暴露问题</li>
<li>某些高级特性（比如：参数对象、结果对象等），对业务代码有一定的侵入性</li>
<li>众多 option 配置复杂，case 很多，接口不太直观，相对难以理解一些</li>
</ul>

<h2 id="fx">fx</h2>

<blockquote>
<ul>
<li>version: v1.14.2</li>
<li><a href="https://pkg.go.dev/go.uber.org/fx">go docs</a></li>
</ul>
</blockquote>

<p>fx 是对 dig 库做的一层封装，以框架的方式提供能力，并添加了 App 生命周期管理，日志等相关能力。</p>

<h3 id="添加依赖-1">添加依赖</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go get go.uber.org/fx@v1</code></pre></div>
<h3 id="例子">例子</h3>

<p>创建包目录 <code>mkdir fx</code></p>

<p><code>fx/sample.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;os&#34;</span>

	<span style="color:#e6db74">&#34;github.com/rectcircle/go-dependency-injection-learn/bean/sample&#34;</span>
	<span style="color:#e6db74">&#34;go.uber.org/fx&#34;</span>
	<span style="color:#e6db74">&#34;go.uber.org/fx/fxevent&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SampleIn</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">In</span>

	<span style="color:#a6e22e">A</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">A</span>
	<span style="color:#a6e22e">B</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">B</span>
	<span style="color:#a6e22e">C</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunSample</span>(<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) {

	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sampleC</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span>
	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sampleIn</span> <span style="color:#a6e22e">SampleIn</span>
	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">g</span> <span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">DotGraph</span>

	<span style="color:#a6e22e">app</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">New</span>(
		<span style="color:#75715e">// fx.NopLogger, // 关闭日志
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 配置 fx 框架的日志
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">WithLogger</span>(
			<span style="color:#66d9ef">func</span>() <span style="color:#a6e22e">fxevent</span>.<span style="color:#a6e22e">Logger</span> {
				<span style="color:#75715e">// 默认 log 如下所示
</span><span style="color:#75715e"></span>				<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fxevent</span>.<span style="color:#a6e22e">ConsoleLogger</span>{<span style="color:#a6e22e">W</span>: <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stderr</span>}
			},
		),
		<span style="color:#75715e">// fx.ErrorHook(), // 错误处理
</span><span style="color:#75715e"></span>
		<span style="color:#75715e">// fx.Supply 等价于 fx.Provide(func() (string, int) { return a, b })
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Supply</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>),
		<span style="color:#75715e">// 和 dig 用法类似， dig.Option 能力需通过 fx.Annotated 实现，支持参数对象和结果对象
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// fx.Lifecycle 可以作为构造函数参数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>, <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>,
			<span style="color:#75715e">// 如果想使用命名值和组，可以通过 fx.Annotated 包裹一下
</span><span style="color:#75715e"></span>			<span style="color:#75715e">// 目前还不支持 dig.As 类似的接口绑定
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Annotated</span>{
				<span style="color:#a6e22e">Name</span>:   <span style="color:#e6db74">&#34;namedC&#34;</span>,
				<span style="color:#a6e22e">Target</span>: <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>,
			}),
		<span style="color:#75715e">// 和 dig 用法类似
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// fx.New 执行完成后，Invoke 就会被调用完成，支持参数对象和结果对象
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// fx.Lifecycle Invoke 函数的参数
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">lc</span> <span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Lifecycle</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span>) {
			<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - Invoke: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
			<span style="color:#75715e">// fx.Hook 事件函数，不允许阻塞，默认超时为 fx.DefaultTimeout (15 s)
</span><span style="color:#75715e"></span>			<span style="color:#75715e">// 可以通过 fx.StartTimeout() 和 fx.StopTimeout() 配置
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">lc</span>.<span style="color:#a6e22e">Append</span>(<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Hook</span>{
				<span style="color:#75715e">// 启动回调函数
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">OnStart</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
					<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - hooks[0] - OnStart: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
					<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
				},
				<span style="color:#75715e">// 停止回调函数
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">OnStop</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
					<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - hooks[0] - OnStop: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
					<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
				},
			})
			<span style="color:#75715e">// 存在多个，onStart 按照 append 的顺序调用，onSop 按照 append 的逆序调用
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">lc</span>.<span style="color:#a6e22e">Append</span>(<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Hook</span>{
				<span style="color:#75715e">// 启动回调函数
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">OnStart</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
					<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample -  hooks[1] - OnStart: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
					<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
				},
				<span style="color:#75715e">// 停止回调函数
</span><span style="color:#75715e"></span>				<span style="color:#a6e22e">OnStop</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
					<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample -  hooks[1] - OnStop: %s\n&#34;</span>, <span style="color:#a6e22e">c</span>)
					<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
				},
			})
		}),
		<span style="color:#75715e">// 将容器内的通类型的对象赋值给变量，注意，必须是容器内对象的指针类型。也就是说：
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 如果容器内是 struct 类型，这里传递的是 *struct
</span><span style="color:#75715e"></span>		<span style="color:#75715e">// 如果容器内是 *struct，这里传递的就是**struct
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Populate</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">sampleC</span>),
		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Populate</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">sampleIn</span>), <span style="color:#75715e">// 不支持 参数对象
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">fx</span>.<span style="color:#a6e22e">Populate</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">g</span>),        <span style="color:#75715e">// 拿到 DotGraph
</span><span style="color:#75715e"></span>	)

	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - Populate *sample.C: %s\n&#34;</span>, <span style="color:#a6e22e">sampleC</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - Populate sampleIn: %#v\n&#34;</span>, <span style="color:#a6e22e">sampleIn</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - DotGraph: \n%s\n&#34;</span>, <span style="color:#a6e22e">g</span>)

	<span style="color:#75715e">// app.Run()
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
	<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Stop</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>())
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
}</code></pre></div>
<p><code>fx/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">RunSample</span>(<span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#ae81ff">1</span>)
}</code></pre></div>
<h3 id="核心-api-1">核心 API</h3>

<ul>
<li><code>fx.New</code> 创建一个 fx app，支持如下 Option

<ul>
<li>日志： <code>fx.WithLogger</code> 自定义 fx 内部日志，<code>fx.NopLogger</code> 禁用日志，默认打到 stdout 中</li>
<li>错误和错误处理： <code>fx.Error</code> 和 <code>fx.ErrorHook</code></li>
<li>提供外部参数 <code>fx.Supply</code></li>
<li>注册构造函数 <code>fx.Provide</code>，可通过 <code>fx.Annotated</code> 实现 dig.Option 的能力，可以使用 <code>lc fx.Lifecycle</code> 作为参数注入 Hook</li>
<li>执行并注入 <code>fx.Invoke</code>，可以使用 <code>lc fx.Lifecycle</code> 作为参数注入 Hook</li>
<li>超时 <code>fx.StartTimeout</code> 与 <code>fx.StopTimeout</code>，默认为 <code>fx.DefaultTimeout</code> 15 秒</li>
<li><code>fx.Populate</code> 将依赖注入容器内的值赋值给外部变量，注意，必须是容器内对象的指针类型。也就是说：

<ul>
<li>如果容器内是 <code>struct</code> 类型，这里传递的是 <code>*struct</code></li>
<li>如果容器内是 <code>*struct</code>，这里传递的就是 <code>**struct</code></li>
</ul></li>
</ul></li>
<li><code>fx.Lifecycle</code> 和 <code>fx.Hook</code> 为 fx 生命周期回调，<code>fx.Lifecycle</code> 可以作为 <code>fx.Provide</code> 和 <code>fx.Invoke</code> 的参数</li>
<li><code>fx.DotGraph</code> 可通过 <code>fx.Populate</code> 获取到</li>
<li><code>app.Run</code> 阻塞运行</li>
<li><code>app.Start</code> 启动</li>
<li><code>app.Stop</code> 停止</li>
</ul>

<h3 id="原理-2">原理</h3>

<p>对 dig 进行封装</p>

<h3 id="缺点-2">缺点</h3>

<ul>
<li>包含 dig 的所有缺点</li>
<li>暂时没有 <code>dig.As</code> 的能力，无法进行接口绑定</li>
</ul>

<h2 id="go-spring">go-spring</h2>

<blockquote>
<ul>
<li>version: v1.0.5</li>
</ul>
</blockquote>

<h3 id="添加依赖-2">添加依赖</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">go get github.com/go-spring/go-spring</code></pre></div>
<h3 id="例子-1">例子</h3>

<p>创建包目录 <code>mkdir go-spring</code></p>

<p><code>go-spring/sample.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;context&#34;</span>
	<span style="color:#e6db74">&#34;errors&#34;</span>
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;log&#34;</span>

	<span style="color:#e6db74">&#34;github.com/go-spring/spring-core/gs&#34;</span>
	<span style="color:#e6db74">&#34;github.com/rectcircle/go-dependency-injection-learn/bean/sample&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SampleApp</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">C</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">C</span> <span style="color:#e6db74">`autowire:&#34;&#34;`</span>
	<span style="color:#a6e22e">D2</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">D2</span>       <span style="color:#e6db74">`autowire:&#34;&#34;`</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">D1</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">D2</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">D2</span> <span style="color:#e6db74">`autowire:&#34;&#34;`</span>
}

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">D2</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">D1</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">D1</span> <span style="color:#e6db74">`autowire:&#34;&#34;`</span>
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">SampleApp</span>) <span style="color:#a6e22e">OnStartApp</span>(<span style="color:#a6e22e">e</span> <span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Environment</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - gs.AppEvent - OnStartApp: e=%v, c=%s, d2=%v\n&#34;</span>, <span style="color:#a6e22e">e</span>, <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">C</span>, <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">D2</span>)
	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">ShutDown</span>(<span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;&#34;</span>))
}

<span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">SampleApp</span>) <span style="color:#a6e22e">OnStopApp</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;RunSample - gs.AppEvent - OnStopApp: %v\n&#34;</span>, <span style="color:#a6e22e">ctx</span>)
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RunSample</span>(<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">b</span> <span style="color:#66d9ef">int</span>) {
	<span style="color:#75715e">// 按照官方的 demo，`gs.Object` 以及 `gs.Provide` 应定义处的 init 函数中
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// go-spring 对 bean 的定义为：一个变量赋值给另一个变量后二者指向相同的内存地址（指针类型）
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// 因此 bean 只有这四种 ptr、interface、chan、func
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Object</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">a</span>) <span style="color:#75715e">// gs.Object(a) // 这种写将报错
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Object</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b</span>) <span style="color:#75715e">// gs.Object(b) // 这种写法报错
</span><span style="color:#75715e"></span>
	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">A</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewA</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">a</span>) })
	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">int</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">B</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewB</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">b</span>) })
	<span style="color:#75715e">// 如下两种写法不对，会找不到类型
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// gs.Provide(sample.NewA)
</span><span style="color:#75715e"></span>	<span style="color:#75715e">// gs.Provide(sample.NewB)
</span><span style="color:#75715e"></span>
	<span style="color:#75715e">// 循环引用测试
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Object</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">D1</span>{})
	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Object</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">D2</span>{})

	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Provide</span>(<span style="color:#a6e22e">sample</span>.<span style="color:#a6e22e">NewC</span>)

	<span style="color:#75715e">// 注册 AppEvent
</span><span style="color:#75715e"></span>	<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Provide</span>(new(<span style="color:#a6e22e">SampleApp</span>)).<span style="color:#a6e22e">Export</span>(new(<span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">AppEvent</span>))
	<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">gs</span>.<span style="color:#a6e22e">Run</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
	}
}</code></pre></div>
<p><code>go-spring/main.go</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">RunSample</span>(<span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#ae81ff">1</span>)
}</code></pre></div>
<h3 id="缺点-3">缺点</h3>

<ul>
<li>只有中文社区，且没有系统的文档</li>
<li>对标 Java Spring 框架，是一种大而全的框架，比较重，不符合 Go 的设计哲学</li>
<li>API 尚未稳定，不符合 Go 语义化版本的规范，名义 major 在 1，但是已经发生不兼容的情况</li>
<li>API 缺乏设计，公开 API 不够闭环，比如没有 Start Stop 等函数</li>
<li>初始化阶段异常直接 panic</li>
<li>单元测试不足</li>
<li>值类型无法放入 Bean 容器中，只有 ptr、interface、chan、func 四种类型可以作为 Bean 容器</li>
</ul>

<h2 id="对比">对比</h2>

<table>
<thead>
<tr>
<th>仓库</th>
<th><a href="https://github.com/google/wire">google/wire</a></th>
<th><a href="https://github.com/uber-go/dig">uber-go/dig</a></th>
<th><a href="https://github.com/uber-go/fx">uber-go/fx</a></th>
<th><a href="https://github.com/go-spring/go-spring">go-spring/go-spring</a></th>
</tr>
</thead>

<tbody>
<tr>
<td>stars（截止 2021-10-03）</td>
<td>6.6k</td>
<td>2.1K</td>
<td>2.3k</td>
<td>930</td>
</tr>

<tr>
<td>维护组织</td>
<td>google</td>
<td>uber</td>
<td>uber</td>
<td>didi</td>
</tr>

<tr>
<td>原理</td>
<td>编译时（代码生成+条件编译）</td>
<td>运行时（反射）</td>
<td>运行时（反射）</td>
<td>运行时（反射）</td>
</tr>

<tr>
<td>库/框架</td>
<td>库</td>
<td>库</td>
<td>框架</td>
<td>框架</td>
</tr>

<tr>
<td>构造器注入</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>属性注入</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>

<tr>
<td>提供常量值</td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>

<tr>
<td>接口绑定</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
<td>?</td>
</tr>

<tr>
<td>对象命名</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
<td>?</td>
</tr>

<tr>
<td>对象组（注入多个同类型对象组成一个切片）</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
<td>?</td>
</tr>

<tr>
<td>循环依赖</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>

<tr>
<td>侵入性</td>
<td>3(需要定义很多基础类型)</td>
<td>4</td>
<td>4</td>
<td>4</td>
</tr>

<tr>
<td>测试覆盖度</td>
<td>60%</td>
<td>98%</td>
<td>98%</td>
<td>无数据</td>
</tr>

<tr>
<td>轻量级</td>
<td>5</td>
<td>5</td>
<td>4</td>
<td>2</td>
</tr>

<tr>
<td>文档</td>
<td>5</td>
<td>5</td>
<td>5</td>
<td>1</td>
</tr>

<tr>
<td>社区</td>
<td>5</td>
<td>5</td>
<td>5</td>
<td>3</td>
</tr>
</tbody>
</table>

<h2 id="如何选择">如何选择</h2>

<p>首先，排除掉 go-spring 在生产环境使用。在以上对比中表现较差。</p>

<p>剩余三个质量均比较高</p>

<ul>
<li>如果想使用编译时依赖注入，使用 wire 是最好的选择</li>
<li>如果想使用运行时依赖植入，且只想用依赖注入特性，dig 是较好的选择</li>
<li>如果需求不仅仅是依赖注入，而是想找一个轻量级 App 框架，name fx 可以满足需求</li>
</ul>

<p>但是，这三个依赖注入库均存在能力不足的问题。因此，基于 dig，开发了 digpro。</p>

<p>更多参见： <a href="/posts/digpro/">digpro</a></p>
]]></description></item><item><title>依赖注入</title><link>https://www.rectcircle.cn/posts/dependency-injection/</link><pubDate>Sun, 03 Oct 2021 16:10:30 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/dependency-injection/</guid><description type="html"><![CDATA[

<h2 id="简述">简述</h2>

<p>关于依赖注入，比较权威的讨论来源于 Martin Fowler 的 <a href="https://martinfowler.com/articles/injection.html">Inversion of Control Containers and the Dependency Injection pattern</a> （<a href="https://www.cnblogs.com/me-sa/archive/2008/07/30/iocdi.html">中文翻译</a>）一文。</p>

<p>该文以 “查询某导演执导的电影列表” 服务为例，描述了 “服务” 和 “组件” 的概念，控制反转的概念，依赖注入的形式，服务定位器模式 等内容。</p>

<h2 id="推演">推演</h2>

<h3 id="问题引出">问题引出</h3>

<p>编程语言作为人和硬件沟通的桥梁，其从面向机器、面向过程，到面向对象，面向接口的发展中，有了越来越好的抽象能力。</p>

<p>抽象意味着，一个程序，由将会拆分成多个相对独立而又相互依赖的组件。</p>

<p>因为进行了拆分，对应的，如何将各个组件按照其依赖关系组合起来，就是每一个程序，特别是大型程序，需要解决的问题。</p>

<h3 id="问题转换">问题转换</h3>

<p>假设将 A 依赖 B，表示为 <code>A -&gt; B</code>。那么，就可以用一个有向图的边来表示组件与组件之间的依赖关系。</p>

<p>因此问题就被转化为：构造一个依赖图，即组件为节点，依赖关系为有向边的有向图。</p>

<h3 id="手动处理">手动处理</h3>

<p>假设所有组件都已经开完成了，开发人员可以按照组件依赖图的关系，从有向图的起点按照顺序，对组件进行组装。</p>

<p>最终将可以得到一个完成的程序。</p>

<h3 id="依赖注入">依赖注入</h3>

<p>手段处理，对于组件较多的场景，比较繁琐。考虑是否可以自动的执行手动处理的过程，答案是肯定的，这就是依赖注入。</p>

<p>因为问题本身就是构造一个有向图，因此只需知道每个节点和每个节点的入边，即可完备的表述出这个张图。</p>

<p>具体而言，假设有一个注入器，只需要个向这个注入器中声明每个组件以及每个组件的依赖组件，那么这个注入器即可自动的对组件进行组装。</p>

<p>最终将可以得到一个完成的程序。</p>

<h2 id="实现">实现</h2>

<h3 id="编译时-vs-运行时">编译时 VS 运行时</h3>

<ul>
<li>编译时注入，依赖注入器工作在编译阶段，利用编程语言提供的编译时能力（比如代码生成/宏），在编译阶段直接生成代码。

<ul>
<li>优点

<ul>
<li>尽早的暴露问题，编译阶段即可发现问题</li>
<li>无性能损失，和手动编写性能一致</li>
</ul></li>
<li>缺点

<ul>
<li>灵活性较差</li>
<li>使用是来相对不够方便</li>
</ul></li>
</ul></li>
<li>运行时注入，依赖注入器工作在运行阶段，利用编程语言提供的反射能力，在运行时构造组件，完成依赖注入

<ul>
<li>优点

<ul>
<li>灵活性好，扩展型好</li>
<li>用起来相对方便</li>
</ul></li>
<li>缺点

<ul>
<li>如果存在问题，需要运行时才能暴露出来</li>
<li>有性能损失，初始化耗时将大大增加</li>
</ul></li>
</ul></li>
</ul>

<p>目前主流的依赖注入库/框架为：运行时</p>

<h3 id="构造函数注入-vs-属性注入">构造函数注入 VS 属性注入</h3>

<h4 id="构造函数注入">构造函数注入</h4>

<p>该方式用户需要提供一个包含所有依赖的构造函数，才能使用依赖注入。</p>

<p>优点</p>

<ul>
<li>对使用者来说，基本无侵入</li>
</ul>

<p>缺点</p>

<ul>
<li>无法处理循环依赖，因此依赖图就退化成了有向无环图，某些特殊场景可能无法得到支持</li>
<li>需要编写一个构造函数，如果依赖比较多，构造函数的参数会过多会显得有些混乱，另外构造函数基本上都是一些样板代码</li>
</ul>

<h4 id="属性注入">属性注入</h4>

<p>使用者需要提供分别提供一个，类型 T 和 依赖的组件类型列表 deps，其中 deps 可以通过注解或tag或配置文件进行声明。</p>

<p>优点</p>

<ul>
<li>可以支持循环依赖的情况</li>
<li>无需编写大量样板的构造函数</li>
<li>对使用者来说更方便一些</li>
</ul>

<p>缺点</p>

<ul>
<li>注解和 tag 有一定的侵入性</li>
<li>可能破坏了私有依赖的不可见性</li>
</ul>

<h4 id="如何选择">如何选择</h4>

<p>依赖注入器，可能会支持如上两种中的一种或者都支持。从使用者角度，优先使用支持属性依赖注入的依赖注入库或框架。</p>

<h3 id="声明方式">声明方式</h3>

<ul>
<li>配置文件</li>
<li>注解/Tag方式</li>
<li>编程方式</li>
</ul>

<p>推荐使用编程方式+注解/tag方式</p>

<h3 id="运行时依赖注入流程">运行时依赖注入流程</h3>

<p>依赖注入器使用者，在注册一个组件时，提供的的信息一般称为 <code>Provider</code>，该 <code>Provider</code> 的构造函数声明为 <code>new Provider(constructor: function, propertyDeps: []type)</code></p>

<ul>
<li>对于构造函数依赖注入 <code>new Provider(某个构造函数, null)</code></li>
<li>对于属性依赖注入 <code>new Provider(空构造函数, 属性依赖列表)</code></li>
<li>对于构造函数依赖注入 和 属性依赖注入同时使用 <code>new Provider(某个构造函数, 属性依赖列表)</code></li>
</ul>

<p>此时流程为（仅表述流程，具体实现应该有所优化）</p>

<ul>
<li><code>Provider</code>

<ul>
<li>有四种状态 （<code>registered</code>, <code>created</code>, <code>injected</code>, <code>error</code>）</li>
<li>存在一个 <code>cache</code> 字段，记录构建出的组件</li>
<li>存在一个 <code>passed</code> 字段，判断是否遍历过</li>
</ul></li>
<li>用户通过多种方式声明 <code>Provider</code>，此时这些 <code>Provider</code> 的状态为 <code>registered</code></li>
<li>用户需要提取某个组件时

<ul>
<li>构造阶段，递归<strong>后续</strong>遍历该 <code>Provider</code>

<ul>
<li>如果已经遍历过了，设置状态为 <code>error</code>（构造函数循环依赖），返回异常</li>
<li>如果该 <code>Provider</code> 的状态为 <code>error</code> 直接抛出异常</li>
<li>如果该 <code>Provider</code> 的状态为 <code>created</code>、<code>injected</code>，返回 <code>cache</code></li>
<li>如果该 <code>Provider</code> 的状态为 <code>registered</code>

<ul>
<li>检查 <code>constructor</code> 的每个依赖项目

<ul>
<li>如果发现其依赖项不存在 <code>Provider</code>，设置状态为 <code>error</code>（依赖不存在），返回异常</li>
<li>如果发现其依赖项存在 <code>Provider</code>，<strong>递归调用</strong>，如果返回 <code>error</code>，设置状态为 <code>error</code>，返回异常</li>
</ul></li>
<li>使用依赖项返回值作为参数，调用该 <code>Provider</code> 的 <code>constructor</code> 并记录 <code>cache</code>，状态这是为 <code>created</code></li>
</ul></li>
<li>返回 <code>cache</code></li>
</ul></li>
<li>属性注入阶段，递归<strong>后续</strong>遍历

<ul>
<li>如果该 <code>Provider</code> 的状态为 <code>error</code> 直接抛出异常</li>
<li>如果该 <code>Provider</code> 的状态为 <code>injected</code> 返回 <code>cache</code></li>
<li>如果该 <code>Provider</code> 的状态为 <code>registered</code>，调用<strong>构造阶段</strong>函数，<strong>不返回</strong></li>
<li>如果该 <code>Provider</code> 的状态为 <code>created</code>

<ul>
<li>先将该 <code>Provider</code> 的状态为 <code>injected</code></li>
<li>检查 <code>propertyDeps</code> 的每个依赖项目

<ul>
<li>如果发现其依赖项不存在 <code>Provider</code>，设置状态为 <code>error</code>（依赖不存在），返回异常</li>
<li>如果发现其依赖项存在 <code>Provider</code>，<strong>递归调用</strong>，如果返回 <code>error</code>，设置状态为 <code>error</code>，返回异常</li>
</ul></li>
<li>使用依赖项返回值作为参数，执行属性注入逻辑并记录 <code>cache</code></li>
</ul></li>
<li>返回 <code>cache</code></li>
</ul></li>
</ul></li>
</ul>

<h2 id="如何评价一个依赖注入库-框架">如何评价一个依赖注入库/框架</h2>

<ul>
<li>口碑

<ul>
<li>维护组织</li>
<li>stars</li>
</ul></li>
<li>不可兼得

<ul>
<li>运行时（更佳）/编译时</li>
<li>轻量级（更佳）/重量级</li>
<li>库/框架</li>
</ul></li>
<li>Features

<ul>
<li>构造器注入</li>
<li>属性注入</li>
<li>提供常量值</li>
<li>接口绑定</li>
<li>对象命名</li>
<li>对象组（注入同类型多想多个组成一个切片）</li>
<li>循环依赖</li>
</ul></li>
<li>质量

<ul>
<li>侵入性</li>
<li>测试覆盖度</li>
<li>文档丰富度</li>
<li>社区活跃度</li>
</ul></li>
</ul>
]]></description></item><item><title>家庭无线组网</title><link>https://www.rectcircle.cn/posts/home-wireless-network-construction/</link><pubDate>Thu, 30 Sep 2021 10:51:17 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/home-wireless-network-construction/</guid><description type="html"><![CDATA[

<h2 id="技术和标准">技术和标准</h2>

<h3 id="互联网标准">互联网标准</h3>

<h4 id="ieee-802">IEEE 802</h4>

<p>目前，工业界/民用的局域网标准是 <a href="https://en.wikipedia.org/wiki/IEEE_802">IEEE 802</a>。</p>

<p>IEEE 802 定义了 <a href="https://zh.wikipedia.org/zh-hans/OSI%E6%A8%A1%E5%9E%8B">OSI 网络模型</a>的最底两层：物理层和数据链路层。</p>

<p>目前仍在广泛在使用的协议是：</p>

<ul>
<li>IEEE 802.1 - Higher Layer LAN Protocols Working Group 主要包括局域网体系结构、网络互联、网络管理、性能测试</li>
<li>IEEE 802.3 - <a href="https://en.wikipedia.org/wiki/Ethernet">以太网</a></li>
<li>IEEE 802.11 - Wireless LAN (WLAN) &amp; Mesh (Wi-Fi certification)</li>
</ul>

<h4 id="ietf-标准">IETF 标准</h4>

<p>IETF 即 互联网工程任务组，制定了一系列互联网相关标准。其最重要的贡献是 <a href="https://zh.wikipedia.org/wiki/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F">TCP/IP 协议族</a>，其标准以 <a href="https://en.wikipedia.org/wiki/List_of_RFCs">RFC 方式</a>在互联网上开放。</p>

<p>其关注 OSI 上层相关的定义。对于 OSI 最底两层（物理层和数据链路层），IETF 相关标准只感知到 MAC 地址。</p>

<h3 id="wifi-技术">WIFI 技术</h3>

<h4 id="概述">概述</h4>

<p>WIFI 是对 IEEE 802.11 相关协议的实现。目前在广泛使用的是，WIFI 4、5、6 三代技术标准。</p>

<p>过去， WIFI 联盟 使用 <code>IEEE 802.11 xxx</code> 技术标准来对相关设备进行认证。对与普通用户难以理解和认知。因此，2019 年起，WIFI 现在开始使用世代命令方法：</p>

<ul>
<li>WIFI 4 - IEEE 802.11n</li>
<li>WIFI 5 - IEEE 802.11ac</li>
<li>WIFI 6 - IEEE 802.11ax</li>
</ul>

<h4 id="wifi-4-5-6-对比">WIFI 4 5 6 对比</h4>

<p>|  | WIFI 4 | WIFI 5 | WIFI 6 |
|&ndash;|&mdash;&mdash;&ndash;|&mdash;&mdash;&ndash;|&mdash;&mdash;&ndash;|
| 协议| IEEE 802.11n | IEEE 802.11ac | IEEE 802.11ax |
| 年份 | 2009 | Ware1 - 2013 / Ware2 - 2016 | 2018+ |
| 频段 | 2.4 GHz / 5 GHz |  5 GHz | 2.4 GHz / 5 GHz |
| 最大频宽 | 40 MHz | 80 MHz / 160 MHz | 160 MHz |
| MCS 范围 | 0-7 | 0-9 | 0 -11 |
| 最高调制 | 64QAM | 256QAM | 1024QAM |
| 单流带宽 | 150 Mbps | 433 / 867 Mbps | 1021 Mbps |
| 最大空间流 | 4*4 | 8*8 | 8*8 |
| MU-MIMO |  | 下行 | 上/下行 |
| OFDMA | | | 上/下行 |</p>

<p>MU-MIMO 和 OFDMA 参见： <a href="https://zhuanlan.zhihu.com/p/212162678">文章</a></p>

<h4 id="ieee-802-11k-v-r">IEEE 802.11k/v/r</h4>

<p>用于实现无缝漫游</p>

<ul>
<li>802.11k Radio Resource Measurment 无线资源管理，如果当前接入点的信号强度变弱时，AP 将发送“优化的频道列表”，设备将进行扫描来确定是否有此列表中的 AP。</li>
<li>802.11r Fast Basic Service Set Transition 快速BSS切换，用于简化握手协议，降低握手耗时</li>
<li>802.11v WNM (Wireless Network Management) 基于 802.11k，向终端发送更多负载均衡相关信息，以辅助终端进行切换</li>
</ul>

<h4 id="ieee-802-11s">IEEE 802.11s</h4>

<p>Hybrid WirelESS Mesh Protocol,HWMP 混合无线Mesh 协议。众多宣称支持 Mesh 的家用型路由器为该协议的实现。</p>

<h2 id="术语-概念-黑话">术语/概念/黑话</h2>

<ul>
<li>路由器 - 三层网络设备，工作在 OSI 第三层（网络层）。普通的家用路由器，主要负责连接连接两个网段（子网）</li>
<li>路由器 LAN - 在家用级路由器中，一般存在多个 LAN 口，用来连接内网设备。广义上来说，通过 WIFI 连入该路由器的设备也是通过 LAN 口连接的（专有名词为 WLAN）</li>
<li>路由器 WAN - 在家用级路由器中，一般存在一个 WAN 口，用来上层网络/外网。</li>
<li>主路由 - 直接连接光猫，负责宽带配置、DHCP 以及和上层网络通讯的职责，通过 WAN 和上层网络连接</li>
<li>旁路由 - 又称旁路网关，负责一些而外的职责，将自己的 LAN 口（推荐）和主路由的 LAN 口连接，主要有两种类型的旁路由：

<ul>
<li>设备自行选择网关设备（旁路由关闭 DHCP，更改自己的 IP 为主路由 DHCP 外的地址）</li>
<li>所有设备通过旁路由联网（旁路由关闭 DHCP，更改自己的 IP 为主路由 DHCP 外的地址；主路由 DHCP 默认网关配置为旁路由）</li>
</ul></li>
<li>AP - Wireless Access Point 无线接入，一般语境下 AP 代指 WIFI 接入点</li>
<li>AC - Wireless Access Point Controller 无线控制器，主要应用于对 AP 管理，一般是企业级无线组网的核心设备</li>
</ul>

<h2 id="无线组网方案">无线组网方案</h2>

<h3 id="目标">目标</h3>

<ul>
<li>全覆盖</li>
<li>无缝漫游（在不同 AP 间移动可以自动无感切换）</li>
<li>相同内网（不同AP接入进入统一个内网）</li>
<li>异地组网（暂不涉及）</li>
</ul>

<h3 id="桥接">桥接</h3>

<p><a href="https://fenghe.us/padavan-ap-wireless-isp-lan-bridge/">https://fenghe.us/padavan-ap-wireless-isp-lan-bridge/</a></p>

<p>不推荐，该方案无法实现无缝漫游</p>

<h4 id="ap-client-ap-wan-wireless-isp">AP Client &amp; AP - WAN (Wireless ISP)</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    外网
     |
     |
    路由 A (内网 A, SSID A, 开启 DHCP)
     |
     |
    路由 B (内网 B, SSID A, 开启 DHCP)</pre></div>
<h4 id="ap-client-ap-lan-bridge">AP Client &amp; AP - LAN Bridge</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">    外网
     |
     |
    路由 A (内网 A, SSID A, 开启 DHCP, 路由 B添加到静态路由表)
     |
     |
    路由 B (内网 A, SSID B, 关闭 DHCP)</pre></div>
<h3 id="ac-ap">AC + AP</h3>

<p><a href="https://zhuanlan.zhihu.com/p/100999740">参考</a></p>

<p>历史相对比较悠久，设备之间通过有线连接，采用中心式的管理方式，AC 可能存在单点故障。一般为企业级私有协议实现。价格昂贵。</p>

<h3 id="mesh">Mesh</h3>

<p><a href="https://zhuanlan.zhihu.com/p/100999740">参考</a></p>

<p>分布式无线连接，效果上基本完全兼容 AC + AP。诞生时间较短，因此没有 AC + AP 成熟，是未来的趋势。有标准的协议支持（IEEE 802.11s）。价格比较亲民。</p>

<p>设备之间，既可以通过有线连接（有线回程），也可以通过无线连接（无线回程）。</p>

<h2 id="无线组网实施">无线组网实施</h2>

<h3 id="网络">网络</h3>

<p>直接购买各大品牌的支持 Mesh 的 WIFI6 路由器，并尽量采用有线回程。</p>

<h3 id="第三方固件方案">第三方固件方案</h3>

<ul>
<li><a href="https://post.smzdm.com/p/a6l8r8wn/">集客OS</a>，<a href="https://new.qq.com/omn/20191114/20191114A0M76200.html">集客OS 文章 2</a></li>
<li><a href="https://post.smzdm.com/p/a992e0l5/">OpenWrt 方案</a>，<a href="https://post.smzdm.com/p/aqxzgn6x/">OpenWrt 方案 Easy Mesh</a></li>
<li><a href="https://blog.csdn.net/weixin_43868990/article/details/113820013">Padavan Mesh</a></li>
</ul>
]]></description></item><item><title>常见的权限模型</title><link>https://www.rectcircle.cn/posts/permission-model/</link><pubDate>Mon, 27 Sep 2021 16:11:13 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/permission-model/</guid><description type="html"><![CDATA[

<h2 id="名词定义">名词定义</h2>

<ul>
<li>资源 (Subject)，是后端系统对实体的抽象，需要进行权限控制的目标（暂不用 Resource 名词）</li>
<li>主体 (Principal)，表示一个或者多个用户，对资源操作的发起方</li>
<li>权限 (Permission)，动词，表示主体对资源的一种操作</li>
<li>角色 (Role)，名词，表示一组权限的集合</li>
</ul>

<h2 id="acl-dcl">ACL &amp; DCL</h2>

<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Access-control_list">wikipedia - ACL</a></p>
</blockquote>

<p>access-control list (ACL) 访问控制表。逻辑上，每种资源都拥有一张 acls 表，该表主要有两个字段：主体和赋予的权限集合。包含 Resource、Principal 和 Permission 三种实体</p>

<p>关系</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">                                                            |-------------&gt; Permission （读）
                      |------------&gt; Principal （用户 1）-------------------&gt;  Permission （写）
Subject （文件 A）-----
                      |------------&gt; Principal （用户组 2）-----------------&gt;  Permission （读）</pre></div>
<p>表设计： <code>acls</code></p>

<table>
<thead>
<tr>
<th>约束</th>
<th>字段名</th>
<th>数据类型</th>
<th>描述</th>
</tr>
</thead>

<tbody>
<tr>
<td>PK</td>
<td>id</td>
<td>bigint unsigned</td>
<td>主键</td>
</tr>

<tr>
<td>UNIQ_SUBJECT(1)</td>
<td>subject_type</td>
<td>varchar(32)</td>
<td>资源类型</td>
</tr>

<tr>
<td>UNIQ_SUBJECT(2)</td>
<td>subject_id</td>
<td>varchar(64)</td>
<td>资源 id</td>
</tr>

<tr>
<td>UNIQ_PRINCIPAL(1)</td>
<td>principal_type</td>
<td>varchar(36)</td>
<td>访问主体类型</td>
</tr>

<tr>
<td>UNIQ_PRINCIPAL(2)</td>
<td>principal_id</td>
<td>varchar(64)</td>
<td>访问主体 id</td>
</tr>

<tr>
<td></td>
<td>permission</td>
<td>varchar(32)</td>
<td>权限标识符</td>
</tr>
</tbody>
</table>

<p>ACL 主要应用场景为：文件系统访问控制、网络控制等以资源为核心抽象的系统。</p>

<p>DLC 相较 ALC 区别在于，“自主（Discretionary）”控制即，拥有该权限的用户可以将权限赋给其他用户</p>

<h2 id="mac">MAC</h2>

<p>Mandatory Access Control  (MAC) 强制访问控制，适用于不同密级保密场景，可以看如下一个例子：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">资源配置表
        资源: 财务文档
                主体: 财务人员
                等级：机密级
                操作：查看
主体配置表
    
        主体: 李女士
                类别: 财务人员
                等级：机密级</pre></div>
<h2 id="rbac">RBAC</h2>

<blockquote>
<p><a href="https://tsejx.github.io/blog/architect-design-based-on-rbac/">基于 RBAC 权限模型的架构设计</a></p>
</blockquote>

<p>Role Based Access Control (RBAC) 基于角色的权限访问控制。在逻辑上，可以为每个 User 赋予几种 Role，另外 Role 关联几个权限。包含 User Role 和 Permission 三种实体。</p>

<p><img src="/image/permission-model-rbac.png" alt="rbac" /></p>

<p>RBAC 主要应用场景为：企业信息管理系统。</p>

<h2 id="abac">ABAC</h2>

<p>Attribute-Based Access Control (ABAC) 基于属性的权限验证。通过编写权限规则 + 规则引擎来实现权限控制。针对复杂场景可以使用该模型，但是不够直观，理解比较困难。更多参见：<a href="https://juejin.cn/post/6941734947551969288#heading-5">文章</a></p>

<h2 id="reference">Reference</h2>

<ul>
<li><a href="https://zhuanlan.zhihu.com/p/70548562">https://zhuanlan.zhihu.com/p/70548562</a></li>
<li><a href="https://www.jianshu.com/p/ce0944b4a903">https://www.jianshu.com/p/ce0944b4a903</a></li>
</ul>
]]></description></item><item><title>一次 SpringBoot2 性能问题的定位和解决</title><link>https://www.rectcircle.cn/posts/a-java-spring-boot2-performance-problem-positioning-and-resolution/</link><pubDate>Wed, 22 Sep 2021 22:39:58 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/a-java-spring-boot2-performance-problem-positioning-and-resolution/</guid><description type="html"><![CDATA[

<h2 id="现象">现象</h2>

<p>对一个开源项目 <a href="https://github.com/eclipse/openvsx">https://github.com/eclipse/openvsx</a> 进行一定的改造后，进行了私有化部署。该项目是一个典型的 Java 项目，技术栈如下：</p>

<ul>
<li>Java 11</li>
<li>Spring Boot 2</li>
<li>Spring Security</li>
<li>Spring Session</li>
<li>JPA</li>
<li>Spring Data Elasticsearch</li>
</ul>

<p>运行几个月后，出现如下问题：</p>

<p>后端有多台实例，流量大的实例所有接口均非常缓慢，均大于 5 秒，流量小的实例，接口延迟正常。</p>

<p>另外，数据库为多个项目公用，负载本身就很高。</p>

<h2 id="定位过程">定位过程</h2>

<h3 id="尝试复现">尝试复现</h3>

<ul>
<li>使用 IDE 启动 Debug，并连接到生产环境的数据库。</li>
<li><code>curl</code> 或 Postman 请求Debug，观察延迟。结果：接口延迟正常。</li>

<li><p>使用压测软件 <code>ab</code>，进行压测。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 安装</span>
sudo apt update
sudo apt install -y apache2-utils
<span style="color:#75715e"># 压测 QPS 比较高接口</span>
ab -n <span style="color:#ae81ff">10000</span> -c <span style="color:#ae81ff">100</span> <span style="color:#e6db74">&#39;http://localhost:xxx/xxx1&#39;</span> <span style="color:#75715e"># 纯数据库查询 接口</span>
ab -n <span style="color:#ae81ff">10000</span> -c <span style="color:#ae81ff">100</span> <span style="color:#e6db74">&#39;http://localhost:xxx/xxx2&#39;</span> <span style="color:#75715e"># 包含 ES 查询 接口</span>
<span style="color:#75715e"># 同时在执行压测的时候，另起一个 terminal，curl 其他接口，观察延迟</span>
curl http://localhost:xxx/xxx2</code></pre></div></li>

<li><p>以上压测结果显示，当压测接口并发度比较高的时候，延迟将上升，且其他包含数据库请求的接口都变得缓慢。</p></li>
</ul>

<p>基本可以确认，是数据库的问题。推测是数据库连接池耗尽。</p>

<h3 id="问题确认">问题确认</h3>

<h4 id="手动打日志">手动打日志</h4>

<p>在压测 QPS 高的接口时，在其他接口添加如下逻辑，获取拿到第一个数据库连接的耗时日志。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">long</span> <span style="color:#a6e22e">start</span> <span style="color:#f92672">=</span> 0, end <span style="color:#f92672">=</span> 0;
var <span style="color:#a6e22e">hds</span> <span style="color:#f92672">=</span>  (HikariDataSource) <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">datasource</span>;
<span style="color:#66d9ef">try</span> {
    start <span style="color:#f92672">=</span> System.<span style="color:#a6e22e">currentTimeMillis</span>();
    var <span style="color:#a6e22e">c</span> <span style="color:#f92672">=</span> hds.<span style="color:#a6e22e">getConnection</span>();
    end <span style="color:#f92672">=</span> System.<span style="color:#a6e22e">currentTimeMillis</span>();
    logger.<span style="color:#a6e22e">info</span>(<span style="color:#e6db74">&#34;test get connection spend time: &#34;</span> <span style="color:#f92672">+</span> (end<span style="color:#f92672">-</span>start));
} <span style="color:#66d9ef">catch</span> (SQLException <span style="color:#a6e22e">e</span>) {
    e.<span style="color:#a6e22e">printStackTrace</span>();
}</code></pre></div>
<p>经测试，发现在并发 100 的场景下，获取一个数据库连接耗时月 4 秒。</p>

<h4 id="打开-hikari-连接池日志">打开 Hikari 连接池日志</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># src/main/resources/application.yml</span>
logging.level.com.zaxxer.hikari.pool.HikariPool: trace</code></pre></div>
<p>同样执行压测，发现，waiting 状态的申请数据库连接的数目高达 70。</p>

<p>经过以上定位。可以确定，是数据库连接池耗尽导致的性能问题。</p>

<h2 id="问题分析">问题分析</h2>

<p>由于该项目使用的是 JPA，即 Hibernate，且存在大量的级联操作。同时 QPS 高得那个请求，每个请求，均需查询大量的数据。另外随着时间推移，QPS 不断上升，数据库连接池时刻处于耗尽状态，导致，所有请求均需排队等待数据库连接，导致整体接口响应缓慢。</p>

<h2 id="解决方案">解决方案</h2>

<p>以下三种方案，能支持的 QPS 依次增加。</p>

<h3 id="方案-1-扩大数据库连接池">方案 1：扩大数据库连接池</h3>

<p>该方案，最简单，但是上限就是数据库的最大连接数。如果数据库负载已经很高了，该方案就无法使用了。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># src/main/resources/application.yml</span>
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      maximum-pool-size: <span style="color:#ae81ff">100</span></code></pre></div>
<h3 id="方案-2-数据库读写分离">方案 2：数据库读写分离</h3>

<p>该方案，需数据库实现了读写分离，且 QPS 上限线比单纯数据库连接池方案高很多，且可以通过添加从库的方案进行水平扩容。另外，对业务代码没有侵入较小。是一个比较好方案。</p>

<p>需要特别注意的是，可能存在读写库同步延迟，可能导致不一致。</p>

<p>实现参考：</p>

<ul>
<li><a href="https://ethendev.github.io/2018/12/17/JPA-MySQL-read-write-separation/">博客 1</a></li>
<li><a href="https://juejin.cn/post/6844904175424241677#heading-6">博客 2</a></li>
</ul>

<p>思路说明（Spring Boot 2）：</p>

<ul>
<li>原本，单数据源，只有一个 DataSource 对象。支持多数据源后，将需要初始化三个 DataSource

<ul>
<li>masterDataSource（可以不放到 Spring 容器中）</li>
<li>slaveDataSource （可以不放到 Spring 容器中）</li>
<li>dynamicDataSource （注意该数据源不能有名字，必须是 <code>@Bean</code> 的方式声明）</li>
</ul></li>
<li>切换 DataSource，有两种方式

<ul>
<li>方式 1：根据 <code>@Transactional</code> 是否是 ReadOnly 自动切换（没有走通）</li>
<li>方式 2：通过 <code>ThreadLocal</code> 实现一个切换 <code>Context</code>，来记录切换情况，下文将以本方式为例，注意注意 ThreadLocal 读过一次后就立即设置为默认值，防止在一个线程中的请求，前面的影响后面的。</li>
</ul></li>
<li>其他依赖 DataSource 的 Spring 组件，防止出现无法写入问题，需手动配置为 <code>masterDataSource</code> 数据源。比如 Spring Session，可以通过 <code>@SpringSessionDataSource</code> 方式来配置</li>
</ul>

<p><strong>配置文件规划</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># src/main/resources/application.yml</span>
spring:
  datasource:
    master:
      url: xxx
    slave:
      url: xxx</code></pre></div>
<p><strong>Java Config</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#a6e22e">@Configuration</span>
<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> MyConfig {

    <span style="color:#a6e22e">@Autowired</span>
    <span style="color:#a6e22e">@Qualifier</span>(value <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dataSourceMasterProperties&#34;</span>)
    Properties <span style="color:#a6e22e">masterProperties</span>;

    <span style="color:#a6e22e">@Bean</span>(name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dataSourceMasterProperties&#34;</span>)
    <span style="color:#a6e22e">@ConfigurationProperties</span>(prefix <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;spring.datasource.master&#34;</span>)
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Properties</span> masterProperties() {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Properties();
    }

    <span style="color:#a6e22e">@Autowired</span>
    <span style="color:#a6e22e">@Qualifier</span>(value <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dataSourceSlaveProperties&#34;</span>)
    Properties <span style="color:#a6e22e">slaveProperties</span>;

    <span style="color:#a6e22e">@Bean</span>(name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dataSourceSlaveProperties&#34;</span>)
    <span style="color:#a6e22e">@ConfigurationProperties</span>(prefix <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;spring.datasource.slave&#34;</span>)
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Properties</span> slaveProperties() {
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Properties();
    }

    DataSource <span style="color:#a6e22e">masterDataSource</span>;

    <span style="color:#a6e22e">@Bean</span>(<span style="color:#e6db74">&#34;masterDataSource&#34;</span>)
    <span style="color:#a6e22e">@Qualifier</span>(<span style="color:#e6db74">&#34;masterDataSource&#34;</span>)
	<span style="color:#a6e22e">@SpringSessionDataSource</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DataSource</span> masterDataSource() {
        <span style="color:#66d9ef">if</span> (masterDataSource <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
            <span style="color:#66d9ef">return</span> masterDataSource;
        }
        HikariDataSource <span style="color:#a6e22e">dataSource</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> HikariDataSource();
        dataSource.<span style="color:#a6e22e">setDataSourceProperties</span>(masterProperties);
        <span style="color:#75715e">// 其他配置
</span><span style="color:#75715e"></span>        masterDataSource <span style="color:#f92672">=</span> dataSource;
        <span style="color:#66d9ef">return</span> dataSource;
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DataSource</span> slaveDataSource() {
        HikariDataSource <span style="color:#a6e22e">dataSource</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> HikariDataSource();
        dataSource.<span style="color:#a6e22e">setDataSourceProperties</span>(slaveProperties);
        <span style="color:#75715e">// 其他配置
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">return</span> dataSource;
    }

    <span style="color:#a6e22e">@Bean</span>
    <span style="color:#a6e22e">@Primary</span>
    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DataSource</span> dynamicDataSource() <span style="color:#66d9ef">throws</span> <span style="color:#a6e22e">IOException</span> {
        Map<span style="color:#f92672">&lt;</span>Object, Object<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">targetDataSources</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> HashMap<span style="color:#f92672">&lt;&gt;</span>();
        targetDataSources.<span style="color:#a6e22e">put</span>(DynamicRoutingDataSourceContext.<span style="color:#a6e22e">MASTER</span>, masterDataSource());
        targetDataSources.<span style="color:#a6e22e">put</span>(DynamicRoutingDataSourceContext.<span style="color:#a6e22e">SLAVE</span>, slaveDataSource());
        AbstractRoutingDataSource <span style="color:#a6e22e">dynamicDataSource</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> AbstractRoutingDataSource() {
            <span style="color:#a6e22e">@Override</span>
            <span style="color:#66d9ef">protected</span> <span style="color:#a6e22e">Object</span> determineCurrentLookupKey() {
                <span style="color:#66d9ef">try</span> {
                    <span style="color:#66d9ef">return</span> DynamicRoutingDataSourceContext.<span style="color:#a6e22e">getDataSourceKey</span>();
                } <span style="color:#66d9ef">finally</span> {
                    <span style="color:#75715e">// 一定要 reset
</span><span style="color:#75715e"></span>                    DynamicRoutingDataSourceContext.<span style="color:#a6e22e">reset</span>();
                }
            }
        };

        dynamicDataSource.<span style="color:#a6e22e">setDefaultTargetDataSource</span>(targetDataSources.<span style="color:#a6e22e">get</span>(DynamicRoutingDataSourceContext.<span style="color:#a6e22e">MASTER</span>));
        dynamicDataSource.<span style="color:#a6e22e">setTargetDataSources</span>(targetDataSources);
        <span style="color:#66d9ef">return</span> dynamicDataSource;
    }
}</code></pre></div>
<p><strong>切换数据源 Context</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">class</span> DynamicRoutingDataSourceContext {

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">String</span> MASTER <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;master&#34;</span>;

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">String</span> SLAVE <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;slave&#34;</span>;

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">ThreadLocal</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">threadLocalDataSourceKey</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> ThreadLocal<span style="color:#f92672">&lt;&gt;</span>();

    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setRoutingDataSourceKey</span>(String <span style="color:#a6e22e">dataSource</span>) {
        threadLocalDataSourceKey.<span style="color:#a6e22e">set</span>(dataSource);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setMaster</span>() {
        setRoutingDataSourceKey(MASTER);
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setSlave</span>() {
        setRoutingDataSourceKey(SLAVE);
    }


    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> String <span style="color:#a6e22e">getDataSourceKey</span>() {
        var <span style="color:#a6e22e">k</span> <span style="color:#f92672">=</span> threadLocalDataSourceKey.<span style="color:#a6e22e">get</span>();
        <span style="color:#66d9ef">if</span> (k <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span>) {
            <span style="color:#66d9ef">return</span> MASTER;
        }
        <span style="color:#66d9ef">return</span> k;
    }

    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">reset</span>() {
        threadLocalDataSourceKey.<span style="color:#a6e22e">remove</span>();
	}
}</code></pre></div>
<p><strong>只读的 Service 方法设置为读库</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">XxxDTO</span> getXxx() {
    DynamicRoutingDataSourceContext.<span style="color:#a6e22e">setSlave</span>();
    <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}</code></pre></div>
<h3 id="方案-3-添加缓存">方案 3：添加缓存</h3>

<ul>
<li>通过 Nginx 添加缓存，参考：<a href="https://www.app-scope.com/tutorial/configure-caching-with-nginx.html">博客</a>。</li>
<li>业务侧手动添加缓存。</li>
</ul>

<h2 id="经验教训">经验教训</h2>

<ul>
<li>对性能有一定要求的场景，尽量不要使用 JPA 这种不可控的 ORM。</li>
</ul>
]]></description></item><item><title>开发者角度理解 XSS 攻击</title><link>https://www.rectcircle.cn/posts/xss-by-developer/</link><pubDate>Mon, 16 Aug 2021 21:05:19 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/xss-by-developer/</guid><description type="html"><![CDATA[

<h2 id="什么是-xss">什么是 XSS</h2>

<p>Cross-site scripting 跨站脚本攻击。</p>

<p>攻击者输入的内容，会被渲染到其他用户页面的 HTML 中，导致攻击代码被执行。</p>

<h2 id="发生条件">发生条件</h2>

<ul>
<li>攻击者输入的内容，会被渲染到其他用户页面的 HTML 中。</li>
</ul>

<h2 id="发生后果">发生后果</h2>

<ul>
<li>盗取用户身份信息，如 Cookie</li>
<li>破坏正常功能</li>
</ul>

<h2 id="举例">举例</h2>

<ul>
<li>留言板 / 论坛，直接将用户发布的内容未经校验，直接渲染到页面中去了。</li>
<li>允许上产 svg 文件，并直接渲染到页面中</li>
<li>浏览器重定向， <code>a</code> 标签的 <code>href</code>、 <code>location.href</code> 的内容由用户输入，可以执行任意脚本，比如 <code>location.href=javascript:alert('abc')</code></li>
</ul>

<h2 id="更多参见">更多参见</h2>

<p><a href="https://tech.meituan.com/2018/09/27/fe-security.html">美团博客</a></p>
]]></description></item><item><title>开发者角度理解 SSRF 攻击</title><link>https://www.rectcircle.cn/posts/ssrf-by-developer/</link><pubDate>Mon, 16 Aug 2021 20:33:37 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/ssrf-by-developer/</guid><description type="html"><![CDATA[

<h2 id="ssrf-是什么">SSRF 是什么</h2>

<p>Server-Side Request Forgery 服务器端请求伪造。</p>

<p>指，攻击者让服务端发起一个绕过权限控制的 HTTP 请求，并获取到了相关数据。</p>

<p>说人就是话：后端需要发起请求，但请求URL来自用户</p>

<h2 id="发生条件">发生条件</h2>

<p>服务端一定程度上承担了 HTTP Proxy 的角色，并让用户可以指定访问地址的情况。</p>

<h2 id="发生后果">发生后果</h2>

<ul>
<li>暴露攻击者本没有权限访问的资源（因为内网一般被认为是可信任的）</li>
<li>暴露内网状况</li>
</ul>

<h2 id="举例">举例</h2>

<ul>
<li>一个接口，可以指定 URL 并代理下载相关资源</li>
</ul>

<h2 id="防范方式">防范方式</h2>

<ul>
<li>服务端去其他资源的请求的参数不允许用户配置 URL</li>
</ul>
]]></description></item><item><title>netcat 使用：问题定位 HTTP chunked 格式非法</title><link>https://www.rectcircle.cn/posts/netcat-and-http-chunked-invaild/</link><pubDate>Thu, 03 Jun 2021 17:37:16 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/netcat-and-http-chunked-invaild/</guid><description type="html"><![CDATA[

<h2 id="起因">起因</h2>

<p>在修改一个后端项目的上传文件的接口，然后使用该后端项目对应的前端 cli 工具，调试该上传文件的接口过程中</p>

<p>后端报错（后端是典型的 Java 11  Spring Boot 项目）</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-log" data-lang="log">11:42:36.747 [http-nio-8020-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/extensions] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: org.apache.catalina.connector.ClientAbortException: java.io.IOException: Invalid end of line sequence (character other than CR or LF found)] with root cause
java.io.IOException: Invalid end of line sequence (character other than CR or LF found)
        at org.apache.coyote.http11.filters.ChunkedInputFilter.throwIOException(ChunkedInputFilter.java:606)
        at org.apache.coyote.http11.filters.ChunkedInputFilter.parseCRLF(ChunkedInputFilter.java:424)
        at org.apache.coyote.http11.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:202)
        at org.apache.coyote.http11.Http11InputBuffer.doRead(Http11InputBuffer.java:248)
        at org.apache.coyote.Request.doRead(Request.java:555)
        at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:336)
        at org.apache.catalina.connector.InputBuffer.checkByteBufferEof(InputBuffer.java:632)
        at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:362)
        at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132)
        at com.google.common.io.ByteStreams.toByteArrayInternal(ByteStreams.java:181)
        at com.google.common.io.ByteStreams.toByteArray(ByteStreams.java:221)
        ...</code></pre></div>
<h2 id="定位">定位</h2>

<p>由异常栈可以看出，是后端在读取 Request 的 InputStream 的时候，报错。具体到 ChunkedInputFilter 可以看出应该是与 Chunked 格式编码异常有关。</p>

<p>这种问题比较难以确定，因此可以通过对客户端请求进行抓包，进行确认。</p>

<h3 id="客户端请求抓包">客户端请求抓包</h3>

<p>执 nc 命令，启动一个 TCP Server，监听在 8022 端口，并将客户端发送的请求重定向到 request.bin 文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nc -lv -p <span style="color:#ae81ff">8022</span> &gt; request.bin</code></pre></div>
<p>重新执行 cli，后端配置成 8022</p>

<p>此时查看 request.bin 文件当做文本文件打开间将看到如下内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-http" data-lang="http"><span style="color:#a6e22e">POST</span> /xxx <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span>
Content-Type<span style="color:#f92672">:</span> <span style="color:#ae81ff">application/octet-stream</span>
Host<span style="color:#f92672">:</span> <span style="color:#ae81ff">xxxx</span>
Connection<span style="color:#f92672">:</span> <span style="color:#ae81ff">close</span>
Transfer-Encoding<span style="color:#f92672">:</span> <span style="color:#ae81ff">chunked</span>

10000
乱码</code></pre></div>
<p>使用 xxd 命令生成该文件的 16 进制编码情况</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">xxd request.bin request.hex</code></pre></div>
<p>查看  request.hex 文件，将看到如下内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">...
00000090: 3032 320d 0a43 6f6e 6e65 6374 696f 6e3a  022..Connection:
000000a0: 2063 6c6f 7365 0d0a 5472 616e 7366 6572   close..Transfer
000000b0: 2d45 6e63 6f64 696e 673a 2063 6875 6e6b  -Encoding: chunk
000000c0: 6564 0d0a 0d0a 3130 3030 300d 0a50 4b03  ed....10000..PK.
...</pre></div>
<h3 id="将抓到的包发送给-server-复现问题">将抓到的包发送给 Server 复现问题</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nc <span style="color:#ae81ff">127</span>.0.0.1 <span style="color:#ae81ff">8020</span> &lt; request.bin</code></pre></div>
<p>上文提到的问题又复现了，说明这 request.bin 确实存在问题。后面直接分析 request.bin 文件即可</p>

<h3 id="其他正常设备作为客户端进行抓包">其他正常设备作为客户端进行抓包</h3>

<p>使用其他设备进行抓包 （参考 客户端请求抓包），发现没有问题，对两次抓包的请求 hex 进行 diff 发现，有几大段数据存在 diff</p>

<h3 id="根据-http-chunked-编码协议编写程序进行分析">根据 http chunked 编码协议编写程序进行分析</h3>

<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding">https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
    <span style="color:#e6db74">&#34;bytes&#34;</span>
    <span style="color:#e6db74">&#34;fmt&#34;</span>
    <span style="color:#e6db74">&#34;io/ioutil&#34;</span>
    <span style="color:#e6db74">&#34;log&#34;</span>
    <span style="color:#e6db74">&#34;os&#34;</span>
    <span style="color:#e6db74">&#34;strconv&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#a6e22e">f</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Open</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Args</span>[<span style="color:#ae81ff">1</span>])
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
    }

    <span style="color:#a6e22e">content</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">f</span>)
    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
    }

    <span style="color:#a6e22e">rnFlag</span> <span style="color:#f92672">:=</span> []byte(<span style="color:#e6db74">&#34;\r\n&#34;</span>)
    <span style="color:#a6e22e">dataStart</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">4</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">content</span>); {
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">dataStart</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">false</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">i</span><span style="color:#f92672">-</span><span style="color:#ae81ff">4</span>:<span style="color:#a6e22e">i</span><span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>], <span style="color:#a6e22e">rnFlag</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">i</span><span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>:<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">rnFlag</span>) {
            <span style="color:#a6e22e">dataStart</span> = <span style="color:#66d9ef">true</span>
        }
        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">dataStart</span> {
            <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span>
            <span style="color:#66d9ef">continue</span>
        }
        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;%x\n&#34;</span>, <span style="color:#a6e22e">i</span>)
        <span style="color:#a6e22e">j</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">i</span>
        <span style="color:#66d9ef">for</span> ; ; <span style="color:#a6e22e">j</span><span style="color:#f92672">++</span> {
            <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&gt;=</span> byte(<span style="color:#e6db74">&#39;0&#39;</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&lt;=</span> byte(<span style="color:#e6db74">&#39;9&#39;</span>)) <span style="color:#f92672">||</span> (<span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&gt;=</span> byte(<span style="color:#e6db74">&#39;A&#39;</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&lt;=</span> byte(<span style="color:#e6db74">&#39;F&#39;</span>)) <span style="color:#f92672">||</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&gt;=</span> byte(<span style="color:#e6db74">&#39;a&#39;</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">&lt;=</span> byte(<span style="color:#e6db74">&#39;f&#39;</span>) {
                <span style="color:#66d9ef">continue</span>
            }
            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span>] <span style="color:#f92672">==</span> byte(<span style="color:#e6db74">&#39;\r&#39;</span>) {
                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>] <span style="color:#f92672">!=</span> byte(<span style="color:#e6db74">&#39;\n&#39;</span>) {
                    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;ERR1 offset %x : want \\n but got %x&#34;</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>])
                }
                <span style="color:#66d9ef">break</span>
            }
            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;ERR2 offset %x : want \\r or number but got %x&#34;</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>])
        }
        <span style="color:#a6e22e">chunkedLen</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">ParseUint</span>(string(<span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">i</span>:<span style="color:#a6e22e">j</span>]), <span style="color:#ae81ff">16</span>, <span style="color:#ae81ff">64</span>)

        <span style="color:#75715e">// 10000\r\n len()= 10000 \r\n
</span><span style="color:#75715e"></span>        <span style="color:#75715e">// i     j 1  10000        2
</span><span style="color:#75715e"></span>        <span style="color:#a6e22e">j</span> = <span style="color:#a6e22e">j</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">+</span> int(<span style="color:#a6e22e">chunkedLen</span>) <span style="color:#f92672">+</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>

        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>] <span style="color:#f92672">!=</span> byte(<span style="color:#e6db74">&#39;\r&#39;</span>) {
            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;ERR3 offset %x : want \\r but got %x&#34;</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>, <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>])
        }
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>] <span style="color:#f92672">!=</span> byte(<span style="color:#e6db74">&#39;\n&#39;</span>) {
            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;ERR4 offset %x : want \\r but got %x&#34;</span>, <span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">content</span>[<span style="color:#a6e22e">j</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>])
        }

        <span style="color:#a6e22e">i</span> = <span style="color:#a6e22e">j</span>
    }
    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Close</span>()
}</code></pre></div>
<p>分析两个请求包</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 异常的请求</span>
go run main.go request.bin
<span style="color:#75715e"># c6</span>
<span style="color:#75715e"># 100cf</span>
<span style="color:#75715e"># 200d8</span>
<span style="color:#75715e"># 300e1</span>
<span style="color:#75715e"># 400ea</span>
<span style="color:#75715e"># 500f3</span>
<span style="color:#75715e"># 600fc</span>
<span style="color:#75715e"># 70105</span>
<span style="color:#75715e"># 8010e</span>
<span style="color:#75715e"># 90117</span>
<span style="color:#75715e"># 2021/06/04 13:23:25 ERR3 offset a011e : want \r but got 1e</span>
<span style="color:#75715e"># exit status 1</span>

<span style="color:#75715e"># 正常请求</span>
go run main.go request2.bin
<span style="color:#75715e"># 无输出</span></code></pre></div>
<h3 id="结论">结论</h3>

<p>在我的 设备上 ，cli 发送的请求，chunked 编码异常。</p>

<h2 id="netcat-调试分析基于-tcp-的协议">Netcat 调试分析基于 TCP 的协议</h2>

<p>Netcat 是一个功能强大的网络工具，上述定位过程使用到了 netcat 的两个能力：监听某个端口创建一个 TCP Server 以及 作为一个客户端向 TCP Server 发送消息</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">nc -lv -p <span style="color:#ae81ff">8022</span> &gt; request.bin
nc <span style="color:#ae81ff">127</span>.0.0.1 <span style="color:#ae81ff">8020</span> &lt; request.bin</code></pre></div>
<p>更多 netcat 用法参见
- <a href="https://mjd507.github.io/2018/01/15/Use-netcat-to-transfer-TCP-UDP-Data/">博客 1</a>
- <a href="https://zhuanlan.zhihu.com/p/83959309">博客 2</a></p>
]]></description></item><item><title>移动办公探索（适用于软件开发工程师）</title><link>https://www.rectcircle.cn/posts/software-engineer-mobile-office/</link><pubDate>Sat, 29 May 2021 16:53:19 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/software-engineer-mobile-office/</guid><description type="html"><![CDATA[

<h2 id="目标">目标</h2>

<ul>
<li>能通过总重量 &lt; 1000 g 的主设备（目前为平板 / 手机）完成全部的编码开发，即能够安装官方 VSCode （不是 codeserver，原因是希望可以通过 remote ssh 连接到开发机） 和 各种开发环境</li>
<li>有全功能的 Linux 开发环境</li>
<li>兼顾娱乐和办公</li>
<li>可扩展连接各种外接设备（显示器），以获取更好的体验</li>
</ul>

<h2 id="chrome-os-生态">Chrome OS 生态</h2>

<p>略</p>

<h2 id="android-生态">Android 生态</h2>

<h3 id="设备">设备</h3>

<p>（全功能 TypeC 用于实现外接显示器扩展）</p>

<ul>
<li>方案 1：具有全功能 TypeC 的较高性能的安卓手机 + 拓展坞 + 便携式显示器（小米手机全系不支持）

<ul>
<li><a href="https://www.smartisan.com/item/100187101">坚果 R2 + TNT Go</a></li>
</ul></li>
<li>方案 2：具有全功能 TypeC 的较高性能的安卓平板 + 拓展坞

<ul>
<li><a href="https://item.lenovo.com.cn/product/1014616.html">联想平板小新 Pad Pro 2021</a></li>
</ul></li>
<li>方案 3（推荐）：具有全功能 TypeC 的普通性能安卓平板 + 高性能安卓手机 + 拓展坞

<ul>
<li><a href="https://item.lenovo.com.cn/product/1014618.html">联想平板小新Pad Plus</a> + 任意高性能安卓手机</li>
</ul></li>
</ul>

<h3 id="思路">思路</h3>

<p>设备可以划分为 2 种角色：</p>

<ul>
<li>计算设备：负责运行 Linux 开发环境，需要较高的性能</li>
<li>显示设备：负责通过 VNC 协议连接到计算设备中的 Linux 环境</li>
</ul>

<p>其他技术参数推荐</p>

<ul>
<li>计算设备：CPU 骁龙 870 以上 + 内存 8g 以上 + 存储 256g 以上（空余空间 100g 以上），重量（手机 &lt; 200g，pad &lt; 700g）</li>
<li>显示设备：屏幕11寸以上，100% P3 色域，内存 6g 以上，存储 128g 以上</li>
</ul>

<h3 id="操作过程">操作过程</h3>

<h4 id="f-droid-安装"><code>F-Droid</code> 安装</h4>

<p>（计算设备）</p>

<p>F-Droid 是一个用来安装开源软件的安卓市场，后面需要用到 <code>termux</code> 及 <code>termux:api</code> 需要使用该软件进行安装。访问 <a href="https://f-droid.org/">https://f-droid.org/</a> 下载安装即可</p>

<h4 id="termux-和-termux-api"><code>termux</code> 和 <code>termux:api</code></h4>

<p>（计算设备）</p>

<p>打开 <code>F-Droid</code>，搜索 <code>termux</code> 和 <code>termux:api</code> 进行安装</p>

<p>特别注意</p>

<ul>
<li>不要在 Play 商店安装，因为无法过审，商店里面的是旧版，无法执行 <code>apt update</code></li>
<li>termux 的 shell 并不是完整的 Linux 环境，参见 <a href="https://wiki.termux.com/wiki/Differences_from_Linux">wiki</a></li>
</ul>

<p>关于 termux，更多参见</p>

<ul>
<li><a href="https://www.sqlsec.com/2018/05/termux.html">博客</a></li>
<li><a href="https://termux.com/">官方网站</a></li>
</ul>

<h4 id="安装-tmoe-linux">安装 <code>tmoe-linux</code></h4>

<p>（计算设备）</p>

<p>tmoe-linux （<a href="https://gitee.com/mo2/linux">gitee</a> | <a href="https://github.com/2moe/tmoe-linux">github</a>），是一个国人编写的 shell 脚本工具集，该项目的主要用于，在 <code>Android-termux</code>、<code>Window-WLS</code> 环境（宿主机）中，利用容器（<a href="https://wiki.termux.com/wiki/PRoot">proot/chroot</a>）和 qemu 技术，安装和配置一个全功能的 Linux（容器环境）。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 宿主机环境</span>
apt update
apt install -y curl
. &lt;<span style="color:#f92672">(</span>curl -L l.tmoe.me<span style="color:#f92672">)</span></code></pre></div>
<p>一路 Yes，将进入一个主菜单，选择 <code>proot容器</code>，选择 <code>arm64发行版</code>，&hellip; 一路选择第一个即可。</p>

<p>安装完成后将安装一个基本完整的 <code>Kali</code> arm64 Linux系统（细节可通过 <code>neofetch</code> 查看）（后文称容器环境）</p>

<p>特别注意，以上安装的是利用 proot 方式，也并不是完全完整的 Linux 环境。如果想安装 x64 的环境或者想要更完整的环境，需要选择 <code>proot容器 -&gt; cross-arch...</code>（基于 qemu），但是该方案性能存在问题（软件虚拟化，性能存在问题）</p>

<h4 id="安装-vnc-viewer">安装 VNC Viewer</h4>

<p>（显示设备）</p>

<p>通过 Play 商店下载，或者 百度 搜下</p>

<h4 id="tmoe-操作">tmoe 操作</h4>

<p>以上简述的是安装完成后。计算设备重新打开 termux 安卓软件后，进入的是 Termux 这个宿主机环境，此时需要执行一些命令才能进入容器环境。</p>

<p>宿主机</p>

<ul>
<li><code>debian</code> 即可进入容器环境的 shell</li>
<li><code>startvnc</code> 即可进入容器环境的 shell，同时启动 vnc</li>
<li><code>startx11vnc</code> 即可进入容器环境的 shell，同时启动 vnc</li>
<li><code>stopvnc</code> 停止 vnc</li>
<li><code>tmoe</code> 重新进入脚本主菜单，可以安装其他的环境，或者卸载当前环境（功能众多可自行探索）</li>
</ul>

<p>容器</p>

<ul>
<li><code>tmoe</code> 进入容器管理的脚本主菜单，可以安装一些软件，配置 zsh，配置 vnc</li>
<li><code>startvnc</code> 启动 vnc</li>
<li><code>startx11vnc</code> 启动 vnc</li>
<li><code>stopvnc</code> 停止 vnc</li>
</ul>

<p>非交互式命令</p>

<ul>
<li><code>tmoe --help</code></li>
</ul>

<p>建议和问题</p>

<ul>
<li>建议使用 tigerVNC 或 x11VNC，连接效果较好</li>
<li>VSCode 可能导致 vnc 直接断开连接：

<ul>
<li>在配置文件中 <code>.zshrc</code> 或 <code>.bashrc</code> 中 添加 <code>alias rcode='sudo code --user-data-dir /root/.vscode'</code></li>
<li>打开 VSCode 直接从终端，输入 <code>rcode</code> 打开</li>
</ul></li>
</ul>

<h4 id="termux-常用配置">termux 常用配置</h4>

<ul>
<li>安装 sshd 从远程设备进行管理</li>
<li>安装 <a href="https://wiki.termux.com/wiki/Termux:Boot">Boot</a> 插件，配置开机自启（不生效可以重启手机试试），并配置 <a href="https://wiki.termux.com/wiki/Termux-services">service</a>，配置 sshd 开机自启</li>
</ul>

<h3 id="性能测试">性能测试</h3>

<blockquote>
<p><a href="https://www.geekbench.com/blog/2021/03/geekbench-54/">geekbench</a></p>
</blockquote>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget https://cdn.geekbench.com/Geekbench-5.4.0-LinuxARMPreview.tar.gz
tar xf Geekbench-5.4.0-LinuxARMPreview.tar.gz
cd Geekbench-5.4.0-LinuxARMPreview
./geekbench_aarch64</code></pre></div>
<p>结果（小米 11 骁龙 888）</p>

<ul>
<li>单核 <code>955</code>，多核 <code>2983</code> （小米 11 官方分数 单核 1132、多核 3758）</li>
</ul>

<p>说明该方案性能损失不大</p>
]]></description></item><item><title>Linux Shell 初始化文件 —— 环境变量写在哪里？</title><link>https://www.rectcircle.cn/posts/linux-shell-initialization-files/</link><pubDate>Sat, 17 Apr 2021 18:53:59 +0800</pubDate><author>rectcircle96@gmail.com (Rectcircle)</author><guid>https://www.rectcircle.cn/posts/linux-shell-initialization-files/</guid><description type="html"><![CDATA[

<blockquote>
<p><a href="http://feihu.me/blog/2014/env-problem-when-ssh-executing-command-on-remote/">推荐</a>
<a href="https://blog.opstree.com/2020/02/11/shell-initialization-files/">文章 1</a>
<a href="http://c.biancheng.net/view/3045.html">文章 2</a>
<a href="https://www.cnblogs.com/zhenyuyaodidiao/p/9287497.html">博客 3</a></p>
</blockquote>

<h2 id="流程图">流程图</h2>

<p>大差不差，请以自己机器为准</p>

<p><img src="/image/shell-initailization-files.png" alt="流程图" /></p>

<h2 id="shell-四种模式">Shell 四种模式</h2>

<blockquote>
<p>参考：<a href="http://c.biancheng.net/view/3045.html">http://c.biancheng.net/view/3045.html</a></p>
</blockquote>

<p>不同的模式，执行的初始化脚本不同</p>

<ul>
<li>登录交互式：登录系统得到的第一个终端（tty、本地、SSH 都是的）</li>
<li>非登录交互式：Linux GUI 打开的终端窗口；直接执行 <code>bash</code> 进入的 shell。</li>
<li>非登录非交互：

<ul>
<li><code>.sh</code> 脚本执行所在的环境</li>
<li><code>ssh user@remote script.sh</code></li>
</ul></li>
<li>登录非交互：

<ul>
<li>在脚本中通过 <code>#!/bin/bash --login</code> 头指定</li>
<li><code>ssh user@remote 非脚本的命令</code></li>
</ul></li>
</ul>

<p>判断一个 Shell 是否是登录式</p>

<ul>
<li>方式 1：破坏性的，执行 logout 是否退出</li>
<li>方式 2：仅支持 Bash，<code>shopt login_shell</code></li>
<li>方式 3：仅支持 Bash 和 ZSH <code>[[ -o login ]]</code></li>
</ul>

<p>判断一个 Shell 是否是交互式</p>

<ul>
<li>方式 1：通用，<code>$-</code> 环境变量是否包含 <code>i</code> 字符</li>
<li>方式 3：仅支持 Bash 和 ZSH，通过判断 <code>$PS1</code> 是否存在判断（sh 不支持）</li>
</ul>

<h2 id="实验过程">实验过程</h2>

<h3 id="实验环境">实验环境</h3>

<p>Debian 9</p>

<h3 id="准备">准备</h3>

<p>在所有初始化文件头部添加 Log</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">add_str_to_file_head_if_not_exist<span style="color:#f92672">(){</span>
    str<span style="color:#f92672">=</span>$1
    f<span style="color:#f92672">=</span>$2
    grep -F -q <span style="color:#e6db74">&#34;</span>$str<span style="color:#e6db74">&#34;</span> $f <span style="color:#f92672">||</span> printf <span style="color:#e6db74">&#39;%s\n%s\n&#39;</span> <span style="color:#e6db74">&#34;</span>$str<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>cat $f<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> | sudo tee $f
<span style="color:#f92672">}</span>
<span style="color:#75715e"># bash</span>
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/profile&#34;&#39;</span> /etc/profile
echo <span style="color:#e6db74">&#39;echo &#34;[source] /etc/profile.d/*&#34;&#39;</span> | sudo tee /etc/profile.d/bash_init_test.sh

add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/bash.bashrc&#34;&#39;</span> /etc/bash.bashrc
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.bashrc&#34;&#39;</span> ~/.bashrc
touch ~/.bash_profile <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.bash_profile&#34;&#39;</span> ~/.bash_profile
touch ~/.bash_logout <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.bash_logout&#34;&#39;</span> ~/.bash_logout

<span style="color:#75715e"># sh</span>
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.profile&#34;&#39;</span> ~/.profile

<span style="color:#75715e"># zsh</span>
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/zsh/zshenv&#34;&#39;</span> /etc/zsh/zshenv
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/zsh/zlogin&#34;&#39;</span> /etc/zsh/zlogin
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/zsh/zprofile&#34;&#39;</span> /etc/zsh/zprofile
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/zsh/zlogout&#34;&#39;</span> /etc/zsh/zlogout
add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] /etc/zsh/zshrc&#34;&#39;</span> /etc/zsh/zshrc

add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.zshrc&#34;&#39;</span> ~/.zshrc
touch ~/.zshenv <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.zshenv&#34;&#39;</span> ~/.zshenv
touch ~/.zprofile <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.zprofile&#34;&#39;</span> ~/.zprofile
touch ~/.zlogin <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.zlogin&#34;&#39;</span> ~/.zlogin
touch ~/.zlogout <span style="color:#f92672">&amp;&amp;</span> add_str_to_file_head_if_not_exist <span style="color:#e6db74">&#39;echo &#34;[source] ~/.zlogout&#34;&#39;</span> ~/.zlogout</code></pre></div>
<h3 id="测试-zsh">测试 zsh</h3>

<p>准备</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo usermod -s /bin/zsh <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span></code></pre></div>
<p>登录交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span>@localhost
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/zsh/zshenv</span>
<span style="color:#75715e"># [source] ~/.zshenv</span>
<span style="color:#75715e"># [source] /etc/zsh/zprofile</span>
<span style="color:#75715e"># [source] ~/.zprofile</span>
<span style="color:#75715e"># [source] /etc/zsh/zshrc</span>
<span style="color:#75715e"># [source] ~/.zshrc</span>
<span style="color:#75715e"># [source] /etc/zsh/zshenv</span>
<span style="color:#75715e"># [source] /etc/zsh/zlogin</span>
<span style="color:#75715e"># [source] ~/.zlogin</span></code></pre></div>
<p>非登录交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">zsh
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/zsh/zshenv</span>
<span style="color:#75715e"># [source] ~/.zshenv</span>
<span style="color:#75715e"># [source] /etc/zsh/zshrc</span>
<span style="color:#75715e"># [source] ~/.zshrc</span></code></pre></div>
<p>非登录非交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">zsh -c <span style="color:#e6db74">&#34;&#34;</span>
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/zsh/zshenv</span>
<span style="color:#75715e"># [source] ~/.zshenv</span></code></pre></div>
<p>登录非交互</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">zsh -c --login <span style="color:#e6db74">&#34;&#34;</span>
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/zsh/zshenv</span>
<span style="color:#75715e"># [source] ~/.zshenv</span>
<span style="color:#75715e"># [source] /etc/zsh/zprofile</span>
<span style="color:#75715e"># [source] ~/.zprofile</span>
<span style="color:#75715e"># [source] /etc/zsh/zlogin</span>
<span style="color:#75715e"># [source] ~/.zlogin</span></code></pre></div>
<h3 id="测试-bash">测试 bash</h3>

<p>准备</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo usermod -s /bin/bash <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span></code></pre></div>
<p>登录交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ssh <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span>@localhost
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/profile</span>
<span style="color:#75715e"># [source] /etc/bash.bashrc</span>
<span style="color:#75715e"># [source] /etc/profile.d/*</span>
<span style="color:#75715e"># [source] ~/.bash_profile</span></code></pre></div>
<p>非登录交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">bash
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/bash.bashrc</span>
<span style="color:#75715e"># [source] ~/.bashrc</span></code></pre></div>
<p>非登录非交互式</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">bash -c <span style="color:#e6db74">&#34;&#34;</span>
<span style="color:#75715e"># 没有输出，会执行 $BASH_ENV</span></code></pre></div>
<p>登录非交互</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">bash -c --login <span style="color:#e6db74">&#34;&#34;</span>
<span style="color:#75715e"># 输出如下</span>
<span style="color:#75715e"># [source] /etc/profile</span>
<span style="color:#75715e"># [source] /etc/profile.d/*</span>
<span style="color:#75715e"># [source] ~/.bash_profile</span></code></pre></div>
<h3 id="测试-sh">测试 sh</h3>

<p>略</p>

<h2 id="常见问题和建议">常见问题和建议</h2>

<h3 id="ssh直接执行脚本环境变量的问题">SSH直接执行脚本环境变量的问题</h3>

<blockquote>
<p>参考：<a href="https://unix.stackexchange.com/questions/349425/ssh-command-and-non-interactive-non-login-shell">问答</a></p>
</blockquote>

<ul>
<li>将本地文件在远端执行：非交互登录模式

<ul>
<li><code>ssh remote &lt; 本地文件.sh</code></li>
</ul></li>
<li>直接执行远端文件：非交互非登录模式（可能存在环境变量问题）

<ul>
<li><code>ssh remote '远端文件.sh'</code></li>
</ul></li>
<li>直接执行远端文件：非交互登录模式

<ul>
<li><code>ssh remote 'bash --login 远端文件.sh'</code></li>
</ul></li>
<li>伪终端模式：<code>ssh -t remote</code></li>
</ul>

<h3 id="报告-command-not-found-但是登录">报告 Command not found，但是登录</h3>

<p>原因在于 Shell 的四种模式执行的初始化文件不同，解决方案参见</p>

<p><a href="#如果导出自己的环境变量">如果导出自己的环境变量</a></p>

<h3 id="不要使用-bin-sh">不要使用 /bin/sh</h3>

<p>/bin/sh 能力较弱不要使用</p>

<h3 id="如果导出自己的环境变量">如果导出自己的环境变量</h3>

<p>考虑兼容 zsh 和 bash 的四种模式，防止因为模式不同导致的</p>

<ul>
<li>在家目录添加自己的环境变量脚本 比如 <code>~/.my_env</code></li>
<li>在 <code>~/.bashrc</code>、<code>~/.zshenv</code>、<code>~/.profile</code> 中添加 <code>source ~/.my_env</code></li>
<li>如果存在 <code>~/.bash_profile</code>，请检查 <code>~/.bash_profile</code> 中是否包含 <code>source ~/.bashrc</code> 若没有，需要加上</li>
</ul>

<p>另外，不要使用 非登录非交互式 的 顶级 shell 执行命令（顶级 shell 指其祖宗进程都是非登录非交互式的，