<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>jarv.org</title><link>https://jarv.org/</link><description>Recent content on jarv.org</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 05 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jarv.org/index.xml" rel="self" type="application/rss+xml"/><item><title>My RSS client has an MCP server</title><link>https://jarv.org/posts/rss-mcp/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/rss-mcp/</guid><description>&lt;p>Although the days of every site having an RSS feed have long past, I still find that it remains one of the few ways left to curate your own garden of content on the internet.
There&amp;rsquo;s no algorithm deciding what you see or optimized feed pushing rage bait to the top (like the terrible trend of short-form video content).&lt;/p>
&lt;p>In &lt;a href="https://jarv.org/posts/use-rss">a previous post&lt;/a>, I mentioned how I am using RSS to monitor individual files or directories in GitHub and GitLab repositories, like tracking changes to a &lt;code>CHANGELOG&lt;/code>, a &lt;code>package.json&lt;/code>.
My RSS client is configured with tech blogs belonging to people I know, some interesting ones I find randomly, YouTube channel feeds, GitLab activity feeds, and some traditional news sources.&lt;/p>
&lt;p>Because for me, all of this content sits in a local database, already fetched and cached it makes it a perfect candidate for an &lt;a href="https://modelcontextprotocol.io/">MCP&lt;/a> server.&lt;/p>
&lt;h2 id="why-mcp--rss">Why MCP + RSS&lt;/h2>
&lt;p>MCP (Model Context Protocol) lets AI assistants call tools to retrieve information.
An RSS reader that exposes its cached articles via MCP essentially gives an AI assistant access to your curated view of the world.&lt;/p>
&lt;p>Some examples of what this enables and how I am using it:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;Summarize all GitLab activity from the last week&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;Were there any changes to the Go release notes this month?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;What&amp;rsquo;s been happening in my news feeds today?&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The responses end up being grounded in content that I&amp;rsquo;ve already chosen to follow instead of a global web search.&lt;/p>
&lt;h2 id="implementation">Implementation&lt;/h2>
&lt;p>I don&amp;rsquo;t see terminal based readers implementing this yet, but I assume it is going to catch on given how useful it is.
If you know of ones that do have this already please let me know!
In the meantime, I added this to my customized news reader &lt;a href="https://github.com/jarv/newsgoat">newsgoat&lt;/a>.
This reader of is highly tailored to myself, and might not be for everyone.
With that said, if you have used or are using newsbeuter/newsboat you should give it a test drive!&lt;/p>
&lt;p>The MCP implementation exposes four tools:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>search_articles&lt;/code>&lt;/strong> — full-text search across titles, descriptions, and content with a time window&lt;/li>
&lt;li>&lt;strong>&lt;code>list_recent_articles&lt;/code>&lt;/strong> — list recent articles, filterable by folder or feed name&lt;/li>
&lt;li>&lt;strong>&lt;code>get_article&lt;/code>&lt;/strong> — get the full content of a specific article&lt;/li>
&lt;li>&lt;strong>&lt;code>list_feeds&lt;/code>&lt;/strong> — list all feeds with their folders and unread counts&lt;/li>
&lt;/ul>
&lt;p>The configuration for an MCP client:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;newsgoat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;newsgoat&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mcp-server&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The MCP server opens the same SQLite database as the TUI, runs read-only queries, and returns results as text.&lt;/p>
&lt;p>So I think every RSS client should do this and hope it catches on.
If you are not using RSS or maybe if you used to use RSS but gave up on it, I would encourage you to give it another look!&lt;/p></description></item><item><title>Filtering YouTube Shorts from RSS Feeds</title><link>https://jarv.org/posts/newsgoat-filters/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/newsgoat-filters/</guid><description>&lt;p>I subscribe to a lot of YouTube channels via RSS in &lt;a href="https://github.com/jarv/newsgoat">NewsGoat&lt;/a>, and the one thing that I can&amp;rsquo;t stand are Shorts.
Every channel seems to produce them now, and they clutter up the feed with content I never want to watch.&lt;/p>
&lt;p>To help, I just added regex-based filters to NewsGoat to deal with this.&lt;/p>
&lt;h2 id="setting-up-a-filter">Setting Up a Filter&lt;/h2>
&lt;p>In newsgoat, press &lt;kbd>f&lt;/kbd> on a feed or folder to open a filter in &lt;code>$EDITOR&lt;/code>.
The filter is a simple YAML file with regex patterns for URL, Title, and Description fields:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Filter for folder: YouTube&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Regexes are ANDed together. Prefix with ! to negate.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Examples:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># - &amp;#34;golang&amp;#34; # items must match &amp;#34;golang&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># - &amp;#34;!/shorts/&amp;#34; # items must NOT match &amp;#34;/shorts/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c">#&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">URL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;!/shorts/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">Title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">Description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>!&lt;/code> prefix negates the pattern — items matching &lt;code>!/shorts/&lt;/code> are excluded.
Setting this on a YouTube folder applies it to every feed inside that folder, so I only had to configure it once.&lt;/p>
&lt;h2 id="how-it-works">How It Works&lt;/h2>
&lt;p>Filters can be scoped at three levels: &lt;strong>global&lt;/strong> (&lt;kbd>F&lt;/kbd>), &lt;strong>per-folder&lt;/strong> (&lt;kbd>f&lt;/kbd> on a folder), and &lt;strong>per-feed&lt;/strong> (&lt;kbd>f&lt;/kbd> on a feed).
They compose with AND logic, for example if you have a global filter and a folder filter, both must pass for an item to show.&lt;/p>
&lt;p>The regex patterns within each field are also ANDed together, so you can stack multiple conditions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">URL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;!/shorts/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;!/playlist&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">Title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;!LIVE&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This would exclude Shorts, playlists, and anything with &amp;ldquo;LIVE&amp;rdquo; in the title.&lt;/p>
&lt;p>Feeds and folders with a configured filter show a &lt;code>✦&lt;/code> indicator in the feed list, so it&amp;rsquo;s easy to see where filters are active.
Items that don&amp;rsquo;t match the filter are hidden from the item list — they&amp;rsquo;re still in the database, just not displayed.&lt;/p></description></item><item><title>Neovim Configuration for 2026</title><link>https://jarv.org/posts/neovim-config-2026/</link><pubDate>Fri, 13 Feb 2026 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/neovim-config-2026/</guid><description>&lt;style>
table {
border-collapse: collapse;
width: 100%;
}
td {
padding: 10px;
text-align: left;
border: 1px solid gray;
}
&lt;/style>
&lt;p>A year after my &lt;a href="https://jarv.org/posts/neovim-config/">last Neovim configuration post&lt;/a>, I&amp;rsquo;ve made another significant change to how I manage my setup.
This time, I switched from &lt;a href="https://github.com/dam9000/kickstart-modular.nvim">kickstart-modular.nvim&lt;/a> to &lt;a href="https://github.com/echasnovski/MiniMax">MiniMax&lt;/a>, a minimal configuration built on top of the &lt;a href="https://github.com/echasnovski/mini.nvim">mini.nvim&lt;/a> ecosystem.&lt;/p>
&lt;h2 id="why-i-switched">Why I Switched&lt;/h2>
&lt;p>While I was happy with &lt;code>kickstart-modular.nvim&lt;/code>, the upstream project (&lt;code>kickstart.nvim&lt;/code>) appears to be &lt;a href="https://github.com/nvim-lua/kickstart.nvim/issues/1821">no longer actively maintained&lt;/a>.
There is &lt;a href="https://github.com/oriori1703/kickstart-modular.nvim/tree/maintained-upstream-modular">an effort to maintain a fork&lt;/a>, but I decided to explore alternatives rather than rely on a community fork.&lt;/p>
&lt;p>MiniMax embraces the &lt;code>mini.nvim&lt;/code> philosophy of having small, focused, well-integrated plugins.
It&amp;rsquo;s actively maintained by the author of &lt;code>mini.nvim&lt;/code> itself, and provides a solid foundation that&amp;rsquo;s both minimal and fully-featured.&lt;/p>
&lt;h2 id="my-configuration-and-workflow">My Configuration and Workflow&lt;/h2>
&lt;ol>
&lt;li>I maintain my &lt;a href="https://github.com/jarv/MiniMax">own fork&lt;/a> of &lt;a href="https://github.com/echasnovski/MiniMax">MiniMax&lt;/a>.&lt;/li>
&lt;li>I apply my customizations in a &lt;a href="https://github.com/echasnovski/MiniMax/compare/main...jarv:MiniMax:main">single commit&lt;/a>.&lt;/li>
&lt;li>I periodically pull in upstream changes, staying up-to-date with improvements while keeping my modifications intact.&lt;/li>
&lt;/ol>
&lt;p>This workflow continues to work well for me, it&amp;rsquo;s the same approach I used with &lt;code>kickstart-modular.nvim&lt;/code>.&lt;/p>
&lt;h2 id="what-changed-in-the-migration">What Changed in the Migration&lt;/h2>
&lt;p>The transition from &lt;code>kickstart-modular.nvim&lt;/code> to MiniMax was surprisingly smooth because both configurations share similar philosophies.
Here are the key differences I encountered:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Feature&lt;/th>
&lt;th>Before (kickstart-modular)&lt;/th>
&lt;th>After (MiniMax)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Plugin Management&lt;/td>
&lt;td>&lt;code>lazy.nvim&lt;/code>&lt;/td>
&lt;td>&lt;code>mini.deps&lt;/code> (simpler, more integrated)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>File Explorer&lt;/td>
&lt;td>&lt;code>telescope.nvim&lt;/code> for file browsing&lt;/td>
&lt;td>&lt;code>mini.files&lt;/code> (lightweight, tree-style interface)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auto-completion&lt;/td>
&lt;td>&lt;code>nvim-cmp&lt;/code> with multiple completion sources&lt;/td>
&lt;td>&lt;code>mini.completion&lt;/code> (simpler, built-in)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Configuration Structure&lt;/td>
&lt;td>Multiple Lua files in &lt;code>lua/custom/&lt;/code> directory&lt;/td>
&lt;td>Single customization file at &lt;code>configs/nvim-0.11/plugin/50_custom_plugins.lua&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="customizations">Customizations&lt;/h2>
&lt;p>I continue to keep my customizations minimal. MiniMax already includes most of what I need through its carefully selected &lt;code>mini.nvim&lt;/code> modules and a few additional plugins like LSP support, Tree-sitter, and formatters.&lt;/p>
&lt;p>Here are the plugins and customizations I added on top of MiniMax:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Plugin/Feature&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/mason-org/mason.nvim">mason.nvim&lt;/a>&lt;/td>
&lt;td>LSP server package manager. Makes it easy to install and manage language servers.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/dfendr/clipboard-image.nvim">clipboard-image.nvim&lt;/a>&lt;/td>
&lt;td>Paste images directly from clipboard into Markdown posts—essential for my blog workflow.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/NickvanDyke/opencode.nvim">opencode.nvim&lt;/a>&lt;/td>
&lt;td>AI-powered code assistant integrated with my terminal. Mapped to &lt;code>&amp;lt;Leader&amp;gt;c&lt;/code> prefix.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Spell checking&lt;/td>
&lt;td>Custom commands (&lt;code>:Spell&lt;/code>, &lt;code>:NoSpell&lt;/code>, &lt;code>:SpellToggle&lt;/code>) with auto-enable for Markdown files.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Custom formatters&lt;/td>
&lt;td>Added formatters for Caddy, Markdown, JSON, and shell scripts to &lt;code>conform.nvim&lt;/code>.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="additional-customizations">Additional Customizations&lt;/h3>
&lt;p>Beyond plugins, I made several configuration tweaks:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Buffer Navigation:&lt;/strong> Mapped &lt;code>&amp;lt;Tab&amp;gt;&lt;/code> and &lt;code>&amp;lt;Shift-Tab&amp;gt;&lt;/code> to cycle through buffers.&lt;/li>
&lt;li>&lt;strong>Mouse Behavior:&lt;/strong> Changed from &lt;code>mouse='a'&lt;/code> to &lt;code>mouse='nci'&lt;/code> to disable visual selection mode while keeping other mouse features.&lt;/li>
&lt;li>&lt;strong>Diagnostic Display:&lt;/strong> Enhanced inline diagnostics with custom icons (✘ for errors, ▲ for warnings) and a &lt;code>&amp;lt;Leader&amp;gt;od&lt;/code> toggle.&lt;/li>
&lt;li>&lt;strong>Relative Line Numbers:&lt;/strong> Enabled with &lt;code>vim.o.relativenumber = true&lt;/code> (commented out in base config).&lt;/li>
&lt;li>&lt;strong>Shell Integration:&lt;/strong> Set default shell to &lt;code>/usr/bin/zsh&lt;/code> for &lt;code>:term&lt;/code> commands.&lt;/li>
&lt;li>&lt;strong>Hidden Characters:&lt;/strong> Commented out &lt;code>vim.o.list = true&lt;/code> to hide tab characters.&lt;/li>
&lt;/ol>
&lt;h2 id="plugins-i-no-longer-need">Plugins I No Longer Need&lt;/h2>
&lt;p>Some plugins from my &lt;code>kickstart-modular.nvim&lt;/code> setup became unnecessary with MiniMax:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/akinsho/bufferline.nvim">&lt;strong>bufferline.nvim&lt;/strong>&lt;/a>: MiniMax&amp;rsquo;s &lt;code>mini.tabline&lt;/code> provides similar functionality&lt;/li>
&lt;li>&lt;a href="https://github.com/tenxsoydev/karen-yank.nvim">&lt;strong>karen-yank.nvim&lt;/strong>&lt;/a>: I have a love/hate relationship with this plugin. For now I&amp;rsquo;m going to go back to the default cut/paste behavior.&lt;/li>
&lt;li>&lt;a href="https://github.com/rose-pine/neovim">&lt;strong>rose-pine.nvim&lt;/strong>&lt;/a>: Using the default MiniMax colorscheme (though I may add this back)&lt;/li>
&lt;li>&lt;a href="https://github.com/tpope/vim-fugitive">&lt;strong>vim-fugitive&lt;/strong>&lt;/a>: &lt;code>mini.git&lt;/code> covers most of my Git workflow needs&lt;/li>
&lt;/ul>
&lt;h2 id="thoughts-on-the-switch">Thoughts on the Switch&lt;/h2>
&lt;p>I&amp;rsquo;m impressed with how cohesive MiniMax feels. The &lt;code>mini.nvim&lt;/code> ecosystem provides a surprisingly complete feature set with minimal overhead.
The configuration is easier to understand because most functionality comes from a single, well-documented plugin family.&lt;/p>
&lt;p>The main trade-off is that some &lt;code>mini.nvim&lt;/code> modules are less feature-rich than their standalone counterparts (e.g., &lt;code>mini.completion&lt;/code> vs &lt;code>nvim-cmp&lt;/code>).
However, I&amp;rsquo;ve found the simpler implementations to be sufficient for my needs, and the performance benefits are noticeable.&lt;/p>
&lt;p>If you&amp;rsquo;re looking for a minimal, maintainable Neovim configuration in 2026, I highly recommend giving &lt;a href="https://github.com/echasnovski/MiniMax">MiniMax&lt;/a> a try.
You can see my complete configuration with customizations &lt;a href="https://github.com/jarv/MiniMax">here&lt;/a>.&lt;/p></description></item><item><title>Monitor a single file on GitHub/GitLab with RSS</title><link>https://jarv.org/posts/use-rss/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/use-rss/</guid><description>&lt;p>I&amp;rsquo;ve been using RSS to monitor single files or directories on GitHub and GitLab for a long time, and it still surprises me when people don’t know this is possible.
Granted, not everyone uses RSS so that part isn&amp;rsquo;t surprising, but it is quite a convenient way to keep track of files in repositories without having to manage email notifications for the entire project.&lt;/p>
&lt;p>Why would you do this?&lt;/p>
&lt;p>For one, I find it nice to track updates to &lt;code>README.md&lt;/code> files in a repository, a &lt;code>CHANGELOG&lt;/code>, or a file that tracks dependencies like &lt;code>package.json&lt;/code> or &lt;code>Gemfile&lt;/code>.*&lt;/p>
&lt;p>Both GitHub and GitLab give you the ability to do this easily, and it works on both individual files and directories containing them.&lt;/p>
&lt;ul>
&lt;li>For &lt;code>GitHub&lt;/code> adding &lt;code>.atom&lt;/code> to the end of the file will give you the atom feed (&lt;a href="https://github.com/golang/go/commits/master/README.md.atom">example&lt;/a>).&lt;/li>
&lt;li>For &lt;code>GitLab&lt;/code> you can create a feed by adding &lt;code>?format=atom&lt;/code> to the end of the file (&lt;a href="https://gitlab.com/gitlab-org/gitaly/-/commits/master/README.md?format=atom">example&lt;/a>).&lt;/li>
&lt;/ul>
&lt;p>This works for both public and private repositories, for private repositories it requires a &lt;code>?feed_token=xxxxx&lt;/code> query parameter.
However, you shouldn&amp;rsquo;t expose your feed token to a third party website, so if it is necessary I would only use a local feed application.&lt;/p>
&lt;hr>
&lt;p>&lt;em>* It is possible to do this with a site like &lt;a href="https://app.github-file-watcher.com/">github-file-watcher.com&lt;/a> if you really want to do this with email notifications.&lt;/em>&lt;/p></description></item><item><title>The wild vibe</title><link>https://jarv.org/posts/wild-vibe/</link><pubDate>Wed, 08 Oct 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/wild-vibe/</guid><description>&lt;p>It&amp;rsquo;s wild that these days you can vibe code an entire app in the same time it would previously take to contribute a single feature.
Take the RSS reader I use regularly, Newsboat.
It always had a few stability issues and was only about 80% there as far as the features I wanted in a terminal-based reader.
There were other alternatives that came along, but nothing matched what I was used to layout-wise.
While there were a few things I would have liked to improve, I never had the will or time to contribute any improvements myself.&lt;/p>
&lt;p>That all changed recently with Claude, as it suddenly became very easy to bootstrap a new project and create a very polished-looking TUI application in Go with barely any effort.
In about a single day, I created a TUI RSS reader alternative &lt;a href="https://github.com/jarv/newsgoat">NewsGoat&lt;/a> and couldn&amp;rsquo;t be happier with the results.
Not only was it extremely satisfying to have full control over every single feature that I wanted in the TUI, but it was actually less effort and hassle than contributing new features to the existing project.&lt;/p>
&lt;p>Consider the two options:&lt;/p>
&lt;ol>
&lt;li>Create a fork of an open source project, refactor and remove the features you don&amp;rsquo;t want, add the new features you do.&lt;/li>
&lt;li>Create a brand new project from scratch and add only the features you want.&lt;/li>
&lt;/ol>
&lt;p>Now what is wild is that with vibe coding, (2) is not only possible to do but is often more straightforward and easier than (1).
I can also choose the language I want to use, the build process, toolchain, distribution, and the boring parts of bootstrapping the project, creating build scripts, and workflows are all done by AI without even opening a text editor.&lt;/p>
&lt;p>Here has been my experience with this and other projects to efficiently create new projects from scratch:&lt;/p>
&lt;ul>
&lt;li>Pick an implementation that I am already familiar with. For me, this is usually Go and/or JavaScript.&lt;/li>
&lt;li>Create a project README that outlines exactly the features you want and be as specific as possible on implementation constraints. For example, for Go projects that require state, I will be specific about the database driver and specify to use &lt;a href="https://sqlc.dev/">sqlc&lt;/a>.&lt;/li>
&lt;li>Create the barebones scaffolding of the project in addition to the README. This includes bringing in all the common dependencies for lint checking and &lt;a href="https://mise.jdx.dev/">mise&lt;/a> for task management.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ve been doing this so often recently that I even created a &lt;a href="https://github.com/jarv/vibe-kickstart/">vibe-kickstart&lt;/a> template for bootstrapping new (web-based) projects that use Go and JavaScript.&lt;/p></description></item><item><title>Using Mise and 1Password for secrets</title><link>https://jarv.org/posts/mise-and-1password/</link><pubDate>Mon, 11 Aug 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/mise-and-1password/</guid><description>&lt;p>&lt;strong>2025-08-17 UPDATE&lt;/strong>: I&amp;rsquo;ve modified this post to use &lt;code>hooks.enter&lt;/code> and &lt;code>hooks.leave&lt;/code> after seeing how my previous solution was not working in all cases.&lt;/p>
&lt;p>Since &lt;a href="https://jarv.org/posts/mise">my last post on using mise&lt;/a> I&amp;rsquo;ve updated how I use &lt;a href="https://github.com/jdx/mise">Mise&lt;/a> with 1Password.&lt;/p>
&lt;p>Before, I was only using &lt;code>hooks.enter&lt;/code> to set env variables from 1password.
This had some disadvantages:&lt;/p>
&lt;ol>
&lt;li>Variables would be left set when leaving the project directory&lt;/li>
&lt;li>It made assumptions that you would cd into the root of the project directory first&lt;/li>
&lt;/ol>
&lt;p>This modification addresses those two issues by sourcing a script that unsets variables when leave the project directory.
This still has a major shortcoming which is that it doesn&amp;rsquo;t restore variables that have the same name when leaving, so that is something to be mindful of.&lt;/p>
&lt;p>Here is the approach using &lt;code>hooks.{enter,leave}&lt;/code> in &lt;code>mise.toml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">hooks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shell&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">script&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">source &amp;#34;&lt;/span>&lt;span class="p">{{&lt;/span> &lt;span class="nx">config_root&lt;/span> &lt;span class="p">}}&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="nx">pass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sh&lt;/span>&lt;span class="s2">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">hooks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">leave&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shell&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">script&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">source &amp;#34;&lt;/span>&lt;span class="p">{{&lt;/span> &lt;span class="nx">config_root&lt;/span> &lt;span class="p">}}&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="nx">pass&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sh&lt;/span>&lt;span class="s2">&amp;#34; unset
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add secrets by adding to the &lt;code>ENV_MAPPINGS&lt;/code> array.&lt;/p>
&lt;p>&lt;code>1pass.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ENV_MAPPINGS&lt;/span>&lt;span class="o">=(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;GOOGLE_KEY:op://personal/direnv/GOOGLE_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;GOOGLE_SECRET:op://personal/direnv/GOOGLE_SECRET&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># etc.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">unset_env_vars&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;⏳Unsetting environment variables ..&amp;#39;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> mapping in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ENV_MAPPINGS&lt;/span>&lt;span class="p">[@]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">env_var&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">mapping&lt;/span>&lt;span class="p">%%:*&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">unset&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$env_var&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;☑️ Environment variables unset!&amp;#39;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set_env_vars&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> ! &lt;span class="nb">command&lt;/span> -v op &amp;gt;/dev/null 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>1&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;error: 1Password CLI (op) not found in PATH&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;⏳Setting secrets in environment ..&amp;#39;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Build the template for op inject&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">template&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> mapping in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ENV_MAPPINGS&lt;/span>&lt;span class="p">[@]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">env_var&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">mapping&lt;/span>&lt;span class="p">%%:*&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">op_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">mapping&lt;/span>&lt;span class="p">#*:&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">template&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">template&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">export &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">env_var&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">={{ &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">op_path&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">eval&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$template&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> op --account my.1password.com inject&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;☑️ Done!&amp;#39;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;error: failed to inject/source secrets&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;unset&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> unset_env_vars
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> set_env_vars
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>See also &lt;a href="https://github.com/jdx/mise/discussions/3542#discussioncomment-14071436">this GitHub discussion&lt;/a> for others who are trying similar things with &lt;code>mise&lt;/code> to use 1password to populate the env.&lt;/p></description></item><item><title>Thinking about Trade Wars trading mechanics</title><link>https://jarv.org/posts/stardewar-v02/</link><pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/stardewar-v02/</guid><description>&lt;p>It&amp;rsquo;s been a while (3 months!) since my previous post on this little game I&amp;rsquo;ve been writing in my free time, so I thought it was probably a good time to give an update.&lt;/p>
&lt;p>The game is called &lt;a href="https://stardewar.com">STAR DEWAR&lt;/a> which is a space trading game that is inspired by the early BBS game &lt;a href="https://en.wikipedia.org/wiki/Trade_Wars">Trade Wars&lt;/a>.
Three months ago, I had created the basic hexagon map generation, multiplayer, and movement UI, but that was about it.
This update includes a few more features:&lt;/p>
&lt;ol>
&lt;li>Trading at ports!&lt;/li>
&lt;li>Updated UI&lt;/li>
&lt;li>Navigation computer messages&lt;/li>
&lt;/ol>
&lt;p>&lt;b>tldr; you can play a &lt;a href="https://stardewar.com/v02">(pre-alpha) demo here&lt;/a> that illustrates the new trading mechanics.&lt;/b>&lt;/p>
&lt;p>There isn&amp;rsquo;t much to discuss regarding (2) or (3), you can see these changes yourself by playing the demo.
The trading mechanics seemed interesting enough to warrant a post explaining how they&amp;rsquo;re implemented.&lt;/p>
&lt;p>In the game, you have three commodities, &lt;code>Fuel Ore&lt;/code>, &lt;code>Organics&lt;/code> and &lt;code>Equipment&lt;/code>.
I found myself going back to the original game (yes, there are still a small number of people who play) to remind myself how trading worked, how negotiations functioned, and how prices were set.&lt;/p>
&lt;p>In general, I wanted to implement something that captures the original feeling of the BBS game and trading experience while giving it a modern interface.&lt;/p>
&lt;p>To start, every port is designated with &lt;code>B&lt;/code> or &lt;code>S&lt;/code> for each commodity and shown on the mini-map like this:&lt;/p>
&lt;img src="https://jarv.org/img/tw2002-ports.png" style="width:300px">
&lt;p>In the original game, when you traded at a port, a commodity table would be presented like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> Docking...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> One turn deducted, 14 turns left.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Commerce Report for Tarsus: 08/19/05 02:01:48 PM
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -=-=- Docking Log -=-=-
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> USS Enterprise NCC-1701-D docked 1 day(s) ago.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Items Status Trading % of max OnBoard
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ----- ------ ------- -------- -------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Fuel Ore Buying 23 4% 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Organics Buying 193 17% 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Equipment Selling 741 42% 0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The way I understand it worked in the original game, there was a finite limit of resources for each type, and they would slowly get replenished on a daily interval.
There was also a negotiation step where you could offer more or fewer credits than the original offer.&lt;/p>
&lt;p>The counter-offer&amp;rsquo;s success was based on various factors including the port&amp;rsquo;s characteristics, your experience, and probably other things I don&amp;rsquo;t remember.&lt;/p>
&lt;p>The trading screen in STAR DEWAR has a similar look with some notable differences:&lt;/p>
&lt;img src="https://jarv.org/img/tw2002-trading-screen.png" style="width:300px">
&lt;p>There&amp;rsquo;s no text input for trades, so negotiations are fixed at &amp;ldquo;full price,&amp;rdquo; &amp;ldquo;10%,&amp;rdquo; or &amp;ldquo;20%&amp;rdquo; more or less than the offer.
Accepting or rejecting a trade is where things get interesting, there&amp;rsquo;s a deterministic score given to every trade that sets bounds for the minimum and maximum accepted values.
This determination uses the following factors:&lt;/p>
&lt;ol>
&lt;li>How experienced the player is (experience points)&lt;/li>
&lt;li>The port&amp;rsquo;s willingness to negotiate (set per port)&lt;/li>
&lt;li>How much of the commodity is present at the port&lt;/li>
&lt;li>How reasonable the price is&lt;/li>
&lt;/ol>
&lt;p>In STAR DEWAR, each of these factors is assigned a weight, which results in a minimum and maximum trade value for the commodity.
Like the original game, a successful negotiation will give you some experience.
Conversely, a trade rejection will result in losing experience.
As the port reaches its maximum capacity, it will be less inclined to negotiate.
If the port is rejecting many trades, it will reduce its willingness to haggle.
Lastly, successful trades will self-reinforce future successful trades as players increase their experience.&lt;/p>
&lt;p>Counter-offers are where some randomness comes into play.
If an offer comes in that&amp;rsquo;s outside the port&amp;rsquo;s acceptable range, there&amp;rsquo;s a 50% chance of a counter-offer, which will be a random value within the acceptable range.
Upon receiving the offer, the player can either accept it for the full amount or take an additional chance and try to get a better deal.&lt;/p>
&lt;p>I&amp;rsquo;ve been thinking a little bit about how to make trading less of a grind and more interesting, which was a problem that was definitely present in the original game.
Maybe automating trade routes that consume unused turns?&lt;/p>
&lt;p>You can check out how trading works &lt;a href="https://stardewar.com/v02">by playing the pre-alpha demo&lt;/a>, which is a universe loaded up with a large number of ports to illustrate how it works.
If you have any feedback or suggestions, please don&amp;rsquo;t hesitate to &lt;a href="https://jarv.org/contact/">reach out&lt;/a> or &lt;a href="https://stardewar.com/">submit your email for these infrequent updates&lt;/a>.&lt;/p></description></item><item><title>Accounts and Centralized Survey View for FlyEmoji</title><link>https://jarv.org/posts/flyemoji-accounts/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/flyemoji-accounts/</guid><description>&lt;p>A quick update about some new features recently added to &lt;a href="https://flyemoji.com">FlyEmoji&lt;/a>.
These are a couple of changes that should make the site easier to use, especially for anyone running multiple surveys or wanting to track what they&amp;rsquo;ve participated in.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>User accounts and login&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>With the new account system you can sign up with an email and password (or log in if you already have an account).
Once logged in, any survey you create will be linked to your account automatically.&lt;/p>
&lt;p>This also lays the groundwork for better access control in the future (like making surveys private, sharing them with specific users, etc.).&lt;/p>
&lt;ol start="2">
&lt;li>&lt;strong>Central survey dashboard&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>Another piece that&amp;rsquo;s now live is a consolidated view of your surveys. Once logged in, you&amp;rsquo;ll land on a new dashboard page that lists:&lt;/p>
&lt;ul>
&lt;li>All surveys you&amp;rsquo;ve created&lt;/li>
&lt;li>Surveys you&amp;rsquo;re participating in (for example if someone invited you to respond)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>If you want to try it out, head over to &lt;a href="https://flyemoji.com">https://flyemoji.com&lt;/a> and register for an account.
If you have any feedback please let me know!&lt;/p></description></item><item><title>A Trade Wars 2002 inspired pocket game</title><link>https://jarv.org/posts/stardewar-v01/</link><pubDate>Mon, 26 May 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/stardewar-v01/</guid><description>&lt;p>As I mentioned in &lt;a href="https://jarv.org/posts/drunkards-walk-with-hiding-spots/">my last post&lt;/a> in my free-time I&amp;rsquo;ve been tinkering around with multi-player space themed game.
The game is called STAR DEWAR, and will be a tribute to an old BBS game called Trade Wars 2002 that I played as a kid.&lt;/p>
&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Trade_Wars">Trade Wars&lt;/a> was a classic multi-player BBS game that had its peak popularity in the 1990s.
It was one of the insiprations for Eve Online (early 2000s) and plenty of other games that focus on space exploration and trading.&lt;/p>
&lt;p>I&amp;rsquo;ve always thought about extracting some of the elements I loved the most in the game and turning it into a &amp;ldquo;pocket game&amp;rdquo;, or something that is approachable by the more casual gamer.
This is the first (hopefully not only) journal entry, to help keep myself motivated and to chart progress as the game develops.&lt;/p>
&lt;p>The first version is quite minimal; it includes map generation, player tracking, ship navigation and support for multiple simultaneous clients.
One of the main goals was to write something that works well on mobile devices which is a challenge when your main inspiration is game played on a text based interface.&lt;/p>
&lt;p>If any of this sounds interesting, please read on, or if you would like to see what is working so far check out &lt;a href="https://stardewar.com/v01">https://stardewar.com/v01&lt;/a>.&lt;/p>
&lt;ol>
&lt;li>When the game server starts, a random hex based sector map is generated.&lt;/li>
&lt;li>When you connect your browser, I assign you a player name (stored in a session cookie) and place your ship on the map.&lt;/li>
&lt;li>All other players are automatically get &amp;ldquo;limpet mines&amp;rdquo; that allow you to see where they are.&lt;/li>
&lt;li>You can move around the map and see other player movement in real time.&lt;/li>
&lt;/ol>
&lt;p>That&amp;rsquo;s it for now! Notthing terribly exciting but getting this far did take a fair bit of time thinking about design and writing code.&lt;/p>
&lt;h3 id="the-universe">The universe&lt;/h3>
&lt;p>The classic TradeWars 2002 BBS game had a map divided up into sectors.
Each sector might contain a planet, a trading port, as well as other ships, mines, fighters, etc.
The typical TW2002 map had around 1000 sectors that were represented as a directed graph.
Adjacent sectors were both bidirectional or one-way.&lt;/p>
&lt;p>My first obstacle was to think about how to replicate the experience of exploration, finding optimal trade routes, and looking for places that aren&amp;rsquo;t easily discovered by other players.
For this inspired version, I went with a tiled map with hexigon tiles.
For now, all movement is bidirectional but that could change if I can manage to come up with a nice way to show that visually.&lt;/p>
&lt;p>It is quite different than the original, where there wasn&amp;rsquo;t a 2D representation of space.
What I think I would like to add later is the ability to connect distant sectors using wormhole paths.&lt;/p>
&lt;p>&lt;a href="https://stardewar.com/v01">&lt;img src="https://jarv.org/img/sector-map.png" style="width:300px">&lt;/a>&lt;/p>
&lt;p>&lt;em>The universe fully zoomed out with all sectors discovered&lt;/em>&lt;/p>
&lt;p>After five minutes, a &amp;ldquo;Big Bang&amp;rdquo; button will appear allowing anyone to restart the game server and start from scratch.&lt;/p>
&lt;h3 id="development">Development&lt;/h3>
&lt;p>I&amp;rsquo;ve never written code for a game like this and definitely never have written a 2D game UI for the browser.
Here were some of the more challenging parts:&lt;/p>
&lt;ul>
&lt;li>Sequencing movement on a 2d board for multiple players&lt;/li>
&lt;li>Zoom, drag, and select with touch events&lt;/li>
&lt;li>Centrally managing multiplayer real-time map updates&lt;/li>
&lt;li>Tracking per player visited sectors and fog-of-war&lt;/li>
&lt;li>Dealing with a hexagon tile map on both the front-end and back-end&lt;/li>
&lt;/ul>
&lt;h4 id="front-end">Front-end&lt;/h4>
&lt;p>For developing the front-end, I decided to use &lt;a href="https://phaser.io/">phaser.io&lt;/a>.
I probably could have gotten away without using a game engine at all, but Phaser has been a great tool so for managing sprites and animating them.&lt;/p>
&lt;p>Funny one of the more difficult parts of the front-end (something I take for granted), was coding a natural way to distinguishing a drag, select and a pinch zoom.
Modern browsers allow you use multiple pointers for pinch detection.
That, along with manual event tracking of how long the pointer is down made it possible to handle all three on a 2D map.&lt;/p>
&lt;p>The other part was queuing movement and animations.
Because the backend is authoritative for player state, all movement needs to be sent over a websocket.
The &amp;ldquo;shortest path&amp;rdquo; calculation is done on the backend as well.
When you click to a cell that is on a different spot in the map (even if you haven&amp;rsquo;t explored tiles in-between), the backend generates the calculated route.
If multiple movements commands are issued before the movement animation completes they need to be queued up.
The same applies to other player movements that are broadcast to players that have tracking mines attached to the ship.&lt;/p>
&lt;p>Movement updates from the server provide only the new movement path (so we don&amp;rsquo;t redraw the entire map on every move), however, any time there is a reconnection is lost or if the browser loses focus the map is redrawn from scratch.&lt;/p>
&lt;h4 id="back-end">Back-end&lt;/h4>
&lt;p>On the back-end I decided to use Go as I&amp;rsquo;m already familiar with writing Go for non-game software.
Most of the work was coming up with a schema and queries and then generating code with &lt;a href="https://github.com/sqlc-dev/sqlc">sqlc&lt;/a>.
All player state is tracked in sqlite&lt;/p>
&lt;p>The map generation is done on the back-end and for more details see &lt;a href="https://jarv.org/posts/drunkards-walk-with-hiding-spots/">my earlier post on my variation of Drunkard&amp;rsquo;s Walk&lt;/a>.
After playing around with it a bit I went with spherical map that sets a boundary using a radius from the center.&lt;/p>
&lt;p>For package dependencies I used the following:&lt;/p>
&lt;ul>
&lt;li>github.com/coder/websocket&lt;/li>
&lt;li>github.com/dustinkirkland/golang-petname (for generating player names)&lt;/li>
&lt;li>github.com/gorilla/sessions v1.4.0 (for tracking players in cookie based sessions)&lt;/li>
&lt;li>github.com/ncruces/go-sqlite3&lt;/li>
&lt;/ul>
&lt;p>Using &lt;a href="https://pkg.go.dev/embed">embed&lt;/a>, all assets are compiled into the binary making deployments a breeze, especially with a in-memory db to start.&lt;/p>
&lt;h3 id="what-next">What next?&lt;/h3>
&lt;p>The most challenging part of modern adaptations of games like Trade Wars, is coming up with the mechanics around trading.
This was a central part of the original game and I would like to keep it that way with this &amp;ldquo;pocket size version&amp;rdquo;.
Doing that, but also not making it too much of a grinding game going back-and-forth between ports is a challenge.
Right now, I&amp;rsquo;m thinking that I want to keep the trading routes concept of &lt;code>Fuel Ore&lt;/code>, &lt;code>Organics&lt;/code> and &lt;code>Equipment&lt;/code> but also have a way to set up trading routes to automatically use-up unused turns to trade.&lt;/p>
&lt;p>For the next addition, what I have in mind is to create artwork and framework for placing ports.
Who knows how long that will take or whether I can keep up, but my love and nostalgia for this game is keeping me motivated.&lt;/p>
&lt;p>If you would like to receive updates when I make future posts like these, submit your email on &lt;a href="https://stardewar.com">https://stardewar.com&lt;/a>.&lt;/p></description></item><item><title>A Drunkard's Walk with hiding spots</title><link>https://jarv.org/posts/drunkards-walk-with-hiding-spots/</link><pubDate>Wed, 26 Mar 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/drunkards-walk-with-hiding-spots/</guid><description>&lt;style>
.slider {
-webkit-appearance: none;
width: 100%;
height: 15px;
border-radius: 10px;
background: #e0e0e0;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
transition: background 0.15s ease-in-out;
}
.slider::-moz-range-thumb {
width: 30px;
height: 30px;
border: 0;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
transition: background 0.15s ease-in-out;
}
.slider::-webkit-slider-thumb:hover {
background: #2E8B57;
}
.slider:active::-webkit-slider-thumb {
background: #2E8B57;
}
.slider::-moz-range-thumb:hover {
background: #2E8B57;
}
.sliderControls {
max-width: 40ch;
margin: auto;
padding-bottom: 20px;
}
.value {
text-align: center;
}
button#reseed {
background-color: #4CAF50;
color: white;
border: none;
padding: 0.5em 1.0em;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
margin: auto;
}
button#reseed:hover {
background-color: #2E8B57;
}
/* New styles for the control container */
.controls-container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin: 10px auto;
text-align: center;
width: 100%;
}
&lt;/style>
&lt;p>I&amp;rsquo;ve been playing around lately with procedural map generation and different ways to generate contiguous areas on a hexagonal grid.
To keep things a simple as possible I started with a &lt;a href="https://en.wikipedia.org/wiki/Random_walk">Random Walk&lt;/a> (also known as &amp;ldquo;Drunkard&amp;rsquo;s Walk&amp;rdquo;) but found it a bit lacking.
Part of my map generation needs to include dead-ends (or hiding spots) that will be difficult to discover when traversing distance from one hex to another hex.&lt;/p>
&lt;p>A simple &amp;ldquo;Drunkard&amp;rsquo;s Walk&amp;rdquo; is to start at one cell and pick a random neighbor that hasn&amp;rsquo;t been visited yet.
If there are no neighbors that haven&amp;rsquo;t already been visited, take all remaining visited cells that have unvisited neighbors and pick one at random.
This can be repeated until a percentage of cells have been picked and will result in a contiguous area that can be used for a map.&lt;/p>
&lt;p>Here is how it looks, taking a random contiguous path for 70% of the cells on the grid:&lt;/p>
&lt;div id="game-container-no-dead-ends">&lt;/div>
&lt;p>Note that in the above hex map, every cell has at least 2 entry points.
What I would like to change is an increased likelyhood of &amp;ldquo;hiding spots&amp;rdquo; or cells with only one entry point so that when traversed, players in these dead-end cells are less likely to be discovered.&lt;/p>
&lt;p>Here is the same walk, filling 70% of the cells but with more dead-ends:&lt;/p>
&lt;div id="game-container-dead-ends">&lt;/div>
&lt;p>This is very similar to the other logic, except with a set probability, if the current cell has 5 neighbors that haven&amp;rsquo;t been visited, it is added to a list of dead-ends.
Then, instead of picking one of those neighbors to visit next, we pick a random hex in the list of visited cells.
If there are no cells that meet that criteria, then we start at a random cell in the dead-end list.&lt;/p>
&lt;p>As the map density is increased, there are fewer dead-ends as they will be re-purposed to fill the map, resulting in mostly &amp;ldquo;horseshoe&amp;rdquo; hexes.
Below you can play with the cell density and the probability of marking a dead-end.
For my own map, a density of 60% and a dead-end probability of 40% is about right.&lt;/p>
&lt;div class="controls-container">
&lt;div style="display: flex; align-items: center; gap: 10px;">
&lt;button id="reseed">Reseed&lt;/button>
&lt;span>Dead end count: &lt;span id="deadEndCount">0&lt;/span>&lt;/span>
&lt;/div>
&lt;/div>
&lt;div id="game-container">&lt;/div>
&lt;div class="sliderControls">
&lt;div>
&lt;input type="range" min="0" max="1" value="0.5" step="0.01" id="densitySlider" class="slider">
&lt;div class="value">Density: &lt;span id="densityValue">0.5&lt;/span>&lt;/div>
&lt;/div>
&lt;div>
&lt;input type="range" min="0" max="1" value="0" step="0.01" id="deadEndSlider" class="slider">
&lt;div class="value">Dead end probability: &lt;span id="deadEndValue">0&lt;/span>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>If you read this far, I&amp;rsquo;m hoping to write a few posts on writing a multi-player 2D game and hopefully this is the first post of many (we will see).
The game is called &lt;a href="https://stardewar.com">Star Dewar&lt;/a> which is an anagram for an old BBS game I liked as a kid.&lt;/p>
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.55.2/phaser.min.js">&lt;/script>
&lt;script src="https://jarv.org/js/hexmap.js?1779258602
">&lt;/script></description></item><item><title>Neovim Configuration for 2025</title><link>https://jarv.org/posts/neovim-config/</link><pubDate>Sun, 23 Feb 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/neovim-config/</guid><description>&lt;style>
table {
border-collapse: collapse;
width: 100%;
}
td {
padding: 10px;
text-align: left;
border: 1px solid gray;
}
&lt;/style>
&lt;p>Here are some notes on how my Neovim configuration management evolved in the last year.
For longtime Vim → Neovim users like me who prefer a simple and sane way to manage their configuration, I highly recommend &lt;a href="https://github.com/nvim-lua/kickstart.nvim">kickstart.nvim&lt;/a> as a starting point.
It provides a minimal yet functional configuration that is easy to understand, making it ideal for those who don&amp;rsquo;t want to commit to a pre-built template without knowing what it does.&lt;/p>
&lt;h2 id="my-configuration-and-workflow">My Configuration and Workflow&lt;/h2>
&lt;ol>
&lt;li>I maintain my &lt;a href="https://github.com/jarv/kickstart-modular.nvim">own fork&lt;/a> of &lt;a href="https://github.com/dam9000/kickstart-modular.nvim">kickstart-modular.nvim&lt;/a>.
This is essentially &lt;code>kickstart.nvim&lt;/code>, but structured into directories rather than a single &lt;code>init.lua&lt;/code>, making it easier to manage.&lt;/li>
&lt;li>I apply my customizations in a &lt;a href="https://github.com/dam9000/kickstart-modular.nvim/compare/master...jarv:kickstart-modular.nvim:master">single commit&lt;/a>.&lt;/li>
&lt;li>I periodically pull in upstream changes, allowing me to stay up-to-date with core plugins while maintaining my modifications.&lt;/li>
&lt;/ol>
&lt;h2 id="customizations">Customizations&lt;/h2>
&lt;p>I try to keep my customizations on top of &lt;code>kickstart.nvim&lt;/code> minimal and avoid having too many plugins in general.
Even though I customize it a bit, the base plugins might be all you need as it already includes the essentials like plugin management, LSP, and general quality-of-life improvements for writing code.&lt;/p>
&lt;p>Here are seven plugins that I add on top of ones included in &lt;code>kickstart.nvim&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Plugin&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/akinsho/bufferline.nvim">bufferline.nvim&lt;/a>&lt;/td>
&lt;td>Displays open buffers as tabs at the top of the session. I find this helpful for keeping track of my open buffers but there are a lot of different methods to do that.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ekickx/clipboard-image.nvim">clipboard-image.nvim&lt;/a>&lt;/td>
&lt;td>Useful for quickly inserting images into Markdown blog posts via copy/paste.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/f3fora/cmp-spell">cmp-spell&lt;/a>&lt;/td>
&lt;td>Adds spell-checking suggestions to &lt;code>nvim-cmp&lt;/code> auto-completion.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/tenxsoydev/karen-yank.nvim">karen-yank&lt;/a>&lt;/td>
&lt;td>Prevents deletions (&lt;code>D&lt;/code>, &lt;code>dd&lt;/code>) from copying content into the paste register, making yanking more explicit.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/meznaric/key-analyzer.nvim">key-analyzer.nvim&lt;/a>&lt;/td>
&lt;td>Provides a quick overview of key mappings.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/rose-pine/neovim">rose-pine.nvim&lt;/a>&lt;/td>
&lt;td>My current colorscheme.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/tpope/vim-fugitive">vim-fugitive&lt;/a>&lt;/td>
&lt;td>Git integration for Neovim.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In addition to these plugins, I have several customizations to the base config that enhance my workflow:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Backups:&lt;/strong> I enable backups using &lt;code>set backup&lt;/code>, which has saved me a few times.&lt;/li>
&lt;li>&lt;strong>Buffer Navigation:&lt;/strong> I map &lt;code>&amp;lt;Tab&amp;gt;&lt;/code> and &lt;code>&amp;lt;Shift-Tab&amp;gt;&lt;/code> to switch between buffers, which pairs well with &lt;code>bufferline.nvim&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Auto-complete:&lt;/strong> I add &lt;code>hrsh7th/cmp-buffer&lt;/code> to &lt;code>cmp.lua&lt;/code> to enable auto-completion of words from open buffers.&lt;/li>
&lt;li>&lt;strong>Mini.nvim Enhancements:&lt;/strong> While &lt;code>kickstart.nvim&lt;/code> includes some &lt;a href="https://github.com/echasnovski/mini.nvim">&lt;code>mini.nvim&lt;/code>&lt;/a> plugins, I also add &lt;code>mini.indentscope&lt;/code>, &lt;code>mini.icons&lt;/code>, and &lt;code>mini.files&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Telescope Optimization:&lt;/strong> I modify &lt;code>telescope.lua&lt;/code> so that when inside a Git repository, searches start from the repository root.&lt;/li>
&lt;li>&lt;strong>Relative Line Numbers:&lt;/strong> I set &lt;code>vim.opt.relativenumber = true&lt;/code> for relative line numbering.&lt;/li>
&lt;li>&lt;strong>Mouse Behavior:&lt;/strong> I disable visual selection with the mouse, as I find it more disruptive than helpful.&lt;/li>
&lt;/ol>
&lt;p>Not many changes to be honest, and I&amp;rsquo;ve been really happy with this setup so far.
If you would like to see the configuration in its entirety, it can be viewed &lt;a href="https://github.com/jarv/kickstart-modular.nvim">here&lt;/a>.&lt;/p></description></item><item><title>Notes on Cookie partitioning</title><link>https://jarv.org/posts/cookie-partitioning/</link><pubDate>Sun, 05 Jan 2025 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/cookie-partitioning/</guid><description>&lt;p>The &lt;code>Partitioned&lt;/code> attribute on browser Cookies is supported on Firefox and Chrome and these are some notes on changes I observed recently with it, and how these browsers handle Cookies. This post assumes you have a basic understanding of how HTTP cookies work.&lt;/p>
&lt;h3 id="what-is-cookie-partitioning">What is Cookie Partitioning?&lt;/h3>
&lt;p>This feature targets analytics providers, specifically those that drop the same cookie in your browser when visiting multiple domains.&lt;/p>
&lt;p>Let&amp;rsquo;s assume you are loading two webpages, one on &lt;code>siteA.com&lt;/code> and another on &lt;code>siteB.com&lt;/code>.&lt;br>
Both sites make requests to an analytics provider on &lt;code>analytics.com&lt;/code>.&lt;/p>
&lt;p>Without partitioning, when you visit &lt;code>siteA.com&lt;/code> and get a cookie from &lt;code>analytics.com&lt;/code>, and then later visit &lt;code>siteB.com&lt;/code>, which also makes a request to &lt;code>analytics.com&lt;/code>, &lt;code>analytics.com&lt;/code> will receive the same cookie that was received from &lt;code>siteA.com&lt;/code>. This is one of the primary reasons why browsers often block third-party cookies.&lt;/p>
&lt;p>With partitioning, the browser maintains partitioned cookie stores so that &lt;code>analytics.com&lt;/code> can&amp;rsquo;t access the same cookie for both domains.&lt;/p>
&lt;h3 id="how-are-browsers-using-the-partitioned-attribute">How are Browsers Using the &lt;code>Partitioned&lt;/code> Attribute?&lt;/h3>
&lt;p>Here, we can see that how the browser treats &lt;code>Secure&lt;/code> and &lt;code>Paritioned&lt;/code> is quite different depending on the browser.&lt;/p>
&lt;style>
table {
border-collapse: collapse;
width: 100%;
}
td {
padding: 10px;
text-align: center;
border: 1px solid gray;
}
tr:last-child td {
}
.grayscale {
filter: grayscale(1);
display: inline-block; /* Ensures the filter applies properly to inline content */
}
&lt;/style>
&lt;p>This table shows what cookies are stored with different values for &lt;code>SameSite&lt;/code> in the browser after a web request.&lt;/p>
&lt;p>(These screenshots are taken from one of the &lt;a href="https://samesite.flyemoji.com/#explain1">SameSite scenarios on this cookie playground&lt;/a>)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>(S)&lt;/th>
&lt;th>(P)&lt;/th>
&lt;th>Chrome (131.0.6778.205)&lt;/th>
&lt;th>Firefox (133.0.3)*&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>t&lt;/strong>&lt;/td>
&lt;td>&lt;strong>t&lt;/strong>&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=trueP=true-Chrome.png" style="width:200px">&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=trueP=true-Firefox.png" style="width:200px">&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>t&lt;/strong>&lt;/td>
&lt;td>&lt;strong>f&lt;/strong>&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=trueP=false-Chrome.png" style="width:200px">&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=trueP=false-Firefox.png" style="width:200px">&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>f&lt;/strong>&lt;/td>
&lt;td>&lt;strong>t&lt;/strong>&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=falseP=true-Chrome.png" style="width:200px">&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=falseP=true-Firefox.png" style="width:200px">&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>f&lt;/strong>&lt;/td>
&lt;td>&lt;strong>f&lt;/strong>&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=falseP=false-Chrome.png" style="width:200px">&lt;/td>
&lt;td>&lt;img src="https://jarv.org/img/S=falseP=false-Firefox.png" style="width:200px">&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;ul>
&lt;li>&lt;code>Browser Cookies&lt;/code>: request sent to the same Domain&lt;/li>
&lt;li>&lt;code>IFrame Cookies&lt;/code>: request sent to a different Domain&lt;/li>
&lt;li>🍪: Cookie received in the browser&lt;/li>
&lt;li>&lt;span class="grayscale">🍪&lt;/span>: Cookie not received in the browser&lt;/li>
&lt;li>&lt;strong>(S)&lt;/strong>: &lt;code>Secure&lt;/code> attribute&lt;/li>
&lt;li>&lt;strong>(P)&lt;/strong>: &lt;code>Partitioned&lt;/code> attribute&lt;/li>
&lt;/ul>
&lt;p>&lt;em>*Firefox is running in Strict mode&lt;/em>&lt;/p>
&lt;p>Some surprising differences seen here:&lt;/p>
&lt;ol>
&lt;li>With &lt;code>Secure=true&lt;/code> and &lt;code>Partitioned=true&lt;/code>, if the &lt;code>SameSite&lt;/code> attribute is not set, third party cookies will not be stored. This is due to Chrome treating the default &lt;code>Samesite&lt;/code> value as &lt;code>Lax&lt;/code>.&lt;/li>
&lt;li>We see the descrepency for &lt;code>Secure=true&lt;/code> and &lt;code>Partitioned=false&lt;/code> for the same reason.&lt;/li>
&lt;li>With &lt;code>Secure=false&lt;/code> and &lt;code>Partitioned=true&lt;/code>, Chrome does not store cookies, regardless of the &lt;code>SameSite&lt;/code> value.&lt;/li>
&lt;li>With &lt;code>Secure=false&lt;/code> and &lt;code>Partitioned=false&lt;/code>, Chrome does store cookies, but none from a different Domain.&lt;/li>
&lt;/ol>
&lt;h3 id="why-is-the-partitioned-attribute-necessary">Why is the &lt;code>partitioned&lt;/code> Attribute Necessary?&lt;/h3>
&lt;p>What I found a bit confusing is why we need a &lt;code>partitioned&lt;/code> attribute on cookies if this is a browser feature? The purpose of the attribute is to signal to the browser that the cookie works with browser partitioning. Without it, the browser might reject the cookie outright by default.&lt;/p>
&lt;blockquote>
&lt;p>cookie has been rejected because it is foreign and does not have the “Partitioned“ attribute.&lt;/p>
&lt;/blockquote>
&lt;p>Before, I believe this was just a warning.
I&amp;rsquo;ve updated the &lt;a href="https://samesite.flyemoji.com">samesite testing sandbox&lt;/a> with the option to enable and disable &lt;code>partitioned&lt;/code> cookies to play around with this.&lt;/p></description></item><item><title>Using Mise for All the Things</title><link>https://jarv.org/posts/mise/</link><pubDate>Mon, 30 Dec 2024 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/mise/</guid><description>&lt;p>I&amp;rsquo;ve been using &lt;a href="https://github.com/jdx/mise">Mise&lt;/a> for some time now for both personal projects and work to manage versions of the tools used in my projects. Recently, Mise has added functionality for tasks and environment handling, leading me to consolidate tools like &lt;a href="https://direnv.net/">&lt;code>direnv&lt;/code>&lt;/a>, &lt;a href="https://www.gnu.org/software/make/">&lt;code>Make&lt;/code>&lt;/a>, and &lt;a href="https://eradman.com/entrproject/">&lt;code>entr&lt;/code>&lt;/a>.&lt;/p>
&lt;p>When I create a new project, I often set up the following:&lt;/p>
&lt;ol>
&lt;li>&lt;code>Mise&lt;/code> for versioning binaries like &lt;code>node&lt;/code>, &lt;code>go&lt;/code>, etc.&lt;/li>
&lt;li>A &lt;code>bin/&lt;/code> directory with scripts for executing tasks using &lt;code>entr&lt;/code>, and sometimes a &lt;code>Makefile&lt;/code> for calling scripts.&lt;/li>
&lt;li>&lt;code>direnv&lt;/code> for setting up the environment.&lt;/li>
&lt;/ol>
&lt;p>With the recent addition of Mise &lt;a href="https://mise.jdx.dev/hooks.html">hooks&lt;/a> and &lt;a href="https://mise.jdx.dev/tasks/">tasks&lt;/a>, all three of these can now be handled effectively with &lt;code>Mise&lt;/code> itself.&lt;/p>
&lt;h2 id="environment-and-secrets">Environment and Secrets&lt;/h2>
&lt;p>&lt;strong>Note&lt;/strong>: See my updated post on &lt;a href="https://jarv.org/posts/mise-and-1password">using mise and 1Password&lt;/a>.&lt;/p>
&lt;p>In this example, I have an environment variable named &lt;code>CLOUDFLARE_EMAIL&lt;/code> and a secret &lt;code>CLOUDFLARE_API_KEY&lt;/code>.&lt;br>
Both are now set in my &lt;code>Mise&lt;/code> configuration and loaded when I enter the project directory.&lt;/p>
&lt;p>The &lt;code>.mise.toml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">CLOUDFLARE_EMAIL&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;john@jarv.org&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">hooks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shell&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">script&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">echo -e &amp;#39;⏳ Setting secrets in environment...&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">source &amp;lt;(cat .1pass | op --account my.1password.com inject)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">echo -e &amp;#39;☑️ Done!&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every project has a file &lt;code>.1pass&lt;/code> that uses &lt;a href="https://developer.1password.com/docs/cli/secrets-template-syntax/">1Password template syntax&lt;/a> to inject secrets as environment variables:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">CLOUDFLARE_API_KEY&lt;/span>&lt;span class="o">={{&lt;/span> op://personal/direnv/CF_GLOBAL_API_KEY &lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="tasks-and-watching-files-on-changes">Tasks and Watching Files on Changes&lt;/h2>
&lt;p>Now that the environment is sorted out, we can replace utility scripts and a &lt;code>Makefile&lt;/code> for running them with &lt;a href="https://mise.jdx.dev/tasks/">&lt;code>Mise&lt;/code> tasks&lt;/a>.&lt;br>
These are also configured in the project&amp;rsquo;s &lt;code>.mise.toml&lt;/code> file.&lt;br>
For example, for this blog I have:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">tasks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">serve&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">run&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;hugo server -D -F&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With this, I can run &lt;code>mise run serve&lt;/code> to start the &lt;code>hugo&lt;/code> server.&lt;/p>
&lt;p>Watching files and restarting a process on changes can be configured in &lt;code>.mise.toml&lt;/code> using &lt;a href="https://github.com/watchexec/watchexec">watchexec&lt;/a>.&lt;/p>
&lt;p>Here is a &lt;code>.mise.toml&lt;/code> config for a task named &lt;code>run&lt;/code> that builds assets and recompiles my Go app on changes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">tasks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">run&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">depends&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;build-assets&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">sources&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;**/*.{tmpl,go}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;src/**/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;package.json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">run&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;go run . -dbFile /tmp/db.sqlite3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>mise watch -r run&lt;/code> will start watching files and rebuild everything whenever one of the files in &lt;code>sources&lt;/code> is modified.&lt;br>
The &lt;code>-r&lt;/code> is passed to &lt;code>watchexec&lt;/code> to force a process restart.&lt;/p>
&lt;p>Or if that is too hard to remember, you can add it as a task:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">tasks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">watch&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">run&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;mise watch -r run&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, all you need to do is type &lt;code>mise run watch&lt;/code>.&lt;/p></description></item><item><title>Centering an emoji character on a button</title><link>https://jarv.org/posts/unable-to-center-an-emoji/</link><pubDate>Thu, 15 Aug 2024 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/unable-to-center-an-emoji/</guid><description>&lt;p>One of my emoji-based side-projects has button elements and centering emoji characters on them is a key part of the design.&lt;/p>
&lt;p>My initial thought was that this would be simple for a single emoji character by setting font-family, font-size and line-height for consistent centering.
I mean, how complicated can it be, right?&lt;/p>
&lt;p>Once I had things looking decent on chrome/firefox/safari it seemed as though my work was done, but then I pulled up the page on Linux/Firefox and my
nice centering shown here:&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/EmojiOSX.png" alt="">
&lt;em>Firefox/Chrome/Safari on OSX&lt;/em>&lt;/p>
&lt;p>looked something like this :(&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/EmojiLinux.png" alt="">
&lt;em>Firefox on Linux&lt;/em>&lt;/p>
&lt;p>Around this time I read &lt;a href="https://tonsky.me/blog/centering/">Hardest Problem in Computer Science: Centering Things&lt;/a> and realized that maybe I shouldn&amp;rsquo;t spend too much time on it.&lt;/p>
&lt;p>As an alternative, I thought to bundle a font like &lt;a href="https://fonts.google.com/noto/specimen/Noto+Emoji">Noto Emoji&lt;/a> but seeing how many MB it would add to page I decided not to go that route.&lt;/p>
&lt;p>In the end I only needed around 30 emojis, so the simplest workaround was to take my emojis used for the layout and convert them to images.
Following that all was well again and I was even able to let people select multiple emoji types (google, apple, facebook, openemoji).&lt;/p>
&lt;p>If you want to see the new image based emoji layout in action &lt;a href="https://flyemoji.com">check it out here&lt;/a>.&lt;/p></description></item><item><title>Using Strict for the SameSite attribute</title><link>https://jarv.org/posts/samesite/</link><pubDate>Mon, 27 May 2024 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/samesite/</guid><description>&lt;p>I was building a site that uses QR codes that when scanned take the user back to the same site.
For example, if the site was &lt;code>https://example.com&lt;/code> the QR code would bring them to &lt;code>https://example.com/qr&lt;/code> like this:&lt;/p>
&lt;img src="https://jarv.org/img/qr-code-samesite.png" style="width:350px">
&lt;p>The site uses a session cookie that is necessary to track user settings for anonymous users.
Initially, I set the &lt;code>SameSite&lt;/code> attribute to &lt;code>Strict&lt;/code> on the cookie.
What was not obvious to me then was how SameSite treats QR code navigation.
For the browser, this is no different than a clicking link from a different site, even though you are scanning a QR code.&lt;/p>
&lt;p>Here is a high level description of the different values for &lt;code>SameSite&lt;/code> on a browser cookie:&lt;/p>
&lt;style>
div.samesite table {
border-collapse: collapse;
width: 100%;
}
div.samesite td {
padding: 10px;
text-align: center;
vertical-align: top;
border-bottom: 1px solid #000;
}
div.samesite td:nth-child(odd) {
border-right: 2px solid #000;
}
div.samesite td:first-child {
white-space: nowrap;
}
div.samesite tr:last-child td {
border-bottom: none;
}
&lt;/style>
&lt;div class="samesite">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Strict&lt;/td>
&lt;td style="text-align:left">Cookies are only sent in a first-party context or &lt;strong>navigating directly to a site&lt;/strong>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Lax&lt;/td>
&lt;td style="text-align:left">Cookies are sent in some cross-site contexts, but not with embedded content.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">None&lt;/td>
&lt;td style="text-align:left">Cookies are sent in all contexts, including cross-origin requests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">(not set)&lt;/td>
&lt;td style="text-align:left">If the SameSite attribute is not specified, the behavior is browser dependent.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;p>Scanning a QR code does not count &amp;ldquo;navigating directly to a site&amp;rdquo;, instead it is a navigation event from a &lt;em>different site&lt;/em>.
The problem with using &lt;code>Strict&lt;/code> is that after page load following the QR code scan, the session cookie was not sent to the server.
This meant I was unable to load the user&amp;rsquo;s default settings on the first page load.&lt;/p>
&lt;p>The solution was to set the &lt;code>SameSite&lt;/code> attribute to &lt;code>Lax&lt;/code>.
This covers this QR code navigation scenario.
After fixing the issue, I was curious about what other things don&amp;rsquo;t count as direct navigation in the context of &lt;code>SameSite&lt;/code>.
This led me create a sandbox for testing SameSite behavior.
&lt;a href="https://samesite.flyemoji.com">Check it out here&lt;/a>.&lt;/p></description></item><item><title>Creating a ring buffer using Go Channels for Server-Sent Events</title><link>https://jarv.org/posts/go-channels-sse/</link><pubDate>Fri, 05 Apr 2024 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/go-channels-sse/</guid><description>&lt;p>Server-Sent Events (SSE) are a simple way to send real-time updates from the server to the browser.
Go channels pair nicely with SSE as they allow easy communication between Go routines.
This post will examine using SSE in a recent side project and the implementation of a ring buffer in Go.&lt;/p>
&lt;p>If you aren&amp;rsquo;t already familiar with SSE, like WebSockets, they enable updates on the web browser without having to poll or refresh the browser.
However, unlike WebSockets, they don&amp;rsquo;t support bidirectional communication.
This makes them more suitable for sending updates in one direction.
The main advantage of using SSE is that it builds on top of HTTP (normal HTTP request with additional headers).
This allows requests to pass through firewalls and proxies.&lt;/p>
&lt;p>&lt;a href="https://flyemoji.com">flyemoji.com&lt;/a> sets up a reaction poll that users access with a QR code.
The idea is that during a presentation or a video call, you can collect feedback from an audience.
From the poll, users select an emoji reaction to a question.
The reactions get tallied and sent to one or more connected clients listening for results.&lt;/p>
&lt;p>It looks something like this:&lt;/p>
&lt;h3 style="text-align:center">How are you feeling today?&lt;/h3>
&lt;div style="text-align:center;font-style:italic">Press one of the reactions, see tallies in real-time.&lt;/div>
&lt;div style="width:300px;margin:auto">
&lt;iframe id="flyemoji" src="https://flyemoji.com/view/DEMO
" style="width:300px;height:250px;border:0;margin:auto">&lt;/iframe>
&lt;/div>
&lt;p>For simplicity, we will assume a large number of clients are sending reactions, and there is a single client receiving the tallies.&lt;/p>
&lt;ol>
&lt;li>The browser makes an SSE request to get real-time updates for the emoji tallies and blocks on a Go channel.&lt;/li>
&lt;li>Emoji presses send a PUT request; the server takes that request and updates emoji tallies in an SQLite database and sends it to the channel.&lt;/li>
&lt;li>The browser displays the tally result after receiving the tallies via the SSE request.&lt;/li>
&lt;/ol>
&lt;img src="https://jarv.org/img/tallies.png" alt="tallies" class="light">
&lt;img src="https://jarv.org/img/tallies-dark.png" alt="tallies" class="dark">
&lt;p>On the server, sending the tallies in real-time is the easy part since all it requires is a HTTP handler and headers set on the response:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">mux&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;GET /events&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;text/event-stream&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Cache-Control&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;no-cache&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Connection&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;keep-alive&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Setup a channel for receiving data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">tally&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">ch&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Send the data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;data: %s\n\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">tally&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Flusher&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">Flush&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The trickier part is how to send the SSE data through a Go channel.
By default a Go channel blocks on send until the other side receives data.
Obviously for web clients, we cannot assume that the receiver will be continuously available for receiving tallies.&lt;/p>
&lt;p>For this reason a &lt;a href="https://go.dev/tour/concurrency/3">buffered channel&lt;/a> is necessary.
It takes a buffer length parameter on initialization and when data is received the channel won&amp;rsquo;t block until it&amp;rsquo;s full.
Because we only care about the most recent tally data, it&amp;rsquo;s fine to discard old tallies as newer tallies come in.
This is essentially a &lt;a href="https://en.wikipedia.org/wiki/Circular_buffer">ring buffer&lt;/a> with a buffer length of one, that discards the oldest value when new data comes.&lt;/p>
&lt;h2 id="a-channel-based-ring-buffer-in-go">A channel based ring buffer in Go&lt;/h2>
&lt;p>Below we will discuss two implementations of a ring buffer that pass messages using channels for SSE notifications.
Here is the first implementation that has a couple issues, can you spot them?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">RingBuffer&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">ch&lt;/span> &lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewRingBuffer&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">ch&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">item&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">item&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">C&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewRingBuffer&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;some value&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;some other value&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="nx">complete&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">complete&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">C&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">v&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl"> &lt;span class="nx">complete&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://go.dev/play/p/5q1AkSklE8z">run on the go playground&lt;/a>&lt;/p>
&lt;ul>
&lt;li>When an item is sent, we call &lt;code>select { ... }&lt;/code> on the channel.&lt;/li>
&lt;li>If the channel is empty, we add the item&lt;/li>
&lt;li>If the channel is full, we remove an item and add a new one&lt;/li>
&lt;/ul>
&lt;p>Two race conditions exist in this code, one is fairly obvious and other one might be so at first glance.&lt;/p>
&lt;h3 id="first-issue-concurrent-calls-to-send">First issue: Concurrent calls to Send()&lt;/h3>
&lt;p>If we have a large number of clients, calling &lt;code>Send()&lt;/code> concurrently then we may end up in a race condition causing the channel to block.
In the above code if one sender adds an item to the channel on line &lt;code>18&lt;/code> as another sender adds an item on line &lt;code>21&lt;/code> the channel will block because it&amp;rsquo;s trying to add to a full channel.&lt;/p>
&lt;p>Here are two possible options to resolve it:&lt;/p>
&lt;ol>
&lt;li>Wrap &lt;code>Send()&lt;/code> in a locking Mutex&lt;/li>
&lt;li>Use a single Go routine that moves data in and out of the channel&lt;/li>
&lt;/ol>
&lt;p>Option (2) creates an input and output channel and moves data between them.&lt;/p>
&lt;p>Here is an implementation using two channels:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">RingBuffer&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">inputChannel&lt;/span> &lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">outputChannel&lt;/span> &lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewRingBuffer&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">inputChannel&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">outputChannel&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">// https://tanzu.vmware.com/content/blog/a-channel-based-ring-buffer-in-go
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">inputChannel&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">v&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">C&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">inputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">item&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewRingBuffer&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="k">go&lt;/span> &lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;some value&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl"> &lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;some other value&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">45&lt;/span>&lt;span class="cl"> &lt;span class="nx">complete&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">46&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">complete&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">47&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">48&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">rb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">C&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">v&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl"> &lt;span class="nx">complete&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://go.dev/play/p/m2D_7DkMEmr">run on the go playground&lt;/a>&lt;/p>
&lt;p>Now the &lt;code>Send()&lt;/code> function does not have the same issue.
To setup the ring buffer we call &lt;code>go rb.Run()&lt;/code> before sending data that in the background will move data from the input channel to the output channel.&lt;/p>
&lt;p>Note that in this version you might see both values printed or just the first one printed instead.
This is because we are reading from the output channel while a background Go routine moves the messages to it.&lt;/p>
&lt;h3 id="second-issue-concurrent-reads-and-calls-to-send">Second issue: Concurrent reads and calls to Send()&lt;/h3>
&lt;p>As I mentioned earlier, there is another race condition that is also present in this version.
Not only do we have to account for clients sending concurrently, but also sending and receiving concurrently!
In the above code, if we read all the data from the channel (line &lt;code>48&lt;/code>) while trying to Send simultaneously on line &lt;code>23&lt;/code>, then it will block forever since the channel will be empty.&lt;/p>
&lt;p>Here is a modification to &lt;code>Run()&lt;/code> that resolves it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RingBuffer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">inputChannel&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">v&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="c1">// channel is empty
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">v&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outputChannel&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This adds an additional &lt;code>select {...}&lt;/code> to catch the scenario where the channel is empty immediately after we detect it&amp;rsquo;s full.&lt;/p>
&lt;p>For the full implementations of both the good and bad ring buffers see
&lt;a href="https://github.com/jarv/ringbuffer/tree/master">https://github.com/jarv/ringbuffer/tree/master&lt;/a> .&lt;/p>
&lt;h2 id="benchmarks">Benchmarks&lt;/h2>
&lt;p>After writing these impelementations, I was curious which one can move data faster.
Not surprisignly, a background Go routine that moves data between channels is much slower than calling moving data in and out of a channel in the call to &lt;code>Send()&lt;/code>.&lt;/p>
&lt;p>Here are benchmarks of both ring buffer implementations with a buffer length of &lt;code>1&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">$ go &lt;span class="nb">test&lt;/span> -bench&lt;span class="o">=&lt;/span>. ./...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goos: darwin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goarch: arm64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## 1 channel where data is moved in the call to Send() with a locking Mutex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pkg: github.com/jarv/ringbuffer/good/singlechan
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkParallelSendReceive/BufSize:1-10 &lt;span class="m">6284988&lt;/span> 191.2 ns/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkSendReceive/BufSize:1-10 &lt;span class="m">21763442&lt;/span> 54.01 ns/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## 2 channels with a Go routine moving data between them&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pkg: github.com/jarv/ringbuffer/good/dualchan
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkParallelSendReceive/BufSize:1-10 &lt;span class="m">4342477&lt;/span> 288.8 ns/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkSendReceive/BufSize:1-10 &lt;span class="m">5103054&lt;/span> 229.2 ns/op
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://github.com/jarv/ringbuffer/tree/master/good">https://github.com/jarv/ringbuffer/tree/master/good&lt;/a>&lt;/p>
&lt;p>The two channel implementation is slower because when the buffer is full, sends will block until the Go routine moves the data to the output channel.
It is exacerbated by a short buffer because it&amp;rsquo;s necessary for the background Go routine to move data between channels frequently.
While performance improves when the buffer length increases, it&amp;rsquo;s generally slower than discarding data in the call to &lt;code>Send()&lt;/code> even though it requires a locking Mutex.&lt;/p>
&lt;p>Notice that in the first benchmark test, it slows down considerably when we add parallelism, which is likely due to contention because of the locking Mutex.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Figuring out how to use channels for SSE was a lot of fun!
It gave me the opportunity to learn a bit more about how to troubleshoot and think better about concurrency in Go.
One valuable takeaway for me is that it&amp;rsquo;s important to validate background Go routines in unit tests.
This was essential to uncover bugs that were not immediately apparent.&lt;/p>
&lt;p>If you like this post, check out my other post on &lt;a href="https://jarv.org/posts/go-deadlock/">debugging a deadlock in Go&lt;/a> or subscribe to the &lt;a href="https://jarv.org/index.xml">RSS feed&lt;/a>.
If you would like a fun way to send out a reaction poll during your next video or conference presentation, check out &lt;a href="https://flyemoji.com">flyemoji.com&lt;/a>.&lt;/p></description></item><item><title>The joys of self-hosting and tiny side-projects</title><link>https://jarv.org/posts/joys-self-hosting/</link><pubDate>Fri, 01 Dec 2023 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/joys-self-hosting/</guid><description>&lt;style>
div.proj table {
border-collapse: collapse;
width: 100%;
}
div.proj td {
padding: 10px;
text-align: center;
vertical-align: top;
border-bottom: 1px solid #000;
}
div.proj td:nth-child(odd) {
border-right: 2px solid #000;
}
div.proj td:first-child {
white-space: nowrap;
}
div.proj tr:last-child td {
border-bottom: none;
}
&lt;/style>
&lt;h3 id="tiny-side-projects">Tiny side-projects&lt;/h3>
&lt;p>Lately there hasn&amp;rsquo;t been much time in my life for recreational programming but I still find time to create tiny services and put them on the public internet.
I&amp;rsquo;m sure this stretches the definition of &amp;ldquo;side-projects&amp;rdquo;, but one advantage of having a VM somewhere in the cloud and a registered domain is you can create your own little kingdom of subdomains.
Below are some examples of tiny side-projects, some no more than webserver configuration that either serves a small practical need or written for fun.&lt;/p>
&lt;div class="proj">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">🌐 &lt;a href="//ip.jarv.org">ip.jarv.org&lt;/a>&lt;/td>
&lt;td style="text-align:left">Responds with the IP address of the request.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">🤯 &lt;a href="//500.resp.jarv.org">*.resp.jarv.org&lt;/a>&lt;/td>
&lt;td style="text-align:left">Generates an http response based on the subdomain. E.g., &lt;a href="//200.resp.jarv.org">200&lt;/a>, &lt;a href="//404.resp.jarv.org">404&lt;/a>, &lt;a href="//418.resp.jarv.org">418&lt;/a>, etc.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">🔓 &lt;a href="//nossl.jarv.org">nossl.jarv.org&lt;/a>&lt;/td>
&lt;td style="text-align:left">Forces an http connection, sometimes useful for getting to the wifi login page, same as &lt;code>neverssl.com&lt;/code> or &lt;code>example.com&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">💤 &lt;a href="//sleep.jarv.org">sleep.jarv.org&lt;/a>&lt;/td>
&lt;td style="text-align:left">Sleeps for an arbitrary number of seconds or milliseconds. E.g., &lt;a href="//sleep.jarv.org/1">1s&lt;/a>, &lt;a href="//sleep.jarv.org/100ms">100ms&lt;/a>.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;p>I was thinking recently about doing little projects like this, and I think that not only does it make learning something new fun, but it is also nice to simply build something that is very small and self-contained.
More than anything else it ends up being a forcing-function to learn new things that I wouldn&amp;rsquo;t learn otherwise.
For example, when writing front-end code there is always a bit more I learn about css media queries since I don&amp;rsquo;t spend much time in the front-end and the landscape is constantly changing.
Or another is that I just learned that there is no browser enforced CORS with websockets, and I also figured out how to properly respond to a pre-flight cors request.&lt;/p>
&lt;p>&lt;em>Read more about some of these on the post about &lt;a href="https://jarv.org/posts/cool-caddy-config-tricks/">cool caddy config tricks&lt;/a>.&lt;/em>&lt;/p>
&lt;h3 id="self-hosting">Self-hosting&lt;/h3>
&lt;p>At one point I would have never considered hosting a simple static site like this blog on a VM but now that I host this static blog on a VM I would recommend it over hosting a static site on object storage or using service like GitHub Pages.
For one, you have more control over access, headers, responses, caching, etc.
Another reason to consider self-hosting is that it used to be a pain to deal with SSL certificates but this has gotten so much easier with webservers like &lt;a href="https://caddyserver.com/">Caddy&lt;/a> that come with out-of-the-box support for LetsEncrypt.&lt;/p>
&lt;p>As far as spending money, self-hosting a static site, running a webserver, and running services in Docker containers can be done a small VM for around 5eu/month (or less!).
Once you have your own server you can self-host applications you write yourself or select from a large selection of open-source services.
I run all of the following on the same host:&lt;/p>
&lt;ul>
&lt;li>Prometheus and node exporter&lt;/li>
&lt;li>Loki for logs&lt;/li>
&lt;li>Grafana&lt;/li>
&lt;li>GoatCounter&lt;/li>
&lt;/ul>
&lt;p>Self-hosting analytics (e.g., &lt;a href="https://www.goatcounter.com">GoatCounter&lt;/a>) means you know you will be seeing an accurate view of traffic since browser ad-blockers won&amp;rsquo;t block requests.
Collecting metrics with Prometheus, dashboarding with Grafana , and log management with Loki gives a nice consolidated view into all the services running on the host.
There is nothing is quite as as simple as running everything on a single VM with systemd managing monitoring, logging and application services with docker.&lt;/p></description></item><item><title>Debugging a deadlock in Go</title><link>https://jarv.org/posts/go-deadlock/</link><pubDate>Wed, 11 Oct 2023 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/go-deadlock/</guid><description>&lt;p>&lt;img src="https://jarv.org/img/empty-solutions-email.png" alt="empty-solutions-email">&lt;/p>
&lt;p>Occasionally, &lt;a href="https://cmdchallenge.com">cmdchallenge.com&lt;/a> would get into a state where requests to list user-submitted solutions to challenges would hang.
The first few times it happened, I restarted the service which was enough to get it working again.
Only later did I try to figure out what is going on, and thought I would do a small write-up as it ended up being a very common problem with deadlocks in Go.&lt;/p>
&lt;h2 id="background">Background&lt;/h2>
&lt;p>When I rewrote this application in Go, rate limiting was added using a popular package called &lt;a href="https://github.com/didip/tollbooth">tollbooth&lt;/a>.&lt;/p>
&lt;p>The reasons for rate limiting were:&lt;/p>
&lt;ol>
&lt;li>Every solution submission starts a new Docker container so I didn&amp;rsquo;t want that happening too frequently per IP.&lt;/li>
&lt;li>Every solution request does a lookup on a sqlite3 DB so I wanted to limit that as well.&lt;/li>
&lt;/ol>
&lt;p>Adding rate limits for (2) was hardly necessary since it only does a simple sqlite lookup, but this was where I was seeing the issue and also the place where I was mostly likely to receive concurrent requests.&lt;/p>
&lt;p>The first time I investigated this problem I created a test deployment and ran a bunch of concurrent requests.
I didn&amp;rsquo;t see the issue and was a bit perplexed, &lt;strong>the problem was that my testing environment had rate-limiting disabled!&lt;/strong>.
A good lesson here is that when you are trying to reproduce a bug, keep your testing environment as similar as possible to where it is happening.&lt;/p>
&lt;p>The bug itself was pretty easy to extract, here is a small program that reproduces it (also &lt;a href="https://github.com/jarv/godeadlock/blob/master/app.go">on GitHub&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;net/http/pprof&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;github.com/didip/tollbooth/v7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">testHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;OK\n&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">listenStr&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;:%d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6060&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">lmt&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">tollbooth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewLimiter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">float64&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 1 req/s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">lmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetOnLimitReached&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Rate limit reached StatusCode: %d\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">lmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GetStatusCode&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">tollbooth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">LimitFuncHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">lmt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">testHandler&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Server started %s\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">listenStr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ListenAndServe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">listenStr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you would like to see the bug yourself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/jarv/godeadlock
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">go run app.go
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And then send concurrent requests using &lt;a href="https://httpd.apache.org/docs/2.4/programs/ab.html">ab&lt;/a> or &lt;a href="https://github.com/codesenberg/bombardier">bombardier&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ab https://httpd.apache.org/docs/2.4/programs/ab.html&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ab -n &lt;span class="m">100&lt;/span> -c &lt;span class="m">50&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:6060/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># bombardier https://github.com/codesenberg/bombardier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bombardier -n &lt;span class="m">100&lt;/span> -c &lt;span class="m">50&lt;/span> -l http://localhost:6060
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once connections start timing out you will have a deadlock and nothing will get through. You can try to &lt;code>curl http://localhost:6060&lt;/code> to be sure.
Once it is in this state, try to figure out what caused the deadlock yourself, or read on if you would like to learn how to find the cause.&lt;/p>
&lt;h2 id="view-the-stack-traces-of-all-go-routines">View the stack traces of all Go routines&lt;/h2>
&lt;p>Once the deadlock occurs, the best way to figure out where your program is stuck is by viewing the stack trace of all Go routines.
There are two options, one is using &lt;a href="https://pkg.go.dev/net/http/pprof">pprof&lt;/a> which is configured in the example code, another is to issue a &lt;code>SIGQUIT&lt;/code> (CTRL-&lt;code>/&lt;/code> in your terminal) which will dump the Go routines to STDERR.&lt;/p>
&lt;p>The &lt;code>pprof&lt;/code> method is the easiest since we have it setup in the code to reproduce the issue.
Connect your browser to &lt;a href="http://localhost:6060/debug/pprof/goroutine?debug=1">http://localhost:6060/debug/pprof/goroutine?debug=1&lt;/a> to see the traces for all Go routines.
The ones to focus on are the ones that call &lt;code>sync&lt;/code>.
From these we can see that there are multiple go routines blocked on &lt;code>RLock()&lt;/code> and &lt;code>Lock()&lt;/code>.&lt;/p>
&lt;p>&lt;code>Lock()&lt;/code> allows only one Go routine to read/write at a time, &lt;code>RLock()&lt;/code> allows multiple Go routines to read, but not write.&lt;/p>
&lt;p>The traces reveal that there are multiple Go routines are waiting for a &lt;code>Rlock()&lt;/code>, and another Go routine is waiting for a &lt;code>Lock()&lt;/code> (abbreviated output below):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">197 @ 0x1027596c8 0x10276b6f8 0x10276b6d5 0x102788358 0x102954a3c 0x1029549e9 0x102956e34 0x102958afc 0x102958f80 0x1029049b8 0x102905ff4 0x102906ccc 0x102903a48 0x10278c8f4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102788357 sync.runtime_SemacquireRWMutexR+0x27 /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/runtime/sema.go:82
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102954a3b sync.(*RWMutex).RLock+0x7b /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/sync/rwmutex.go:71
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x1029549e8 github.com/didip/tollbooth/v7/limiter.(*Limiter).GetMax+0x28 /Users/jarv/go/pkg/mod/github.com/didip/tollbooth/v7@v7.0.1/limiter/limiter.go:182
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1 @ 0x1027596c8 0x10276b6f8 0x10276b6d5 0x102788358 0x102955238 0x1029551e5 0x1029590d0 0x10295549c 0x102958f9c 0x1029049b8 0x102905ff4 0x102906ccc 0x102903a48 0x10278c8f4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102788357 sync.runtime_SemacquireRWMutexR+0x27 /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/runtime/sema.go:82
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102955237 sync.(*RWMutex).RLock+0x77 /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/sync/rwmutex.go:71
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x1029551e4 github.com/didip/tollbooth/v7/limiter.(*Limiter).GetStatusCode+0x24 /Users/jarv/go/pkg/mod/github.com/didip/tollbooth/v7@v7.0.1/limiter/limiter.go:247
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x1029590cf main.main.func1+0x1f /Users/jarv/src/jarv/godeadlock/app.go:21
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x10295549b github.com/didip/tollbooth/v7/limiter.(*Limiter).ExecOnLimitReached+0xdb /Users/jarv/go/pkg/mod/github.com/didip/tollbooth/v7@v7.0.1/limiter/limiter.go:268
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1 @ 0x1027596c8 0x10276b6f8 0x10276b6d5 0x1027883b8 0x1027a47e8 0x102956ac4 0x102956cf8 0x102957724 0x102958b74 0x102958f80 0x1029049b8 0x102905ff4 0x102906ccc 0x102903a48 0x10278c8f4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x1027883b7 sync.runtime_SemacquireRWMutex+0x27 /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/runtime/sema.go:87
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x1027a47e7 sync.(*RWMutex).Lock+0xf7 /Users/jarv/.local/share/rtx/installs/go/1.21.1/go/src/sync/rwmutex.go:152
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102956ac3 github.com/didip/tollbooth/v7/limiter.(*Limiter).limitReachedWithTokenBucketTTL+0x63 /Users/jarv/go/pkg/mod/github.com/didip/tollbooth/v7@v7.0.1/limiter/limiter.go:572
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 0x102956cf7 github.com/didip/tollbooth/v7/limiter.(*Limiter).LimitReached+0x57 /Users/jarv/go/pkg/mod/github.com/didip/tollbooth/v7@v7.0.1/limiter/limiter.go:599
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>&lt;code>limiter.go:182&lt;/code>:&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// GetMax is thread-safe way of getting maximum number of requests to limit per second.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Limiter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">GetMax&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">float64&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">max&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>&lt;code>limiter.go:247&lt;/code>&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Limiter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">GetStatusCode&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">statusCode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>&lt;code>limiter.go:572&lt;/code>&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Limiter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">limitReachedWithTokenBucketTTL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">key&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">tokenBucketTTL&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Duration&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">lmtMax&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GetMax&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">lmtBurst&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GetBurst&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Lock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Unlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>GetStatusCode()&lt;/code> stands out here as it is a function we call in the example.
Also, if we delete &lt;code>lmt.GetStatusCode()&lt;/code> the deadlock is resolved, but why?&lt;/p>
&lt;h2 id="fixing-the-deadlock">Fixing the deadlock&lt;/h2>
&lt;p>To figure this out, we follow the trace up to see how function we pass to &lt;code>SetOnLimitReached&lt;/code> function is called:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ExecOnLimitReached is thread-safe way of executing after-rejection function when limit is reached.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Limiter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">ExecOnLimitReached&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fn&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">onLimitReached&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">fn&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">fn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Right there is the issue, there is an &lt;code>RLock()&lt;/code> acquired right before the function call.
In that function we are calling &lt;code>GetStatusCode()&lt;/code> which calls &lt;code>RLock()&lt;/code> again.
This is a very common source of deadlocks in Go and is even called out in the &lt;a href="https://pkg.go.dev/sync#RWMutex">documentation&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://jarv.org/img/deadlock.png" alt="deadlock">&lt;/p>
&lt;p>This illustrates why recursive read locking is problematic, if a concurrent request calls lock between the two read locks, the second read lock and the write lock will wait forever for the first read lock to release.&lt;/p>
&lt;p>This can be resolved by releasing the read lock immediately after assigning the function from &lt;code>l.onLimitReached&lt;/code>, which is exactly what I did in my local fork of this package to resolve the issue and submitted &lt;a href="https://github.com/didip/tollbooth/issues/106">an upstream issue&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> func (l *Limiter) ExecOnLimitReached(w http.ResponseWriter, r *http.Request) {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> l.RLock()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- defer l.RUnlock()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">-
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span> fn := l.onLimitReached
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ l.RUnlock()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> if fn != nil {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> fn(w, r)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After this there are no more deadlocks and we can send a large number of concurrent requests that are rate limited.
I hope you found this explanation clear and maybe learned something new about deadlocks in Go.
If you have any comments or questions please don&amp;rsquo;t hesitate to &lt;a href="https://jarv.org/contact/">reach out&lt;/a>!.&lt;/p></description></item><item><title>One huge shell script</title><link>https://jarv.org/posts/one-huge-shell-script/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/one-huge-shell-script/</guid><description>&lt;p>There are a lot of DevOps tools for managing both configuration and infrastructure as code.
While some can be extremely helpful for managing configuring for a large distributed service, not all are as helpful for deploying something simple in the cloud, or a personal project that runs on a single virtual machine.&lt;/p>
&lt;hr>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">IaC or a configuration thingy&lt;/th>
&lt;th style="text-align:right">$job&lt;/th>
&lt;th style="text-align:right">personal stuff&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Kubernetes&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Configuration management (Ansible, Chef, etc.)&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">CI pipelines for code&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">CI pipelines for infra&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Code reviews&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Changes without downtime&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Docker registry&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Prebuilt machine images&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Cloud &amp;ldquo;appliances&amp;rdquo;, load balancers, hosted DB, etc.&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">CDN (CloudFlare/CloudFront)&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Hosted Git&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Terraform&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">One friggin huge shell script&lt;/td>
&lt;td style="text-align:right">❌&lt;/td>
&lt;td style="text-align:right">✅&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;p>Over time, my approach to using tooling for my personal projects has changed.
From a high level, my own lessons have been:&lt;/p>
&lt;ul>
&lt;li>Avoid complexity for troubleshooting, monitoring and logging by leaning too much on &amp;ldquo;severless&amp;rdquo; components.&lt;/li>
&lt;li>For the same reason, putting everything on a single VM is &amp;ldquo;good enough&amp;rdquo; for most applications.&lt;/li>
&lt;li>CDN is probably not necessary unless you are very worried about downtime, self-hosting static files is just as easy as hosted options.&lt;/li>
&lt;li>Put all of the infrastructure configuration (scripts, IaC, etc) in a single repository.&lt;/li>
&lt;li>Avoid cloud dependencies in general, make it work locally and then replicate that on a virtual machine as close as possible.&lt;/li>
&lt;li>Use a minimal base image and avoid putting complexity or configuration in user-data scripts.&lt;/li>
&lt;/ul>
&lt;p>This resulted in what I have now, which is a single VM provisioned in Hetzner Cloud where everything is configured in a single shell script and most services run in Docker containers.&lt;/p>
&lt;p>Before converting to a single VM, I would do something like this any time I had an experiment or idea to play around with:&lt;/p>
&lt;ol>
&lt;li>Create a new AWS account.&lt;/li>
&lt;li>Create a bucket for Terraform state&lt;/li>
&lt;li>Terraform configuration for CloudFront, Lambda, EC2, Route53, etc.&lt;/li>
&lt;li>In the user-data script configuration the image from scratch to support running docker and create systemd unit files.&lt;/li>
&lt;li>In the CI pipeline copy a binary, or tag a new docker image for every push to master.&lt;/li>
&lt;li>Deploy the container using a public or private registry, pull the new image on the instance with a systemd unit configuration to manage the service.&lt;/li>
&lt;/ol>
&lt;p>For this to work, A lot of complexity was baked into the user-data script associated with the instance.
It also meant keeping track of multiple AWS accounts (usually using the free-tier resources) and then possibly paying money if an EC2 instance was required after the free-tier ended.
Often, there would be a separate testing environment which was ephemeral, that spun up an identical instance and cloud configuration.&lt;/p>
&lt;p>My current approach of a single VM eliminates the need for AWS and a lot of complexity that comes along with it. For the configuration management part of it, I was confronted with the following questions:&lt;/p>
&lt;ol>
&lt;li>How do I deal with configuration drift if manual changes are made on the instance while it is running?&lt;/li>
&lt;li>Will it be possible to rebuild a single VM from scratch easily, and without much downtime?&lt;/li>
&lt;li>If there will be multiple containers running on the instance, does it make sense to any container orchestration?&lt;/li>
&lt;li>How much should be automated in CI if I am the only person deploying changes?&lt;/li>
&lt;li>Should I depend on a container registry?&lt;/li>
&lt;/ol>
&lt;h2 id="my-approach-to-managing-the-single-vm-deployment">My approach to managing the single VM deployment&lt;/h2>
&lt;p>The questions above lead me to the following approach for configuration management when deploying to a single VM:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://caddyserver.com/">Caddy&lt;/a> is the main webserver. It has a lot of great features including automatic HTTPs and some other great features that I talked about in a &lt;a href="https://jarv.org/posts/cool-caddy-config-tricks/">recent post&lt;/a>.&lt;/li>
&lt;li>For packages, add them to the VM and simply &lt;a href="https://cloudinit.readthedocs.io/en/latest/reference/examples.html#install-arbitrary-packages">update the packages in cloud-init&lt;/a> as I go, there is no extra boot logic other than installing packages.&lt;/li>
&lt;li>For other manual changes, maintain a single bash script that is idempotent, so all changes are made in a single script and the script can be re-run when it is updated.&lt;/li>
&lt;li>Run almost everything in Docker and use Systemd for service management including stopping and starting docker containers.&lt;/li>
&lt;li>Use (self-hosted) Prometheus, Grafana for monitoring.&lt;/li>
&lt;li>Don&amp;rsquo;t lean too much (or at all) on CI pipelines, run deploys locally.&lt;/li>
&lt;li>To deploy and update individual services that are built using Docker, dump and copy the image directly to the VM instead of pushing it to a public registry.&lt;/li>
&lt;/ul>
&lt;h2 id="the-big-shell-script">The big shell script&lt;/h2>
&lt;p>I keep one repository named &lt;code>config-mgmt&lt;/code> that has the following content:&lt;/p>
&lt;ul>
&lt;li>Terraform for provisioning an instance, and configuring DNS in CloudFlare&lt;/li>
&lt;li>A directory named &lt;code>files/&lt;/code>, this has all of my OS configuration files like Systemd unit files, Caddy config files, and a single big shell script called &lt;code>bootstrap&lt;/code>.&lt;/li>
&lt;li>A shell script named &lt;code>configure&lt;/code> that runs &lt;code>rsync&lt;/code> to sync everything under &lt;code>files/&lt;/code> to the single VM and then after the sync runs the script named &lt;code>bootstrap&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>The &lt;code>bootstrap&lt;/code> script along with all files in &lt;code>files/&lt;/code> are rsync&amp;rsquo;d to the VM first.
Then &lt;code>bootstrap&lt;/code> is run which does all the little shell maintenance stuff like ensuring services are enabled, installing and configuring things that run on the VM, reloads SystemD, etc.
The script ends up being around 500 lines of bash, a dozen or so simple functions that are called in sequence at the end of the script.&lt;/p>
&lt;p>If I have a new project to deploy to my single VM I simply create a new &lt;code>config_&lt;/code> function, add it to the script and in most cases create a small &lt;code>bin/deploy&lt;/code> shell script in the project&amp;rsquo;s repository that dumps and imports the docker container to the host.&lt;/p>
&lt;h2 id="using-docker-and-a-single-vm-for-side-projects">Using Docker and a single VM for side-projects&lt;/h2>
&lt;p>There is no Docker container orchestration in this setup, just a bunch of Systemd unit files for starting Docker containers with Caddy configurations to route traffic to them.
I do however not run everything in Docker, on the VM I run the following services:&lt;/p>
&lt;ul>
&lt;li>Prometheus for collecting node metrics and for scraping application metrics&lt;/li>
&lt;li>Grafana for dashboarding&lt;/li>
&lt;li>Self-hosted GoatCounter for web stats&lt;/li>
&lt;/ul>
&lt;h2 id="rebuilding-my-single-vm-from-scratch">Rebuilding my single VM from scratch&lt;/h2>
&lt;p>This is something that is important as you don&amp;rsquo;t want to create a &amp;ldquo;snowflake&amp;rdquo; that will be impossible to recreate.
To make this work in Terraform I maintain a list of servers and an &amp;ldquo;active server&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-terraform" data-lang="terraform">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">locals&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">servers&lt;/span> = &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;lisa&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;bart&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">active_server&lt;/span> = &lt;span class="s2">&amp;#34;lisa&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I can add a new server to the list to create a new one, and then switch the active to the new one I created which points all of the service DNS entries to it.&lt;/p></description></item><item><title>Cool Caddy config tricks for your self-hosted domain</title><link>https://jarv.org/posts/cool-caddy-config-tricks/</link><pubDate>Wed, 23 Aug 2023 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/cool-caddy-config-tricks/</guid><description>&lt;p>Very recently I switched from running a couple side-projects that were hosted on AWS EC2 to a single &lt;a href="https://www.hetzner.com/cloud">Hetzner Cloud&lt;/a> VM.
During that transition, this page (jarv.org) was moved to it from GitHub pages.
Static generation using &lt;a href="https://gohugo.io">Hugo&lt;/a> stayed the same, but in the process of switching VMs I checked out &lt;a href="https://caddyserver.com/">Caddy&lt;/a> as a new webserver.
After making the switch, this ended up being a great NGINX replacement for all the sites hosted on what is now a single VM.
The ergonomics of the config language was particularly impressive so I wanted share how simple and flexible it can be!&lt;/p>
&lt;h2 id="https-made-easy">HTTPs made easy&lt;/h2>
&lt;p>First to state the obvious, Caddy is incredible easy to configure when it comes to HTTPs.
It supports &lt;a href="https://caddyserver.com/docs/automatic-https">automatic HTTPs&lt;/a> out of the box with Let&amp;rsquo;s Encrypt or Zero SSL.
With only a few lines of config you can setup a large number of sites without having to go through the certificate issuer dance, or use a CDN like CloudFlare or CloudFront to handle certificates for you.&lt;/p>
&lt;p>For example, here is how simple the configuration is for this site:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-caddy" data-lang="caddy">&lt;span class="line">&lt;span class="cl">&lt;span class="gh">jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@cache&lt;/span> &lt;span class="k">path&lt;/span> &lt;span class="s">/font/*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="nd">@cache&lt;/span> &lt;span class="s">Cache-Control&lt;/span> &lt;span class="s">max-age=604800&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">handle&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">root&lt;/span> &lt;span class="nd">*&lt;/span> &lt;span class="s">/var/opt/www/jarv.org&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">file_server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">precompressed&lt;/span> &lt;span class="s">gzip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This does the following:&lt;/p>
&lt;ul>
&lt;li>Sets a cache-control header for &lt;code>/font/&lt;/code> requests with a long expiry&lt;/li>
&lt;li>Tells Caddy that files under &lt;code>/var/opt/www/jarv.org&lt;/code> serve the site, and to also to expect &lt;code>.gz&lt;/code> files in the same directory so we can serve assets pre-compressed&lt;/li>
&lt;/ul>
&lt;p>This will automatically redirect HTTP requests to HTTPs, which is likely what you want most of the time.
If you want to have a plain HTTP version and HTTPs version that is possible too, for that see the next example.&lt;/p>
&lt;h2 id="creating-a-site-that-echos-an-ip-address">Creating a site that echos an IP address&lt;/h2>
&lt;p>There are some sites that I sometimes use to echo my public IP like &lt;code>curl ifconfig.io&lt;/code>.
With Caddy, it&amp;rsquo;s very simple to replicate this with a Caddy configuration, so I replicated it with &lt;code>curl ip.jarv.org&lt;/code>.&lt;/p>
&lt;p>Caddy has a simple &lt;a href="https://caddyserver.com/docs/caddyfile/directives/respond">&lt;code>respond&lt;/code> directive&lt;/a>, here is how &lt;a href="https://ip.jarv.org">&lt;code>ip.jarv.org&lt;/code>&lt;/a> echos your IP address:&lt;/p>
&lt;p>&lt;em>Note: This uses the new heredoc syntax in version 2.7.0&lt;/em>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-caddy" data-lang="caddy">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">(echoIP)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">templates&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="s">Content-Type&lt;/span> &lt;span class="s">text/plain&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">respond&lt;/span> &lt;span class="sh">&amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{{.RemoteIP}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="sh">EOF&lt;/span> &lt;span class="mi">200&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">encode&lt;/span> &lt;span class="s">zstd&lt;/span> &lt;span class="s">gzip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> logging &lt;span class="s">ip.jarv.org&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">ip.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoIP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">http://ip.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoIP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>Instead of only &lt;code>ip.jarv.org&lt;/code> there are two servers, &lt;code>ip.jarv.org&lt;/code> and &lt;code>http://ip.jarv.org&lt;/code> for HTTP and HTTPs requests. This is so we don&amp;rsquo;t automatically redirect HTTP to HTTPs which is nice for using &lt;code>curl&lt;/code> on the command line.&lt;/li>
&lt;li>&lt;code>echoIP&lt;/code> is a template, which includes the content in each block. The template is used for both HTTP and HTTPs to respond with the requester&amp;rsquo;s IP address.&lt;/li>
&lt;li>Again, using the same logging snippet from above, a dedicated request log is created on disk&lt;/li>
&lt;/ul>
&lt;h2 id="creating-a-subdomain-that-echos-a-http-status-code">Creating a subdomain that echos a HTTP status code&lt;/h2>
&lt;p>Finally, I was thinking that it would be handy to have a way to echo back an HTTP status code, using &lt;code>curl &amp;lt;http status code&amp;gt;.resp.jarv.org&lt;/code>.
This requires a wildcard certificate, which is a bit more involved since it requires a DNS provider module to be compiled into Caddy (only required here for wildcard certs).&lt;/p>
&lt;p>I run Caddy on a Debian server in Hetzner Cloud, with Cloudflare provisioning DNS.
Caddy is installed using the &lt;a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy debian package&lt;/a>, and &lt;code>xcaddy&lt;/code> to install the &lt;a href="https://github.com/caddy-dns/cloudflare">Cloudflare DNS provider module&lt;/a>.
The module is compiled using &lt;code>xcaddy&lt;/code> and there is &lt;a href="https://caddyserver.com/docs/build#package-support-files-for-custom-builds-for-debianubunturaspbian">great documentation&lt;/a> for how to properly incorporate your custom build using the Debian package.&lt;/p>
&lt;p>Here is the configuration for responding with any specified HTTP status code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-caddy" data-lang="caddy">&lt;span class="line">&lt;span class="cl">&lt;span class="n">(echoResp)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">templates&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="s">Content-Type&lt;/span> &lt;span class="s">text/plain&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">respond&lt;/span> &lt;span class="sh">&amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{http.request.host.labels.3}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="sh">EOF&lt;/span> &lt;span class="se">{http.request.host.labels.3}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">encode&lt;/span> &lt;span class="s">zstd&lt;/span> &lt;span class="s">gzip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> logging &lt;span class="s">resp.jarv.org&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">*.resp.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoResp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">tls&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">dns&lt;/span> &lt;span class="s">cloudflare&lt;/span> &lt;span class="s">REDACTED&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">http://*.resp.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoResp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>REDACTED&lt;/code> is a CloudFlare API token with &lt;code>Zone Read&lt;/code> and &lt;code>DNS Write&lt;/code> permissions.&lt;/li>
&lt;li>&lt;code>http.request.host.labels.3&lt;/code> returns whatever is used as the wildcard for &lt;code>*.resp.jarv.org&lt;/code>.&lt;/li>
&lt;li>In addition to returning the passed in status code, it will respond in plain text whatever status code was sent.&lt;/li>
&lt;/ul>
&lt;p>Now, if I have want to generate a &lt;code>404&lt;/code> I can request &lt;a href="https://404.resp.jarv.org">404.resp.jarv.org&lt;/a>, or for a &lt;code>500&lt;/code> &lt;a href="https://500.resp.jarv.org">500.resp.jarv.org&lt;/a> or any other status code as a subdomain!&lt;/p>
&lt;p>&lt;strong>Update&lt;/strong>: Below is the more complicated configuration running jarv.org that responds with emojis, short description, and also catches strings that are not valid HTTP response codes. Here are some example responses:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ curl 200.resp.jarv.org
200 😃 OK
$ curl 201.resp.jarv.org
201 🎉 Created
$ curl 499.resp.jarv.org
499 http status code
$ curl 418.resp.jarv.org
418 🍵 I&amp;#39;m a teapot
$ curl herpderp.resp.jarv.org
💥 herpderp doesn&amp;#39;t look like a valid HTTP status code!
&lt;/code>&lt;/pre>&lt;p>And this is the Caddy configuration, that utilizes the &lt;a href="https://caddyserver.com/docs/caddyfile/directives/map">&lt;code>map&lt;/code> directive&lt;/a> to look up the short descriptions for status codes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-caddy" data-lang="caddy">&lt;span class="line">&lt;span class="cl">&lt;span class="n">(echoResp)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">templates&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="s">Content-Type&lt;/span> &lt;span class="s2">&amp;#34;text/html; charset=utf-8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@valid&lt;/span> &lt;span class="k">header_regexp&lt;/span> &lt;span class="s">host&lt;/span> &lt;span class="s">Host&lt;/span> &lt;span class="s">^([1-5]\d&lt;/span>&lt;span class="se">{2}&lt;/span>&lt;span class="s">|599)\..*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">map&lt;/span> &lt;span class="se">{http.request.host.labels.3}&lt;/span> &lt;span class="se">{status_desc}&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">100&lt;/span> &lt;span class="s2">&amp;#34;Continue&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">101&lt;/span> &lt;span class="s2">&amp;#34;Switching Protocols&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">102&lt;/span> &lt;span class="s2">&amp;#34;Processing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">103&lt;/span> &lt;span class="s2">&amp;#34;Early Hints&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">200&lt;/span> &lt;span class="s2">&amp;#34;😃 OK&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">201&lt;/span> &lt;span class="s2">&amp;#34;🎉 Created&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">202&lt;/span> &lt;span class="s2">&amp;#34;Accepted&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">203&lt;/span> &lt;span class="s2">&amp;#34;Non-Authoritative Information&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">204&lt;/span> &lt;span class="s2">&amp;#34;🙅 No Content&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">205&lt;/span> &lt;span class="s2">&amp;#34;Reset Content&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">206&lt;/span> &lt;span class="s2">&amp;#34;Partial Content&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">207&lt;/span> &lt;span class="s2">&amp;#34;Multi-Status&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">208&lt;/span> &lt;span class="s2">&amp;#34;Already Reported&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">226&lt;/span> &lt;span class="s2">&amp;#34;IM Used&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">300&lt;/span> &lt;span class="s2">&amp;#34;Multiple Choices&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">301&lt;/span> &lt;span class="s2">&amp;#34;Moved Permanently&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">302&lt;/span> &lt;span class="s2">&amp;#34;Found&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">303&lt;/span> &lt;span class="s2">&amp;#34;See Other&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">304&lt;/span> &lt;span class="s2">&amp;#34;Not Modified&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">305&lt;/span> &lt;span class="s2">&amp;#34;Use Proxy&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">306&lt;/span> &lt;span class="s2">&amp;#34;Switch Proxy&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">307&lt;/span> &lt;span class="s2">&amp;#34;Temporary Redirect&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">308&lt;/span> &lt;span class="s2">&amp;#34;Permanent Redirect&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">400&lt;/span> &lt;span class="s2">&amp;#34;❌ Bad Request&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">401&lt;/span> &lt;span class="s2">&amp;#34;🔒 Unauthorized&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">402&lt;/span> &lt;span class="s2">&amp;#34;Payment Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">403&lt;/span> &lt;span class="s2">&amp;#34;🚫 Forbidden&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">404&lt;/span> &lt;span class="s2">&amp;#34;🕳️ Not Found&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">405&lt;/span> &lt;span class="s2">&amp;#34;Method Not Allowed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">406&lt;/span> &lt;span class="s2">&amp;#34;Not Acceptable&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">407&lt;/span> &lt;span class="s2">&amp;#34;Proxy Authentication Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">408&lt;/span> &lt;span class="s2">&amp;#34;Request Timeout&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">409&lt;/span> &lt;span class="s2">&amp;#34;Conflict&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">410&lt;/span> &lt;span class="s2">&amp;#34;Gone&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">411&lt;/span> &lt;span class="s2">&amp;#34;Length Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">412&lt;/span> &lt;span class="s2">&amp;#34;Precondition Failed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">413&lt;/span> &lt;span class="s2">&amp;#34;Payload Too Large&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">414&lt;/span> &lt;span class="s2">&amp;#34;URI Too Long&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">415&lt;/span> &lt;span class="s2">&amp;#34;Unsupported Media Type&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">416&lt;/span> &lt;span class="s2">&amp;#34;Range Not Satisfiable&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">417&lt;/span> &lt;span class="s2">&amp;#34;Expectation Failed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">418&lt;/span> &lt;span class="s2">&amp;#34;🍵 I&amp;#39;m a teapot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">421&lt;/span> &lt;span class="s2">&amp;#34;Misdirected Request&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">422&lt;/span> &lt;span class="s2">&amp;#34;Unprocessable Entity&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">423&lt;/span> &lt;span class="s2">&amp;#34;Locked&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">424&lt;/span> &lt;span class="s2">&amp;#34;Failed Dependency&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">425&lt;/span> &lt;span class="s2">&amp;#34;Too Early&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">426&lt;/span> &lt;span class="s2">&amp;#34;Upgrade Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">428&lt;/span> &lt;span class="s2">&amp;#34;Precondition Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">429&lt;/span> &lt;span class="s2">&amp;#34;Too Many Requests&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">431&lt;/span> &lt;span class="s2">&amp;#34;Request Header Fields Too Large&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">451&lt;/span> &lt;span class="s2">&amp;#34;Unavailable For Legal Reasons&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">500&lt;/span> &lt;span class="s2">&amp;#34;🤯 Internal Server Error&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">501&lt;/span> &lt;span class="s2">&amp;#34;Not Implemented&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">502&lt;/span> &lt;span class="s2">&amp;#34;Bad Gateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">503&lt;/span> &lt;span class="s2">&amp;#34;🚧 Service Unavailable&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">504&lt;/span> &lt;span class="s2">&amp;#34;Gateway Timeout&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">505&lt;/span> &lt;span class="s2">&amp;#34;HTTP Version Not Supported&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">506&lt;/span> &lt;span class="s2">&amp;#34;Variant Also Negotiates&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">507&lt;/span> &lt;span class="s2">&amp;#34;Insufficient Storage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">508&lt;/span> &lt;span class="s2">&amp;#34;Loop Detected&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">510&lt;/span> &lt;span class="s2">&amp;#34;Not Extended&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">511&lt;/span> &lt;span class="s2">&amp;#34;Network Authentication Required&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">default&lt;/span> &lt;span class="s2">&amp;#34;http status code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">handle&lt;/span> &lt;span class="nd">@valid&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">respond&lt;/span> &lt;span class="sh">&amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{http.request.host.labels.3} {status_desc}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="sh">EOF&lt;/span> &lt;span class="se">{http.request.host.labels.3}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">handle&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">respond&lt;/span> &lt;span class="sh">&amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">💥 {http.request.host.labels.3} doesn&amp;#39;t look like a valid HTTP status code!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="sh">EOF&lt;/span> &lt;span class="mi">400&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> logging &lt;span class="s">resp.jarv.org&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">*.resp.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoResp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">tls&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">dns&lt;/span> &lt;span class="s">cloudflare&lt;/span> &lt;span class="s">REDACTED&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">http://*.resp.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> echoResp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="forcing-plain-http">Forcing plain HTTP&lt;/h2>
&lt;p>This is something similar to &lt;a href="http://neverssl.com">neverssl.com&lt;/a>, or &lt;a href="http://example.com">example.com&lt;/a>, I think both of these are still used frequently when connecting to wifi when you want to get to the wifi login which required a plain http connection.
This couldn&amp;rsquo;t be simpler with Caddy, below is the configuration I use for &lt;a href="http://nossl.jarv.org">nossl.jarv.org&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-caddy" data-lang="caddy">&lt;span class="line">&lt;span class="cl">&lt;span class="gh">nossl.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">redir&lt;/span> &lt;span class="s">http://nossl.jarv.org&lt;/span> &lt;span class="s">permanent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">http://nossl.jarv.org&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="s">Content-Type&lt;/span> &lt;span class="s2">&amp;#34;text/html; charset=utf-8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">header&lt;/span> &lt;span class="s">Cache-Control&lt;/span> &lt;span class="s2">&amp;#34;no-cache, no-store, must-revalidate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">respond&lt;/span> &lt;span class="sh">&amp;lt;&amp;lt;NOSSL&lt;/span>&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;head&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;title&amp;gt;nossl&amp;lt;/title&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;link rel=&amp;#34;icon&amp;#34; href=&amp;#34;data:image/svg+xml,&amp;lt;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22&amp;gt;&amp;lt;text y=%22.9em%22 font-size=%2290%22&amp;gt;🔓&amp;lt;/text&amp;gt;&amp;lt;/svg&amp;gt;&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;meta charset=&amp;#34;utf-8&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/head&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;style&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">pre {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> font-family: &amp;#34;Courier New&amp;#34;, Courier, monospace;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">div {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> display: flex;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> align-items: center;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> justify-content: center;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> height: 100vh;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/style&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;body&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;pre&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣤⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⣰⣿⣿⣿⠟⠉⠀⠀⠀⠈⠙⠿⣿⣿⣷⡄⠀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⢰⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠙⣿⣿⣿⡀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⣸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⡇⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⢿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⡇⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⢠⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠉⠉⠛⣿⣿⣿⣿⣿⣿⣿⣿⣷
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⣿⣿⣿⣿⣿⣿⣿⣿⣿⡶⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⢻⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⠏
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠙⢿⣿⣿⣿⣿⣿⡏⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⡿⠃⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⠈⠛⢿⣿⣿⣶⣶⣶⣶⣶⣾⣿⣿⠿⠛⠁⠀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">⠀⠀⠀⠀⠀⠀⠀⠉⠉⠙⠛⠛⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/pre&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/body&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;lt;/html&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="sh">NOSSL&lt;/span> &lt;span class="mi">200&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Running a command with a timeout in Go</title><link>https://jarv.org/posts/command-with-timeout/</link><pubDate>Tue, 27 Sep 2022 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/command-with-timeout/</guid><description>&lt;p>I&amp;rsquo;ve been slowly re-writing most of &lt;a href="https://cmdchallenge.com">cmdchallenge&lt;/a> in Go.
It started with &lt;a href="https://jarv.org/posts/from-serverless-to-server/">porting all of the Python code running in AWS Lambda&lt;/a>, and now I am in the process of re-writing the command runner.
This is the component that executes user provided commands, originally written in &lt;a href="https://nim-lang.org/">Nim&lt;/a>.&lt;/p>
&lt;p>The command runner takes user submitted shell commands and executes them in a Docker container.
In the Go program, I initially used &lt;code>exec.CommandContext()&lt;/code> and &lt;code>.CombinedOutput()&lt;/code>.
This met the following requirements:&lt;/p>
&lt;ul>
&lt;li>A command to be executed but the execution needs to be time constrained&lt;/li>
&lt;li>Fetching the output of the command with combined STDOUT and STDERR&lt;/li>
&lt;li>Getting the command&amp;rsquo;s exit code&lt;/li>
&lt;/ul>
&lt;p>Initially using &lt;code>exec.CommandContext()&lt;/code> and &lt;code>.CombinedOutput()&lt;/code> seemed like a good fit since I could use &lt;a href="https://pkg.go.dev/context">context&lt;/a> to send a kill signal to constrain how long the command would run.
However, I noticed some odd behavior as it related to setting timeouts.&lt;/p>
&lt;p>Take the following two examples:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;starting&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shArgs&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;sleep 10&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">exec&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">CommandContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">shArgs&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">CombinedOutput&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;finished&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Output:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2022/09/27 19:08:04 starting
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2022/09/27 19:08:14 signal: killed
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;starting&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shArgs&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;sleep 10&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">exec&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">CommandContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">shArgs&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">Run&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;finished&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Output:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2022/09/27 19:10:13 starting
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2022/09/27 19:10:15 signal: killed
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note how the first example reports that the program is killed, but not until a full 10 seconds despite setting a 2 second timout on the context.
Passing the context doesn&amp;rsquo;t seem to do anything at all!
In the second example, the program resumes after the 2 second timeout as we would expect.
The key difference is that in the first example we are using &lt;code>CombinedOutput()&lt;/code>.&lt;/p>
&lt;p>Let&amp;rsquo;s take a closer look at the process tree and what happens to the &lt;code>sleep 10&lt;/code>.
Assuming our program is called &lt;code>timeouttest&lt;/code>, the process tree starts like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>bash,1
`-timeouttest,8019
|-sh,8024 -c sleep 10
| `-sleep,7694 10
&lt;/code>&lt;/pre>&lt;p>After 2 seconds, &lt;code>sh&lt;/code> is sent a &lt;code>SIGKILL&lt;/code> and we are left with:&lt;/p>
&lt;pre tabindex="0">&lt;code>bash,1
|-sleep,7694 10
`-timeouttest,8019
&lt;/code>&lt;/pre>&lt;p>As seen above, &lt;code>sleep 10&lt;/code> becomes an orphan that is adopted by PID 1.
&lt;code>sleep 10&lt;/code> is not killed, because the kill was sent to &lt;code>sh&lt;/code>, and the signal is not propagated to its child.
The main difference between calling &lt;code>.Run()&lt;/code> and &lt;code>.CombinedOutput()&lt;/code> is that the latter creates a buffer for &lt;code>Stdout&lt;/code> for the process and its children.
The progrem will will wait for that descriptor to close. This causes Go to hang, while it waits to copy sleep&amp;rsquo;s standard output to the buffer.&lt;/p>
&lt;p>To ensure we can timeout properly, and that the &lt;code>sleep 10&lt;/code> is also killed, we will need to send a &lt;code>SIGKILL&lt;/code> to the process group.
This solves the problem because children created via fork will inherit the parent&amp;rsquo;s process group ID, and a kill sent to the group ID will kill the process and all of its descendants.&lt;/p>
&lt;p>Here is code to kill the group ID after a 2 second timeout. The implementation below uses a channel with select:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;starting&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shArgs&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;sleep 10&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">cmd&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">exec&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">shArgs&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">cmd&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SysProcAttr&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SysProcAttr&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">Setpgid&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">cmdResult&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">outb&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">err&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">cmdDone&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">cmdResult&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">go&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">outb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">cmd&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">CombinedOutput&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cmdDone&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">cmdResult&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">outb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Kill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">cmd&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Pid&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGKILL&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;signal: killed&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">cmdDone&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;finished&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This sends a &lt;code>SIGKILL&lt;/code> to the process ID (negated) which is how you send a signal to the process group, which is documented on the &lt;a href="https://man7.org/linux/man-pages/man2/kill.2.html">kill man page&lt;/a>.&lt;/p>
&lt;pre tabindex="0">&lt;code>If pid is less than -1, then sig is sent to every process in the
process group whose ID is -pid.
&lt;/code>&lt;/pre>&lt;p>This ensures that we will both timeout after the given time, and that all processes including children are killed.&lt;/p></description></item><item><title>From serverless to server</title><link>https://jarv.org/posts/from-serverless-to-server/</link><pubDate>Wed, 29 Sep 2021 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/from-serverless-to-server/</guid><description>&lt;p>I&amp;rsquo;m pleased to announce that the transition from a distributed serverless architecture in Python to a single VM with a service written in Go is done for &lt;a href="https://cmdchallenge.com">CMDChallenge&lt;/a>.
The final patch was merged and there is nothing sweeter than removing close to 100K lines of vendored Python.&lt;/p>
&lt;p>It was around &lt;a href="https://jarv.org/posts/go-rewrite-cmdchallenge/">4 months ago&lt;/a> that I started on this adventure but honestly a lot of life got in the way and I probably spent a total of around 4 full days of work for the transition.
Overall, I&amp;rsquo;m quite satisfied with the result, as you can see here many of the &amp;ldquo;cloud native&amp;rdquo; services went &lt;em>poof&lt;/em>&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/cmd-single-server-arch.png" alt="single server arch">&lt;/p>
&lt;p>What enabled this was moving all of the dispatching to the same server that was running containers for commands.
I realize now in hindsight that other than wanting to play around with API gateway when I first built this, there was really no good reason why I shouldn&amp;rsquo;t have done this from the beginning.
Like many side-projects, I guess this turned into a yak-shave where you start thinking about how you can use one cloud service, which eventually turns into ten.&lt;/p>
&lt;p>A quick summary of the &amp;ldquo;new&amp;rdquo; architecture for those who are curious:&lt;/p>
&lt;ul>
&lt;li>There is now a single Go service that is responsible for both request handling and launching Docker containers. Previously I was using AWS Lambda and issuing commands to a remote Docker server.&lt;/li>
&lt;li>I still front this with Cloudflare but I removed all command caching at the edge. Every single request goes down to the VM where a Go service is running and returns a cached result (using sqlite) if it was already sent.&lt;/li>
&lt;li>Using Cloudflare means I can use AWS certificates for HTTPs, the origin for Cloudflare is an EC2 instance&lt;/li>
&lt;li>Instead of DynamoDB, I keep the command cache locally on the instance in an SQLite db which is much better and faster for lookups. A backup is periodically kept and restored on instance creation.&lt;/li>
&lt;/ul>
&lt;p>Before, querying solutions from Lambda out of Dynamodb was too slow so I needed to cache solutions with a Lambda cronjob.
Now, with a local SQLite db, they are generated in the context of a request.
For example, &lt;a href="https://cmdchallenge.com/c/s?slug=hello_world">this request&lt;/a> is generated with:&lt;/p>
&lt;pre tabindex="0">&lt;code>SELECT
cmd
FROM challenges
WHERE slug=$1 and correct=1
ORDER BY LENGTH(cmd) LIMIT 50;
&lt;/code>&lt;/pre>&lt;p>I can&amp;rsquo;t overstate how nice this is compared to do doing very slow lookups over the network to Dynamo.&lt;/p>
&lt;p>Previously I was using CloudWatch alerts and logs, now I have logs going to journald and using Prometheus for metrics.
I also got rid of GoatCounter for anlaytics, because most people have ad blockers anyway, not sure how useful it was. I quickly burned through Sentry&amp;rsquo;s free useage cap so I went ahead and removed that as well.&lt;/p>
&lt;p>With Prometheus running the VM for metrics, it&amp;rsquo;s now more convenient to add metrics for everything imaginable for executing commands:&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/processed-cmds.png" alt="processed-cmds">&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/req-per-min.png" alt="req-per-min">&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/cmd-errors.png" alt="cmd-errors">&lt;/p>
&lt;p>Another benefit that came out of this is that now it is much easier to validate and run locally, if you want to run the entire site yourself check out &lt;a href="https://gitlab.com/jarv/cmdchallenge">https://gitlab.com/jarv/cmdchallenge&lt;/a>.&lt;/p></description></item><item><title>Update on the Go rewrite</title><link>https://jarv.org/posts/rewrite-update-1/</link><pubDate>Thu, 02 Sep 2021 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/rewrite-update-1/</guid><description>&lt;p>I&amp;rsquo;ve been stealing small chunks of time in the evening here and there and finally have a functioning Go rewrite of cmdchallenge which will replace all of the Python code that is running in AWS Lambda.&lt;/p>
&lt;p>The first diff for this is massive, I am really looking forward to removing all of this vendored Python!&lt;/p>
&lt;p>&lt;a href="https://gitlab.com/jarv/cmdchallenge/-/merge_requests/165/">&lt;img src="https://jarv.org/img/large-diff.png" alt="large-diff">&lt;/a>&lt;/p>
&lt;p>And we will say goodbye to some AWS services:&lt;/p>
&lt;ul>
&lt;li>Cloudflare&lt;/li>
&lt;li>Route53&lt;/li>
&lt;li>&lt;s>DynamoDB&lt;/s> Replaced with an sqlite DB&lt;/li>
&lt;li>&lt;s>Lambda&lt;/s> Now written as a Go service running on a VM&lt;/li>
&lt;li>&lt;s>CloudWatch&lt;/s> Not needed&lt;/li>
&lt;li>&lt;s>API Gateway&lt;/s> Not needed&lt;/li>
&lt;/ul>
&lt;p>What I didn&amp;rsquo;t expect during this rewrite was the joy of having everything running locally, including end-to-end tests in Go. It&amp;rsquo;s quite nice to be able to validate requests and being able to test using a browser locally without having to go through Lambda in the cloud.&lt;/p>
&lt;p>Since a VM for the runner was already a requirement, I will likely stick the new web service on the same VM and keep a local SQLite DB.
What I haven&amp;rsquo;t figured out yet, is how to manage persistence so that I can rebuild the single VM without losing any (or too much) data.
Right now, I&amp;rsquo;m thinking something as simple as a shutdown script that syncs to object storage, and reads on provision.&lt;/p></description></item><item><title>Go rewrite of cmdchallenge</title><link>https://jarv.org/posts/go-rewrite-cmdchallenge/</link><pubDate>Mon, 05 Jul 2021 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/go-rewrite-cmdchallenge/</guid><description>&lt;p>I&amp;rsquo;ve decided to take what was written years ago for &lt;a href="https://cmdchallenge.com">cmdchallenge.com&lt;/a> and port it to a single Go app. This is a pretty big change because I will be removing most of the AWS serverless components including API Gateway, Dynamo and possibly CloudFront.
In the process, I would also like to simplify the architecture a bit. It seems like a single process with Docker running locally, and a db like sqlite should be sufficient. Here are some notes for the transition:&lt;/p>
&lt;ul>
&lt;li>This will be a small program and I&amp;rsquo;m pretty sure I won&amp;rsquo;t need any frameworks for the REST server&lt;/li>
&lt;li>I&amp;rsquo;ll need to do some lex parsing of the submission, &lt;a href="https://pkg.go.dev/github.com/google/shlex">https://pkg.go.dev/github.com/google/shlex&lt;/a> should work.&lt;/li>
&lt;li>1 years worth of data is about 300K unique entries in the submission table, in dyamoDB this is about 300MB of data.&lt;/li>
&lt;li>Currently I&amp;rsquo;m keeping track of every single request in a larger submissions table that is used for rate limits, I think this won&amp;rsquo;t be necessary anymore since I can implement in-memory rate limiting without a tracking database.&lt;/li>
&lt;li>I recently moved the runner from Go to &lt;a href="https://nim-lang.org/">Nim&lt;/a>, but I think I&amp;rsquo;ll stick with Nim for now for the runner.&lt;/li>
&lt;li>I would like to look into exporting some metrics to prometheus as well via a &lt;code>/metrics&lt;/code> endpoint&lt;/li>
&lt;/ul>
&lt;p>So first thing to do is create a schema for commands:&lt;/p>
&lt;pre tabindex="0">&lt;code> CREATE TABLE `commands` (
`fingerprint` TEXT,
`challenge_slug` TEXT,
`cmd` TEXT,
`cmd_length` INT,
`correct` BOOL,
`create_time` INT,
`output` TEXT,
`source_ip` TEXT,
`resp` TEXT
);
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>We will use the fingerprint of the command as the primary key for fast cache lookups&lt;/li>
&lt;li>Scans on the sqlite db will be much cheaper than against dynamodb (in both time and money) so I don&amp;rsquo;t think we need a sort key like we did for DyamoDB&lt;/li>
&lt;/ul>
&lt;p>On removing Cloudflare caching, I think it will be interesting to remove it altogether, I assume that cache lookups in sqlite will be performant enought to bypass the extra caching layer.&lt;/p></description></item><item><title>Does Serverless make sense for a side project?</title><link>https://jarv.org/posts/does-serverless-make-sense/</link><pubDate>Sat, 28 Nov 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/does-serverless-make-sense/</guid><description>&lt;p>&lt;a href="https://12days.cmdchallenge.com">Command Challenge&lt;/a> has been running for over 3 years now and I would consider it one of those &amp;ldquo;serverless&amp;rdquo; architectures. The front-end services requests using API Gateway and Lambda functions and commands are forwarded to a Docker executor running on a VM.&lt;/p>
&lt;p>Overall, the costs have been minimal thanks to mostly low-ish traffic and keeping it inside the AWS free-tier limits.
By &amp;ldquo;free-tier&amp;rdquo; I mean the &amp;ldquo;always-free&amp;rdquo; limits of an AWS account, since the 12 month trial period for new accounts expired quite some time ago.&lt;/p>
&lt;p>Here is a 2020 update for the services that are used to run the site and a short explanation of what they do.&lt;/p>
&lt;h2 id="serverless-architecture">Serverless Architecture&lt;/h2>
&lt;p>&lt;img src="https://jarv.org/img/cmd-architecture.png" alt="cmd-architecture">&lt;/p>
&lt;p>Where we could probably move all of this to a single VM and reduce some of the configuration complexity, I don&amp;rsquo;t think it would be any cheaper except on a shared hosting provider.&lt;/p>
&lt;p>As you can see in the diagram, this project isn&amp;rsquo;t completely &amp;ldquo;serverless&amp;rdquo; because a server is required for executing user-submitted commands in Docker.
For this, it&amp;rsquo;s a matter of finding the cheapest VM available for this type of workload.&lt;/p>
&lt;h2 id="free-cloud-services">Free Cloud Services&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.goatcounter.com">GoatCounter&lt;/a>&lt;/strong>: An analytics service &lt;a href="https://www.goatcounter.com/why#what-are-goatcounters-goals">that respects your privacy&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Slack&lt;/strong>: Used to receive notifications for unique submissions and errors, useful to keep an eye on what is going on.&lt;/li>
&lt;li>&lt;strong>Grafana Cloud&lt;/strong>: Grafana offers a &lt;a href="https://grafana.com/signup/starter/connect-account">free starter plan&lt;/a>. This connects to Prometheus which is running on the GCP VM for monitoring Docker and node level metrics.&lt;/li>
&lt;li>&lt;strong>Sentry&lt;/strong>: &lt;a href="https://sentry.io">Sentry.io&lt;/a> is very nice for side-projects, offering a free tier that I found indispensable for tracking down front-end JS errors in different browser configurations.&lt;/li>
&lt;li>&lt;strong>GitLab&lt;/strong>: &lt;a href="https://gitlab.com">GitLab.com&lt;/a> drives the CI pipeline, every new commit on master updates &lt;a href="https://testing.cmdchallenge.com">the testing environment&lt;/a>, with a manual promotion to prod.&lt;/li>
&lt;/ul>
&lt;h2 id="aws---2month">AWS - $2/month&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>CloudFront&lt;/strong>: Used as the CDN for all requests to the site. HTTP GET requests to &lt;code>/r/*&lt;/code> are for command submissions and are forwarded to the API Gateway. These requests are cached here, so if multiple submissions are sent for the same challenge CloudFront will return a cached response. Other requests that are not command, assets and the static page are forwarded to S3.&lt;/li>
&lt;li>&lt;strong>S3 bucket&lt;/strong>: Hosts all static content and serves as the origin for CloudFlare. Also receives periodic updates for user solutions, stored as JSON files.&lt;/li>
&lt;li>&lt;strong>API Gateway&lt;/strong>: Previously REST API, this was &lt;a href="https://jarv.org/posts/http-api-gateway/">recently switched to the HTTP API Gateway&lt;/a>. Accepts an HTTP request and forwards it to Lambda function for submissions.&lt;/li>
&lt;li>&lt;strong>Lambda&lt;/strong>:
&lt;ul>
&lt;li>&lt;strong>Submission Handling&lt;/strong>: Responsible for handling submission and rate-limiting logic. It takes a submission and forwards it to the Docker executor. If it receives the same input for a challenge that has already been evaluated, it will return a cached response&lt;/li>
&lt;li>&lt;strong>Solutions Updater&lt;/strong>: This is split into multiple Lambda functions that look up solutions and writes them to S3 as &lt;code>json&lt;/code> files. It runs in multiple jobs because the queries to DynamoDB span many records and take awhile to run.&lt;/li>
&lt;li>&lt;strong>Slack Notfications&lt;/strong>: On every unique successful submission, or error a notification is sent to Slack. This is helpful to keep an eye on what is going on and aids troubleshooting when there are problems.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>DynamoDB&lt;/strong>: Every submission is stored in the database which is used for both rate limiting, and tracking correct/incorrect submissions.&lt;/li>
&lt;li>&lt;strong>CloudWatch Logs&lt;/strong>: Collects logs from Lambda for debugging.&lt;/li>
&lt;li>&lt;strong>Event Bridge&lt;/strong>: Previously called CloudWatch Rules, this triggers periodic events that are used to trigger a Lambda function that generates a list of user-submitted solutions.&lt;/li>
&lt;li>&lt;strong>Route53&lt;/strong>: The one service that doesn&amp;rsquo;t have a free tier, for .50 a month it manages the cmdchallenge.com DNS zone.&lt;/li>
&lt;li>&lt;strong>Amazon Certificate Manager&lt;/strong>: Free HTTPs certificates for all of cmdchallenge.com&lt;/li>
&lt;/ul>
&lt;h3 id="breakdown-of-a-typical-monthly-bill">Breakdown of a typical monthly bill&lt;/h3>
&lt;p>&lt;img src="https://jarv.org/img/aws-bill.png" alt="aws-bill">&lt;/p>
&lt;h2 id="gcp---6month">GCP - $6/month&lt;/h2>
&lt;p>&lt;img src="https://jarv.org/img/gcp-bill.png" alt="gcp-bill">&lt;/p>
&lt;p>The production site uses an &lt;code>e2-micro&lt;/code> instance costing around $6/month, this is a bit more than we need typically but it gives us some headroom when the site gets busy.
There is only a single VM which is a single-point-of-failure for the site. This can cause a bit of disruption if there is a spontaneous reboot or if an upgrade is required.
Thankfully, in GCP this is very fast, and thanks to the multiple-layers of caching (CloudFlare, DynamoDB) in the serverless stack, it only causes a problem for unique submissions that haven&amp;rsquo;t been seen before.&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/cpu-memory.png" alt="cpu-memory">&lt;/p>
&lt;ul>
&lt;li>Memory utilization hovers around 300MB when there isn&amp;rsquo;t a lot of activity, this means that on smaller shared-core VMS like the &lt;code>f1-micro&lt;/code> some swap is necessary.&lt;/li>
&lt;li>CPU Utilization is generally low, though it varies because traffic due to site usage tends to be bursty.&lt;/li>
&lt;/ul>
&lt;h2 id="vm-comparison-for-shared-core-or-burstable-instances">VM Comparison for shared-core or burstable instances&lt;/h2>
&lt;p>This architecture spans cloud providers to keep costs low as I found GCP gives a better value for the money on VMs compared to AWS at this lower tier.
Here is a current-as-of-now overview of cheap VMs under $15 from the main cloud providers.&lt;/p>
&lt;p>All of these are non-preemptable shared core instances or burst-rate limited in the case of AWS.
This means that you cannot count on using the full CPU all of the time.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Instance&lt;/th>
&lt;th>vCPUs&lt;/th>
&lt;th>Memory&lt;/th>
&lt;th>Price $/month&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GCP f1-micro&lt;/td>
&lt;td>.2&lt;/td>
&lt;td>.6GB&lt;/td>
&lt;td>$5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GCP e2-micro&lt;/td>
&lt;td>2&lt;/td>
&lt;td>1GB&lt;/td>
&lt;td>$6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GCP e2-small&lt;/td>
&lt;td>2&lt;/td>
&lt;td>2GB&lt;/td>
&lt;td>$12&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GCP g1-small&lt;/td>
&lt;td>.5&lt;/td>
&lt;td>1.7GB&lt;/td>
&lt;td>$12&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS t2-nano&lt;/td>
&lt;td>1&lt;/td>
&lt;td>.5GB&lt;/td>
&lt;td>$4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS t2-micro&lt;/td>
&lt;td>1&lt;/td>
&lt;td>1GB&lt;/td>
&lt;td>$8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS LightSail&lt;/td>
&lt;td>1&lt;/td>
&lt;td>0.5GB&lt;/td>
&lt;td>$3.50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS LightSail&lt;/td>
&lt;td>1&lt;/td>
&lt;td>1GB&lt;/td>
&lt;td>$5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS LightSail&lt;/td>
&lt;td>1&lt;/td>
&lt;td>2GB&lt;/td>
&lt;td>$10&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Digital Ocean&lt;/td>
&lt;td>1&lt;/td>
&lt;td>1GB&lt;/td>
&lt;td>$5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Digital Ocean&lt;/td>
&lt;td>1&lt;/td>
&lt;td>2GB&lt;/td>
&lt;td>$10&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Digital Ocean&lt;/td>
&lt;td>2&lt;/td>
&lt;td>2GB&lt;/td>
&lt;td>$15&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="notes">Notes&lt;/h3>
&lt;ul>
&lt;li>For GCP, preemptable instances would be cheaper but with only one it doesn&amp;rsquo;t really work since it would mean a service interruption.&lt;/li>
&lt;li>The &lt;code>f1-micro&lt;/code> is a bit too small for the main site, I use it for the &lt;a href="https://testing.cmdchallenge.com">testing environment&lt;/a>.&lt;/li>
&lt;li>AWS LightSail / Digital Ocean have basically the same offering and might also be a good fit.&lt;/li>
&lt;/ul>
&lt;p>For now, the main site is powered by an &lt;code>e2-micro&lt;/code> instance for $6/month.
It offers 2 cores, which is more than the other options in the same price-range though it&amp;rsquo;s not uncommon to see stalls due to CPU steal:&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/cpu-steal.png" alt="cpu-steal">&lt;/p>
&lt;p>It runs &lt;a href="https://cloud.google.com/container-optimized-os/docs">Container Optimized OS&lt;/a> and only runs Docker and a Prometheus exporter for node metrics.&lt;/p>
&lt;h2 id="so-is-it-worth-it">So, is it worth it?&lt;/h2>
&lt;p>From a cost perspective for now I still think so. Of course there is always a risk that something happens where my cloud spend will spiral out of control but if this were to happen I would just shut it down.
There is the meme &amp;ldquo;Wouldn&amp;rsquo;t it be easier to run this on a $5 DO droplet&amp;rdquo;? I think it might apply here, though the type of workload it runs is ideal for serverless because it is a single-page app where API Gateway requests are only required for submissions. This keeps the number of Lambda executions low.&lt;/p>
&lt;p>Ignoring spend I think there are two big disadvantages to running servless. One is the complexity of gluing the cloud-native components together and keeping a configuration that is easy to manage and update.
This is solved by keeping everything in a single &lt;a href="https://gitlab.com/jarv/cmdchallenge/-/blob/master/terraform/site.tf">terraform script&lt;/a>, and though updates are automatically applied in CI I think there is a rather big initial investment in getting it to work properly.&lt;/p>
&lt;p>The other, bigger reason I found is that &lt;strong>it&amp;rsquo;s difficult, if not impossible to test everything locally&lt;/strong>, without a lot more work which would be my number one complaint about this setup.
The way I have worked around this is to have an identical &lt;a href="https://testing.cmdchallenge.com">testing environment&lt;/a> running in parallel, this actually doesn&amp;rsquo;t cost anything extra because it doesn&amp;rsquo;t get any usage beyond a small amount of testing.&lt;/p>
&lt;p>To sum up, while it&amp;rsquo;s been fun for someone who enjoys doing infrastructure, I have been thinking about re-writing it all as a single server that run on a single VM, with sqlite as the db. I&amp;rsquo;m pretty sure it would perform better :)&lt;/p></description></item><item><title>Moving to HTTP from REST for the AWS API Gateway</title><link>https://jarv.org/posts/http-api-gateway/</link><pubDate>Sun, 25 Oct 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/http-api-gateway/</guid><description>&lt;h2 id="http-api-gateway">HTTP API Gateway&lt;/h2>
&lt;p>When HTTP API Gateway was announced in 2019 the Amazon said:&lt;/p>
&lt;blockquote>
&lt;p>Our goal is to make it as easy as possible for developers to build and manage APIs with API Gateway. We encourage you to try the new HTTP APIs and let us know what you think.&lt;/p>
&lt;/blockquote>
&lt;p>Today I decided to switch from using the REST API for cmdchallenge.com to the HTTP API and I must say it is &lt;em>a lot&lt;/em> easier to build and setup for simple HTTP APIS.
In Terraform, it now only requires two resources:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-terraform" data-lang="terraform">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_apigatewayv2_api&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lambda_permission&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Compare this to the previous configuration using REST API:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-terraform" data-lang="terraform">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_api_gateway_rest_api&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_api_gateway_method&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_api_gateway_integration&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lambda_permission&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_api_gateway_deployment&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For a full working example, see &lt;a href="https://gitlab.com/jarv/cmdchallenge/-/blob/master/terraform/modules/api/main.tf">the API Gateway Terraform configuration&lt;/a> for cmdchallenge.com. In addition to making the configuration simpler, it&amp;rsquo;s supposed to be &lt;em>optimized for performance&lt;/em> and this doesn&amp;rsquo;t disappoint either. Testing against an API endpoint that invokes a Lambda function and makes some DynamoDB calls there is a nice saving of around 100ms for the 50th percentile and 200ms for the 90th.&lt;/p>
&lt;p>REST API Gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> $ bombardier --latencies --rate &lt;span class="m">50&lt;/span> -d 30s &lt;span class="s1">&amp;#39;https://g4jrkpyb3d.execute-api.us-east-1.amazonaws.com/r/?cmd=echo+hello+world&amp;amp;challenge_slug=hello_world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bombarding https://g4jrkpyb3d.execute-api.us-east-1.amazonaws.com:443/r/?cmd&lt;span class="o">=&lt;/span>echo+hello+world&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="nv">challenge_slug&lt;/span>&lt;span class="o">=&lt;/span>hello_world &lt;span class="k">for&lt;/span> 30s using &lt;span class="m">125&lt;/span> connection&lt;span class="o">(&lt;/span>s&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[============================================================================================================================================================================]&lt;/span> 30s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Done!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Statistics Avg Stdev Max
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Reqs/sec 48.45 33.81 211.47
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Latency 1.13s 845.04ms 6.77s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Latency Distribution
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 50% 0.88s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 75% 0.99s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 90% 2.71s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 95% 2.92s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 99% 4.78s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HTTP codes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1xx - 0, 2xx - 1501, 3xx - 0, 4xx - 0, 5xx - &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> others - &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Throughput: 86.94KB/s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>HTTP API Gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ bombardier --latencies --rate &lt;span class="m">50&lt;/span> -d 30s &lt;span class="s1">&amp;#39;https://ddzt9hixi4.execute-api.us-east-1.amazonaws.com/?cmd=echo+hello+world&amp;amp;challenge_slug=hello_world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bombarding https://ddzt9hixi4.execute-api.us-east-1.amazonaws.com:443/?cmd&lt;span class="o">=&lt;/span>echo+hello+world&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="nv">challenge_slug&lt;/span>&lt;span class="o">=&lt;/span>hello_world &lt;span class="k">for&lt;/span> 30s using &lt;span class="m">125&lt;/span> connection&lt;span class="o">(&lt;/span>s&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[============================================================================================================================================================================]&lt;/span> 30s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Done!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Statistics Avg Stdev Max
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Reqs/sec 48.44 30.62 235.42
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Latency 0.93s 679.98ms 6.70s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Latency Distribution
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 50% 703.40ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 75% 731.99ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 90% 1.66s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 95% 2.70s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 99% 3.98s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HTTP codes:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1xx - 0, 2xx - 1501, 3xx - 0, 4xx - 0, 5xx - &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> others - &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Throughput: 33.19KB/s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="caching-responses-with-the-http-api-gateway">Caching Responses with the HTTP API Gateway&lt;/h2>
&lt;p>&lt;a href="https://cmdchallenge.com">Command Challenge&lt;/a> has multiple layers of caching for anonymous usage.
If there wasn&amp;rsquo;t any caching, every single submission would execute in a Docker container, which would not only be atrocious from a performance standpoint but also expensive as it would require multiple/large VMs executing commands in containers.&lt;/p>
&lt;p>Every command that is submitted is hashed, and written to a DynamoDB table. If a subsequent command is identical for the same challenge, we return a cached response.
Putting CloudFront in front of API Gateway is a nice way to get additional caching for free, which works for the HTTP Gateway which &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html">doesn&amp;rsquo;t have a caching feature&lt;/a>. To cache HTTP API Gateway responses, create a CloudFront distribution with the API endpoint as the &lt;code>Origin Domain Name&lt;/code>.&lt;/p></description></item><item><title>Displaying the percentage of hosts completed during a rolling Ansible Deploy using serial</title><link>https://jarv.org/posts/host-percentage/</link><pubDate>Fri, 02 Oct 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/host-percentage/</guid><description>&lt;p>Recently we were looking at enhancing our deployment pipeline so that we run validation checks between every batch that is upgraded. For our VM deployments in Ansible we normally have&lt;/p>
&lt;pre tabindex="0">&lt;code>serial: 10%
&lt;/code>&lt;/pre>&lt;p>so that 10% of the fleet is operated on at once, this includes draining from a load balancer before we upgrade the node, and adding it back to the load balancer after.&lt;/p>
&lt;p>With Ansible, it doesn&amp;rsquo;t seem like there is any built-in way to get the host percentage, so here is a hacky solution for others who want to do something similar:&lt;/p>
&lt;ul>
&lt;li>Initialize a fact to an empty array:&lt;/li>
&lt;/ul>
&lt;p>If we put this in &lt;code>pre_tasks:&lt;/code> you will need to keep in mind it is called on every batch, so you don&amp;rsquo;t want to override it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Initialize completed_hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set_fact&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">completed_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] | default(&amp;#39;[]&amp;#39;) }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_facts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run_once&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Another option is initialize it in a play that runs before the play that is operating on hosts, in that case you can just initialize it to an empty array:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Initialize completed_hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set_fact&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">completed_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>In post_tasks, append the current batch to completed_hosts:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Appends to completed hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set_fact&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">completed_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] + ansible_play_batch }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_facts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>Calculate a percentage:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Percentage complete&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">msg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Percentage complete: {{ ((hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] | length) / (ansible_play_hosts | length) * 100) | int }}%&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run_once&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And here is an example test play:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gather_facts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">some_host_group&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serial&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;10%&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pre_tasks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">msg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">drain&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Initialize completed_hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set_fact&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">completed_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] | default(&amp;#39;[]&amp;#39;) }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_facts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run_once&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tasks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">msg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">post_tasks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">msg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Appends to completed hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set_fact&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">completed_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{{ hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] + ansible_play_batch }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_facts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Display current batch&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">var&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ansible_play_batch&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run_once&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Display completed hosts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">debug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">msg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Percentage complete: {{ ((hostvars[&amp;#39;localhost&amp;#39;][&amp;#39;completed_hosts&amp;#39;] | length) / (ansible_play_hosts | length) * 100) | int}}%&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run_once&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delegate_to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Ensuring a consistent PID in a container</title><link>https://jarv.org/posts/pid-docker/</link><pubDate>Fri, 31 Jul 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/pid-docker/</guid><description>&lt;p>Recently a &lt;a href="https://oops.cmdchallenge.com/#/oops_print_process">challenge was added&lt;/a> that asks you to identify a process and kill it. This was a new type of exercise that requires a running process in the docker container where all commands are run. This itself wasn&amp;rsquo;t really an issue except that with caching, it requires that the state of the container to be identical from one run to the next. PIDs however, are not so predictable, even when you only have a very small wrapper.&lt;/p>
&lt;pre tabindex="0">&lt;code>PID 1: wrapper that run the command
PID 6 or 7: forked command passed to the wrapper
&lt;/code>&lt;/pre>&lt;p>I&amp;rsquo;m not sure why sometimes the PID would be &lt;code>6&lt;/code> and other times &lt;code>7&lt;/code>, but this causes bit of a problem since if you executed a command like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ls -d /proc/&lt;span class="o">[&lt;/span>0-9&lt;span class="o">]&lt;/span>*
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You might get a cached result, which doesn&amp;rsquo;t represent the PIDs that are running in the context of your next command.&lt;/p>
&lt;p>So like most, I went to stackoverflow and found &lt;a href="https://stackoverflow.com/questions/18122592/how-to-set-process-id-in-linux-for-a-specific-program">this stackoverflow post&lt;/a> it can be done by setting &lt;code>ns_last_pid&lt;/code>&lt;/p>
&lt;blockquote>
&lt;ul>
&lt;li>Open /proc/sys/kernel/ns_last_pid and get fd&lt;/li>
&lt;li>flock it with LOCK_EX&lt;/li>
&lt;li>write PID-1&lt;/li>
&lt;li>fork&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>But you can&amp;rsquo;t set this in a docker container unless you run it privileged, which is not a great idea for running arbitrary commands passed in by the Internet.&lt;/p>
&lt;p>Instead, this ended up getting solved with a very boring solution. For challenges where we need a running process, the wrapper cycles through all the PIDs up to PID number &lt;code>41&lt;/code>, and then forks the process that needed to be killed. The logic is quite simple, and looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nim" data-lang="nim">&lt;span class="line">&lt;span class="cl">&lt;span class="k">proc &lt;/span>&lt;span class="nf">start&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">oopsProc&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="n">OopsProc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">prog&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OOPS_PROG&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">targetPid&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">oopsProc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">slug&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">startsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;oops&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="kp">true&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">let&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">startProcess&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">command&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">OOPS_PROG&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">=[&lt;/span>&lt;span class="s">&amp;#34;-t&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;0&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">options&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">poUsePath&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">let&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">waitForExit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">close&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">processId&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">targetPid&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">oopsProc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">startProcess&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">command&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">OOPS_PROG&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">options&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">poUsePath&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">oopsProc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pid&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">oopsProc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">processId&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is how for challenge &amp;ldquo;&lt;a href="https://oops.cmdchallenge.com/#/oops_kill_a_process">kill a process&lt;/a>&amp;rdquo;, the answer is always &lt;code>kill -9 42&lt;/code>!&lt;/p></description></item><item><title>Cat without cat on the commandline</title><link>https://jarv.org/posts/cat-without-cat/</link><pubDate>Thu, 30 Jul 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/cat-without-cat/</guid><description>&lt;p>Say you want to display the contents of a file on the command line. The first tool we most of us reach for is &lt;code>cat&lt;/code>, which does a fine job at just this.
But what happens when you are on a Linux machine and when you try to cat a file this happens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ cat file.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-bash: cat: &lt;span class="nb">command&lt;/span> not found
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or even:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ cat file.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bash: fork: retry: No child processes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This post explores this idea and was a feature of this &lt;a href="https://oops.cmdchallenge.com/#/oops_print_file_contents">challenge&lt;/a> where you needed to display a file&amp;rsquo;s contents without using any utility outside of the shell.&lt;/p>
&lt;hr>
&lt;h2 id="using-shell-built-ins-redirection-and-subshell">Using shell built-ins, redirection and subshell&lt;/h2>
&lt;p>Using the shell builtin &lt;code>read&lt;/code> you can display the contents of a file, without a forking a new process:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> line&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$line&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span> &amp;lt;file.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>From the help page for &lt;code>read&lt;/code>:&lt;/p>
&lt;blockquote>
&lt;p>One line is read from the standard input, or from file descriptor FD if the -u option is supplied, and the first word is assigned to the first NAME,&lt;/p>
&lt;/blockquote>
&lt;p>In this example, the contents of file.txt are redirected to the STDIN of &lt;code>read&lt;/code>, which processes the input line by line, until it reaches the end of the file. &lt;code>read&lt;/code> also can take a file descriptor as its input instead of STDIN, so this will also work:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> 3&amp;lt;file.txt &lt;span class="c1"># Assign file descriptor 3 for reading&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> -u &lt;span class="m">3&lt;/span> line&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$line&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This ends up being a lot more typing than just &lt;code>cat file.txt&lt;/code>. With the &lt;code>bash&lt;/code> or &lt;code>zsh&lt;/code> there is a another way to display a file&amp;rsquo;s contents without using &lt;code>cat&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&amp;lt;file.txt&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>this method uses redirection and command substitution, and is mentioned in the bash man page:&lt;/p>
&lt;blockquote>
&lt;p>The command substitution $(cat file) can be replaced by the equivalent but faster $(&amp;lt; file).&lt;/p>
&lt;/blockquote>
&lt;p>It&amp;rsquo;s faster, because you are not forking a cat, but does it matter? Probably not, and may not be clear to everyone what you are doing, but you can see a difference with a quick test on your shell:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ &lt;span class="nb">time&lt;/span> &lt;span class="k">for&lt;/span> n in &lt;span class="o">{&lt;/span>1..1000&lt;span class="o">}&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="k">$(&lt;/span>&amp;lt;/etc/resolv.conf&lt;span class="k">)&lt;/span> &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">real 0m0.977s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user 0m0.380s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sys 0m0.604s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ &lt;span class="nb">time&lt;/span> &lt;span class="k">for&lt;/span> n in &lt;span class="o">{&lt;/span>1..1000&lt;span class="o">}&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cat /etc/resolv.conf &amp;gt;/dev/null&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">real 0m1.980s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user 0m0.626s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sys 0m1.224s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This syntax &lt;code>$(&amp;lt;file.text)&lt;/code> may look a bit strange, what you are doing is command substitution, where the contents of &lt;code>file.txt&lt;/code> are sent to STDIN which is then echo&amp;rsquo;d as STDOUT. If you want to learn a bit more about redirection using &lt;code>&amp;gt;&lt;/code> and &lt;code>&amp;lt;&lt;/code> see my earlier post about &lt;a href="https://jarv.org/posts/shell-redirection/">shell redirection&lt;/a>.&lt;/p>
&lt;h2 id="using-other-utilities">Using other utilities&lt;/h2>
&lt;p>How about other options? Without using shell built-ins but instead using other standard utilities you can also cat without cat:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ul &amp;lt; /file
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ul&lt;/code> might inadvertently underline some words in your file but I think it might be the only way to cat a file with only two characters.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tac /file &lt;span class="p">|&lt;/span> tac
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you didn&amp;rsquo;t already guess, &lt;code>tac&lt;/code> is GNU core util that is the reverse of &lt;code>cat&lt;/code> so if you want to be clever you can pipe the output of &lt;code>tac&lt;/code> to &lt;code>tac&lt;/code> which is just a &lt;code>cat&lt;/code>.&lt;/p>
&lt;p>Of course using tools like &lt;code>sed&lt;/code>, &lt;code>perl&lt;/code>, &lt;code>python&lt;/code>, etc. will allow you to cat files as well, happy cat&amp;rsquo;ing!&lt;/p>
&lt;p>&lt;strong>Interested in other ways to cat without cat? Try the &lt;a href="https://oops.cmdchallenge.com/#/oops_print_file_contents">oops challenge&lt;/a>!&lt;/strong>&lt;/p></description></item><item><title>Switching to Hugo</title><link>https://jarv.org/posts/hugo-blog/</link><pubDate>Sun, 26 Jul 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/hugo-blog/</guid><description>&lt;p>Hello world from the new blog using &lt;a href="https://gohugo.io/">Hugo&lt;/a> as the static site generator. After cutting away the cruft from the old one, which used Pelican as a static site generator, I decided to make a switch on a lazy Sunday. I definitely think the design is much nicer and site generation is much faster! There is a great selection of themes, this one uses the &lt;a href="https://themes.gohugo.io/hugo-kiera/">kiera&lt;/a> which is exactly what I wanted for a layout that doesn&amp;rsquo;t have too many bells and whistles.&lt;/p></description></item><item><title>Understanding shell redirection</title><link>https://jarv.org/posts/shell-redirection/</link><pubDate>Sat, 25 Jul 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/shell-redirection/</guid><description>&lt;p>When I was learning to use the shell for the first time I remember teaching myself to do things as I ran into problems, and the first problem I ran into was how to take the output of one command and write it to a file or pipe it a second command.&lt;/p>
&lt;p>So like most people, I learned:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cmd1 &lt;span class="p">|&lt;/span> cmd2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>to take the output of &lt;code>cmd1&lt;/code> and send it to &lt;code>cmd2&lt;/code>, and&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cmd1 &amp;gt; path/to/file
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>to take the output of &lt;code>cmd1&lt;/code> and write it to a file.&lt;/p>
&lt;p>This got me pretty far for what I needed to do without fully understanding how it worked or why. Eventually, I learned as well that there were multiple types of output, STDOUT and STDERR, and you could redirect one to the other by adding &lt;code>2&amp;gt;&amp;amp;1&lt;/code> to the end of commands. Early on, I wish I took the time to understand how redirection works and how it relates to file descriptors.&lt;/p>
&lt;hr>
&lt;p>To understand this a bit better, first it&amp;rsquo;s helpful to understand file descriptors. At a very high level, file descriptors are numbers assigned by the operating system that reference open files. The shell will assign the following:&lt;/p>
&lt;ul>
&lt;li>&lt;code>0&lt;/code>: STDIN - program input&lt;/li>
&lt;li>&lt;code>1&lt;/code>: STDOUT - program output&lt;/li>
&lt;li>&lt;code>2&lt;/code>: STDERR - program error&lt;/li>
&lt;/ul>
&lt;p>When using a pipe &lt;code>|&lt;/code> or redirection &lt;code>&amp;lt;&lt;/code>/&lt;code>&amp;gt;&lt;/code> it&amp;rsquo;s helpful to think abut these 3 file descriptors and their corresponding number assignments.&lt;/p>
&lt;p>Going back to the example where we are redirecting STDOUT of a program to a file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ls &amp;gt; path/to/file
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is is the same as:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">1&amp;gt; /path/to/file ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Where &lt;code>1&lt;/code> refers to STDOUT of &lt;code>ls&lt;/code>&lt;/p>
&lt;h3 id="redirecting-output">Redirecting Output&lt;/h3>
&lt;p>Let&amp;rsquo;s break down exactly what &lt;code>&amp;gt;&lt;/code> is doing, from the bash man page:&lt;/p>
&lt;blockquote>
&lt;p>The general format for redirecting output is:&lt;/p>
&lt;pre>&lt;code> [n]&amp;gt;word
&lt;/code>&lt;/pre>
&lt;/blockquote>
&lt;p>Where &lt;code>[n]&lt;/code> is a file descriptor, but in the example &lt;code>&amp;gt; /path/to/file cmd&lt;/code> there isn&amp;rsquo;t a file descriptor on left side! But there is actually, if you don&amp;rsquo;t specify a specific file descriptor, for output redirection, STDOUT is the default. Which means all of these redirections are exactly the same:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ ls &amp;gt; path/to/file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &amp;gt; /path/to/file ls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ 1&amp;gt; /path/to/file ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note where it is more intuitive to put the &lt;code>&amp;gt;&lt;/code> after the command, it&amp;rsquo;s not necessary. This is because the position doesn&amp;rsquo;t matter because the descriptor to the left of &lt;code>&amp;gt;&lt;/code> is implicit. Pretty neat!&lt;/p>
&lt;h3 id="redirecting-input">Redirecting Input&lt;/h3>
&lt;p>Redirecting input is almost exactly the same, from the bash man page:&lt;/p>
&lt;blockquote>
&lt;p>The general format for redirecting output is:&lt;/p>
&lt;pre>&lt;code> [n]&amp;lt;word
&lt;/code>&lt;/pre>
&lt;/blockquote>
&lt;p>Note that again there is a file descriptor on the left, and a file on the right. The only difference is that when using a &lt;code>&amp;lt;&lt;/code> the default file descriptor is &lt;code>0&lt;/code> (STDIN) if one isn&amp;rsquo;t specified. Therefore, all of these commands are equivalent:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ cat &amp;lt; path/to/file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &amp;lt; path/to/file cat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ 0&amp;lt; path/to/file cat
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Again, I think the first one is a bit more intuitive but it doesn&amp;rsquo;t matter whether the redirection is placed before or after the command.&lt;/p>
&lt;h3 id="redirecting-stdout-to-stderr">Redirecting stdout to stderr&lt;/h3>
&lt;p>So back to &lt;code>2&amp;gt;&amp;amp;1&lt;/code>, why is the &lt;code>&amp;amp;&lt;/code> necessary and what does it mean? Remember that for &lt;code>&amp;gt;&lt;/code> there needs to be a file descriptor on the left and a file on the right? If we used &lt;code>2&amp;gt;1&lt;/code> this would simply redirect STDOUT to a file named &lt;code>1&lt;/code>. By adding the &lt;code>&amp;amp;&lt;/code> it tells the shell that &lt;code>1&lt;/code> is referring not to a file, but a file descriptor!&lt;/p>
&lt;p>So the main things to remember when it comes to shell redirection:&lt;/p>
&lt;ul>
&lt;li>For redirecting input &lt;strong>and&lt;/strong> redirecting output, there should always be a file descriptor on the left, and a file (for reading, or writing) on the right.&lt;/li>
&lt;li>If a file descriptor is not specified, the default for output &lt;code>&amp;gt;&lt;/code> is &lt;code>1&lt;/code> (STDOUT) and the default for input &lt;code>&amp;lt;&lt;/code> is &lt;code>0&lt;/code> (STDIN).&lt;/li>
&lt;li>For redirecting STDOUT to STDERR, the &lt;code>&amp;amp;&lt;/code> is necessary on the right side, because you are redirecting to another file descriptor, instead of a file&lt;/li>
&lt;li>The best reference for all of this is the bash man page, which contains a lot more in-depth information on ways to redirect!&lt;/li>
&lt;/ul></description></item><item><title>A new new new blog</title><link>https://jarv.org/posts/new-new-new-blog/</link><pubDate>Wed, 17 Jun 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/new-new-new-blog/</guid><description>&lt;p>Yeah it&amp;rsquo;s another blog update, just really some style updates and looking back at something I did a long time ago and say &lt;em>what the heck was I thinking?&lt;/em>. I don&amp;rsquo;t know, I think things that I don&amp;rsquo;t have time to spend up usually end up being a craptastic design by copy-and-paste. Anyway, still using Pelican but upgraded to Python3 and fixed the CI configuration a bit. Got rid of the bourbon/{sass,neat} mixins because that was so 2013. Still using sass though.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/getpelican/pelican">pelican&lt;/a> with a &lt;a href="https://github.com/jarv/water-iris">custom theme&lt;/a>&lt;/li>
&lt;li>&lt;s>&lt;a href="http://bourbon.io/">bourbon/sass&lt;/a> - CSS compilation and general typography&lt;/s>&lt;/li>
&lt;li>&lt;s>&lt;a href="http://neat.bourbon.io/">bourbon/neat&lt;/a> - For layout and responsive design&lt;/s>&lt;/li>
&lt;/ul>
&lt;p>Oh and remove as much as possible like javascript easter-eggs and social links, because who needs that stuff :)&lt;/p></description></item><item><title>Never use git submodules</title><link>https://jarv.org/posts/submodules/</link><pubDate>Wed, 17 Jun 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/submodules/</guid><description>&lt;p>I&amp;rsquo;m writing this as a reminder to my future self never to use git submodules. For most things I would say never say never but let&amp;rsquo;s make an exception for this one.&lt;/p>
&lt;p>I&amp;rsquo;ve used submodules for the following reasons:&lt;/p>
&lt;ul>
&lt;li>There is some logical separation or a clear defined interfaces between two things, and repos are cheap&lt;/li>
&lt;li>There is a 1 to many (usually 2 or 3) relationship between this thing (that has stable interface) and something else&lt;/li>
&lt;/ul>
&lt;p>The problem is that this 1-to-many thing usually starts as 1-to-2 and usually doesn&amp;rsquo;t go much further than that. It&amp;rsquo;s always &lt;em>this could eventually increase in scale&lt;/em> but usually those hunches are wrong.&lt;/p>
&lt;p>So where does it fall down? It seems like it rarely works well:&lt;/p>
&lt;ul>
&lt;li>Cognitive overhead for multiple git commands to keep things updated&lt;/li>
&lt;li>Doing normal things with git (conflict resolution, reviews, etc) feels super clumsy&lt;/li>
&lt;li>With reviews, it often means reviewing multiple times and messes up approval workflows&lt;/li>
&lt;li>It&amp;rsquo;s 2020 and this process still looks unnecessarily complex &lt;a href="https://stackoverflow.com/questions/1260748/how-do-i-remove-a-submodule">https://stackoverflow.com/questions/1260748/how-do-i-remove-a-submodule&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>My alternative for now for better gitops workflows is mono-repo everything, this seems to work better with CI anyway&lt;/p></description></item><item><title>The stages of a side project</title><link>https://jarv.org/posts/side-projects/</link><pubDate>Wed, 17 Jun 2020 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/side-projects/</guid><description>&lt;p>One experience of mine, though it has roughly mapped to previous side projects:&lt;/p>
&lt;ul>
&lt;li>Wow this thing that has no business model is a cool idea, I wonder if anyone has done it before?&lt;/li>
&lt;li>This is fun, let&amp;rsquo;s spend an evening hacking on it&lt;/li>
&lt;li>Wow this is taking way more time than I thought it would &amp;hellip;&lt;/li>
&lt;li>Can I do this without spending any money? Let&amp;rsquo;s use AWS free tier to get this going&lt;/li>
&lt;li>OK let&amp;rsquo;s ship this thing, wow it&amp;rsquo;s getting a lot of attention! Front-page on hackernews, subreddit, etc.&lt;/li>
&lt;li>I&amp;rsquo;m totally ready for this, I have a scaling plan&lt;/li>
&lt;li>Oops my scaling plan is completely useless because the bottlnecks are not where I expected they would be&lt;/li>
&lt;li>Wow traffic really drops off when it isn&amp;rsquo;t on hackernews 🤔&lt;/li>
&lt;li>Oh wait someone tweeted it, there goes traffic again..&lt;/li>
&lt;li>I should probably add CI to make this a bit easier to maintain&lt;/li>
&lt;li>OK things are stable again&lt;/li>
&lt;li>Wait my free tier is about to expire, let&amp;rsquo;s figure out how to host this for free by using multi-cloud&lt;/li>
&lt;li>Whoa this is taking longer than I thought&lt;/li>
&lt;li>.. years pass&lt;/li>
&lt;li>OK something is broken and getting some email complaints, I really have lost interest in this thing&lt;/li>
&lt;li>Have a free weekend, let&amp;rsquo;s look at the code from years ago&lt;/li>
&lt;li>Good god, it&amp;rsquo;s terrible&lt;/li>
&lt;li>Well, a bunch of things are now deprecated, let&amp;rsquo;s at least fix that&lt;/li>
&lt;li>OK so if I am going through this trouble I should probably re-write some of it, especially the last parts that were just thrown together&amp;hellip;&lt;/li>
&lt;li>Whoa this is taking longer than I thought&amp;hellip;&lt;/li>
&lt;/ul>
&lt;p>Introducing a newer version of &lt;a href="https://cmdchallenge.com">cmdchallenge&lt;/a> .. A lone survivor in a sea of broken things I have had an itch to build.&lt;/p></description></item><item><title>Using GitLab CICD for your static blog</title><link>https://jarv.org/posts/jarv-org-cicd/</link><pubDate>Tue, 05 Jun 2018 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/jarv-org-cicd/</guid><description>&lt;p>In the process of making all of my private repositories public on Github and moving off of GitHub
pages I have also decided to move most of my repos over to GitLab.
One reason is that setting up a CICD pipeline makes it
extremely easy to publish these posts automatically. This in combination with GitLab&amp;rsquo;s
web IDE makes minor changes a little bit easier to make.&lt;/p>
&lt;p>CICD piplines in GitLab are controlled with a single file, &lt;code>.gitlab-ci.yml&lt;/code> that is placed
at the root of the repository.
Wit this file, on every commit, the following pipeline runs that deploys to &lt;a href="https://draft.jarv.org">draft.jarv.org&lt;/a>
and on the master branch for &lt;a href="https://jarv.org">jarv.org&lt;/a>.&lt;/p>
&lt;p>GitLab has integrated CI/CD runners that allow you to execute whatever you want in a docker
image of your choice for generating a &lt;a href="https://gitlab.com/jarv/jarv.org/-/pipelines/156823353">deployment pipeline&lt;/a>.&lt;/p>
&lt;p>Here is what the &lt;code>gitlab-ci.yml&lt;/code> configuration looks like for the &lt;a href="https://gitlab.com/jarv/jarv.org/blob/master/.gitlab-ci.yml">jarv.org repository&lt;/a>:&lt;/p>
&lt;p>Some notes about the setup:&lt;/p>
&lt;ul>
&lt;li>There are two stages, build and deploy. Build generates the html for the blog and deploy deploys it to both draft and the main site.&lt;/li>
&lt;li>Deploying to the main site only happens with a manual job&lt;/li>
&lt;li>I am using a custom image that has some of the blog dependencies pre-installed, it is created with &lt;a href="https://gitlab.com/jarv/jarv.org/blob/master/Dockerfile-ci">this docker file&lt;/a>.&lt;/li>
&lt;li>Every time the deploy happens, an invalidate is sent to the cloudfront distribution in front of it.&lt;/li>
&lt;li>Since I am no longer using GitHub pages, the blog is hosted in an S3 bucket so the CICD pipeline does an &lt;code>aws s3 sync ...&lt;/code> to get the static files on the bucket.&lt;/li>
&lt;/ul>
&lt;p>There is a special key named &lt;code>environment&lt;/code> that hints to GitLab that the stage is a deploy step. With this the URL provided will show up under operations-&amp;gt;deployments like this:&lt;/p>
&lt;p>&lt;img src="https://jarv.org/img/jarv-envs.png" alt="">&lt;/p>
&lt;p>That&amp;rsquo;s it! It couldn&amp;rsquo;t be simpler and I think one of the nicest things about this setup is the ability to use the GitLab web ide to make quick changes.&lt;/p>
&lt;p>&lt;em>Disclaimer: I work for GitLab though the opinions here are my own.&lt;/em>&lt;/p></description></item><item><title>Removing all of my private repos from GitHub</title><link>https://jarv.org/posts/removing-private-repos/</link><pubDate>Thu, 31 May 2018 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/removing-private-repos/</guid><description>&lt;p>Today I decided to remove all private repositories from GitHub.
Why? Interesting that having private repositories generally meant that
I was not being as careful about managing secrets properly. Checking in
API tokens, keys and especially cloud tokens into git &lt;em>never&lt;/em> a good idea
and looking through some of my old private repos I was doing exactly that.
So for others who are interested in going from a &lt;em>developer&lt;/em> GitHub account
back to free I highly recommend doing it! Granted it&amp;rsquo;s only $5 a month savings
but I definitely feel a bit more transparent than I did 15 minutes ago. :)
For finding secrets you can use something like &lt;a href="https://github.com/dxa4481/truffleHog">trufflehog&lt;/a>
to ensure that there no sensitive bits in your git history.&lt;/p></description></item><item><title>Building cmdchallenge using Lambda and API Gateway in the AWS free-tier with Docker and Go</title><link>https://jarv.org/posts/building-cmdchallenge/</link><pubDate>Mon, 24 Apr 2017 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/building-cmdchallenge/</guid><description>&lt;p>Have you ever thought about building a side-project for fun without spending a lot on hosting?
This post might be for you. With the most tech-buzz-wordy title I could conjure up here is
a quick overview of how &lt;a href="https://cmdchallenge.com">cmdchallenge.com&lt;/a> is built.
The site is a simple web application side-project that executes shell commands
remotely in a docker container in AWS. The front-end gives the feeling of a normal terminal but underneath
it is sending whatever commands you give it remotely on an EC2 instance inside a Docker container.&lt;/p>
&lt;p>The source code for most of it is located &lt;a href="https://github.com/jarv/cmdchallenge">on github&lt;/a> including
a tiny command executer written in Go, the challenge definitions, and a test harness.&lt;/p>
&lt;p>The following AWS services are used for the site:&lt;/p>
&lt;ul>
&lt;li>Cloudfront&lt;/li>
&lt;li>API Gateway&lt;/li>
&lt;li>S3 bucket&lt;/li>
&lt;li>Lambda function&lt;/li>
&lt;li>DynamoDB&lt;/li>
&lt;li>t2.micro EC2 Instance running coreos&lt;/li>
&lt;li>CloudWatch logs&lt;/li>
&lt;/ul>
&lt;p>In addition to this Amazon Certificate Manager and Route53 was used but for everything above you can keep costs close to zero in AWS. There is no free tier for Route53 (sad panda) but it&amp;rsquo;s like 50 cents a month for a single zone.&lt;/p>
&lt;h2 id="features">Features&lt;/h2>
&lt;ul>
&lt;li>Submit commands, execute them in a bash sub-shell.&lt;/li>
&lt;li>Check the output of the command for different challenges.&lt;/li>
&lt;li>Run tests for challenges that need them in addition or in place of checking output.&lt;/li>
&lt;/ul>
&lt;h2 id="deployment-tools-simple-and-boring">Deployment tools (simple and boring)&lt;/h2>
&lt;ul>
&lt;li>Makefiles.&lt;/li>
&lt;li>Python fabric for running commands and copying files over ssh.&lt;/li>
&lt;li>Kappa, zips up code, sends it to lambda, also manages Lambda permissions.&lt;/li>
&lt;/ul>
&lt;p>With these tools the following automated steps are taken to deploy the site:&lt;/p>
&lt;ul>
&lt;li>Create a Docker image that holds the challenges.&lt;/li>
&lt;li>Launch a new coreos EC2 instance.&lt;/li>
&lt;li>Run a fabric script that does the following on the instance over SSH:
&lt;ul>
&lt;li>Configures TLS so that a Lambda function can communicate to Docker on an EC2 instance.&lt;/li>
&lt;li>Executes some periodic commands to ensure that the host cleans up old containers.&lt;/li>
&lt;li>Downloads the docker image that has the challenges.&lt;/li>
&lt;li>Copies the read-only volume that is used on the container for the tests and command runner.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Update Lambda with new code.&lt;/li>
&lt;li>Sync the static assets to S3.&lt;/li>
&lt;li>Invalidates CF cache for the main site.&lt;/li>
&lt;/ul>
&lt;h2 id="architecture-diagram">Architecture diagram:&lt;/h2>
&lt;p>There are two public entry points for the site, one is the main web-site which is static and served S3.
The other is the API gateway at api.cmdchallenge.com which is also fronted by CloudFront so that it can
use a certificate from ACM and cache requests.&lt;/p>
&lt;pre tabindex="0">&lt;code>
api.cmdchallenge.com cmdchallenge.com
******************** ****************
+---------------------+ +---------------------+
| Cloudfront | | Cloudfront |
+---------------------+ +---------------------+
| |
+---------------------+ +-----------+
| API Gateway | | s3 bucket |
+---------------------+ +-----------+
|
+-----------------+
| |
| Lambda Function | +----------+
| |--- | |
+-----------------+ \| DynamoDB |
| | |
+--------------+ +----------+
| EC2 t2.micro |
| (coreos) |
+--------------+
&lt;/code>&lt;/pre>&lt;p>One nice thing about using AWS server-less components was that &lt;strong>a single t2.micro instance ended up being fine for handling all of the load, even at peak.&lt;/strong>&lt;br />
&lt;em>See section on caching/performance below.&lt;/em>&lt;/p>
&lt;h2 id="here-is-what-happens-when-a-command-is-submitted-in-the-cmdchallengecom-terminalcmdchallengecom">Here is what happens when a command is submitted in the &lt;a href="cmdchallenge.com">cmdchallenge.com terminal&lt;/a>:&lt;/h2>
&lt;ul>
&lt;li>Javascript code sends an HTTP GET to &lt;a href="https://api.cmdchallenge.com">https://api.cmdchallenge.com&lt;/a>&lt;/li>
&lt;li>If it is cached it returns a response immediately. If not, it forwards the request to the API gateway which in turn sends it to a Lambda function.&lt;/li>
&lt;li>The Lambda function looks up the challenge and the command in DynamoDb and if it already has an answer it returns that. If the challenge doesn&amp;rsquo;t exist in DyamoDB it is forwarded to the EC2 instance as a command using the docker API.&lt;/li>
&lt;li>The command that the user provides is passed to a Go command runner that executes the command in a bash sub-shell inside a docker container, checks the output and runs the tests.&lt;/li>
&lt;li>Results are returned to the Lambda function, it writes them to DynamoDb and returns the response.&lt;/li>
&lt;/ul>
&lt;h2 id="the-challenges-are-expressed-in-a-single-yaml-here-an-example-of-one-challenge">The challenges are expressed in a single YAML, here an example of one challenge:&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">slug&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hello_world&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">author&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmdchallenge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Print &amp;#34;hello world&amp;#34;.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Hint: There are many ways to print text on
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> the command line, one way is with the &amp;#39;echo&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> command.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Try it below and good luck!&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">example&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">echo &amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expected_output&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Interested in coming up with your own? You can submit your own challenge with a &lt;a href="https://github.com/jarv/cmdchallenge/pulls">pull request&lt;/a>. Your challenge will be added to the user-contributed section of the site.&lt;/p>
&lt;h2 id="caching">Caching:&lt;/h2>
&lt;p>You may notice that when you do &lt;code>echo hello world&lt;/code> on the &lt;a href="https://cmdchallenge.com/#/hello_world">hello world challenge&lt;/a> it returns almost immediately.
As it is shown above there are two layers of cache, one at CloudFront and one at DynamoDb to reduce
the number of command executions on the Docker container.
API Gateway &lt;em>can&lt;/em> provide caching but it costs money, I worked around this by sticking CloudFront in front of it but this
is only possible with HTTP GETs.
With Cloudfront in front the cache-control header in
the response from Lambda is set to a very long cache lifetime with every request.
The version of the challenge as well as a global cache buster param is passed in so we never
have to worry about returning a response from a stale challenge.&lt;/p>
&lt;h2 id="performance">Performance:&lt;/h2>
&lt;p>If you are wondering how well this would scale for a lot of traffic, the Lambda function currently dispatches
commands to a random host in a statically configured list of EC2 instances making it pretty easy
to add more capacity. So far it seems to be operating fine with a single t2.micro EC2 instance handling
all command requests that are not cached.&lt;/p>
&lt;ul>
&lt;li>Time to get a &lt;code>echo hello world&lt;/code> response from a cached cloudfront command - &lt;strong>~50ms&lt;/strong>&lt;/li>
&lt;li>Time to get a &lt;code>echo hello world&lt;/code> response from a cached command in dynamoDB - &lt;strong>~2.5s&lt;/strong>&lt;/li>
&lt;li>Time to get a &lt;code>echo hello world&lt;/code> response, executed in a container - &lt;strong>~4s&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Without caching this wouldn&amp;rsquo;t be possible and also the caching at CloudFront enables most commands to
return fairly quickly.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>If you like the site please follow &lt;a href="https://twitter.com/thecmdchallenge">@thecmdchallenge&lt;/a> on twitter or if you have
suggestions drop me a mail at &lt;a href="mailto:info@cmdchallenge.com">&lt;a href="mailto:info@cmdchallenge.com">info@cmdchallenge.com&lt;/a>&lt;/a>.&lt;/p>
&lt;p>Thanks!&lt;/p></description></item><item><title>User Submitted Solutions</title><link>https://jarv.org/posts/user-submitted-solutions/</link><pubDate>Sat, 04 Mar 2017 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/user-submitted-solutions/</guid><description>&lt;p>Adding to the interesting &lt;a href="https://jarv.org/191-hello-worlds.html">191 ways to echo hello world&lt;/a> I&amp;rsquo;ve now added the ability
to see &lt;a href="https://cmdchallenge.com/s/">user-submitted solutions&lt;/a> to &lt;a href="https://cmdchallenge.com">cmdchallenge&lt;/a>.&lt;/p>
&lt;p>There are some gems if you dig through them including maybe the longest regex I&amp;rsquo;ve ever seen for
pulling an IP address out of a file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span>25&lt;span class="o">[&lt;/span>0-5&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>2&lt;span class="o">[&lt;/span>0-4&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="o">[&lt;/span>01&lt;span class="o">]&lt;/span>?&lt;span class="o">[&lt;/span>0-9&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>?&lt;span class="o">)&lt;/span>&lt;span class="se">\.&lt;/span>&lt;span class="o">(&lt;/span>25&lt;span class="o">[&lt;/span>0-5&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>2&lt;span class="o">[&lt;/span>0-4&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="o">[&lt;/span>01&lt;span class="o">]&lt;/span>?&lt;span class="o">[&lt;/span>0-9&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>?&lt;span class="o">)&lt;/span>&lt;span class="se">\.&lt;/span>&lt;span class="o">(&lt;/span>25&lt;span class="o">[&lt;/span>0-5&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>2&lt;span class="o">[&lt;/span>0-4&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="o">[&lt;/span>01&lt;span class="o">]&lt;/span>?&lt;span class="o">[&lt;/span>0-9&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>?&lt;span class="o">)&lt;/span>&lt;span class="se">\.&lt;/span>&lt;span class="o">(&lt;/span>25&lt;span class="o">[&lt;/span>0-5&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>2&lt;span class="o">[&lt;/span>0-4&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="o">[&lt;/span>01&lt;span class="o">]&lt;/span>?&lt;span class="o">[&lt;/span>0-9&lt;span class="o">][&lt;/span>0-9&lt;span class="o">]&lt;/span>?&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Also scrolling down the page of &lt;a href="https://cmdchallenge.com/s/#/corrupted_text">solutions to the corrupted text problem&lt;/a>
is glorious.&lt;/p>
&lt;p>&lt;s>The solutions are not updated regularly right now but would be easy enough to do in the future if people want to see
more, let me know on &lt;a href="https://twitter.com/thecmdchallenge">twitter&lt;/a> and also&lt;/s> &lt;strong>Update&lt;/strong>: Solutions are now generated every
five minutes. Feel free to submit suggestions for
new challenges on &lt;a href="https://github.com/jarv/cmdchallenge">github&lt;/a>.&lt;/p></description></item><item><title>figlet breakout</title><link>https://jarv.org/posts/figlet-breakout/</link><pubDate>Fri, 24 Feb 2017 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/figlet-breakout/</guid><description>&lt;p>I was looking for a cool ending for &lt;a href="https://cmdchallenge.com">cmdchallenge&lt;/a> and decided
to dust off a 2 year old javascript project which created a breakout game from figlet
fonts. Not quite a full re-write but fixed a lot of bugs and did away completely with
coffee-script. More info on the &lt;a href="https://github.com/jarv/figlet-breakout">github page&lt;/a>.&lt;/p>
&lt;p>Or you can &lt;a href="https://www.jarv.org/figlet-breakout/#/click%20me%20to%20play">click here to play&lt;/a>.&lt;/p></description></item><item><title>191 ways to echo hello world on the command line</title><link>https://jarv.org/posts/191-hello-worlds/</link><pubDate>Wed, 08 Feb 2017 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/191-hello-worlds/</guid><description>&lt;p>It&amp;rsquo;s been about 12 days since the launch of &lt;a href="https://cmdchallenge.com">cmdchallenge&lt;/a>, a weekend project
to create some common command-line tasks that can be done in a single line of bash.
One common request has been to share user-submitted solutions. Or to put it another way,
you may be wondering &lt;em>what do random people on the internet and hackernews type if you give them
some basic command-line tasks and a shell prompt?&lt;/em> Well lucky for me this is no longer a mystery!&lt;/p>
&lt;p>Starting off with Challenge #1:&lt;/p>
&lt;p>&lt;a href="https://cmdchallenge.com/#/hello_world">CMD Challenge #1&lt;/a>: &lt;strong>print &amp;ldquo;hello world&amp;rdquo; at the bash prompt&lt;/strong>&lt;/p>
&lt;p>There has been a lot of diverse input for such a simple task. I really love how people do weird stuff even when it is totally unnecessary.&lt;/p>
&lt;p>Here are some of my favorites:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span> &lt;span class="k">for&lt;/span> i in h e l l o &lt;span class="se">\ &lt;/span> w o r l d &lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$i&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span>awk -F, &lt;span class="s1">&amp;#39; {print $NR}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span> &lt;span class="o">)&lt;/span> &lt;span class="p">|&lt;/span>tr -d &lt;span class="se">\\&lt;/span>n&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> ifmmp xpsme &lt;span class="p">|&lt;/span>tr bcdefghijklmnopqrstuvwxyza abcdefghijklmnopqrstuvwxyz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch helloworld &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; helloworld &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> cat helloworld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sed &lt;span class="s1">&amp;#39;s.\.\...;yhHh\hh;ywWw\ww;2,$d&amp;#39;&lt;/span> README
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things to note that may clarify some of the more interesting submissions:&lt;/p>
&lt;ul>
&lt;li>There is a &lt;code>README&lt;/code> in each challenge directory, in this case it contains the string &amp;ldquo;hello world&amp;rdquo; so some people took advantage of that.&lt;/li>
&lt;li>The directory itself was named &lt;code>hello world&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Here are all of the correct submissions for the first challenge as of yesterday:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">a&lt;/span>&lt;span class="o">=(&lt;/span>d e h l o r w X &lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="nv">s&lt;/span>&lt;span class="o">=(&lt;/span>&lt;span class="m">2&lt;/span> &lt;span class="m">1&lt;/span> &lt;span class="m">3&lt;/span> &lt;span class="m">3&lt;/span> &lt;span class="m">4&lt;/span> &lt;span class="m">7&lt;/span> &lt;span class="m">6&lt;/span> &lt;span class="m">4&lt;/span> &lt;span class="m">5&lt;/span> &lt;span class="m">3&lt;/span> 0&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="k">for&lt;/span> i in &lt;span class="si">${&lt;/span>&lt;span class="nv">s&lt;/span>&lt;span class="p">[@]&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> -n &lt;span class="si">${&lt;/span>&lt;span class="nv">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nv">$i&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="p">|&lt;/span>tr X&lt;span class="se">\n&lt;/span> &lt;span class="s1">&amp;#39; &amp;#39;&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span> &lt;span class="k">for&lt;/span> i in h e l l o &lt;span class="se">\ &lt;/span> w o r l d &lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$i&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span>awk -F, &lt;span class="s1">&amp;#39; {print $NR}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span> &lt;span class="o">)&lt;/span> &lt;span class="p">|&lt;/span>tr -d &lt;span class="se">\\&lt;/span>n&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> i in h e l l o&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> -n &lt;span class="nv">$i&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">for&lt;/span> j in w o r l d&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> -n &lt;span class="nv">$j&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat README &lt;span class="p">|&lt;/span> head -n &lt;span class="m">4&lt;/span> &lt;span class="p">|&lt;/span> tail -n &lt;span class="m">1&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{ print $3 &amp;#34; &amp;#34; $4 }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> sed -e &lt;span class="s1">&amp;#39;s/\&amp;#34;//g;s/\.//g&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> ifmmp xpsme &lt;span class="p">|&lt;/span>tr bcdefghijklmnopqrstuvwxyza abcdefghijklmnopqrstuvwxyz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt; README grep hello &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;#&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> cut -f3- -d&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch helloworld &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; helloworld &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> cat helloworld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">letters&lt;/span>&lt;span class="o">=(&lt;/span>h e l l o &lt;span class="se">\ &lt;/span> w o r l d&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">letters&lt;/span>&lt;span class="p">[@]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch test.txt &lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; test.txt &lt;span class="p">;&lt;/span> cat test.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grep &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> README &lt;span class="p">|&lt;/span> sed -E &lt;span class="s1">&amp;#39;s/.*(hello world).*/\1/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat README &lt;span class="p">|&lt;/span> grep Print &lt;span class="p">|&lt;/span> sed -E &lt;span class="s1">&amp;#39;s/^.+&amp;#34;([^&amp;#34;]+)&amp;#34;.+$/\1/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; ./hiworld.txt &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> cat ./hiworld.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">head -n &lt;span class="m">1&lt;/span> &amp;lt; README &lt;span class="p">|&lt;/span> sed &lt;span class="s1">&amp;#39;s/# //&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;[A-Z]&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;[a-z]&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="nv">$0&lt;/span>&lt;span class="p">|&lt;/span>cut -d&lt;span class="se">\;&lt;/span> -f4&lt;span class="p">;&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;;hello world;&amp;#34;&lt;/span> &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch file &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; file &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> cat file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat README &lt;span class="p">|&lt;/span> sed -e &lt;span class="s1">&amp;#39;s/.*/hello world/&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> head -1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> ls *hell*&lt;span class="p">;&lt;/span> rm &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">head -1 README &lt;span class="p">|&lt;/span> cut -c3- &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;[A-Z]&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;[a-z]&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; hello.txt &lt;span class="p">|&lt;/span> cat hello.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN {print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span> &amp;lt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; hello.txt&lt;span class="p">;&lt;/span> cat hello.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; testfile&lt;span class="p">;&lt;/span> cat testfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; test.txt&lt;span class="p">;&lt;/span> cat test.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grep hello README &lt;span class="p">|&lt;/span> awk -F&lt;span class="se">\&amp;#34;&lt;/span> &lt;span class="s1">&amp;#39;{ print $2 }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> n in &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">n&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sed &lt;span class="s1">&amp;#39;s.\.\...;yhHh\hh;ywWw\ww;2,$d&amp;#39;&lt;/span> README
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; foo.txt&lt;span class="p">;&lt;/span> cat foo.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;h&amp;#34;&amp;#34;e&amp;#34;&amp;#34;l&amp;#34;&amp;#34;l&amp;#34;&amp;#34;o&amp;#34;&amp;#34; &amp;#34;&amp;#34;w&amp;#34;&amp;#34;o&amp;#34;&amp;#34;r&amp;#34;&amp;#34;l&amp;#34;&amp;#34;d&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ls h*&lt;span class="p">|&lt;/span> xargs &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">msg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$msg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat README&lt;span class="p">|&lt;/span> grep hello &lt;span class="p">|&lt;/span> cut -d &lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span> -f2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34; &amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&amp;gt;hello &lt;span class="p">|&lt;/span> tail hello
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39; BEGIN { print &amp;#34;hello world&amp;#34; } &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">HAHA&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$HAHA&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span> &amp;gt; henk &lt;span class="p">|&lt;/span> cat henk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; &lt;span class="nb">test&lt;/span> &lt;span class="p">&amp;amp;&lt;/span> cat &lt;span class="nb">test&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN { print &amp;#34;hello world&amp;#34;; }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> hello&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> world&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s1">&amp;#39;printf &amp;#34;%s&amp;#34;, &amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ls &lt;span class="p">|&lt;/span> tail -1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN { print &amp;#34;hello world&amp;#34; }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">pwd&lt;/span> &lt;span class="p">|&lt;/span> cut -d&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span> -f4 &lt;span class="p">|&lt;/span> sed &lt;span class="s1">&amp;#39;s/_/ /g&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> ls &lt;span class="p">|&lt;/span> grep ello
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s%s%s%s\n&amp;#39;&lt;/span> hel lo&lt;span class="se">\ &lt;/span> wor ld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat README &lt;span class="p">|&lt;/span> grep -o &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sed &lt;span class="s1">&amp;#39;s/b/h/g&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; tmp&lt;span class="p">;&lt;/span> cat tmp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world &amp;gt; henk&lt;span class="p">;&lt;/span> cat henk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;gt;/dev/stdout &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN{print &amp;#34;hello world&amp;#34;;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN{print(&amp;#34;hello world&amp;#34;)}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;gt;/dev/stderr &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN {print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&amp;gt;algo&lt;span class="p">;&lt;/span>cat algo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">hello&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$hello&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s1">&amp;#39;print &amp;#34;hello world\n&amp;#34;;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">GGG&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">;&lt;/span> cat &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="nv">$GGG&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">echo&lt;span class="p">|&lt;/span>awk &lt;span class="s1">&amp;#39;{print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span> &amp;gt; /dev/stdout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;BEGIN{print &amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ls .. &lt;span class="p">|&lt;/span> grep hel &lt;span class="p">|&lt;/span> sed &lt;span class="s1">&amp;#39;s/_/ /g&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s %s\n&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s1">&amp;#39;print &amp;#34;hello world\n&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> s &lt;span class="p">|&lt;/span> sed &lt;span class="s1">&amp;#39;s/s/hello world/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34; world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#!/bin/bash\necho &amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> -- &lt;span class="s1">&amp;#39;%s\n&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s %s&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;hello&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s %s&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -le &lt;span class="s1">&amp;#39;print &amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$text&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s1">&amp;#39;print &amp;#34;hello world&amp;#34;;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s2">&amp;#34;print &amp;#39;hello world&amp;#39;;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &amp;gt; a &lt;span class="p">;&lt;/span> cat a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n hello &lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34; world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s1">&amp;#39;print &amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e &lt;span class="s2">&amp;#34;print &amp;#39;hello world&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">STR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$STR&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -E &lt;span class="s1">&amp;#39;say &amp;#34;hello world&amp;#34;;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grep hello &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e&lt;span class="s1">&amp;#39;print &amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s\\n&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -E &lt;span class="s1">&amp;#39;say q[hello world]&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> ls *&lt;span class="se">\ &lt;/span>*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s %s\n&amp;#39;&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cat -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">VAR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$VAR&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;lt; &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -E &lt;span class="s1">&amp;#39;say &amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">date&lt;span class="p">|&lt;/span>sed s/.*/hello&lt;span class="se">\ &lt;/span>world/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> ls he*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cat -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s\n&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -e&lt;span class="s1">&amp;#39;print&amp;#34;hello world&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s\n&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello %s\n&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%b\n&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">echo&lt;span class="p">|&lt;/span>sed -n &lt;span class="s2">&amp;#34;i hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> ls h*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cat -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> %s&lt;span class="se">\\&lt;/span>n &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;dlrow olleh&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> rev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s %s&amp;#39;&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">true&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s %s&amp;#34;&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">i&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$i&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF\nhello world\nEOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> -- &lt;span class="s1">&amp;#39;hello world\n&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="sb">`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;hello %s&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello %s&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;%s&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> -- &lt;span class="s2">&amp;#34;hello world\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;dlrow olleh&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> rev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> cat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">PWD&lt;/span>&lt;span class="p">##*/&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="p">|&lt;/span>tr _ &lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world\013&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;%s &amp;#39;&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/bin/echo &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/bin/echo &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> %s &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat - &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -e &lt;span class="s2">&amp;#34;hello world\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">a&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$a&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> -- &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="se">\n&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> %s &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat - &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat - &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -ne &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">|&lt;/span>cat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;hello world\n&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world &lt;span class="p">|&lt;/span> cat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;hello world&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/bin/echo hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -e &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -e &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> dlrow olleh&lt;span class="p">|&lt;/span>rev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;echo&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>hello world&lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>hello world&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> hello&lt;span class="se">\ &lt;/span>world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">$&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">$&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -e hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>&lt;span class="nb">echo&lt;/span> hello world&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;hello world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello&lt;span class="se">\ &lt;/span>world&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;hello world&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;echo&amp;#39;&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>&lt;span class="nb">echo&lt;/span> hello&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>&lt;span class="nb">echo&lt;/span> hello world&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello &lt;span class="se">\w&lt;/span>orld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello&lt;span class="se">\ &lt;/span>world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> hello world
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;br />
&lt;p>Of course there are a lot of quoting and space variations here as well. I will make a
data-dump available soon with the responses for all challenges.&lt;/p>
&lt;p>On the building and hosting side of things this is getting more than the usual tiny trickle of side-project internet traffic.
So far I have tried to contain the entire thing in an AWS free tier account, it has worked out OK so far with a
few hiccups here and there. Since several people have asked, I will share more details about how the site is put
together in a future post, follow &lt;a href="https://twitter.com/thecmdchallenge">cmdchallenge on twitter&lt;/a> or
use &lt;a href="https://jarv.org/rss.xml">rss&lt;/a>.&lt;/p></description></item><item><title>Reddit front-page visualization</title><link>https://jarv.org/posts/reddit-time-lapse/</link><pubDate>Wed, 21 Jan 2015 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/reddit-time-lapse/</guid><description>&lt;p>Using data from &lt;a href="https://page-watch.com/#i/eLK">page-watch.com&lt;/a> this shows
a time-lapse of reddit overnight on January 20th taken at 5minute intervals.
This excludes images which is why there are occasional jumps where advertisements are.&lt;/p>
&lt;p>&lt;video width="100%" height="100%" src="https://cdn.rawgit.com/jarv/page-watch-data/master/reddit/reddit.webm" controls>&lt;/video>&lt;/p></description></item><item><title>A new new blog</title><link>https://jarv.org/posts/new-new-blog/</link><pubDate>Thu, 18 Dec 2014 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/new-new-blog/</guid><description>&lt;p>Before this year is over it&amp;rsquo;s time for a small redesign. I&amp;rsquo;ve replaced the old custom theme, &lt;a href="https://github.com/jarv/water-iris">water-iris&lt;/a> with
a new theme &lt;a href="https://github.com/jarv/jarvican">jarvican&lt;/a>. Most of my updates these days are when I redesign the blog and I think
I&amp;rsquo;m fine with that.&lt;/p></description></item><item><title>A new blog</title><link>https://jarv.org/posts/new-blog/</link><pubDate>Sun, 24 Nov 2013 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/new-blog/</guid><description>&lt;p>Hello world! It&amp;rsquo;s been awhile and what better way to re-visit a blog then to do a re-design.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/getpelican/pelican">pelican&lt;/a> with a &lt;a href="https://github.com/jarv/water-iris">custom theme&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://bourbon.io/">bourbon/sass&lt;/a> - CSS compilation and general typography&lt;/li>
&lt;li>&lt;a href="http://neat.bourbon.io/">bourbon/neat&lt;/a> - For layout and responsive design&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ve cleaned house and removed some old articles, kept some of the AVR hacks and hopefully will post updates more than once a year :)&lt;/p></description></item><item><title>Simple highscore tracking with Python / Flask</title><link>https://jarv.org/posts/simple-highscore-tracking-with-python-flask/</link><pubDate>Mon, 17 Dec 2012 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/simple-highscore-tracking-with-python-flask/</guid><description>&lt;p>Below is a quick tutorial on how flask web framework might be
used for simple highscore tracking. All of the source code the state game &lt;a href="http://github.com/jarv/thestategame">is now on github&lt;/a>.&lt;/p>
&lt;p>This code will accept an AJAX GET or POST at the end of each game so that scores can be tracked and read. Since the use-cases are simple it&amp;rsquo;s easy to write the tests first:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="k">class&lt;/span> &lt;span class="nc">StateTestCase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">unittest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">TestCase&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">setUp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">db_fd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DATABASE&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tempfile&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkstemp&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">state&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;TESTING&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">test_client&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">state&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init_db&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">tearDown&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">db_fd&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">unlink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DATABASE&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This above code will create an sqlite db before each test-case and
remove it when completed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_empty_db&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rv&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_single_entry&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">test_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;john doe&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;score&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1234&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;time&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1234&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">test_data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="s1">&amp;#39;ok&amp;#39;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rv&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rv&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">test_data&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_multiple_entries&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">score&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">25&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;john doe&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;score&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">score&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;time&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1234&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="s1">&amp;#39;ok&amp;#39;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rv&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rv&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>These three test-cases cover basic functionality of the high-score
tracker:&lt;/p>
&lt;ul>
&lt;li>Ensure that if a GET request is made before adding entries an empty
JSON array is returned.&lt;/li>
&lt;li>If a single entry is added with a POST ensure that the same results
are returned with a GET.&lt;/li>
&lt;li>If there are more than 10 entries added ensure that we only get 10
back.&lt;/li>
&lt;/ul>
&lt;p>For the actual app there isn&amp;rsquo;t much more code than what was written for
the tests.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">methods&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;GET&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">get_scores&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">g&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;select name, score, time from hs order by score limit 10&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">q&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">zip&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;score&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;time&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">item&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">item&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">row&lt;/span>&lt;span class="p">]))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">results&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">q&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Retrieve the top 10 scores, return the results as JSON.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">methods&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">post_score&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="ow">not&lt;/span> &lt;span class="n">re&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[\s\w]{1,20}$&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">])):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;invalid name&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="ow">not&lt;/span> &lt;span class="n">re&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[0-9]+\.?[0-9]*$&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;time&amp;#39;&lt;/span>&lt;span class="p">])):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;invalid time&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="ow">not&lt;/span> &lt;span class="n">re&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[0-9]{1,15}$&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;score&amp;#39;&lt;/span>&lt;span class="p">])):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;invalid score&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">g&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;insert into hs (name, score, time) values (?, ?, ?)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;score&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;time&amp;#39;&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">g&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;ok&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;success&amp;#39;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Receive a new score, do some basic input validation and insert the new
high score entry.&lt;/p>
&lt;p>Of course it&amp;rsquo;s easy to inject fake high scores with something like this,
abuse it, cheat, etc. But in this case it&amp;rsquo;s just for fun.&lt;/p></description></item><item><title>The State Game</title><link>https://jarv.org/posts/the-state-game/</link><pubDate>Sun, 22 Jul 2012 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/the-state-game/</guid><description>&lt;p>Below is a recent weekend programming project that I did to play with
canvas. Who doesn&amp;rsquo;t like a geography game?&lt;/p>
&lt;p>It uses a simple svg image for the map with a canvas overlay to detect
state clicks. On the back-end it uses a very short python script with
web.py. Most people who tried it out found the rotation to be annoying
and difficult; for me it makes it more interesting :)&lt;/p>
&lt;p>&lt;a href="https://jarv.github.io/thestategame/">&lt;img src="https://jarv.org/img/thestategame.png" alt="thestategame">&lt;/a>&lt;/p></description></item><item><title>Musical Ms. Pacman Candy Tin Hack</title><link>https://jarv.org/posts/musical-ms-pacman-candy-tin-hack/</link><pubDate>Sun, 04 Dec 2011 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/musical-ms-pacman-candy-tin-hack/</guid><description>&lt;p>Here is a weekend hack that uses the &lt;a href="https://github.com/jarv/PlayTune">midi to AVR
conversion script and library&lt;/a>. With xmas fast approaching I thought
it would be fun to convert a pacman candy tin to an xmas ornament and
have it play music. Below is the result, pressing a button on the tin
will cycle through three Ms. Pacman songs converted from midi files
found online.&lt;/p>
&lt;iframe src="http://www.youtube.com/embed/hyWlVr72n1M" style="border: none">&lt;/iframe></description></item><item><title>Custom musical greeting card for less than $5</title><link>https://jarv.org/posts/custom-musical-greeting-card-for-less-than-5/</link><pubDate>Sun, 20 Nov 2011 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/custom-musical-greeting-card-for-less-than-5/</guid><description>&lt;p>&amp;ldquo;You&amp;rsquo;ve got a friend in me&amp;rdquo; played on an attiny85 and two
speakers converted from a midi version of the song:&lt;/p>
&lt;iframe src="http://www.youtube.com/embed/gntKQZFomi8" style="border: none">&lt;/iframe></description></item><item><title>Blinking motion sensor / Mario Brothers Candy Tin</title><link>https://jarv.org/posts/usb-powered-blinking-ir-motion-sensor-mounted-in-a-mario-brothers-candy-tin/</link><pubDate>Tue, 15 Sep 2009 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/usb-powered-blinking-ir-motion-sensor-mounted-in-a-mario-brothers-candy-tin/</guid><description>&lt;p>With an attiny13A, an IR motion sensor, and a candy Mario Brothers
mushroom tin, this project turns them into a USB powered IR sensor that
also blinks and sings.&lt;/p>
&lt;iframe src="http://www.youtube.com/embed/A2rH835xQhY" style="border: none">&lt;/iframe></description></item><item><title>Singing Marioman</title><link>https://jarv.org/posts/singing-marioman/</link><pubDate>Sun, 24 May 2009 00:00:00 +0000</pubDate><guid>https://jarv.org/posts/singing-marioman/</guid><description>&lt;p>Using an attiny13A and some leds and a speaker create a musical
marioman.&lt;/p>
&lt;iframe src="http://www.youtube.com/embed/rCdSEzUQ3ok" style="border: none">&lt;/iframe></description></item></channel></rss>