Mike Slinn's Blog 2024-07-26T13:22:59-04:00 https://mslinn.github.io/blog Mike Slinn mslinn@gmail.com Working With JTabs 2024-07-22T00:00:00-04:00 https://mslinn.github.io/blog/2024/07/22/jtabs <style> iframe { margin-bottom: 1.5em; height: 15em; width: 100%; } </style> <!-- #region intro --><p> I like the tabs provided by <a href='https://jsuites.net/docs/javascript-tabs' target='_blank' rel="nofollow">jsuites jtabs</a>, because it does not use <code>jQuery</code>, so tabs are lightweight, and because they work well. However, the documentation is thin. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/jekyll/jtabs_demo.svg" type="image/svg"> <!---<source srcset="/blog/jekyll/jtabs_demo.avif" type="image/avif">--> <source srcset="/blog/jekyll/jtabs_demo.webp" type="image/webp"> <source srcset="/blog/jekyll/jtabs_demo.apng" type="image/apng"> <source srcset="/blog/jekyll/jtabs_demo.png" type="image/png"> <source srcset="/blog/jekyll/jtabs_demo.jpg" type="image/jpeg"> <source srcset="/blog/jekyll/jtabs_demo.jpeg" type="image/jpeg"> <source srcset="/blog/jekyll/jtabs_demo.jfif" type="image/jpeg"> <source srcset="/blog/jekyll/jtabs_demo.pjpeg" type="image/jpeg"> <source srcset="/blog/jekyll/jtabs_demo.pjp" type="image/jpeg"> <source srcset="/blog/jekyll/jtabs_demo.gif" type="image/gif"> <source srcset="/blog/jekyll/jtabs_demo.tif" type="image/tiff"> <source srcset="/blog/jekyll/jtabs_demo.tiff" type="image/tiff"> <source srcset="/blog/jekyll/jtabs_demo.bmp" type="image/bmp"> <source srcset="/blog/jekyll/jtabs_demo.ico" type="image/x-icon"> <source srcset="/blog/jekyll/jtabs_demo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/jekyll/jtabs_demo.png" style='width: 100%; ' /> </picture> </div> <p> These are my notes. </p> <!-- endregion --> <!-- #region URLs and URI Fragments --> <h2 id="uauf" class="clear">URLs and URI Fragments</h2> <!-- #region implicit --> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>URL containing a hash with value tab2</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2f8651b68d16'><button class='copyBtn' data-clipboard-target='#id2f8651b68d16' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>https://domain.com/jtabs_demo.html#tab2</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Defining a Few Terms --> <h3 id="terms">Defining a Few Terms</h3> <p> I expect readers of this website will generally know what a <a href='https://en.wikipedia.org/wiki/URL' target='_blank' rel="nofollow">Uniform Resource Locator</a> (URL) is. <a href='https://en.wikipedia.org/wiki/URL' target='_blank' rel="nofollow">Wikipedia has a good page</a> if you would like a quick refresher. </p> <p> A <a href='https://en.wikipedia.org/wiki/URI_fragment' target='_blank' rel="nofollow">URI fragment</a>, is delimited from the web page by an octothorpe (#), also known as a pound sign, also known as a hash mark. For example, the URL <code>http:/<wbr>/<wbr>domain.com/<wbr>page1.html#tab2</code> has a URI fragment of <code>tab2</code>. </p> <p> Popular documentation in 2024 often refers to URI fragments as <i>hash</i>es, so we would normally say that the hash value of the given URL is <code>tab2</code>. </p> <!-- endregion --> <!-- #region Two Uses For a URL Hash --> <h3 id="tufauh">Two Uses For a URL Hash</h3> <ol> <li> When serving a web page whose URL contains a hash, the user's web browser scans the web page, looking for an HTML element id that matches the hash value. If a match is found, the web browser tries to scroll the web page up or down, so the element with the matched id is as near to the top of the browser window as possible. </li> <li> A few lines of JavaScript can cause a URI fragment to activate a specific HTML tab amongst a set of tabs. I will show how to do that using jsuites jtabs in this article. </li> </ol> <!-- endregion --> <!-- endregion --> <!-- #region Opening a Tab From a URL Hash --> <h2 id="oatfauh">Opening a Tab From a URL Hash</h2> <p> Jsuites needs the following HTML structure to define a jtabs instance for every element with class <code>jtabs</code>. The collection of jtabs is saved in the JavaScript variable, also called <code>jtabs</code>: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>jsuites jtabs HTML</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id387912e0bb92'><button class='copyBtn' data-clipboard-target='#id387912e0bb92' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;div class=&quot;jtabs&quot;&gt; &lt;div id=&quot;jtabs-headers&quot;&gt; &lt;div id=&quot;tab1&quot;&gt;Tab 1&lt;/div&gt; &lt;div id=&quot;tab2&quot;&gt;Tab 2&lt;/div&gt; &lt;div id=&quot;tab3&quot;&gt;Tab 3&lt;/div&gt; &lt;/div&gt; &lt;div id=&quot;jtabs-content&quot;&gt; &lt;div&gt; This is the content of tab 1. &lt;/div&gt; &lt;div&gt; This is the content of tab 2. &lt;/div&gt; &lt;div&gt; This is the content of tab 3. &lt;/div&gt; &lt;/div&gt; &lt;/div&gt;</pre> </div> <!-- endregion --> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>JavaScript statement to create a jtabs instance</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb846776dfd79'><button class='copyBtn' data-clipboard-target='#idb846776dfd79' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>jtabs = jSuites.tabs(document.getElementsByClassName('jtabs')[0]);</pre> </div> <!-- endregion --> <p> The following 3 JavaScript incantations that all do the same thing in different ways. They all use the URL hash to activate the second tab of the <code>jtabs</code> instance, labeled "Tab 2", in the following jsuites jtab. </p> <p class="numbered"> The following JavaScript statement invokes the <code>open</code> method provided by the <code>jtabs</code> instance and passes the value 1. Because the first tab has index 0, the second tab has index 1. The second tab is thus revealed and is made active. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Invoke open and pass an index</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2ee25f2e4378'><button class='copyBtn' data-clipboard-target='#id2ee25f2e4378' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>jtabs.open(1); // origin 0</pre> </div> <!-- endregion --> <p class="numbered"> The following JavaScript statement also also opens the tab with id <code>tab2</code>. This JavaScript statement obtains the HTML element with id <code>jtabs2</code> and passes it to <code>selectIndex</code>. The second tab is then revealed and made active. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Invoke selectIndex and pass an HTML element</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide54a6d9b729b'><button class='copyBtn' data-clipboard-target='#ide54a6d9b729b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>jtabs.selectIndex(document.getElementById("jtabs2"));</pre> </div> <!-- endregion --> <p class="numbered"> The third JavaScript incantation is the most interesting. If a URI fragment (aka a hash) was specified, obtain the HTML element for that id and pass it to <code>selectIndex</code> as before. The second tab is then revealed and made active. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Parse the URI fragment if specified and invoke selectIndex</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida010df36bf4b'><button class='copyBtn' data-clipboard-target='#ida010df36bf4b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>const desiredTab = window.location.hash.split('#')[1]; if (typeof desiredTab !== 'undefined') jtabs.selectIndex(document.getElementById(desiredTab));</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Demo --> <h2 id="demo">JTabs Demo</h2> <p> I wrote this example to show how to style tabs and programmatically open them. The following <code>iframe</code> was embedded with URL <code>jtabs_demo.html#tab2</code>, which is why <b>Tab 2</b> was active. You can click on the tabs to verify the page is live. </p> <iframe id="iframe" src="/blog/jekyll/jtabs_demo.html#tab2" ></iframe> <ul> <li><a href='https://codepen.io/mslinn/pen/RwzRKGE' target='_blank' rel="nofollow">JTabs Demo CodePen</a></li> <li><a href='https://github.com/jsuites/jsuites/issues/154' target='_blank' rel="nofollow"><code>Jsuites</code> GitHub project issue</a></li> </ul> <p> Here is the complete HTML for the above JTabs demo: </p> <div class="codeLabel darkLabel"><a href='data:text/plain;charset=UTF-8,jtabs_demo.html' download='jtabs_demo.html' title='Click on the file name to download the file'>jtabs_demo.html</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer dark" id="idd908f95becc3"><button class='copyBtn' data-clipboard-target='#idd908f95becc3'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;html> &lt;head> &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsuites/dist/jsuites.min.css" type="text/css" /> &lt;style> body &#123; background-color: rgba(242, 215, 158, 0.55); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; margin-left: 10%; margin-top: 2em; width: 80%; &#125; .jtabs-content &#123; background-color: #F7E8B4; border: 2px solid rgba(102, 102, 102, 0.226); border-radius: 0 8px 8px 8px; padding: 10px; &#125; .jtabs .jtabs-headers &#123; div &#123; border-color: rgba(102, 102, 102, 0.226); border-style: solid; border-width: 1px 1px 0 1px; border-top-left-radius: 6px; border-top-right-radius: 6px; margin-left: 6px; &#125; div:not(.jtabs-selected) &#123; background-color: rgba(216, 162, 1, 0.184); color: rgba(102, 102, 102, .8); &#125; div.jtabs-selected &#123; background-color: rgba(216, 162, 1, 0.479); color: black; &#125; &#125; &lt;/style> &lt;/head> &lt;body> &lt;h1>JTabs Demo&lt;/h1> &lt;div class="jtabs"> &lt;div id="jtabs-headers"> &lt;div id="tab1">Tab 1&lt;/div> &lt;div id="tab2">Tab 2&lt;/div> &lt;div id="tab3">Tab 3&lt;/div> &lt;/div> &lt;div id="jtabs-content"> &lt;div> This is the content of tab 1. &lt;/div> &lt;div> This is the content of tab 2. &lt;/div> &lt;div> This is the content of tab 3. &lt;/div> &lt;/div> &lt;/div> &lt;/body> &lt;script defer src="https://cdn.jsdelivr.net/npm/jsuites/dist/jsuites.min.js">&lt;/script> &lt;script> document.addEventListener('DOMContentLoaded', function() &#123; jtabs = jSuites.tabs(document.getElementsByClassName('jtabs')[0]); // The following two statements accomplish the same task: open tab 2 // jtabs.open(1); // origin 0 //jtabs.selectIndex(document.getElementById("tab2")); // The following opens tab2 if the URL ends with #tab2 const desiredTab = window.location.hash.split('#')[1]; if (typeof desiredTab !== 'undefined') jtabs.selectIndex(document.getElementById(desiredTab)); &#125;); &lt;/script> &lt;/html> </pre> <!-- endregion --> Thermal Control 2024-07-05T00:00:00-04:00 https://mslinn.github.io/blog/2024/07/05/cooling <!-- #region intro --> <p> This article discusses some of the physics and a few of the electrical components for controlling temperature in a consumer electronics product. </p> <h2 id="miniseries">Mini-Series</h2> <p> This article can be read standalone; however, is part of a mini-series that discusses how to increase the storage capacity and performance of the Ableton Push 3 Standalone (P3S). If you own a P3S, or are thinking of purchasing one, you might want to first <a href='/av_studio/570-ableton-push-standalone.html'>read my review</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='/av_studio/570-ableton-push-standalone.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/p3s_top.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/p3s_top.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/p3s_top.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/p3s_top.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/p3s_top.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/p3s_top.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/p3s_top.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/p3s_top.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/p3s_top.png" style='width: 100%; ' /> </picture> </a> </div> <p> The articles in this mini-series are: </p> <ol> <li><a href='/av_studio/570-ableton-push-nvme.html'>Ableton Push 3 NVMe Replacement</a></li> <li><a href='/blog/2024/07/05/cooling.html'>Thermal Control</a></li> <li><a href='/blog/2024/06/11/virtualbox.html'>VirtualBox</a></li> <li><a href='/blog/2024/06/12/clonezilla.html'>Clonezilla</a></li> <li><a href='/blog/2024/06/12/supersize-fs.html'>Supersizing a Partition and Its File System</a></li> </ol> <!-- endregion --> <!-- #region Thermal Pads --> <h2 id="pads">Thermal Pads</h2> <!-- #region implicit --> <p> A thermal pad is made from <a href='https://en.wikipedia.org/wiki/Thermal_interface_material' target='_blank' rel="nofollow">thermal interface material</a>; its purpose in the article I wrote entitled <a href='/570-ableton-push-nvme.html'>Ableton Push 3 NVMe Replacement</a> is to thermally connect the chips on the NVMe drive to a metal heat sink. Thermal pads are often baby blue; however, as shown in the above photographs, the P3S uses a pink thermal pad between the CPU module and the bottom plate, and, as we saw at the beginning of this article, another pink thermal pad between the external heat sink and the bottom plate. </p> <div class='imgWrapper imgBlock center' style='width: 450px; '> <figure> <a href='https://www.amazon.com/gp/product/B07X82MNF2' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/thermal_pads.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/thermal_pads.cur" type="image/x-icon"> <img alt='Various thicknesses of thermal pads' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/thermal_pads.png" style='width: 100%; padding: 1em;' title='Various thicknesses of thermal pads' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.amazon.com/gp/product/B07X82MNF2" target='_blank' > Various thicknesses of thermal pads </a> </figcaption> </figure> </div> <p> Depending on the quality and conditions of use, <a href='https://www.aok-technologies.com/news/did-the-thermal-pad-paste-degrade-to-a-level-that-is-less-efficient.html' target='_blank' rel="nofollow">a thermal pad can last anywhere from 5 to 10 years</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/push_open_8.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/push_open_8.cur" type="image/x-icon"> <img alt='The pink thermal pad cools the P3S CPU module' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/push_open_8.png" style='width: 100%; ' title='The pink thermal pad cools the P3S CPU module' /> </picture> <figcaption class='imgFigCaption '> The pink thermal pad cools the P3S CPU module </figcaption> </figure> </div> <!-- endregion --> <!-- #region Folklore vs. Provable Facts --> <h2 id="fvpf">Folklore vs. Provable Facts</h2> <p> Some manufacturers state that thermal pads should not ever be reused, citing the introduction of air gaps and bubbles. Apparently reusing a thermal pad would cause the thermal pads to act more as an insulator than as a conductor. I do not have data that would indicate how well-founded the statement is. Someone should test this; the information source obviously has a profit motive. </p> <p> Also from two manufacturers: When as thermal pad is removed, the surface should be cleaned with alcohol wipes and a microfiber cloth to ensure no traces are left behind. Again, I would like to see data on this. </p> <!-- endregion --> <!-- endregion --> <!-- #region Thermal Conductivity --> <h2 id="tc">Thermal Conductivity</h2> <!-- #region implicit --> <p> Heat transfer only occurs 3 ways: via conduction, convection, and radiation. Of the three, conduction is by far the most effective. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.noaa.gov/jetstream/atmosphere/transfer-of-heat-energy' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/conv_rad_cond.cur" type="image/x-icon"> <img alt='NOAA: The Transfer of Heat Energy' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/conv_rad_cond.png" style='width: 100%; ' title='NOAA: The Transfer of Heat Energy' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.noaa.gov/jetstream/atmosphere/transfer-of-heat-energy" target='_blank' > NOAA: The Transfer of Heat Energy </a> </figcaption> </figure> </div> <table class='noborder table'> <tr> <th>Mode</th> <th>Transfer Mechanism</th> <th>Material</th> </tr> <tr> <th>Conduction</th> <td>Diffusion of electrons and <a href='https://en.wikipedia.org/wiki/Phonon' target='_blank' rel="nofollow">phonon</a> vibrations</td> <td>Solids</td> </tr> <tr> <th>Convection</th> <td>Density differences cause currents</td> <td>Gases and liquids</td> </tr> <tr> <th>Radiation</th> <td>Electromagnetic radiation</td> <td>Non-opaque mediums</td> </tr> </table> <p> This article focuses on conduction, because it is the dominant cooling mode for the P3S. </p> <p> The <i>thermal conductivity</i> of a material characterizes its ability to transfer heat. For solids, thermal conductivity is due to the flow of free electrons, rather like how electricity is conducted. Materials with high thermal conductivity transfer heat faster than materials with low thermal conductivity. The thermal conductivity tells you the rate at which thermal energy is transported across a unit length of a material that has a unit temperature difference applied across that length. </p> <p> Thermal conductivity in an electrical device depends upon: </p> <ul> <li>Density of material</li> <li>Pressure</li> <li>Temperature</li> <li>Temperature differential</li> <li>Material structure</li> </ul> <p> In the <a href='https://en.wikipedia.org/wiki/International_System_of_Units' target='_blank' rel="nofollow">SI unit system</a>, thermal conductivity is measured in watts per meter per kelvin (W/mK). This defines the number of watts conducted per meter thickness of the material, per degree of temperature difference between the two surfaces. </p> <p> The following table is ordered by increasing thermal conductivity, with thermal insulators at the top and the best thermal conductors at the bottom. </p> <style> #table1 td:nth-child(3) { text-align: center; } </style> <table id='table1' class='table'> <tr> <th>Material</th> <th>Comment</th> <th>Conductivity</th> </tr> <tr> <th>Air</th> <td> Effectively a thermal insulator. </td> <td>0.026 W/mK</td> </tr> <tr> <th>Powder coating</th> <td> Used on computer chassis, and on the P3S bottom plate. Effectively a thermal insulator. </td> <td>0.174 W/mK</td> </tr> <tr> <th>Anodized aluminum coating</th> <td> Used to coat the P3S heat sink underneath the P3S bottom plate. Anodizing means adding a layer of <a href='https://www.britannica.com/science/alumina' target='_blank' rel="nofollow">alumina</a> to the aluminum surface. This thin layer of oxide is effectively a thermal insulator. Anodized aluminum has a higher surface area than unfinished aluminum and, therefore, radiates more infrared energy; however, radiation is orders of magnitude less effective than conduction for cooling. </td> <td>0.53-1.62 W/mK</td> </tr> <tr> <th>Marble</th> <td> Used for kitchen table tops. </td> <td>2.8 W/mK</td> </tr> <tr> <th>Thermal paste</th> <td> Commonly used between a CPU and its heatsink. Shelf life is 3 to 5 years, durability (working life) is 5 to 8 years. </td> <td>5 &mdash; 8 W/mK</td> </tr> <tr> <th>Thermal pads</th> <td>Easily replaced</td> <td> <p style="text-align: center; "> 3 &mdash; 16 W/mK </p> <p> The blue thermal silica gel pads that I purchased were rated at 6 W/mK. </p> <p> <a href='https://www.tglobaltechnology.com/product/tg-a1450-ultra-soft-thermal-pad/' target='_blank' rel="nofollow">The pink thermal pads of the P3S might be rated at 14.5 W/mK.</a> </p> </td> </tr> <tr> <th>Low-carbon steel</th> <td> Sheet metal, used for computer chassis, which might include the P3S bottom plate. Surface treatment, such as anodizing, generally reduces this value. </td> <td>34-54 W/mK</td> </tr> <tr> <th>Liquid metal paste (<a href='https://www.thermal-grizzly.com/en/conductonaut/s-tg-c-001-r' target='_blank' rel="nofollow">Conductonaut</a>)</th> <td>Can be <a href='https://www.reddit.com/r/overclocking/comments/100vcgm/liquid_metal_thermal_compound_2_years_after/' target='_blank' rel="nofollow">highly dangerous</a> for electronics and aluminum parts.</td> <td>73 W/mK</td> </tr> <tr> <th>Solid aluminum</th> <td> Often used in heat sinks, for example underneath the P3S bottom plate. However, the P3S exterior heat sink is anodized, which greatly reduces its thermal conductivity. </td> <td>150-205 W/mK</td> </tr> <tr> <th>Solid copper</th> <td>Used on motherboards</td> <td>357-413 W/mK</td> </tr> <tr style="border: red thick solid"> <th>Heat pipe</th> <td>Used in a wide variety of electronics, including laptops and mobile devices</td> <td><span class='bg_yellow'>4,000 to 100,000 W/mK</span></td> </tr> </table> <p class="alert rounded shadow"> Notice the huge disparity between the thermal conductivity of thermal pads, the metals and the coatings that the bottom of the P3S is constructed from. This tells us that although thermal pads are much better than simply relying on convection or radiation, thermal pads and the coatings greatly restrict the transfer of heat generated by the P3S electronics. </p> <p> If a heat pipe was used effectively, the overheating problems of the P3S would completely go away. Furthermore, the heat pipe would allow more powerful and faster electrical components could be used. </p> <!-- #region Heat Pipes --> <h3 id="pipes">Heat Pipes</h3> <div class='imgWrapper imgBlock inline' style='width: 100%; '> <figure> <a href='https://commons.wikimedia.org/w/index.php?curid=13144188' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.cur" type="image/x-icon"> <img alt='Heat pipes in a laptop<br>Photo by Kristoferb at English Wikipedia, CC BY-SA 3.0' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/Laptop_Heat_Pipe.png" style='width: 100%; ' title='Heat pipes in a laptop<br>Photo by Kristoferb at English Wikipedia, CC BY-SA 3.0' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://commons.wikimedia.org/w/index.php?curid=13144188" target='_blank' > Heat pipes in a laptop<br>Photo by Kristoferb at English Wikipedia, CC BY-SA 3.0 </a> </figcaption> </figure> </div> <p> Thermal pads, fans and <a href='https://en.wikipedia.org/wiki/Heat_pipe' target='_blank' rel="nofollow">heat pipes</a> are commonly used in laptops for dissipating heat. Heat pipes do not require power, are very light, and provide extremely high <i>heat flux</i> &mdash; from 10x to 200x more than pure copper. <a href='https://en.wikipedia.org/wiki/Heat_flux' target='_blank' rel="nofollow">Heat flux</a> (also known as thermal flux, heat flux density, heat flow density and heat flow rate intensity) is the amount of heat energy passing through a surface, measured as the flow of energy per unit of area per unit of time. It is a vector quantity. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.arquimea.com/products/heat-pipes-satellite-space-applications/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/heat-pipes.cur" type="image/x-icon"> <img alt='Heat pipes for thermally controlling a satellite' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/heat-pipes.png" style='width: 100%; ' title='Heat pipes for thermally controlling a satellite' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.arquimea.com/products/heat-pipes-satellite-space-applications/" target='_blank' > Heat pipes for thermally controlling a satellite </a> </figcaption> </figure> </div> <p> The following video by James Orgill of <a href='https://www.youtube.com/@TheActionLab' target='_blank' rel="nofollow">The Action Lab</a> shows that heat pipes transfer heat energy dramatically faster than solid materials. </p> <a href="https://www.youtube.com/shorts/ZEGazGMEhtw"> <video controls class="rounded shadow" src="/av_studio/images/ableton/push/cooling/The_Fastest_Heat_Conductor.mp4" style="display: block; margin: 0 auto 2em auto;"></video> </a> <p> This video explains how heat pipesmic work in detail: </p> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/51bwzEO8XCw" frameborder="0" allowfullscreen></iframe></div> <p> Perhaps Ableton might look at using a heat pipe in a future version of the P3S that features a more powerful CPU and larger NVMe drive. </p> <!-- endregion --> <!-- endregion --> <!-- #region Heat Sinks --> <h2>Heat Sinks</h2> <p> Heat pipes can be combined with other components for heat transfer, such as <a href='https://www.chtechnology.com/heat-sinks/extruded-heat-sinks.html' target='_blank' rel="nofollow">extruded heat sinks</a>, <a href='https://www.famosheatsink.com/news/die-cast-heat-sinks-vs-extruded-heat-sinks/' target='_blank' rel="nofollow">die-cast heat sinks</a>, <a href='https://www.lorithermal.com/stacked-fin-heat-sink' target='_blank' rel="nofollow">fin-stack heat sinks</a>, and <a href='https://myheatsinks.com/custom/skived-heat-sinks/' target='_blank' rel="nofollow">skived heat sinks</a>. Heat transfer materials thermally bond the heat pipes to the heat sinks. This creates a thermal management system that can be individually tuned for each application. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.lorithermal.com/stacked-fin-heat-sink' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/stacked_fin.cur" type="image/x-icon"> <img alt='Stacked fin heat sinks offer better performance than extrusion heat sinks, due to thinner fin profiles leading to higher surface area and lower pressure drop.' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/stacked_fin.png" style='width: 100%; ' title='Stacked fin heat sinks offer better performance than extrusion heat sinks, due to thinner fin profiles leading to higher surface area and lower pressure drop.' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.lorithermal.com/stacked-fin-heat-sink" target='_blank' > Stacked fin heat sinks offer better performance than extrusion heat sinks, due to thinner fin profiles leading to higher surface area and lower pressure drop. </a> </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://myheatsinks.com/custom/skived-heat-sinks/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/skived_fin.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/skived_fin.cur" type="image/x-icon"> <img alt='Skived heat sinks provide the best possible thermal conductivity between the fins and the base' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/skived_fin.png" style='width: 100%; ' title='Skived heat sinks provide the best possible thermal conductivity between the fins and the base' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://myheatsinks.com/custom/skived-heat-sinks/" target='_blank' > Skived heat sinks provide the best possible thermal conductivity between the fins and the base </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region Vapor Chambers --> <h2 id="vc">Vapor Chambers</h2> <p> <a href='https://en.wikipedia.org/wiki/Heat_pipe#Vapor_chamber' target='_blank' rel="nofollow">Vapor chambers</a>, also known as planar heat pipes, are relatively new. Tubular heat pipes act as heat dissipaters, while planar heat pipes act as heat spreaders. </p> <div class='imgWrapper imgBlock center' style='width: 50%; '> <figure> <a href='https://www.1-act.com/thermal-solutions/passive/heat-pipes/vapor-chambers/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/cooling/vapor-chamber.cur" type="image/x-icon"> <img alt='Vapor chamber assembly' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/cooling/vapor-chamber.png" style='width: 100%; padding: 1em;' title='Vapor chamber assembly' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.1-act.com/thermal-solutions/passive/heat-pipes/vapor-chambers/" target='_blank' > Vapor chamber assembly </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region Aftermarket Upgrade --> <h3 id="au">Aftermarket Upgrade</h3> <p> I can imagine an aftermarket upgrade that would increase the height of a PS3 slightly, uses a heat pipe or vapor chamber for cooling, and has a variable-speed fan. This would support the biggest, fastest and hottest NVMe drive you can afford. I wonder if it might be possible to overclock the CPU if it was properly cooled? </p> <!-- endregion --> <!-- endregion --> <!-- #region Sources --> <h3>Sources</h3> <ul> <li><a href='https://www.engineeringtoolbox.com/thermal-conductivity-metals-d_858.html' target='_blank' rel="nofollow">Metals, Metallic Elements and Alloys - Thermal Conductivities</a></li> <li><a href='https://www.techpowerup.com/forums/threads/kryonaut-12-w-mk-vs-conductonaut-liquid-metal-73-w-mk.229167/' target='_blank' rel="nofollow">Kryonaut vs Conductonaut (Liquid Metal) </a></li> <li><a href='https://www.powerelectronicsnews.com/thermal-paste-is-it-really-necessary' target='_blank' rel="nofollow">Is Thermal Paste Necessary?</a></li> <li><a href='https://www.sciencedirect.com/science/article/abs/pii/S0254058413004409' target='_blank' rel="nofollow">Thermal conductivity of anodized aluminum oxide layer: The effect of electrolyte and temperature</a></li> </ul> <p> The University of Cambridge has an online course which goes into the physics of conductivity: <a href='https://www.doitpoms.ac.uk/tlplib/thermal_electrical/printall.ph' target='_blank' rel="nofollow">Introduction to thermal and electrical conductivity</a>. </p> <!-- endregion --> <!-- endregion --> Upgrading Modular PSUs 2024-06-28T00:00:00-04:00 https://mslinn.github.io/blog/2024/06/28/upgrade_psu <!-- #region intro --> <p> This article discusses modular PSUs conforming to the family of standards for power supplies designed for <a href='https://en.wikipedia.org/wiki/ATX#ATX12V_2.x' target='_blank' rel="nofollow">ATX form factor motherboards</a>, specifically, the ATX 2.x and ATX 3.x PSU specifications. These two major versions of the ATX PSU specification are collectively called the ATX12V specifications. ATX12V PSUs are the most common types of PSU today, having evolved from the original ATX PSU specification. </p> <p> Some of the information in this article applies generally to all modular PSUs, in particular the <a href='#summary'>summary</a>. </p> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/psu/psu.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/psu.avif" type="image/avif">--> <source srcset="/blog/images/psu/psu.webp" type="image/webp"> <source srcset="/blog/images/psu/psu.apng" type="image/apng"> <source srcset="/blog/images/psu/psu.png" type="image/png"> <source srcset="/blog/images/psu/psu.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/psu.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/psu.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/psu.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/psu.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/psu.gif" type="image/gif"> <source srcset="/blog/images/psu/psu.tif" type="image/tiff"> <source srcset="/blog/images/psu/psu.tiff" type="image/tiff"> <source srcset="/blog/images/psu/psu.bmp" type="image/bmp"> <source srcset="/blog/images/psu/psu.ico" type="image/x-icon"> <source srcset="/blog/images/psu/psu.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/psu/psu.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region ATX12V Standard --> <h2 id="atx12v">ATX12V Standard</h2> <p> The ATX12V standard is upwards compatible. This means you can install a PSU that conforms to a more recent version of the standard in an older motherboard that only supports an older version of the standard. For more information, please see the <a href='https://xdevs.com/doc/Standards/ATX/ATX12V_Power_Supply_Design_Guide_Rev1.1.pdf' target='_blank' rel="nofollow">ATX / ATX12V Power Supply Design Guide</a>. </p> <!-- #region ATX 2.x PSUs --> <h3 id="atx3">ATX 2.x PSUs</h3> <p> The current version of the ATX12V standard, v2.53, was released in December 2021. The most recent version of the standard that I could find a PDF of is <a href='https://cdn.instructables.com/ORIG/FS8/5ILB/GU59Z1AT/FS85ILBGU59Z1AT.pdf' target='_blank' rel="nofollow">ATX Specificaiton Version 2.2</a>, published in 2004. </p> <p> This is the <a href='https://www.lifewire.com/atx-24-pin-12v-power-supply-pinout-2624578' target='_blank' rel="nofollow">ATX 2.2</a> 24-pin motherboard power connector pinout: </p> <div class='imgWrapper imgBlock center' style='width: 65%; '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/psu/atx_2_pinout.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/atx_2_pinout.avif" type="image/avif">--> <source srcset="/blog/images/psu/atx_2_pinout.webp" type="image/webp"> <source srcset="/blog/images/psu/atx_2_pinout.apng" type="image/apng"> <source srcset="/blog/images/psu/atx_2_pinout.png" type="image/png"> <source srcset="/blog/images/psu/atx_2_pinout.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_pinout.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_pinout.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_pinout.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_pinout.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_pinout.gif" type="image/gif"> <source srcset="/blog/images/psu/atx_2_pinout.tif" type="image/tiff"> <source srcset="/blog/images/psu/atx_2_pinout.tiff" type="image/tiff"> <source srcset="/blog/images/psu/atx_2_pinout.bmp" type="image/bmp"> <source srcset="/blog/images/psu/atx_2_pinout.ico" type="image/x-icon"> <source srcset="/blog/images/psu/atx_2_pinout.cur" type="image/x-icon"> <img alt='ATX 2.2 24-Pin Connector Pinout Diagram<br>Pin numbering is clearly shown' class="imgImg rounded shadow" src="/blog/images/psu/atx_2_pinout.png" style='width: 100%; padding: 1em;' title='ATX 2.2 24-Pin Connector Pinout Diagram<br>Pin numbering is clearly shown' /> </picture> <figcaption class='imgFigCaption '> ATX 2.2 24-Pin Connector Pinout Diagram<br>Pin numbering is clearly shown </figcaption> </figure> </div> <p> The above diagram shows that pins are numbered in columns from left to right. pin 1 is at the top left, and pin 24 (the highest-numbered pin) is at the bottom right. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <a href='https://cdn.instructables.com/ORIG/FS8/5ILB/GU59Z1AT/FS85ILBGU59Z1AT.pdf' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/psu/atx_2_2_pinout.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/atx_2_2_pinout.avif" type="image/avif">--> <source srcset="/blog/images/psu/atx_2_2_pinout.webp" type="image/webp"> <source srcset="/blog/images/psu/atx_2_2_pinout.apng" type="image/apng"> <source srcset="/blog/images/psu/atx_2_2_pinout.png" type="image/png"> <source srcset="/blog/images/psu/atx_2_2_pinout.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_2_pinout.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_2_pinout.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_2_pinout.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_2_pinout.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/atx_2_2_pinout.gif" type="image/gif"> <source srcset="/blog/images/psu/atx_2_2_pinout.tif" type="image/tiff"> <source srcset="/blog/images/psu/atx_2_2_pinout.tiff" type="image/tiff"> <source srcset="/blog/images/psu/atx_2_2_pinout.bmp" type="image/bmp"> <source srcset="/blog/images/psu/atx_2_2_pinout.ico" type="image/x-icon"> <source srcset="/blog/images/psu/atx_2_2_pinout.cur" type="image/x-icon"> <img alt='All ATX 2.2 Cable Pinout Diagrams<br>See previous diagram for full pinout numbering' class="imgImg rounded shadow" src="/blog/images/psu/atx_2_2_pinout.png" style='width: 100%; ' title='All ATX 2.2 Cable Pinout Diagrams<br>See previous diagram for full pinout numbering' /> </picture> </a> <figcaption class='imgFigCaption fullsize'> <a href="https://cdn.instructables.com/ORIG/FS8/5ILB/GU59Z1AT/FS85ILBGU59Z1AT.pdf" target='_blank' > All ATX 2.2 Cable Pinout Diagrams<br>See previous diagram for full pinout numbering </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region ATX 3.x PSUs --> <h3 id="atx3">ATX 3.x PSUs</h3> <p> ATX 3.x is upwards compatible with ATX 2.x, which means that an ATX 2.x tester can determine if an ATX 3.x PSU works, except for being able to test either of the new high-power PCIe 5.0 connectors for top-tier GPUs. </p> <p> ATX 3.0 introduced the 12VHPWR connector, which was problematic and was replaced by the new 12V-2×6 connector for ATX 3.1. To learn more about ATX 3.0, see <a href='https://www.xda-developers.com/what-is-atx-30-and-why-you-need-to-know-before-your-next-pc-upgrade' target='_blank' rel="nofollow">What is ATX 3.0 and why you need to know before your next PC upgrade.</a> </p> <p> Anandtech has an excellent <a href='https://www.anandtech.com/show/21367/the-xpg-core-reactor-ii-ve-850w-atx-31-psu-review/2' target='_blank' rel="nofollow">description of ATX 3.1</a> that explains why the 12VHPWR connector introduced by ATX 3.0 was a failure and discusses the 12V-2×6 connector introduced by ATX 3.1 to replace the ATX 3.0 12VHPWR connector. </p> <p class="alert rounded shadow"> I would not install an ATX 3.x PSU unless the computer had an NVIDIA 4090 because this PSU standard is still bleeding edge. </p> <!-- endregion --> <!-- endregion --> <!-- #region Modular PSUs --> <h2 id="mpsu">Modular PSUs</h2> <p> I recently encountered a bad batch of computer power supplies (PSUs). This article specifically discusses replacing / upgrading modular PSUs. Most of the issues raised do not arise unless the PSU is at least partially modular. </p> <p class="alert rounded shadow"> Non-modular PSUs lack one significant source of failure: only the cables provided with the PSU can be used because they are permanently attached. </p> <!-- endregion --> <!-- #region Cables --> <h2 id="cables">Cables</h2> <p> Modular power supplies have removable cables; you only connect the cables that you need. This makes building computers easier and improves airflow within a computer. </p> <div class='imgWrapper imgFlex center' style='width: 75%; '> <picture class='imgPicture'> <source srcset="/blog/images/psu/cabling.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/cabling.avif" type="image/avif">--> <source srcset="/blog/images/psu/cabling.webp" type="image/webp"> <source srcset="/blog/images/psu/cabling.apng" type="image/apng"> <source srcset="/blog/images/psu/cabling.png" type="image/png"> <source srcset="/blog/images/psu/cabling.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/cabling.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/cabling.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/cabling.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/cabling.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/cabling.gif" type="image/gif"> <source srcset="/blog/images/psu/cabling.tif" type="image/tiff"> <source srcset="/blog/images/psu/cabling.tiff" type="image/tiff"> <source srcset="/blog/images/psu/cabling.bmp" type="image/bmp"> <source srcset="/blog/images/psu/cabling.ico" type="image/x-icon"> <source srcset="/blog/images/psu/cabling.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/psu/cabling.png" style='width: 100%; ' /> </picture> </div> <p> If you attach a cable from a modular PSU to a modular PSU of a different model, then some or all of the electronics in your computer may be fried. The actual results depend on many variables. </p> <div class="pullQuote"> Replace old PSU cables with the cables that come with the replacement PSU </div> <p> You might want to get in the habit of marking each end of every cable with nail polish, and also paint a dab on a small portion of the PSU label as well. Using a unique color of nail polish for every different model of PSU that you own will make it easy to recognize the cables that came with each model of PSU that you have. Nail polish will not stick to the plastic used in some connectors, so if that is the case, place the dabs of nail polish on the wire sleeves, if present, or on the wires themselves. </p> <!-- endregion --> <!-- #region Four Special Wires --> <h3 id="four">Four Special Wires</h3> <p> Four of the wires in 24-pin ATX v2 and v3 cables have special functions. It is important to test new PSUs before you install them, to verify that the signals in these wires are functioning properly. This section describes the signals to be tested. The next section describes how to test them. </p> <ul> <li> <b>PS_ON</b> (power on) on pin 16 is a signal originating from the motherboard to the power supply. This pin&rsquo;s voltage is internally pulled up to +5 V inside the power supply. The PSU will attempt turn on after the voltage on this pin is pulled low by being connected to ground. </li> <li> <b>PWR_OK</b> (power good) on pin 8 is an signal from the power supply that indicates that the +5VDC and +3.3VDC voltages provided by the PSU&rsquo;s 24-pin connector have stabilized. The motherboard will not attempt to power up unless this signal is provided. Normally the singal remains low for a brief time (100 to 500 milliseconds) after the <b>PS_ON</b> signal is pulled low; however, the signal will only go high after the PSU ascertains that its +5VDC and +3.3VDC outputs are stable and within normal operating parameters. </li> <li> <b>+5 V<sub>SB</sub></b> (+5 V standby) on pin 9 supplies power even when the rest of the supply wire lines are off. This can be used to power the circuitry that controls the power-on signal. </li> <li> <b>+3.3 V sense</b> on pin 13 allows the PSU to sense the voltage drop in the PSU wiring. </li> </ul> <!-- endregion --> <!-- #region Use a PSU Tester --> <h2 id="utpt">Use a PSU Tester</h2> <div class='imgWrapper imgBlock center' style='width: 75%; '> <figure> <a href='https://www.amazon.com/Computer-PC-Tester-Connectors-Enclosure/dp/B076CLNPPK' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/psu/psu_tester.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/psu_tester.avif" type="image/avif">--> <source srcset="/blog/images/psu/psu_tester.webp" type="image/webp"> <source srcset="/blog/images/psu/psu_tester.apng" type="image/apng"> <source srcset="/blog/images/psu/psu_tester.png" type="image/png"> <source srcset="/blog/images/psu/psu_tester.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/psu_tester.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/psu_tester.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/psu_tester.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/psu_tester.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/psu_tester.gif" type="image/gif"> <source srcset="/blog/images/psu/psu_tester.tif" type="image/tiff"> <source srcset="/blog/images/psu/psu_tester.tiff" type="image/tiff"> <source srcset="/blog/images/psu/psu_tester.bmp" type="image/bmp"> <source srcset="/blog/images/psu/psu_tester.ico" type="image/x-icon"> <source srcset="/blog/images/psu/psu_tester.cur" type="image/x-icon"> <img alt='EVGA uses this tester to test their PSUs' class="imgImg " src="/blog/images/psu/psu_tester.png" style='width: 100%; ' title='EVGA uses this tester to test their PSUs' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.amazon.com/Computer-PC-Tester-Connectors-Enclosure/dp/B076CLNPPK" target='_blank' > EVGA uses this tester to test their PSUs </a> </figcaption> </figure> </div> <p class="alert center rounded shadow"> Always use a <a href='https://www.amazon.com/Computer-PC-Tester-Connectors-Enclosure/dp/B076CLNPPK' target='_blank' rel="nofollow">PSU tester</a> to test a new power supply before you install it. </p> <p> Most PSU testers have no battery and are completely powered by the PSU's 20-pin or 24-pin interface. If no power is provided by the 20-pin or 24-pin interface, the tester will not function. </p> <p> To use the PSU tester shown above, plug the 24-pin cable between the PSU and the right-hand-side connector on the tester. Now turn on the PSU. You should observe: </p> <ul> <li> The tester should not start beeping. </li> <li> <b>PG</b> stands for <i>Power Good</i>, also known as &ldquo;Power OK&rdquo;. As previously mentioned, the motherboard will not attempt to power up unless this signal is provided. The LCD indicates how many milliseconds it takes to receive the <b>Power Good</b> signal from the PSU. If it reads 0, your PSU is <i>supposedly</i> non-functional. <br> <div class='quote' style='margin-top: 1em;'> <div class='quoteText clearfix'> PG was inherited from the original IBM PC in the early 1980s. The PC&rsquo;s PSU had a PG signal because of the RAM chips it used and the specific internal design of the PSU. The PSU&rsquo;s design was such that the -5V rail wasn&rsquo;t stable immediately, and this in turn meant that the RAM wasn&rsquo;t 100% reliable at initial power on, so if the mobo tried to boot, it might crash due to memory corruption. PG was designed to make the mobo wait (in hardware) a few hundreds of milliseconds before booting so that the system would be stable. <br><br> 0ms would indicate that the PS is telling the mainboard that power is good from the get-go when in fact it may not be. <br><br> Even today there is a small window of opportunity at the instant of switch on (around 50-100 ms) where supply voltages aren&rsquo;t stable. If the system tries to begin POST during this time it may crash, so PG is used to generate a hardware delay so that booting doesn&rsquo;t start until the voltages are stable. A 0ms PG means that the PG signal is probably <a href='https://whirlpool.net.au/wiki/power_good_signal' target='_blank' rel="nofollow">faked</a>. <br><br> <span class="bg_yellow">Modern motherboards have largely done away with the need for hardware PG</span> (they either simply don't require a delay, or implement the delay by other means), although it is still part of the <a href='https://forums.whirlpool.net.au/go?ftp%3A%2F%2Fdownload.intel.com%2Fdesign%2Fmotherbd%2Fatx_201.pdf' target='_blank' rel="nofollow">ATX specs, page 21</a>. <br><br> A faked PG signal says nothing about the actual functionality of the PSU, and simply cannot be used as a judgment of the functionality of the supply, although the use of a faked PG design can imply that the PSU is of an inferior design quality. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://forums.whirlpool.net.au/archive/715799' rel='nofollow' target='_blank'>@Merlin on Whirlpool 2007-Apr-12</a></div> </div> </li> <li> The text under the <b>+12V2</b> heading will flash <b>LL</b> when no PCI-E device is connected. Its value is only relevant when a PCI-E connector (4-pin, 6-pin or 8-pin) is used by an IDE (HDD) or a SATA drive, or a floppy disk. </li> </ul> <!-- endregion --> <!-- #region Replace The Cabling When Replacing a Power Supply --> <h2 id="rp">Replace The Cabling When Replacing a Power Supply</h2> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/psu/warning-auto.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/warning-auto.avif" type="image/avif">--> <source srcset="/blog/images/psu/warning-auto.webp" type="image/webp"> <source srcset="/blog/images/psu/warning-auto.apng" type="image/apng"> <source srcset="/blog/images/psu/warning-auto.png" type="image/png"> <source srcset="/blog/images/psu/warning-auto.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/warning-auto.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/warning-auto.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/warning-auto.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/warning-auto.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/warning-auto.gif" type="image/gif"> <source srcset="/blog/images/psu/warning-auto.tif" type="image/tiff"> <source srcset="/blog/images/psu/warning-auto.tiff" type="image/tiff"> <source srcset="/blog/images/psu/warning-auto.bmp" type="image/bmp"> <source srcset="/blog/images/psu/warning-auto.ico" type="image/x-icon"> <source srcset="/blog/images/psu/warning-auto.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/psu/warning-auto.png" style='width: 100%; ' /> </picture> </div> <p class="alert rounded shadow"> When replacing a power supply with another model or brand, <i>even if it seems almost identical</i>, <b>also replace the power cabling</b>. Seemingly trivial differences between PSU models can cause the new power supply to fail if an old power supply's cabling is used, and the components it is electrically connected to might be damaged. </p> <span style='font-size: 3em; float: left; margin-right: 5px;;'>&#x1F4A5;</span> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F4A5;</span> <p> In other words, unless you are really, totally, absolutely 100% sure that the cables are in every way identical, replace them when you replace the PSU. Leaving the previous cabling in place and just swapping the power supply can fry the power supply, the motherboard, and more. </p><!-- endregion --> <!-- #region Corsair --> <h2 id="corsair">Corsair</h2> <!-- #region implicit --> <p> I have been purchasing PSUs for the computers that I build since the early 1980s. Some of those PSUs were made by Corsair. </p> <p> Following the lead of other vendors, <a href='https://www.corsair.com/c/psu' target='_blank' rel="nofollow">Corsair</a> introduced its first fully modular PSU in 2012 for computer enthusiasts to build their own computers. </p> <p> Technical support is not provided, citing &lsquo;legal liability issues&rsquo;. I call bullshit. </p> <div class='imgWrapper imgBlock center' style='width: 75%; '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/psu/elephant_shit.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/elephant_shit.avif" type="image/avif">--> <source srcset="/blog/images/psu/elephant_shit.webp" type="image/webp"> <source srcset="/blog/images/psu/elephant_shit.apng" type="image/apng"> <source srcset="/blog/images/psu/elephant_shit.png" type="image/png"> <source srcset="/blog/images/psu/elephant_shit.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/elephant_shit.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/elephant_shit.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/elephant_shit.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/elephant_shit.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/elephant_shit.gif" type="image/gif"> <source srcset="/blog/images/psu/elephant_shit.tif" type="image/tiff"> <source srcset="/blog/images/psu/elephant_shit.tiff" type="image/tiff"> <source srcset="/blog/images/psu/elephant_shit.bmp" type="image/bmp"> <source srcset="/blog/images/psu/elephant_shit.ico" type="image/x-icon"> <source srcset="/blog/images/psu/elephant_shit.cur" type="image/x-icon"> <img alt='&lsquo;Legal liability issues&rsquo;' class="imgImg rounded shadow" src="/blog/images/psu/elephant_shit.png" style='width: 100%; ' title='&lsquo;Legal liability issues&rsquo;' /> </picture> <figcaption class='imgFigCaption '> &lsquo;Legal liability issues&rsquo; </figcaption> </figure> </div> <p> Technical information for specific products is not organized properly and contains inaccurate compatibility statements. Providing incomplete information seems to be a goal, perhaps again due to paranoia about legal liability. I attribute the inaccuracies to sloppiness. </p> <p> Corsair's PSU products are described as mysterious black boxes, with stern warnings of fatal hazards if opened. </p> <!-- endregion --> <!-- #region Connector Labels --> <h3 id="labels">Connector Labels</h3> <p> Corsair has been inconsistent about how it has labeled the modular connectors over the years. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/psu/connectors.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/connectors.avif" type="image/avif">--> <source srcset="/blog/images/psu/connectors.webp" type="image/webp"> <source srcset="/blog/images/psu/connectors.apng" type="image/apng"> <source srcset="/blog/images/psu/connectors.png" type="image/png"> <source srcset="/blog/images/psu/connectors.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/connectors.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/connectors.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/connectors.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/connectors.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/connectors.gif" type="image/gif"> <source srcset="/blog/images/psu/connectors.tif" type="image/tiff"> <source srcset="/blog/images/psu/connectors.tiff" type="image/tiff"> <source srcset="/blog/images/psu/connectors.bmp" type="image/bmp"> <source srcset="/blog/images/psu/connectors.ico" type="image/x-icon"> <source srcset="/blog/images/psu/connectors.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/psu/connectors.png" style='width: 100%; ' /> </picture> </div> <p> In the above photograph, we see: </p> <ul> <li> Three connectors were clearly labeled with the PSUs that they were compatible with (&ldquo;TXM/HX Series only&rdquo;, &ldquo;760/860AX ONLY&rdquo;, &ldquo;Type4&rdquo;) </li> <li>One connector had a label that only had meaning if you knew the PSU it was for (&ldquo;CD08&rdquo;)</li> <li> One connector labels its purpose but does not provide any clue about the PSU that it was designed for (&ldquo;PCI-E&rdquo;). As we shall see, not all PCI-E cables are created equal. </li> </ul> <p> Not shown are connectors without labels. </p> <p> The inconsistent and unhelpful labeling continues today. </p> <!-- endregion --> <!-- #region Inaccurate Compatibility Statements About Cabling Variants --> <h3 id="cv">Inaccurate Compatibility Statements About Cabling Variants</h3> <p> The main Corsair product page for PSUs says: </p> <div class='quote'> <div class='quoteText clearfix'> The only difference between Type 3 and Type 4 cables is the pinout of the 24-pin ATX cable; all other cables (SATA, PCIe, etc) are the same. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://www.corsair.com/ca/en/s/psu-cable-compatibility' rel='nofollow' target='_blank'>PSU Cable Compatibility Statement</a></div> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/psu/compat_statement.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/compat_statement.avif" type="image/avif">--> <source srcset="/blog/images/psu/compat_statement.webp" type="image/webp"> <source srcset="/blog/images/psu/compat_statement.apng" type="image/apng"> <source srcset="/blog/images/psu/compat_statement.png" type="image/png"> <source srcset="/blog/images/psu/compat_statement.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/compat_statement.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/compat_statement.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/compat_statement.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/compat_statement.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/compat_statement.gif" type="image/gif"> <source srcset="/blog/images/psu/compat_statement.tif" type="image/tiff"> <source srcset="/blog/images/psu/compat_statement.tiff" type="image/tiff"> <source srcset="/blog/images/psu/compat_statement.bmp" type="image/bmp"> <source srcset="/blog/images/psu/compat_statement.ico" type="image/x-icon"> <source srcset="/blog/images/psu/compat_statement.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/psu/compat_statement.png" style='width: 100%; ' /> </picture> </div> <p> However, that is incorrect. Corsair&rsquo;s blog states that the 24-pin cables have the same pin-outs, and that the PCIe and EPS12V cables are not the same. This is also incorrect, because the 24-pin cables have different pin-outs. </p> <div class='quote'> <div class='quoteText clearfix'> The Type 4 cables have the same pin-out as Type 3 cables, but include small, solid capacitors on the +12V, +5V and +3.3V leads on the 24-pin, PCIe and EPS12V cables. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://forum.corsair.com/blog/power-supply-units/explanation-of-rmi-new-type-4-cables/' rel='nofollow' target='_blank'>Explanation of RMi's New Type 4 Cables</a></div> </div> <p> In summary, I understand all of the above to mean that, except for the 24-pin ATX cable, type 4 cables are compatible with type 3 cables, but they provide cleaner power. </p> <!-- endregion --> <!-- #region Diagnostics --> <h3 id="diagnostics">Diagnostics</h3> <p> Little <a href='https://help.corsair.com/hc/en-us/articles/360025085372-PSU-How-to-test-a-PSU-Power-Supply-Unit' target='_blank' rel="nofollow">diagnostic information</a> is provided. "Use a paperclip," they say, and then they also mention that might not be very helpful. In general, paperclips have limited value as a diagnostic tool for debugging PSUs. Next, Corsair discusses how to use a PSU tester for an ATX 24-pin cable but does not talk about how to test the other cables. </p> <p> Corsair provides a table on the same page showing pin signal levels for an ATX 2 24-pin cable but neglects to indicate how pins are numbered. </p> <!-- endregion --> <!-- endregion --> <!-- #region Alternative Manufacturers --> <h2 id="alt">Alternative Manufacturers</h2> <p> A brief search yielded the following PSU manufacturers, all of whom provide product support: </p> <ul> <li><a href='https://www.coolermaster.com/en-global/catalog/power-supplies/' target='_blank' rel="nofollow">CoolerMaster</a></li> <li><a href='https://www.evga.com/products/productlist.aspx?type=10' target='_blank' rel="nofollow">EVGA</a></li> <li><a href='https://www.gigabyte.com/Power-Supply' target='_blank' rel="nofollow">Gigabyte</a></li> <li><a href='https://www.rosewill.com/collection/power-supplies' target='_blank' rel="nofollow">Rosewill</a></li> <li><a href='https://seasonic.com/' target='_blank' rel="nofollow">Seasonic</a></li> <li><a href='https://www.super-flower.com.tw/en' target='_blank' rel="nofollow">Super Flower</a></li> <li><a href='https://www.thermaltake.com/products/power-supply.html' target='_blank' rel="nofollow">ThermalTake</a></li> </ul> <!-- endregion --> <!-- #region EVGA SuperNOVA 1000 GT --> <h2 id="EVGA_SuperNOVA">EVGA SuperNOVA 1000 GT</h2> <!-- #region implicit --> <p> I moved away from the bad batch of Corsair PSUs and purchased an <a href='https://www.evga.com/products/product.aspx?pn=220-GT-1000-X1' target='_blank' rel="nofollow">EVGA SuperNOVA 1000 GT</a>. </p> <p> NewEgg now has a new AI-generated <a href='https://www.newegg.ca/evga-supernova-1000-gt-220-gt-1000-x1-1000w/p/N82E16817438221#IsFeedbackTab' target='_blank' rel="nofollow">customer recommendation summary</a> for their products, and those comments helped me make a purchasing decision. I recommend the NewEgg purchasing experience. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.newegg.ca/evga-supernova-1000-gt-220-gt-1000-x1-1000w/p/N82E16817438221#IsFeedbackTab' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.svg" type="image/svg"> <!---<source srcset="/blog/images/psu/evga_supernova_1000_gt_review.avif" type="image/avif">--> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.webp" type="image/webp"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.apng" type="image/apng"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.png" type="image/png"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.jpg" type="image/jpeg"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.jpeg" type="image/jpeg"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.jfif" type="image/jpeg"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.pjpeg" type="image/jpeg"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.pjp" type="image/jpeg"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.gif" type="image/gif"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.tif" type="image/tiff"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.tiff" type="image/tiff"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.bmp" type="image/bmp"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.ico" type="image/x-icon"> <source srcset="/blog/images/psu/evga_supernova_1000_gt_review.cur" type="image/x-icon"> <img alt='NewEgg Customer Review Summary' class="imgImg rounded shadow" src="/blog/images/psu/evga_supernova_1000_gt_review.png" style='width: 100%; ' title='NewEgg Customer Review Summary' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.newegg.ca/evga-supernova-1000-gt-220-gt-1000-x1-1000w/p/N82E16817438221#IsFeedbackTab" target='_blank' > NewEgg Customer Review Summary </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region Setup --> <h3 id="es_setup">Setup</h3> <p> I opened the EVGA SuperNOVA 1000 GT box and found a proper printed manual, in color. The manual showed a US phone number for support (888-881-3842) and an email address, <a href='https://support@evga.com'><code>support@evga.com</code></a>. </p> <p> The included 'tester' was just a jumper that could perform the same function as a paperclip. It was almost completely useless. I used my tester, <a href='#utpt'>shown above</a>. The test failed with PG=0. </p> <!-- endregion --> <!-- #region Communication Breakdown --> <h3 id="dn">Communication Breakdown</h3> <p> I called the EVGA tech support phone number on a Thursday at 4 PM ET (1 PM PT - California time), but the message said EVGA was closed until Monday at 9 AM PT. That was odd. I also called their other customer support phone number, (714) 528-4500, but got the same message. </p> <p> I emailed EVGA&rsquo;s support address, but the email bounced back with the following error. </p> <pre style="margin-bottom: 1em;">&lt;support@evga.com>: host d290004a.ess.barracudanetworks.com[209.222.82.255] said: 550 permanent failure for one or more recipients (support@evga.com:blocked) (in reply to end of DATA command)</pre> <p> I emailed their European support address, <a href='https://supportEU@evga'><code>supportEU@evga</code></a>, with the same error. </p> <p> I then attempted enrolled in the <a href='https://secure.evga.com/us/signup.asp' target='_blank' rel="nofollow">EVGA Member</a> online service. However, I could not log in due to several bizarre errors. </p> <p> The following Monday I was able to call EVGA. I left a message. </p> <p> A tech from EVGA called back the same day, then escalated to Chris Bencivenga, EVGA Operations Manager, who called the day after (Tuesday). Both of them were on top of things; they had read this article and done their homework. </p> <p> I had a half-hour conversation with the second tech on July 16, 2024. Chris said that EVGA PSUs should pass using the type of tester that I used without beeping or flashing LEDs. Chris also said that their PG values are typically 70-85 ms. Chris offered to exchange my unit via prepaid AIR shipping and customs duty. I thought that was pretty nice. </p> <p> It turned out that something about my email signature tripped EVGA&rsquo;s spam filters. Once I sent plain text emails, my transmissions to EVGA succeeded. </p> <p> I received the replacement PSU, and immediately tested it. The PSU failed with PG=0, just like the others. I made a short video that demonstrated the failure and sent it to Chris. Now I am awaiting a response. </p> <!-- endregion --> <!-- endregion --> <!-- #region Summary --> <h2 id="summary">Summary</h2> <p> Replace all cables attached to a PSU when upgrading it. </p> <p> I forgive Corsair's bad batch of product, it happens. I do not forgive the policy of not providing technical support. </p> <p> I will continue to update this article. </p> <!-- endregion --> Supersizing a Partition and Its File System 2024-06-12T00:00:00-04:00 https://mslinn.github.io/blog/2024/06/12/supersize-fs <!-- #region intro --> <p> This article discusses how to clone a Linux drive containing partitions with <code>ext4</code> file systems. If the original drive has the same storage capacity as the clone, your task is complete, and you do not need to read this article. </p> <h2 id="miniseries">Mini-Series</h2> <p> This article can be read standalone; however, is part of a mini-series that discusses how to increase the storage capacity and performance of the Ableton Push 3 Standalone (P3S). If you own a P3S, or are thinking of purchasing one, you might want to first <a href='/av_studio/570-ableton-push-standalone.html'>read my review</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='/av_studio/570-ableton-push-standalone.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/p3s_top.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/p3s_top.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/p3s_top.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/p3s_top.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/p3s_top.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/p3s_top.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/p3s_top.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/p3s_top.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/p3s_top.png" style='width: 100%; ' /> </picture> </a> </div> <p> The articles in this mini-series are: </p> <ol> <li><a href='/av_studio/570-ableton-push-nvme.html'>Ableton Push 3 NVMe Replacement</a></li> <li><a href='/blog/2024/07/05/cooling.html'>Thermal Control</a></li> <li><a href='/blog/2024/06/11/virtualbox.html'>VirtualBox</a></li> <li><a href='/blog/2024/06/12/clonezilla.html'>Clonezilla</a></li> <li><a href='/blog/2024/06/12/supersize-fs.html'>Supersizing a Partition and Its File System</a></li> </ol> <p> Because a Linux file system is being modified, Linux must be used to perform the modification. It does not matter if the Linux OS runs on a physical machine or in a virtualized machine, so long as the file system to be modified is accessible from Linux. I wrote this article using an Ubuntu instance running in VirtualBox, as described in <a href='/blog/2024/06/11/virtualbox.html'>VirtualBox Setup</a>. It does not matter which OS you use to run VirtualBox; Windows, Mac and Linux all work equally well in this regard. </p> <!-- endregion --> <!-- #region Two Scenarios --> <h2 id="ts">Two Scenarios</h2> <p> This article works through the following scenarios when working with a cloned Linux drive that has more storage capacity than the original drive: </p> <ol> <li> <a href='#modestly_enlarge'>Modestly enlarge a partition</a> containing an <code>ext4</code> file system. </li> <li> <a href='#supersize'>Supersize a partition</a> containing a <code>ext4</code> file system by recreating it such that the resulting file system is much larger than the original. </li> </ol> <p> But first, some important technical details. </p> <!-- endregion --> <!-- #region Enlarging a File System --> <h2 id="eafs">Enlarging a File System</h2> <p> After testing to verify that a drive has been successfully cloned to a larger storage device, at least one of the partitions on the newly cloned NVMe drive must be enlarged, or the extra storage capacity of the larger drive will not be used. The file system within the partition also needs to be enlarged. However, there are limits to how much a file system should be enlarged instead of recreating it in the new and larger partition. </p> <p> Tiny file systems like the default file system in a <a href='/av_studio/570-ableton-push-nvme.html'>Push 3 Standalone</a>, which uses 1K blocks, should not be enlarged much, if at all. Instead, tiny file systems should be recreated at the size they will be used. See the next section for an explanation of the technical details of the Linux file system internals behind this statement. </p> <!-- endregion --> <!-- #region Linux File System Internals --> <h3 id="lfsi">Linux File System Internals</h3> <p> <a href='https://en.wikipedia.org/wiki/Theodore_Ts%27o' target='_blank' rel="nofollow">Theodore Tso</a> is a maintainer for the <code>ext4</code> file system. He is also one of the two authors of <a href='https://en.wikipedia.org/wiki/E2fsprogs' target='_blank' rel="nofollow"><code>e2fsprogs</code></a>, a collection of 31 commands for manipulating Linux <code>ext3</code> and <code>ext4</code> file systems. Theodore is one of the best authorities that one might wish for regarding how to work with <code>ext4</code> file systems. </p> <div class='quote'> <div class='quoteText clearfix'> For a small file system, we use a 1k block size, to reduce space wasted by internal fragmentation. We also reduce the size of the <code>ext4</code> journal to reduce file system overhead. But both of these changes have significant performance impacts&mdash;which might not matter if the file system is meant for a small, slow USB thumb drive, but if you are trying to use this for a higher-performance system, you'll be sorry. <br><br> We also don't enable 64-bit block numbers, since this increases metadata overhead. But like the 1K block size, this limits the maximum size that is supported by the base file system, and it is not at all simple to change the fundamental aspects of the file system so that the file system is scalable and performant. So this idea of "drop a small file system on a device" and then blowing it up to a huge size is a Really, REALLY, REALLY bad idea. <br><br> There are some ways that this can be mitigated (you can give options to <code>mke2fs</code> to use a 4k block size, enable 64k blocksize, and force a larger journal size), but if the file system has some files placed in the small file system, the block placement of the small file system will not be optimal after the file system is blown up to a large size. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://github.com/tytso/e2fsprogs/issues/60#issuecomment-783465199' rel='nofollow' target='_blank'><code>e2fsprogs</code> author Theodore Tso</a></div> </div> <p> Theodore also explains why a tiny file system like the one shipped with the Push 3 Standalone should not be resized past 1 TB: </p> <div class='quote'> <div class='quoteText clearfix'> The problem is that when you create a file system that is tiny (e.g., 200MiB), we use a blocksize of 1K. With a block size of 1K, the backup superblock is located at block 8193 (e.g., the first block of the second block group in the file system). But when using a block size of 1K, the number of block group descriptors is so large that it completely fills the first block group and spills into the second block group: <br><br> Group 0: (Blocks 1-8192) csum 0x0e73 Primary superblock at 1, Group descriptors at 2-8193 <br><br> and so when the backup superblock is written, it trashes the block group descriptors: <br><br> Group 1: (Blocks 8193-16384) csum 0x98cc [INODE_UNINIT] Backup superblock at 8193, Group descriptors at 8194-16385 </div> </div> <p> With the above in mind, let&rsquo;s work through resizing the file system on a partition. We will consider both of the scenarios introduced at the beginning of this article. </p> <!-- endregion --> <!-- #region Installing gparted --> <h2 id="gparted">Installing Gparted</h2> <p> The Linux <code>gparted</code> GUI program can change the size of a drive partition, and will attempt to enlarge or shrink the file system as required. </p> <p> I was able to run <code>gparted</code> on a WSL instance; however, I did not know how to map USB drives to the WSL VM, so <code>gparted</code> could work its magic. Maybe one day I will fight through the issues involved, but there is no reason for me to work that hard for no benefit. </p> <p> Instead, I installed Ubuntu Desktop 24.04 in a new virtual machine. Please see the article I wrote on <a href='/blog/2024/06/12/virtualbox.html'>VirtualBox</a> for details. Once Ubuntu was running in a VM, I started a terminal session and installed <code>gparted</code> on the new Ubuntu VM with this incantation: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd67f87384d70'><button class='copyBtn' data-clipboard-target='#idd67f87384d70' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install gparted</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region man gparted --> <h2 id="mg">Gparted Help</h2> <p> The help message for <code>gparted</code> is: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc69c340d3688'><button class='copyBtn' data-clipboard-target='#idc69c340d3688' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man gparted <span class='unselectable'>GPARTED(8) GParted Manual GPARTED(8)<br/> NAME gparted - GNOME Partition Editor for manipulating disk parti&#8208; tions.<br/> SYNOPSIS gparted [device]...<br/> DESCRIPTION The gparted application is the GNOME partition editor for cre&#8208; ating, reorganizing, and deleting disk partitions.<br/> A disk device can be subdivided into one or more partitions. The gparted application enables you to change the partition or&#8208; ganization on a disk device while preserving the contents of the partition.<br/> With gparted you can accomplish the following tasks: - Create a partition table on a disk device. - Enable and disable partition flags such as boot and hidden. - Perform actions with partitions such as create, delete, re&#8208; size, move, check, label, copy, and paste.<br/> More documentation can be found in the application help manual, and online at: https://gparted.org<br/> EXAMPLES You can run gparted from a command line and specify one or more disk devices.<br/> For example, to start gparted with the devices /dev/sda and /dev/sdc you would use the following command:<br/> gparted /dev/sda /dev/sdc<br/> NOTES Editing partitions has the potential to cause LOSS of DATA.<br/> The gparted application is designed to enable you to edit par&#8208; titions while reducing the risk of data loss. The application is carefully tested and is used by the GParted project team. However, loss of data might occur due to software bugs, hard&#8208; ware problems, or power failure.<br/> You can help to reduce the risk of data loss by not mounting or unmounting partitions outside of the gparted application while gparted is running.<br/> You are advised to BACKUP your DATA before using the gparted application.<br/> REPORTING BUGS Report bugs at: https://gparted.org/bugs.php<br/> AUTHOR Manual page written by Curtis Gedak &lt;gedakc@users.sf.net&gt;<br/> SEE ALSO parted(8), fdisk(8), mkfs(8), ntfsprogs(8)<br/> gparted Jan 16th, 2011 GPARTED(8) </span></pre> </div> <!-- endregion --> <p> To expand on what the <code>gparted</code> help information says, <code>gparted</code> does not just resize partitions, it also runs <code>e2fsk</code> before doing the resize to verify that the partition was successfully cloned, then if that succeeds, it tries to run <code>resize2fs</code> to resize the file system in the partition and thereby complete the job. </p> <p> This means that if one of the steps in the above sequence fails, the remaining steps do not get executed. So the partition might resize, and the file system could successfully verify its integrity, but the file system resize could fail. If you clone a 256 GB NVMe drive to a 4 TB NVMe drive, then you are likely to encouter this problem. The <a href='#recreation'>Recreating a File System</a> section later in this article demonstrates the solution to that problem. </p> <!-- endregion --> <!-- #region Practicalities --> <h3 id="pgp">Practicalities</h3> <p> <code>Gparted</code> has no problem enlarging a 256 GB partition with a 1K block size to 500 GB. However, resizing the partition/file system to a much larger partition, for example, 3 TB, will fail. </p> <p> However, as Theodore Tso has <a href='#lfsi'>already informed us</a>, it would be unwise to expand a tiny file system at all, and also unwise to supersize a medium-sized file system to a much larger file system. </p> <p> When enlarging a file system is not possible or advisable, the file system must be recreated and then the contents of the old file system must be copied file-by-file to the new file system. </p> <!-- endregion --> <!-- #region Running Gparted --> <h3 id="rg">Running Gparted</h3> <p> In <a href='/blog/2024/06/12/clonezilla.html'>Clonezilla</a>, I showed how the 256 GB NVMe from an Ableton Push 3 Standalone could be cloned to a 4 TB NVMe. After cloning, the <code>data </code> partition should be enlarged to use all available space. This is a job for <code>gparted</code>! If the <code>data</code> partition is not enlarged, then the extra space on the drive would never be used. </p> <p> Running <code>gparted</code> showed the VM drive used by Ubuntu as <code>/dev/sda</code>, and the 4 TB NVMe drive as <code>/dev/sdb</code>: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_1.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_1.png" style='width: 100%; ' /> </picture> </div> <p> Selecting <code>/dev/sdb</code> showed 5 partitions: </p> <ol> <li> The <code>msdos</code> partition has two flags: <code>legacy_boot</code> and <code>msftdata</code>. This partition&rsquo;s file system is <a href='https://en.wikipedia.org/wiki/File_Allocation_Table' target='_blank' rel="nofollow">FAT16</a>. UEFI boot partitions (Windows calls these <i>EFI system partition</i>s) are always formatted with <code>FAT16</code> or <code>FAT32</code> file systems. There should be no reason to ever modify this partition. </li> <li> The <code>swap1</code> partition has one flag: <a href='https://phoenixnap.com/kb/swap-partition' target='_blank' rel="nofollow"><code>swap</code></a>. This swap partition is equal to the amount of RAM in the P3S. There should no reason to ever modify this partition. </li> <li>I do not know what the <code>primary</code> partition is for or why its file system type is unknown.</li> <li> The <code>secondary</code> partition contains the Linux operating system for the P3S. This partition is 47.7% full; most users will probably not need to modify it. </li> <li> The <code>data</code> partition is where all user data is stored. This partition should be expanded to utilize all available space. </li> <li> 3.41 TB of unused space follows. If you clone a P3S NVMe drive to an NVMe with different capacity, say 1 TB, the amount of unused space will differ. </li> </ol> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_2.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_2.png" style='width: 100%; ' /> </picture> </div> <p> Right-clicking on the <code>data</code> partition displayed the <b>Resize/Move</b> menu item, which I selected: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_3.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_3.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_3.png" style='width: 100%; ' /> </picture> </div> <p> I dragged the right-most handle to the far right. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_4.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_4.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_4.png" style='width: 100%; ' /> </picture> </div> <p> The <code>data</code> partition was now set to include all available free space on the drive: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_5.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_5.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_5.png" style='width: 100%; ' /> </picture> </div> <p> I pressed the <kbd>Resize/Move</kbd> button, then I pressed the green checkbox icon to actually resize the <code>data</code> partition. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_6.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_6.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_6.png" style='width: 100%; ' /> </picture> </div> <p> Finally, I pressed the <kbd>Apply</kbd> button: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_7.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_7.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_7.png" style='width: 100%; ' /> </picture> </div> <p> The resize operation first verified the integrity of the contents of the <code>data</code> partition, which took about 5 minutes. Then it aborted with the following error before resizing the file system: </p> <p class="error rounded shadow code" style="padding-left: 1em;"> resize2fs: New size results in too many block group descriptors. </p> <p> The <code>data</code> partition had been resized, but its file system had not. <code>Gparted</code> shows the unused portion of <code>/dev/sdb5</code> in gray: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gparted_8.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gparted_8.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gparted_8.png" style='width: 100%; ' /> </picture> </div> <p> Not to worry, the partition is fine. The error message tells us that we must recreate the file system inside the partition, and that will necessitate copying all the files and directories into the file system once it has been recreated. </p> <!-- endregion --> <!-- #region Modestly Enlarging the Filesystem --> <h2 id="modestly_enlarge">Modestly Enlarging the Filesystem</h2> <p> A file system can be resized to utilize all the space in the newly expanded partition, subject to the limitations that have already been discussed. </p> <p> This section shows how I enlarged a partition and its file system after cloning a 256 GB NVMe drive to a 500 GB NVMe drive. As previously mentioned, I suggest that you not enlarge tiny file systems, so if you are reading this because you have a P3S and want to clone the 256 GB NVMe drive that it came with to a larger drive, you should skip this section and read the <a href='#findmnt'>Recreating a File System</a> section. </p> <div class="alert rounded shadow clearfix" style="padding-bottom: 0"> <div class='imgWrapper imgFlex right' style='width: 225px; margin-right: 1em;'> <picture class='imgPicture'> <source srcset="/assets/images/control/noRunningWithScissors.svg" type="image/svg"> <!---<source srcset="/assets/images/control/noRunningWithScissors.avif" type="image/avif">--> <source srcset="/assets/images/control/noRunningWithScissors.webp" type="image/webp"> <source srcset="/assets/images/control/noRunningWithScissors.apng" type="image/apng"> <source srcset="/assets/images/control/noRunningWithScissors.png" type="image/png"> <source srcset="/assets/images/control/noRunningWithScissors.jpg" type="image/jpeg"> <source srcset="/assets/images/control/noRunningWithScissors.jpeg" type="image/jpeg"> <source srcset="/assets/images/control/noRunningWithScissors.jfif" type="image/jpeg"> <source srcset="/assets/images/control/noRunningWithScissors.pjpeg" type="image/jpeg"> <source srcset="/assets/images/control/noRunningWithScissors.pjp" type="image/jpeg"> <source srcset="/assets/images/control/noRunningWithScissors.gif" type="image/gif"> <source srcset="/assets/images/control/noRunningWithScissors.tif" type="image/tiff"> <source srcset="/assets/images/control/noRunningWithScissors.tiff" type="image/tiff"> <source srcset="/assets/images/control/noRunningWithScissors.bmp" type="image/bmp"> <source srcset="/assets/images/control/noRunningWithScissors.ico" type="image/x-icon"> <source srcset="/assets/images/control/noRunningWithScissors.cur" type="image/x-icon"> <img class="imgImg " src="/assets/images/control/noRunningWithScissors.png" style='width: 100%; ' /> </picture> </div> <p style="margin-top: 1em;"> We can use the standard Linux command <code>resize2fs</code> to resize the filesystem. As the documentation says, <code>resize2fs</code> can expand the size of an <code>ext4</code> mounted file system. </p> <p> Since I discourage people from running with scissors, I suggest you first <a href='#unmount'>unmount</a> any drives whose file systems will be resized. </p> </div> <p> Before running <code>resize2fs</code> it is a good idea to verify the integrity of the partition to be resized. The <code>e2fsck</code> is the command normally used to do that. </p> <!-- #region pre --> <div class="jekyll_pre clear" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ided6892d67080'><button class='copyBtn' data-clipboard-target='#ided6892d67080' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man e2fsck <span class='unselectable'>E2FSCK(8) System Manager&#39;s Manual E2FSCK(8)<br/> NAME e2fsck - check a Linux ext2/ext3/ext4 file system<br/> SYNOPSIS e2fsck [ -pacnyrdfkvtDFV ] [ -b superblock ] [ -B blocksize ] [ -l|-L bad_blocks_file ] [ -C fd ] [ -j external-journal ] [ -E extended_options ] [ -z undo_file ] device<br/> DESCRIPTION e2fsck is used to check the ext2/ext3/ext4 family of file sys&#8208; tems. For ext3 and ext4 file systems that use a journal, if the system has been shut down uncleanly without any errors, normally, after replaying the committed transactions in the journal, the file system should be marked as clean. Hence, for file systems that use journaling, e2fsck will normally re&#8208; play the journal and exit, unless its superblock indicates that further checking is required.<br/> device is a block device (e.g., /dev/sdc1) or file containing the file system.<br/> Note that in general it is not safe to run e2fsck on mounted file systems. The only exception is if the -n option is speci&#8208; fied, and -c, -l, or -L options are not specified. However, even if it is safe to do so, the results printed by e2fsck are not valid if the file system is mounted. If e2fsck asks whether or not you should check a file system which is mounted, the only correct answer is ``no&#39;&#39;. Only experts who really know what they are doing should consider answering this ques&#8208; tion in any other way.<br/> If e2fsck is run in interactive mode (meaning that none of -y, -n, or -p are specified), the program will ask the user to fix each problem found in the file system. A response of &#39;y&#39; will fix the error; &#39;n&#39; will leave the error unfixed; and &#39;a&#39; will fix the problem and all subsequent problems; pressing Enter will proceed with the default response, which is printed before the question mark. Pressing Control-C terminates e2fsck imme&#8208; diately.<br/> OPTIONS -a This option does the same thing as the -p option. It is provided for backwards compatibility only; it is sug&#8208; gested that people use -p option whenever possible.<br/> -b superblock Instead of using the normal superblock, use an alterna&#8208; tive superblock specified by superblock. This option is normally used when the primary superblock has been cor&#8208; rupted. The location of backup superblocks is dependent on the file system&#39;s blocksize, the number of blocks per group, and features such as sparse_super.<br/> Additional backup superblocks can be determined by using the mke2fs program using the -n option to print out where the superblocks exist, supposing mke2fs is sup&#8208; plied with arguments that are consistent with the file system&#39;s layout (e.g. blocksize, blocks per group, sparse_super, etc.).<br/> If an alternative superblock is specified and the file system is not opened read-only, e2fsck will make sure that the primary superblock is updated appropriately upon completion of the file system check.<br/> -B blocksize Normally, e2fsck will search for the superblock at vari&#8208; ous different block sizes in an attempt to find the ap&#8208; propriate block size. This search can be fooled in some cases. This option forces e2fsck to only try locating the superblock at a particular blocksize. If the su&#8208; perblock is not found, e2fsck will terminate with a fa&#8208; tal error.<br/> -c This option causes e2fsck to use badblocks(8) program to do a read-only scan of the device in order to find any bad blocks. If any bad blocks are found, they are added to the bad block inode to prevent them from being allo&#8208; cated to a file or directory. If this option is speci&#8208; fied twice, then the bad block scan will be done using a non-destructive read-write test.<br/> -C fd This option causes e2fsck to write completion informa&#8208; tion to the specified file descriptor so that the progress of the file system check can be monitored. This option is typically used by programs which are run&#8208; ning e2fsck. If the file descriptor number is negative, then absolute value of the file descriptor will be used, and the progress information will be suppressed ini&#8208; tially. It can later be enabled by sending the e2fsck process a SIGUSR1 signal. If the file descriptor speci&#8208; fied is 0, e2fsck will print a completion bar as it goes about its business. This requires that e2fsck is run&#8208; ning on a video console or terminal.<br/> -d Print debugging output (useless unless you are debugging e2fsck).<br/> -D Optimize directories in file system. This option causes e2fsck to try to optimize all directories, either by re- indexing them if the file system supports directory in&#8208; dexing, or by sorting and compressing directories for smaller directories, or for file systems using tradi&#8208; tional linear directories.<br/> Even without the -D option, e2fsck may sometimes opti&#8208; mize a few directories --- for example, if directory in&#8208; dexing is enabled and a directory is not indexed and would benefit from being indexed, or if the index struc&#8208; tures are corrupted and need to be rebuilt. The -D op&#8208; tion forces all directories in the file system to be op&#8208; timized. This can sometimes make them a little smaller and slightly faster to search, but in practice, you should rarely need to use this option.<br/> The -D option will detect directory entries with dupli&#8208; cate names in a single directory, which e2fsck normally does not enforce for performance reasons.<br/> -E extended_options Set e2fsck extended options. Extended options are comma separated, and may take an argument using the equals (&#39;=&#39;) sign. The following options are supported:<br/> ea_ver=extended_attribute_version Set the version of the extended attribute blocks which e2fsck will require while checking the file system. The version num&#8208; ber may be 1 or 2. The default extended at&#8208; tribute version format is 2.<br/> journal_only Only replay the journal if required, but do not perform any further checks or repairs.<br/> fragcheck During pass 1, print a detailed report of any discontiguous blocks for files in the file system.<br/> discard Attempt to discard free blocks and unused inode blocks after the full file system check (discarding blocks is useful on solid state devices and sparse / thin-provisioned storage). Note that discard is done in pass 5 AFTER the file system has been fully checked and only if it does not contain rec&#8208; ognizable errors. However there might be cases where e2fsck does not fully recognize a problem and hence in this case this option may prevent you from further manual data re&#8208; covery.<br/> nodiscard Do not attempt to discard free blocks and unused inode blocks. This option is exactly the opposite of discard option. This is set as default.<br/> no_optimize_extents Do not offer to optimize the extent tree by eliminating unnecessary width or depth. This can also be enabled in the options sec&#8208; tion of /etc/e2fsck.conf.<br/> optimize_extents Offer to optimize the extent tree by elimi&#8208; nating unnecessary width or depth. This is the default unless otherwise specified in /etc/e2fsck.conf.<br/> inode_count_fullmap Trade off using memory for speed when check&#8208; ing a file system with a large number of hard-linked files. The amount of memory re&#8208; quired is proportional to the number of in&#8208; odes in the file system. For large file systems, this can be gigabytes of memory. (For example, a 40TB file system with 2.8 billion inodes will consume an additional 5.7 GB memory if this optimization is en&#8208; abled.) This optimization can also be en&#8208; abled in the options section of /etc/e2fsck.conf.<br/> no_inode_count_fullmap Disable the inode_count_fullmap optimiza&#8208; tion. This is the default unless otherwise specified in /etc/e2fsck.conf.<br/> readahead_kb Use this many KiB of memory to pre-fetch metadata in the hopes of reducing e2fsck runtime. By default, this is set to the size of two block groups&#39; inode tables (typ&#8208; ically 4MiB on a regular ext4 file system); if this amount is more than 1/50th of total physical memory, readahead is disabled. Set this to zero to disable readahead entirely.<br/> bmap2extent Convert block-mapped files to extent-mapped files.<br/> fixes_only Only fix damaged metadata; do not optimize htree directories or compress extent trees. This option is incompatible with the -D and -E bmap2extent options.<br/> check_encoding Force verification of encoded filenames in case-insensitive directories. This is the default mode if the file system has the strict flag enabled.<br/> unshare_blocks If the file system has shared blocks, with the shared blocks read-only feature enabled, then this will unshare all shared blocks and unset the read-only feature bit. If there is not enough free space then the operation will fail. If the file system does not have the read-only feature bit, but has shared blocks anyway, then this option will have no effect. Note when using this option, if there is no free space to clone blocks, there is no prompt to delete files and in&#8208; stead the operation will fail.<br/> Note that unshare_blocks implies the &quot;-f&quot; option to ensure that all passes are run. Additionally, if &quot;-n&quot; is also specified, e2fsck will simulate trying to allocate enough space to deduplicate. If this fails, the exit code will be non-zero.<br/> -f Force checking even if the file system seems clean.<br/> -F Flush the file system device&#39;s buffer caches before be&#8208; ginning. Only really useful for doing e2fsck time tri&#8208; als.<br/> -j external-journal Set the pathname where the external-journal for this file system can be found.<br/> -k When combined with the -c option, any existing bad blocks in the bad blocks list are preserved, and any new bad blocks found by running badblocks(8) will be added to the existing bad blocks list.<br/> -l filename Add the block numbers listed in the file specified by filename to the list of bad blocks. The format of this file is the same as the one generated by the bad&#8208; blocks(8) program. Note that the block numbers are based on the blocksize of the file system. Hence, bad&#8208; blocks(8) must be given the blocksize of the file system in order to obtain correct results. As a result, it is much simpler and safer to use the -c option to e2fsck, since it will assure that the correct parameters are passed to the badblocks program.<br/> -L filename Set the bad blocks list to be the list of blocks speci&#8208; fied by filename. (This option is the same as the -l option, except the bad blocks list is cleared before the blocks listed in the file are added to the bad blocks list.)<br/> -n Open the file system read-only, and assume an answer of `no&#39; to all questions. Allows e2fsck to be used non-in&#8208; teractively. This option may not be specified at the same time as the -p or -y options.<br/> -p Automatically repair (&quot;preen&quot;) the file system. This option will cause e2fsck to automatically fix any file system problems that can be safely fixed without human intervention. If e2fsck discovers a problem which may require the system administrator to take additional cor&#8208; rective action, e2fsck will print a description of the problem and then exit with the value 4 logically or&#39;ed into the exit code. (See the EXIT CODE section.) This option is normally used by the system&#39;s boot scripts. It may not be specified at the same time as the -n or -y options.<br/> -r This option does nothing at all; it is provided only for backwards compatibility.<br/> -t Print timing statistics for e2fsck. If this option is used twice, additional timing statistics are printed on a pass by pass basis.<br/> -v Verbose mode.<br/> -V Print version information and exit.<br/> -y Assume an answer of `yes&#39; to all questions; allows e2fsck to be used non-interactively. This option may not be specified at the same time as the -n or -p op&#8208; tions.<br/> -z undo_file Before overwriting a file system block, write the old contents of the block to an undo file. This undo file can be used with e2undo(8) to restore the old contents of the file system should something go wrong. If the empty string is passed as the undo_file argument, the undo file will be written to a file named e2fsck-de&#8208; vice.e2undo in the directory specified via the E2FSPROGS_UNDO_DIR environment variable.<br/> WARNING: The undo file cannot be used to recover from a power or system crash.<br/> EXIT CODE The exit code returned by e2fsck is the sum of the following conditions: 0 - No errors 1 - File system errors corrected 2 - File system errors corrected, system should be rebooted 4 - File system errors left uncorrected 8 - Operational error 16 - Usage or syntax error 32 - E2fsck canceled by user request 128 - Shared library error<br/> SIGNALS The following signals have the following effect when sent to e2fsck.<br/> SIGUSR1 This signal causes e2fsck to start displaying a comple&#8208; tion bar or emitting progress information. (See discus&#8208; sion of the -C option.)<br/> SIGUSR2 This signal causes e2fsck to stop displaying a comple&#8208; tion bar or emitting progress information.<br/> REPORTING BUGS Almost any piece of software will have bugs. If you manage to find a file system which causes e2fsck to crash, or which e2fsck is unable to repair, please report it to the author.<br/> Please include as much information as possible in your bug re&#8208; port. Ideally, include a complete transcript of the e2fsck run, so I can see exactly what error messages are displayed. (Make sure the messages printed by e2fsck are in English; if your system has been configured so that e2fsck&#39;s messages have been translated into another language, please set the the LC_ALL environment variable to C so that the transcript of e2fsck&#39;s output will be useful to me.) If you have a writable file system where the transcript can be stored, the script(1) program is a handy way to save the output of e2fsck to a file.<br/> It is also useful to send the output of dumpe2fs(8). If a spe&#8208; cific inode or inodes seems to be giving e2fsck trouble, try running the debugfs(8) command and send the output of the stat(1u) command run on the relevant inode(s). If the inode is a directory, the debugfs dump command will allow you to extract the contents of the directory inode, which can sent to me after being first run through uuencode(1). The most useful data you can send to help reproduce the bug is a compressed raw image dump of the file system, generated using e2image(8). See the e2image(8) man page for more details.<br/> Always include the full version string which e2fsck displays when it is run, so I know which version you are running.<br/> ENVIRONMENT E2FSCK_CONFIG Determines the location of the configuration file (see e2fsck.conf(5)).<br/> AUTHOR This version of e2fsck was written by Theodore Ts&#39;o &lt;tytso@mit.edu&gt;.<br/> SEE ALSO e2fsck.conf(5), badblocks(8), dumpe2fs(8), debugfs(8), e2im&#8208; age(8), mke2fs(8), tune2fs(8)<br/> E2fsprogs version 1.47.0 February 2023 E2FSCK(8) </span></pre> </div> <!-- endregion --> <p> Here is the documentation for <code>resize2fs</code>: </p> <!-- #region pre man resize2fs --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0472f029426e'><button class='copyBtn' data-clipboard-target='#id0472f029426e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man resize2fs <span class='unselectable'>RESIZE2FS(8) System Manager&#39;s Manual RESIZE2FS(8)<br/> NAME resize2fs - ext2/ext3/ext4 file system resizer<br/> SYNOPSIS resize2fs [ -fFpPMbs ] [ -d debug-flags ] [ -S RAID-stride ] [ -z undo_file ] device [ size ]<br/> DESCRIPTION The resize2fs program will resize ext2, ext3, or ext4 file sys&#8208; tems. It can be used to enlarge or shrink an unmounted file system located on device. If the file system is mounted, it can be used to expand the size of the mounted file system, as&#8208; suming the kernel and the file system supports on-line resiz&#8208; ing. (Modern Linux 2.6 kernels will support on-line resize for file systems mounted using ext3 and ext4; ext3 file systems will require the use of file systems with the resize_inode fea&#8208; ture enabled.)<br/> The size parameter specifies the requested new size of the file system. If no units are specified, the units of the size para&#8208; meter shall be the file system blocksize of the file system. Optionally, the size parameter may be suffixed by one of the following units designators: &#39;K&#39;, &#39;M&#39;, &#39;G&#39;, &#39;T&#39; (either upper- case or lower-case) or &#39;s&#39; for power-of-two kilobytes, megabytes, gigabytes, terabytes or 512 byte sectors respec&#8208; tively. The size of the file system may never be larger than the size of the partition. If size parameter is not specified, it will default to the size of the partition.<br/> The resize2fs program does not manipulate the size of parti&#8208; tions. If you wish to enlarge a file system, you must make sure you can expand the size of the underlying partition first. This can be done using fdisk(8) by deleting the partition and recreating it with a larger size or using lvextend(8), if you&#39;re using the logical volume manager lvm(8). When recreat&#8208; ing the partition, make sure you create it with the same start&#8208; ing disk cylinder as before! Otherwise, the resize operation will certainly not work, and you may lose your entire file sys&#8208; tem. After running fdisk(8), run resize2fs to resize the ext2 file system to use all of the space in the newly enlarged par&#8208; tition.<br/> If you wish to shrink an ext2 partition, first use resize2fs to shrink the size of file system. Then you may use fdisk(8) to shrink the size of the partition. When shrinking the size of the partition, make sure you do not make it smaller than the new size of the ext2 file system!<br/> The -b and -s options enable and disable the 64bit feature, re&#8208; spectively. The resize2fs program will, of course, take care of resizing the block group descriptors and moving other data blocks out of the way, as needed. It is not possible to resize the file system concurrent with changing the 64bit status.<br/> OPTIONS -b Turns on the 64bit feature, resizes the group descrip&#8208; tors as necessary, and moves other metadata out of the way.<br/> -d debug-flags Turns on various resize2fs debugging features, if they have been compiled into the binary. debug-flags should be computed by adding the numbers of the desired fea&#8208; tures from the following list: 2 - Debug block relocations 4 - Debug inode relocations 8 - Debug moving the inode table 16 - Print timing information 32 - Debug minimum file system size (-M) calcula&#8208; tion<br/> -f Forces resize2fs to proceed with the file system resize operation, overriding some safety checks which resize2fs normally enforces.<br/> -F Flush the file system device&#39;s buffer caches before be&#8208; ginning. Only really useful for doing resize2fs time trials.<br/> -M Shrink the file system to minimize its size as much as possible, given the files stored in the file system.<br/> -p Print out percentage completion bars for each resize2fs phase during an offline (non-trivial) resize operation, so that the user can keep track of what the program is doing. (For very fast resize operations, no progress bars may be displayed.)<br/> -P Print an estimate of the number of file system blocks in the file system if it is shrunk using resize2fs&#39;s -M op&#8208; tion and then exit.<br/> -s Turns off the 64bit feature and frees blocks that are no longer in use.<br/> -S RAID-stride The resize2fs program will heuristically determine the RAID stride that was specified when the file system was created. This option allows the user to explicitly specify a RAID stride setting to be used by resize2fs instead.<br/> -z undo_file Before overwriting a file system block, write the old contents of the block to an undo file. This undo file can be used with e2undo(8) to restore the old contents of the file system should something go wrong. If the empty string is passed as the undo_file argument, the undo file will be written to a file named resize2fs-de&#8208; vice.e2undo in the directory specified via the E2FSPROGS_UNDO_DIR environment variable.<br/> WARNING: The undo file cannot be used to recover from a power or system crash.<br/> KNOWN BUGS The minimum size of the file system as estimated by resize2fs may be incorrect, especially for file systems with 1k and 2k blocksizes.<br/> AUTHOR resize2fs was written by Theodore Ts&#39;o &lt;tytso@mit.edu&gt;.<br/> COPYRIGHT Resize2fs is Copyright 1998 by Theodore Ts&#39;o and PowerQuest, Inc. All rights reserved. As of April, 2000 Resize2fs may be redistributed under the terms of the GPL.<br/> SEE ALSO fdisk(8), e2fsck(8), mke2fs(8), lvm(8), lvextend(8)<br/> E2fsprogs version 1.47.0 February 2023 RESIZE2FS(8) </span></pre> </div> <!-- endregion --> <p> Now let&rsquo;s finally get the job done. First, we verify and optimize the filesystem on the <code>data</code> partition: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb0762ed7ada2'><button class='copyBtn' data-clipboard-target='#idb0762ed7ada2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo e2fsck -Dfp /dev/sdb5 <span class='unselectable'>data: 48424/27583488 files (2.1% non-contiguous), 98269049/220661060 blocks </span></pre> </div> <!-- endregion --> <p> The file system is good, and very little of it is non-continuous, so let&rsquo;s go ahead and resize it. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id258582ffc8b0'><button class='copyBtn' data-clipboard-target='#id258582ffc8b0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo resize2fs /dev/sdb5 <span class='unselectable'>resize2fs 1.47.0 (5-Feb-2023) The filesystem is already 458988544 (1k) blocks long. Nothing to do! </span></pre> </div> <!-- endregion --> <p> You are done, <a href='#done'>time to clean up</a>. </p> <!-- endregion --> <!-- #region Recreating a File System --> <h2 id="recreation">Recreating a File System</h2> <!-- #region implicit --> <p> When resizing an existing file system is either impossible or inadvisable, a new file system should be created using <code>mke2fs</code>, and the contents of the old file system copied to the new file system. </p> <p> This is the help information for <code>mke2fs</code>: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>man mke2fs</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2aee05e49ce1'><button class='copyBtn' data-clipboard-target='#id2aee05e49ce1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>MKE2FS(8) System Manager&#39;s Manual MKE2FS(8)<br/> NAME mke2fs - create an ext2/ext3/ext4 file system<br/> SYNOPSIS mke2fs [ -c | -l filename ] [ -b block-size ] [ -C cluster-size ] [ -d root-directory ] [ -D ] [ -g blocks-per-group ] [ -G number-of-groups ] [ -i bytes-per-inode ] [ -I inode-size ] [ -j ] [ -J journal-options ] [ -N number-of-inodes ] [ -n ] [ -m reserved-blocks-percentage ] [ -o creator-os ] [ -O [^]fea&#8208; ture[,...] ] [ -q ] [ -r fs-revision-level ] [ -E extended-op&#8208; tions ] [ -v ] [ -F ] [ -L volume-label ] [ -M last-mounted-di&#8208; rectory ] [ -S ] [ -t fs-type ] [ -T usage-type ] [ -U UUID ] [ -V ] [ -e errors-behavior ] [ -z undo_file ] device [ fs-size ]<br/> mke2fs -O journal_dev [ -b block-size ] [ -L volume-label ] [ -n ] [ -q ] [ -v ] external-journal [ fs-size ]<br/> DESCRIPTION mke2fs is used to create an ext2, ext3, or ext4 file system, usually in a disk partition (or file) named by device.<br/> The file system size is specified by fs-size. If fs-size does not have a suffix, it is interpreted as power-of-two kilobytes, unless the -b blocksize option is specified, in which case fs- size is interpreted as the number of blocksize blocks. If the fs-size is suffixed by &#39;k&#39;, &#39;m&#39;, &#39;g&#39;, &#39;t&#39; (either upper-case or lower-case), then it is interpreted in power-of-two kilobytes, megabytes, gigabytes, terabytes, etc. If fs-size is omitted, mke2fs will create the file system based on the device size.<br/> If mke2fs is run as mkfs.XXX (i.e., mkfs.ext2, mkfs.ext3, or mkfs.ext4) the option -t XXX is implied; so mkfs.ext3 will cre&#8208; ate a file system for use with ext3, mkfs.ext4 will create a file system for use with ext4, and so on.<br/> The defaults of the parameters for the newly created file sys&#8208; tem, if not overridden by the options listed below, are con&#8208; trolled by the /etc/mke2fs.conf configuration file. See the mke2fs.conf(5) manual page for more details.<br/> OPTIONS -b block-size Specify the size of blocks in bytes. Valid block-size values are powers of two from 1024 up to 65536 (however note that the kernel is able to mount only file systems with block-size smaller or equal to the system page size - 4k on x86 systems, up to 64k on ppc64 or aarch64 de&#8208; pending on kernel configuration). If omitted, block- size is heuristically determined by the file system size and the expected usage of the file system (see the -T option). In most common cases, the default block size is 4k. If block-size is preceded by a negative sign (&#39;-&#39;), then mke2fs will use heuristics to determine the appropriate block size, with the constraint that the block size will be at least block-size bytes. This is useful for certain hardware devices which require that the blocksize be a multiple of 2k.<br/> -c Check the device for bad blocks before creating the file system. If this option is specified twice, then a slower read-write test is used instead of a fast read- only test.<br/> -C cluster-size Specify the size of cluster in bytes for file systems using the bigalloc feature. Valid cluster-size values are from 2048 to 256M bytes per cluster. This can only be specified if the bigalloc feature is enabled. (See the ext4 (5) man page for more details about bigalloc.) The default cluster size if bigalloc is enabled is 16 times the block size.<br/> -d root-directory Copy the contents of the given directory into the root directory of the file system.<br/> -D Use direct I/O when writing to the disk. This avoids mke2fs dirtying a lot of buffer cache memory, which may impact other applications running on a busy server. This option will cause mke2fs to run much more slowly, however, so there is a tradeoff to using direct I/O.<br/> -e error-behavior Change the behavior of the kernel code when errors are detected. In all cases, a file system error will cause e2fsck(8) to check the file system on the next boot. error-behavior can be one of the following:<br/> continue Continue normal execution.<br/> remount-ro Remount file system read-only.<br/> panic Cause a kernel panic.<br/> -E extended-options Set extended options for the file system. Extended op&#8208; tions are comma separated, and may take an argument us&#8208; ing the equals (&#39;=&#39;) sign. The -E option used to be -R in earlier versions of mke2fs. The -R option is still accepted for backwards compatibility, but is deprecated. The following extended options are supported:<br/> encoding=encoding-name Enable the casefold feature in the super block and set encoding-name as the encoding to be used. If encoding-name is not speci&#8208; fied, the encoding defined in mke2fs.conf(5) is used.<br/> encoding_flags=encoding-flags Define parameters for file name character encoding operations. If a flag is not changed using this parameter, its default value is used. encoding-flags should be a comma-separated lists of flags to be en&#8208; abled. To disable a flag, add it to the list with the prefix &quot;no&quot;.<br/> The only flag that can be set right now is strict which means that invalid strings should be rejected by the file system. In the default configuration, the strict flag is disabled.<br/> mmp_update_interval=interval Adjust the initial MMP update interval to interval seconds. Specifying an interval of 0 means to use the default interval. The specified interval must be less than 300 seconds. Requires that the mmp feature be enabled.<br/> stride=stride-size Configure the file system for a RAID array with stride-size file system blocks. This is the number of blocks read or written to disk before moving to the next disk, which is sometimes referred to as the chunk size. This mostly affects placement of file system metadata like bitmaps at mke2fs time to avoid placing them on a single disk, which can hurt performance. It may also be used by the block allocator.<br/> stripe_width=stripe-width Configure the file system for a RAID array with stripe-width file system blocks per stripe. This is typically stride-size * N, where N is the number of data-bearing disks in the RAID (e.g. for RAID 5 there is one parity disk, so N will be the number of disks in the array minus 1). This allows the block allocator to prevent read-modify- write of the parity in a RAID stripe if pos&#8208; sible when the data is written.<br/> offset=offset Create the file system at an offset from the beginning of the device or file. This can be useful when creating disk images for vir&#8208; tual machines.<br/> resize=max-online-resize Reserve enough space so that the block group descriptor table can grow to support a file system that has max-online-resize blocks.<br/> lazy_itable_init[= &lt;0 to disable, 1 to enable&gt;] If enabled and the uninit_bg feature is en&#8208; abled, the inode table will not be fully initialized by mke2fs. This speeds up file system initialization noticeably, but it re&#8208; quires the kernel to finish initializing the file system in the background when the file system is first mounted. If the option value is omitted, it defaults to 1 to enable lazy inode table zeroing.<br/> lazy_journal_init[= &lt;0 to disable, 1 to enable&gt;] If enabled, the journal inode will not be fully zeroed out by mke2fs. This speeds up file system initialization noticeably, but carries some small risk if the system crashes before the journal has been over&#8208; written entirely one time. If the option value is omitted, it defaults to 1 to enable lazy journal inode zeroing.<br/> assume_storage_prezeroed[= &lt;0 to disable, 1 to en&#8208; able&gt;] If enabled, mke2fs assumes that the storage device has been prezeroed, skips zeroing the journal and inode tables, and annotates the block group flags to signal that the inode table has been zeroed.<br/> no_copy_xattrs Normally mke2fs will copy the extended at&#8208; tributes of the files in the directory hier&#8208; archy specified via the (optional) -d op&#8208; tion. This will disable the copy and leaves the files in the newly created file system without any extended attributes.<br/> num_backup_sb=&lt;0|1|2&gt; If the sparse_super2 file system feature is enabled this option controls whether there will be 0, 1, or 2 backup superblocks cre&#8208; ated in the file system.<br/> packed_meta_blocks[= &lt;0 to disable, 1 to enable&gt;] Place the allocation bitmaps and the inode table at the beginning of the disk. This option requires that the flex_bg file system feature to be enabled in order for it to have effect, and will also create the jour&#8208; nal at the beginning of the file system. This option is useful for flash devices that use SLC flash at the beginning of the disk. It also maximizes the range of contiguous data blocks, which can be useful for certain specialized use cases, such as supported Shingled Drives.<br/> root_owner[=uid:gid] Specify the numeric user and group ID of the root directory. If no UID:GID is specified, use the user and group ID of the user run&#8208; ning mke2fs. In mke2fs 1.42 and earlier the UID and GID of the root directory were set by default to the UID and GID of the user running the mke2fs command. The root_owner= option allows explicitly specifying these values, and avoid side-effects for users that do not expect the contents of the file system to change based on the user running mke2fs.<br/> test_fs Set a flag in the file system superblock in&#8208; dicating that it may be mounted using exper&#8208; imental kernel code, such as the ext4dev file system.<br/> orphan_file_size=size Set size of the file for tracking unlinked but still open inodes and inodes with trun&#8208; cate in progress. Larger file allows for better scalability, reserving a few blocks per cpu is ideal.<br/> discard Attempt to discard blocks at mkfs time (dis&#8208; carding blocks initially is useful on solid state devices and sparse / thin-provisioned storage). When the device advertises that discard also zeroes data (any subsequent read after the discard and before write re&#8208; turns zero), then mark all not-yet-zeroed inode tables as zeroed. This significantly speeds up file system initialization. This is set as default.<br/> nodiscard Do not attempt to discard blocks at mkfs time.<br/> quotatype Specify the which quota types (usrquota, grpquota, prjquota) which should be enabled in the created file system. The argument of this extended option should be a colon sepa&#8208; rated list. This option has effect only if the quota feature is set. The default quota types to be initialized if this option is not specified is both user and group quo&#8208; tas. If the project feature is enabled that project quotas will be initialized as well.<br/> -F Force mke2fs to create a file system, even if the speci&#8208; fied device is not a partition on a block special de&#8208; vice, or if other parameters do not make sense. In or&#8208; der to force mke2fs to create a file system even if the file system appears to be in use or is mounted (a truly dangerous thing to do), this option must be specified twice.<br/> -g blocks-per-group Specify the number of blocks in a block group. There is generally no reason for the user to ever set this para&#8208; meter, as the default is optimal for the file system. (For administrators who are creating file systems on RAID arrays, it is preferable to use the stride RAID pa&#8208; rameter as part of the -E option rather than manipulat&#8208; ing the number of blocks per group.) This option is generally used by developers who are developing test cases.<br/> If the bigalloc feature is enabled, the -g option will specify the number of clusters in a block group.<br/> -G number-of-groups Specify the number of block groups that will be packed together to create a larger virtual block group (or &quot;flex_bg group&quot;) in an ext4 file system. This improves meta-data locality and performance on meta-data heavy workloads. The number of groups must be a power of 2 and may only be specified if the flex_bg file system feature is enabled.<br/> -i bytes-per-inode Specify the bytes/inode ratio. mke2fs creates an inode for every bytes-per-inode bytes of space on the disk. The larger the bytes-per-inode ratio, the fewer inodes will be created. This value generally shouldn&#39;t be smaller than the blocksize of the file system, since in that case more inodes would be made than can ever be used. Be warned that it is not possible to change this ratio on a file system after it is created, so be care&#8208; ful deciding the correct value for this parameter. Note that resizing a file system changes the number of inodes to maintain this ratio.<br/> -I inode-size Specify the size of each inode in bytes. The inode-size value must be a power of 2 larger or equal to 128. The larger the inode-size the more space the inode table will consume, and this reduces the usable space in the file system and can also negatively impact performance. It is not possible to change this value after the file system is created.<br/> File systems with an inode size of 128 bytes do not sup&#8208; port timestamps beyond January 19, 2038. Inodes which are 256 bytes or larger will support extended time&#8208; stamps, project id&#39;s, and the ability to store some ex&#8208; tended attributes in the inode table for improved per&#8208; formance.<br/> The default inode size is controlled by the mke2fs.conf(5) file. In the mke2fs.conf file shipped with e2fsprogs, the default inode size is 256 bytes for most file systems, except for small file systems where the inode size will be 128 bytes.<br/> -j Create the file system with an ext3 journal. If the -J option is not specified, the default journal parameters will be used to create an appropriately sized journal (given the size of the file system) stored within the file system. Note that you must be using a kernel which has ext3 support in order to actually make use of the journal.<br/> -J journal-options Create the ext3 journal using options specified on the command-line. Journal options are comma separated, and may take an argument using the equals (&#39;=&#39;) sign. The following journal options are supported:<br/> size=journal-size Create an internal journal (i.e., stored in&#8208; side the file system) of size journal-size megabytes. The size of the journal must be at least 1024 file system blocks (i.e., 1MB if using 1k blocks, 4MB if using 4k blocks, etc.) and may be no more than 10,240,000 file system blocks or half the total file system size (whichever is smaller)<br/> fast_commit_size=fast-commit-size Create an additional fast commit journal area of size fast-commit-size kilobytes. This option is only valid if fast_commit feature is enabled on the file system. If this option is not specified and if fast_commit feature is turned on, fast com&#8208; mit area size defaults to journal-size / 64 megabytes. The total size of the journal with fast_commit feature set is journal-size + ( fast-commit-size * 1024) megabytes. The total journal size may be no more than 10,240,000 file system blocks or half the total file system size (whichever is smaller).<br/> location=journal-location Specify the location of the journal. The argument journal-location can either be specified as a block number, or if the num&#8208; ber has a units suffix (e.g., &#39;M&#39;, &#39;G&#39;, etc.) interpret it as the offset from the beginning of the file system.<br/> device=external-journal Attach the file system to the journal block device located on external-journal. The ex&#8208; ternal journal must already have been cre&#8208; ated using the command<br/> mke2fs -O journal_dev external-journal<br/> Note that external-journal must have been created with the same block size as the new file system. In addition, while there is support for attaching multiple file systems to a single external journal, the Linux ker&#8208; nel and e2fsck(8) do not currently support shared external journals yet.<br/> Instead of specifying a device name di&#8208; rectly, external-journal can also be speci&#8208; fied by either LABEL=label or UUID=UUID to locate the external journal by either the volume label or UUID stored in the ext2 su&#8208; perblock at the start of the journal. Use dumpe2fs(8) to display a journal device&#39;s volume label and UUID. See also the -L op&#8208; tion of tune2fs(8).<br/> Only one of the size or device options can be given for a file system.<br/> -l filename Read the bad blocks list from filename. Note that the block numbers in the bad block list must be generated using the same block size as used by mke2fs. As a re&#8208; sult, the -c option to mke2fs is a much simpler and less error-prone method of checking a disk for bad blocks be&#8208; fore formatting it, as mke2fs will automatically pass the correct parameters to the badblocks program.<br/> -L new-volume-label Set the volume label for the file system to new-volume- label. The maximum length of the volume label is 16 bytes.<br/> -m reserved-blocks-percentage Specify the percentage of the file system blocks re&#8208; served for the super-user. This avoids fragmentation, and allows root-owned daemons, such as syslogd(8), to continue to function correctly after non-privileged processes are prevented from writing to the file system. The default percentage is 5%.<br/> -M last-mounted-directory Set the last mounted directory for the file system. This might be useful for the sake of utilities that key off of the last mounted directory to determine where the file system should be mounted.<br/> -n Causes mke2fs to not actually create a file system, but display what it would do if it were to create a file system. This can be used to determine the location of the backup superblocks for a particular file system, so long as the mke2fs parameters that were passed when the file system was originally created are used again. (With the -n option added, of course!)<br/> -N number-of-inodes Overrides the default calculation of the number of in&#8208; odes that should be reserved for the file system (which is based on the number of blocks and the bytes-per-inode ratio). This allows the user to specify the number of desired inodes directly.<br/> -o creator-os Overrides the default value of the &quot;creator operating system&quot; field of the file system. The creator field is set by default to the name of the OS the mke2fs exe&#8208; cutable was compiled for.<br/> -O [^]feature[,...] Create a file system with the given features (file sys&#8208; tem options), overriding the default file system op&#8208; tions. The features that are enabled by default are specified by the base_features relation, either in the [defaults] section in the /etc/mke2fs.conf configuration file, or in the [fs_types] subsections for the usage types as specified by the -T option, further modified by the features relation found in the [fs_types] subsec&#8208; tions for the file system and usage types. See the mke2fs.conf(5) manual page for more details. The file system type-specific configuration setting found in the [fs_types] section will override the global default found in [defaults].<br/> The file system feature set will be further edited using either the feature set specified by this option, or if this option is not given, by the default_features rela&#8208; tion for the file system type being created, or in the [defaults] section of the configuration file.<br/> The file system feature set is comprised of a list of features, separated by commas, that are to be enabled. To disable a feature, simply prefix the feature name with a caret (&#39;^&#39;) character. Features with dependen&#8208; cies will not be removed successfully. The pseudo-file system feature &quot;none&quot; will clear all file system fea&#8208; tures.<br/> For more information about the features which can be set, please see the manual page ext4(5).<br/> -q Quiet execution. Useful if mke2fs is run in a script.<br/> -r revision Set the file system revision for the new file system. Note that 1.2 kernels only support revision 0 file sys&#8208; tems. The default is to create revision 1 file systems.<br/> -S Write superblock and group descriptors only. This is an extreme measure to be taken only in the very unlikely case that all of the superblock and backup superblocks are corrupted, and a last-ditch recovery method is de&#8208; sired by experienced users. It causes mke2fs to reini&#8208; tialize the superblock and group descriptors, while not touching the inode table and the block and inode bitmaps. The e2fsck program should be run immediately after this option is used, and there is no guarantee that any data will be salvageable. Due to the wide va&#8208; riety of possible options to mke2fs that affect the on- disk layout, it is critical to specify exactly the same format options, such as blocksize, fs-type, feature flags, and other tunables when using this option, or the file system will be further corrupted. In some cases, such as file systems that have been resized, or have had features enabled after format time, it is impossible to overwrite all of the superblocks correctly, and at least some file system corruption will occur. It is best to run this on a full copy of the file system so other op&#8208; tions can be tried if this doesn&#39;t work.<br/> -t fs-type Specify the file system type (i.e., ext2, ext3, ext4, etc.) that is to be created. If this option is not specified, mke2fs will pick a default either via how the command was run (for example, using a name of the form mkfs.ext2, mkfs.ext3, etc.) or via a default as defined by the /etc/mke2fs.conf file. This option controls which file system options are used by default, based on the fstypes configuration stanza in /etc/mke2fs.conf.<br/> If the -O option is used to explicitly add or remove file system options that should be set in the newly cre&#8208; ated file system, the resulting file system may not be supported by the requested fs-type. (e.g., &quot;mke2fs -t ext3 -O extent /dev/sdXX&quot; will create a file system that is not supported by the ext3 implementation as found in the Linux kernel; and &quot;mke2fs -t ext3 -O ^has_journal /dev/hdXX&quot; will create a file system that does not have a journal and hence will not be supported by the ext3 file system code in the Linux kernel.)<br/> -T usage-type[,...] Specify how the file system is going to be used, so that mke2fs can choose optimal file system parameters for that use. The usage types that are supported are de&#8208; fined in the configuration file /etc/mke2fs.conf. The user may specify one or more usage types using a comma separated list.<br/> If this option is is not specified, mke2fs will pick a single default usage type based on the size of the file system to be created. If the file system size is less than 3 megabytes, mke2fs will use the file system type floppy. If the file system size is greater than or equal to 3 but less than 512 megabytes, mke2fs(8) will use the file system type small. If the file system size is greater than or equal to 4 terabytes but less than 16 terabytes, mke2fs(8) will use the file system type big. If the file system size is greater than or equal to 16 terabytes, mke2fs(8) will use the file system type huge. Otherwise, mke2fs(8) will use the default file system type default.<br/> -U UUID Set the universally unique identifier (UUID) of the file system to UUID. The format of the UUID is a series of hex digits separated by hyphens, like this: &quot;c1b9d5a2-f162-11cf-9ece-0020afc76f16&quot;. The UUID para&#8208; meter may also be one of the following:<br/> clear clear the file system UUID<br/> random generate a new randomly-generated UUID<br/> time generate a new time-based UUID<br/> -v Verbose execution.<br/> -V Print the version number of mke2fs and exit.<br/> -z undo_file Before overwriting a file system block, write the old contents of the block to an undo file. This undo file can be used with e2undo(8) to restore the old contents of the file system should something go wrong. If the empty string is passed as the undo_file argument, the undo file will be written to a file named mke2fs-de&#8208; vice.e2undo in the directory specified via the E2FSPROGS_UNDO_DIR environment variable or the undo_dir directive in the configuration file.<br/> WARNING: The undo file cannot be used to recover from a power or system crash.<br/> ENVIRONMENT MKE2FS_SYNC If set to non-zero integer value, its value is used to determine how often sync(2) is called during inode table initialization.<br/> MKE2FS_CONFIG Determines the location of the configuration file (see mke2fs.conf(5)).<br/> MKE2FS_FIRST_META_BG If set to non-zero integer value, its value is used to determine first meta block group. This is mostly for de&#8208; bugging purposes.<br/> MKE2FS_DEVICE_SECTSIZE If set to non-zero integer value, its value is used to determine logical sector size of the device.<br/> MKE2FS_DEVICE_PHYS_SECTSIZE If set to non-zero integer value, its value is used to determine physical sector size of the device.<br/> MKE2FS_SKIP_CHECK_MSG If set, do not show the message of file system automatic check caused by mount count or check interval.<br/> AUTHOR This version of mke2fs has been written by Theodore Ts&#39;o &lt;tytso@mit.edu&gt;.<br/> AVAILABILITY mke2fs is part of the e2fsprogs package and is available from http://e2fsprogs.sourceforge.net.<br/> SEE ALSO mke2fs.conf(5), badblocks(8), dumpe2fs(8), e2fsck(8), tune2fs(8), ext4(5)<br/> E2fsprogs version 1.47.0 February 2023 MKE2FS(8)</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Journaling --> <h3 id="journal">Journaling</h3> <p> The <code>mke2fs</code> command will create an <code>ext4</code> <a href='https://www.kernel.org/doc/html/latest/filesystems/ext4/journal.html' target='_blank' rel="nofollow">journal</a> when creating an <code>ext4</code> file system. </p> <p class="alert rounded shadow"> There is no <code>mke2fs</code> option to specify that an <code>ext4</code> journal should be created; instead, an <code>ext4</code> journal is created by default. <br><br> Unfortunately, lots of the &lsquo;advice&rsquo; you can find on the interwebs advise people to issue options for the <code>mke2fs</code> command that create a journal &hellip; however, those commands create <code>ext3</code> journals, not <code>ext4</code> journals. <br><br> This is undesirable because <code>ext4</code> journals are better than <code>ext3</code> journals. </p> <!-- endregion --> <!-- #region UUID --> <h3 id="uuid">UUID</h3> <p> File systems are best mounted by specifying their <a href='https://www.linux.com/news/what-uuids-can-do-you/' target='_blank' rel="nofollow">UUID</a>. In contrast, specifying the drive to be mounted by their device name is problematic when device assignments shift as hardware is modified. </p> <p> When you clone a drive, you should also replicate the original UUIDs of the file systems that should be mounted. That allows you to replace the original drive with the clone without modifying the system. </p> <p> You can obtain the original file system UUID for <code>/dev/sdb5</code> whether or not it is mounted with the following incantation: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3377af28a3a2'><button class='copyBtn' data-clipboard-target='#id3377af28a3a2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lsblk -no UUID /dev/sdb5 <span class='unselectable'>64f5365e-edfc-4781-a285-9a053e6937fa </span></pre> </div> <!-- endregion --> <p> The above shows that the UUID for <code>/dev/sdb5</code> on my virtualized Ubuntu system was <code>64f5365e-edfc-4781-a285-9a053e6937fa</code>. </p> <p> Let&rsquo;s store the file system UUID in an environment variable called <code>UUID</code> for convenience in the next step: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfdee010555b9'><button class='copyBtn' data-clipboard-target='#idfdee010555b9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>UUID="$( lsblk -no UUID /dev/sdb5 )"<br> <span class='unselectable'>$ </span>echo $UUID <span class='unselectable'>64f5365e-edfc-4781-a285-9a053e6937fa </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Example --> <h3 id="ex">Example</h3> <p> As an example, let&rsquo;s consider the case of cloning the NVMe drive of an Ableton P3S to a larger NVMe drive. The <code>data</code> partition is the only partition that should be enlarged, and it should be enlarged to utilize all available storage capacity. </p> <p> Before we throw away the file system on <code>/dev/sdb5</code> we need to unmount it: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbf5d9462afca'><button class='copyBtn' data-clipboard-target='#idbf5d9462afca' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo umount /dev/sdb5</pre> </div> <!-- endregion --> <p> Let&rsquo;s use <code>mke2fs</code> to create an optimal file system in the <code>data</code> partition, mapped as <code>/dev/sdb5</code>. This file system will automatically be optimized to use all available space, with the same <code>UUID</code> as the original file system. </p> <p class="alert rounded shadow"> The <code>mke2fs</code> command is destructive. It places a new, formatted filesystem onto the specified partition. Typing the wrong device name will wipe all existing data in that device! </p> <p> We can use the <code>-n</code> option to do a <a href='https://www.merriam-webster.com/dictionary/dry%20run' target='_blank' rel="nofollow">dry run</a> to see what would happen: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id794904d7ceb2'><button class='copyBtn' data-clipboard-target='#id794904d7ceb2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo mke2fs \ -n \ -c -L data -O filetype,extents -t ext4 -U "$UUID" \ /dev/sdb5 <span class='unselectable'>mke2fs 1.47.0 (5-Feb-2023) /dev/sdb5 contains a ext4 file system labelled 'data' last mounted on /media/mslinn/data1 on Thu Jun 20 06:18:21 2024 Proceed anyway? (y,N) </span>y <span class='unselectable'>Creating filesystem with 969404928 4k blocks and 242352128 inodes Filesystem UUID: 64f5365e-edfc-4781-a285-9a053e6937fa Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 102400000, 214990848, 512000000, 550731776, 644972544 </span></pre> </div> <!-- endregion --> <p> This is the actual invocation: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8748415fc77'><button class='copyBtn' data-clipboard-target='#idc8748415fc77' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo mke2fs \ -c -L data -O filetype,extents -t ext4 -U "$UUID" \ /dev/sdb5 <span class='unselectable'>mke2fs 1.47.0 (5-Feb-2023) /dev/sdb5 contains a ext4 file system labelled 'data' last mounted on /media/mslinn/data1 on Thu Jun 20 06:18:21 2024 Proceed anyway? (y,N) </span>y <span class='unselectable'>Creating filesystem with 969404928 4k blocks and 242352128 inodes Filesystem UUID: 64f5365e-edfc-4781-a285-9a053e6937fa Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 102400000, 214990848, 512000000, 550731776, 644972544 </span></pre> </div> <!-- endregion --> <p> Now the <code>data</code> partition contains an optimized file system that is completely empty. We need to copy the files and directories from the <code>data</code> partition of the original drive to the empty <code>data</code> partition on the clone. </p> <!-- endregion --> <!-- #region Copying Files and Directories --> <h3 id="copy">Copying Files and Directories</h3> <!-- #region implicit --> <p> Partitions need to be mounted before files and directories can be copied to them, or from them. I inserted the original P3S NVMe drive into my second Sabrent NVMe / USB enclosure, and connected the enclosure to the computer. The VirtualBox VM for the Ubuntu instance I was working with already knew to take control of both Sabrent USB devices. </p> <p> Once plugged in, the virtualized Ubuntu instance running in VirtualBox presented me with all the file systems on both of the NVMe drives in Sabrent enclosures, including the file systems called <code>data</code> found on each of the NVMe drives. </p> <p> Clicking on each of the <code>data</code> partitions in the Ubuntu file manager mounted them. </p> <p> The <code>data</code> partition on the original drive (now mounted as <code>/dev/sdc5</code>) contained all the files and directories to be copied, and the <code>data</code> partition on the clone (still mounted as <code>/dev/sdb5</code>) was empty because we had just recreated its file system. </p> <!-- endregion --> <!-- #region Cut and Paste --> <h4 id="rsync">Cut and Paste</h4> <p> One way to copy all the files would be to just copy and paste. This is likely to require superuser privilege to copy some files or directories. Unfortunately, Ubuntu 20.04 removed the ability for a file manager such as <code>nautilus</code> or <code>thunar</code> to run with elevated permissions. Thus, you are likely to find that some or even most files copy fine, but the process terminates with an error. </p> <p> If you were able to perform the copy without incident, skip to the <a href='#done'>All Done</a> section. </p> <!-- endregion --> <h4 id="findmnt"><span class="code">Findmnt</span></h4> <p> If you prefer to use the command line instead of drag and drop, or if you used drag and drop, but an error occurred after copying some files, you should use the <code>rsync</code> command to complete the copy. </p> <p> Before you can use <code>rsync</code>, however, you need to know the mount points for the file systems in both <code>data</code> partitions. </p> <p> The <code>findmnt</code> command shows information about mounted file systems. Here is the <code>man</code> page: </p> <!-- #region pre man findmnt --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>man findmnt</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6711f2983f6e'><button class='copyBtn' data-clipboard-target='#id6711f2983f6e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>FINDMNT(8) System Administration FINDMNT(8)<br/> NAME findmnt - find a filesystem<br/> SYNOPSIS findmnt [options]<br/> findmnt [options] device|mountpoint<br/> findmnt [options] [--source] device [--target path|--mountpoint mountpoint]<br/> DESCRIPTION findmnt will list all mounted filesystems or search for a filesystem. The findmnt command is able to search in /etc/fstab, /etc/mtab or /proc/self/mountinfo. If device or mountpoint is not given, all filesystems are shown.<br/> The device may be specified by device name, major:minor numbers, filesystem label or UUID, or partition label or UUID. Note that findmnt follows mount(8) behavior where a device name may be interpreted as a mountpoint (and vice versa) if the --target, --mountpoint or --source options are not specified.<br/> The command-line option --target accepts any file or directory and then findmnt displays the filesystem for the given path.<br/> The command prints all mounted filesystems in the tree-like format by default. The default output, is subject to change. So whenever possible, you should avoid using default output in your scripts. Always explicitly define expected columns by using --output columns-list in environments where a stable output is required.<br/> The relationship between block devices and filesystems is not always one-to-one. The filesystem may use more block devices. This is why findmnt provides SOURCE and SOURCES (pl.) columns. The column SOURCES displays all devices where it is possible to find the same filesystem UUID (or another tag specified in fstab when executed with --fstab and --evaluate).<br/> OPTIONS -A, --all Disable all built-in filters and print all filesystems.<br/> -a, --ascii Use ascii characters for tree formatting.<br/> -b, --bytes Print the sizes in bytes rather than in a human-readable format.<br/> By default, the unit, sizes are expressed in, is byte, and unit prefixes are in power of 2^10 (1024). Abbreviations of symbols are exhibited truncated in order to reach a better readability, by exhibiting alone the first letter of them; examples: &quot;1 KiB&quot; and &quot;1 MiB&quot; are respectively exhibited as &quot;1 K&quot; and &quot;1 M&quot;, then omitting on purpose the mention &quot;iB&quot;, which is part of these abbreviations.<br/> -C, --nocanonicalize Do not canonicalize paths at all. This option affects the comparing of paths and the evaluation of tags (LABEL, UUID, etc.).<br/> -c, --canonicalize Canonicalize all printed paths.<br/> --deleted Print filesystems where target (mountpoint) is marked as deleted by kernel.<br/> -D, --df Imitate the output of df(1). This option is equivalent to -o SOURCE,FSTYPE,SIZE,USED,AVAIL,USE%,TARGET but excludes all pseudo filesystems. Use --all to print all filesystems.<br/> -d, --direction word The search direction, either forward or backward.<br/> -e, --evaluate Convert all tags (LABEL, UUID, PARTUUID, or PARTLABEL) to the corresponding device names for the SOURCE column. It&rsquo;s an unusual situation, but the same tag may be duplicated (used for more devices). For this purpose, there is SOURCES (pl.) column. This column displays by multi-line cell all devices where the tag is detected by libblkid. This option makes sense for fstab only.<br/> -F, --tab-file path Search in an alternative file. If used with --fstab, --mtab or --kernel, then it overrides the default paths. If specified more than once, then tree-like output is disabled (see the --list option).<br/> -f, --first-only Print the first matching filesystem only.<br/> -i, --invert Invert the sense of matching.<br/> -J, --json Use JSON output format.<br/> -k, --kernel Search in /proc/self/mountinfo. The output is in the tree-like format. This is the default. The output contains only mount options maintained by kernel (see also --mtab).<br/> -l, --list Use the list output format. This output format is automatically enabled if the output is restricted by the -t, -O, -S or -T option and the option --submounts is not used or if more that one source file (the option -F) is specified.<br/> -M, --mountpoint path Explicitly define the mountpoint file or directory. See also --target.<br/> -m, --mtab Search in /etc/mtab. The output is in the list format by default (see --tree). The output may include user space mount options.<br/> -N, --task tid Use alternative namespace /proc/&lt;tid&gt;/mountinfo rather than the default /proc/self/mountinfo. If the option is specified more than once, then tree-like output is disabled (see the --list option). See also the unshare(1) command.<br/> -n, --noheadings Do not print a header line.<br/> -O, --options list Limit the set of printed filesystems. More than one option may be specified in a comma-separated list. The -t and -O options are cumulative in effect. It is different from -t in that each option is matched exactly; a leading no at the beginning does not have global meaning. The &quot;no&quot; can used for individual items in the list. The &quot;no&quot; prefix interpretation can be disabled by &quot;+&quot; prefix.<br/> -o, --output list Define output columns. See the --help output to get a list of the currently supported columns. The TARGET column contains tree formatting if the --list or --raw options are not specified.<br/> The default list of columns may be extended if list is specified in the format +list (e.g., findmnt -o +PROPAGATION).<br/> --output-all Output almost all available columns. The columns that require --poll are not included.<br/> -P, --pairs Produce output in the form of key=&quot;value&quot; pairs. All potentially unsafe value characters are hex-escaped (\x&lt;code&gt;). See also option --shell.<br/> -p, --poll[=list] Monitor changes in the /proc/self/mountinfo file. Supported actions are: mount, umount, remount and move. More than one action may be specified in a comma-separated list. All actions are monitored by default.<br/> The time for which --poll will block can be restricted with the --timeout or --first-only options.<br/> The standard columns always use the new version of the information from the mountinfo file, except the umount action which is based on the original information cached by findmnt. The poll mode allows using extra columns:<br/> ACTION mount, umount, move or remount action name; this column is enabled by default<br/> OLD-TARGET available for umount and move actions<br/> OLD-OPTIONS available for umount and remount actions<br/> --pseudo Print only pseudo filesystems.<br/> --shadow Print only filesystems over-mounted by another filesystem.<br/> -R, --submounts Print recursively all submounts for the selected filesystems. The restrictions defined by options -t, -O, -S, -T and --direction are not applied to submounts. All submounts are always printed in tree-like order. The option enables the tree-like output format by default. This option has no effect for --mtab or --fstab.<br/> -r, --raw Use raw output format. All potentially unsafe characters are hex-escaped (\x&lt;code&gt;).<br/> --real Print only real filesystems.<br/> -S, --source spec Explicitly define the mount source. Supported specifications are device, maj:min, LABEL=label, UUID=uuid, PARTLABEL=label and PARTUUID=uuid.<br/> -s, --fstab Search in /etc/fstab. The output is in the list format (see --list).<br/> -T, --target path Define the mount target. If path is not a mountpoint file or directory, then findmnt checks the path elements in reverse order to get the mountpoint (this feature is supported only when searching in kernel files and unsupported for --fstab). It&rsquo;s recommended to use the option --mountpoint when checks of path elements are unwanted and path is a strictly specified mountpoint.<br/> -t, --types list Limit the set of printed filesystems. More than one type may be specified in a comma-separated list. The list of filesystem types can be prefixed with no to specify the filesystem types on which no action should be taken. For more details see mount(8).<br/> --tree Enable tree-like output if possible. The options is silently ignored for tables where is missing child-parent relation (e.g., fstab).<br/> --shadowed Print only filesystems over-mounted by another filesystem.<br/> -U, --uniq Ignore filesystems with duplicate mount targets, thus effectively skipping over-mounted mount points.<br/> -u, --notruncate Do not truncate text in columns. The default is to not truncate the TARGET, SOURCE, UUID, LABEL, PARTUUID, PARTLABEL columns. This option disables text truncation also in all other columns.<br/> -v, --nofsroot Do not print a [/dir] in the SOURCE column for bind mounts or btrfs subvolumes.<br/> -w, --timeout milliseconds Specify an upper limit on the time for which --poll will block, in milliseconds.<br/> -x, --verify Check mount table content. The default is to verify /etc/fstab parsability and usability. It&rsquo;s possible to use this option also with --tab-file. It&rsquo;s possible to specify source (device) or target (mountpoint) to filter mount table. The option --verbose forces findmnt to print more details.<br/> --verbose Force findmnt to print more information (--verify only for now).<br/> --vfs-all When used with VFS-OPTIONS column, print all VFS (fs-independent) flags. This option is designed for auditing purposes to list also default VFS kernel mount options which are normally not listed.<br/> -y, --shell The column name will be modified to contain only characters allowed for shell variable identifiers. This is usable, for example, with --pairs. Note that this feature has been automatically enabled for --pairs in version 2.37, but due to compatibility issues, now it&rsquo;s necessary to request this behavior by --shell.<br/> -h, --help Display help text and exit.<br/> -V, --version Print version and exit.<br/> EXIT STATUS The exit value is 0 if there is something to display, or 1 on any error (for example if no filesystem is found based on the user&rsquo;s filter specification, or the device path or mountpoint does not exist).<br/> ENVIRONMENT LIBMOUNT_FSTAB=&lt;path&gt; overrides the default location of the fstab file<br/> LIBMOUNT_MTAB=&lt;path&gt; overrides the default location of the mtab file<br/> LIBMOUNT_DEBUG=all enables libmount debug output<br/> LIBSMARTCOLS_DEBUG=all enables libsmartcols debug output<br/> LIBSMARTCOLS_DEBUG_PADDING=on use visible padding characters.<br/> EXAMPLES findmnt --fstab -t nfs Prints all NFS filesystems defined in /etc/fstab.<br/> findmnt --fstab /mnt/foo Prints all /etc/fstab filesystems where the mountpoint directory is /mnt/foo. It also prints bind mounts where /mnt/foo is a source.<br/> findmnt --fstab --target /mnt/foo Prints all /etc/fstab filesystems where the mountpoint directory is /mnt/foo.<br/> findmnt --fstab --evaluate Prints all /etc/fstab filesystems and converts LABEL= and UUID= tags to the real device names.<br/> findmnt -n --raw --evaluate --output=target LABEL=/boot Prints only the mountpoint where the filesystem with label &quot;/boot&quot; is mounted.<br/> findmnt --poll --mountpoint /mnt/foo Monitors mount, unmount, remount and move on /mnt/foo.<br/> findmnt --poll=umount --first-only --mountpoint /mnt/foo Waits for /mnt/foo unmount.<br/> findmnt --poll=remount -t ext3 -O ro Monitors remounts to read-only mode on all ext3 filesystems.<br/> AUTHORS Karel Zak &lt;kzak@redhat.com&gt;<br/> SEE ALSO fstab(5), mount(8)<br/> REPORTING BUGS For bug reports, use the issue tracker at https://github.com/util-linux/util-linux/issues.<br/> AVAILABILITY The findmnt command is part of the util-linux package which can be downloaded from Linux Kernel Archive &lt;https://www.kernel.org/pub/linux/utils/util-linux/&gt;.<br/> util-linux 2.39.3 2023-12-01 FINDMNT(8)</pre> </div> <!-- endregion --> <p> The following incantation shows the mount points for the <code>data</code> partitions: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0561ab5ef22a'><button class='copyBtn' data-clipboard-target='#id0561ab5ef22a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>findmnt -lo source,target | grep data <span class='unselectable'>/dev/sdb5 /media/mslinn/data /dev/sdc5 /media/mslinn/data1 </span></pre> </div> <!-- endregion --> <p> We now know that <b>for my Ubuntu instance</b>, the files and directories mounted at <code>/media/mslinn/data1/</code> need to be copied to <code>/media/mslinn/data/</code>. </p> <!-- endregion --> <!-- #region Rsync --> <h4 id="rsync"><span class="code">Rsync</span></h4> <p> <code>Rsync</code> can resume a large copy that failed earlier, without recopying files that were successfully copied. You can rerun <code>rsync</code> as many times as you like without fear of duplicating information. </p> <p> This is the general form of the <code>rsync</code> command that you should use. Instead of <code>source_directory</code> and <code>destination_directory</code>, specify the mount points for the two <code>data</code> directories. </p> <p class="alert rounded shadow"> Be sure to append a trailing slash to both directories when using <code>rsync</code>. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id116ff96c14bb'><button class='copyBtn' data-clipboard-target='#id116ff96c14bb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>rsync -axHAXS --numeric-ids --info=progress2 \ source_directory<span style='background:yellow;'>/</span> destination_directory<span style='background:yellow;'>/</span></pre> </div> <!-- endregion --> <p> The <code>rsync</code> options I used are: </p> <table class='table'> <tr> <td><code>-a</code></td> <td>Copy all files, with permissions, etc.</td> </tr> <tr> <td><code>-x</code></td> <td>Stay on one file system</td> </tr> <tr> <td><code>-H</code> </td> <td>Preserve hard links</td> </tr> <tr> <td><code>-A</code></td> <td>Preserve ACLs/permissions</td> </tr> <tr> <td><code>-X</code></td> <td>Preserve extended attributes</td> </tr> <tr> <td><code>-S</code></td> <td>Handle sparse files</td> </tr> <tr> <td nowrap><code>--numeric-ids</code></td> <td>Transfer numeric group and user IDs, rather than using user and group names and mapping them at both ends</td> </tr> <tr> <td nowrap><code>--info=progress2</code></td> <td>shows statistics based on the whole transfer, rather than individual files</td> </tr> </table> <p> We can now write the proper incantation to run <code>rsync</code> to copy from <code>/media/mslinn/data1/</code> to <code>/media/mslinn/data/</code>. </p> <p class="alert rounded shadow"> Note that <code>sudo</code> is used to ensure that all files and directories are copied, without problems from insufficient permissions. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1bcff96ee931'><button class='copyBtn' data-clipboard-target='#id1bcff96ee931' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span style='background:yellow;'>sudo</span> rsync -axHAXS --numeric-ids --info=progress2 \ /media/mslinn/data1<span style='background:yellow;'>/</span> /media/mslinn/data<span style='background:yellow;'>/</span></pre> </div> <!-- endregion --> <p> The copy took 25 seconds on my system. </p> <!-- endregion --> <!-- endregion --> <!-- #region All Done --> <h2 id="done">All Done</h2> <p> Unmount both <code>data</code> partitions, unplug the Sabrent enclosures, and remove the NVMe drives. Store the original NVMe drive somewhere safe, and install the clone into the computer that it belongs to; in my case, this was the P3S. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.cur" type="image/x-icon"> <img alt='Keep your NVMe drive safe' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/p3s_medicine_crop.png" style='width: 100%; ' title='Keep your NVMe drive safe' /> </picture> <figcaption class='imgFigCaption '> Keep your NVMe drive safe </figcaption> </figure> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> We did what we set out to do: resized the partition and its file system. </p> <!-- endregion --> Clonezilla 2024-06-12T00:00:00-04:00 https://mslinn.github.io/blog/2024/06/12/clonezilla <!-- #region intro --> <p> Although other OSes are mentioned, this article focuses on using Clonezilla to clone drives used by Linux systems. </p> <h2 id="miniseries">Mini-Series</h2> <p> This article can be read standalone; however, is part of a mini-series that discusses how to increase the storage capacity and performance of the Ableton Push 3 Standalone (P3S). If you own a P3S, or are thinking of purchasing one, you might want to first <a href='/av_studio/570-ableton-push-standalone.html'>read my review</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='/av_studio/570-ableton-push-standalone.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/p3s_top.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/p3s_top.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/p3s_top.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/p3s_top.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/p3s_top.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/p3s_top.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/p3s_top.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/p3s_top.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/p3s_top.png" style='width: 100%; ' /> </picture> </a> </div> <p> The articles in this mini-series are: </p> <ol> <li><a href='/av_studio/570-ableton-push-nvme.html'>Ableton Push 3 NVMe Replacement</a></li> <li><a href='/blog/2024/07/05/cooling.html'>Thermal Control</a></li> <li><a href='/blog/2024/06/11/virtualbox.html'>VirtualBox</a></li> <li><a href='/blog/2024/06/12/clonezilla.html'>Clonezilla</a></li> <li><a href='/blog/2024/06/12/supersize-fs.html'>Supersizing a Partition and Its File System</a></li> </ol> <!-- endregion --> <!-- #region Nomenclature --> <h2 id="nomenclature">Nomenclature</h2> <p> An <i>original</i> is the drive that needs to be cloned, and a <i>clone</i> is a drive containing an identical copy of the original drive&rsquo;s contents. </p> <div class='imgWrapper imgFlex right' style='width: 35%; '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/snapshot.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/snapshot.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/snapshot.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/snapshot.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/snapshot.png" style='width: 100%; ' /> </picture> </div> <p> A <i>snapshot</i> is a read-only copy of an entire file system and all the files contained in the file system. The contents of each snapshot reflect the state of the file system at the time the snapshot was created. It is easy to navigate through each snapshot as if it were still active. The directories, folders and files will appear as they were at the time that the snapshot was created. </p> <p> By default, Linux systems are not configured to use snapshots; for example, Ableton Push 3 Standalone devices do not use snapshots. It is not difficult to set up snapshots using <a href='https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)' target='_blank' rel="nofollow"><code>LVM</code></a>; perhaps I will write an article about how to do that one day. </p><!-- endregion --> <!-- #region About Clonezilla --> <h2 id="about">About Clonezilla</h2> <p> <a href='https://clonezilla.org/' target='_blank' rel="nofollow">Clonezilla</a> can clone just about any type of Linux hard drive. </p> <div class='quote'> <div class='quoteText clearfix'> <p>Many file systems are supported:</p> <ol> <li><code>ext2</code>, <code>ext3</code>, <code>ext4</code>, <code>reiserfs</code>, <code>reiser4</code>, <code>xfs</code>, <code>jfs</code>, <code>btrfs</code>, <code>f2fs</code> and <code>nilfs2</code> of GNU/Linux</li> <li><code>FAT12</code>, <code>FAT16</code>, <code>FAT32</code>, <code>exFAT</code> and <code>NTFS</code> of MS Windows</li> <li><code>HFS+</code> and <code>APFS</code> of macOS</li> <li><code>UFS</code> of FreeBSD, NetBSD, and OpenBSD</li> <li>Minix</li> <li><code>VMFS3</code> and <code>VMFS5</code> of VMWare ESX</li> </ol> <p> Therefore, you can clone GNU/Linux, Microsoft Windows, Intel-based macOS, FreeBSD, NetBSD, OpenBSD, Minix, VMWare ESXi and Chrome OS/Chromium OS, no matter if it's 32-bit (x86) or 64-bit (x86-64) OS. For these file systems, only used blocks in partitions are saved and restored by <a href='http://partclone.org/' target='_blank' rel="nofollow">Partclone</a>. For unsupported file systems, sector-to-sector copy is done by <code>dd</code> in Clonezilla. </p> </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://clonezilla.org/' rel='nofollow' target='_blank'>What is Clonezilla?</a></div> </div> <p> Clonezilla works with MBR- and GPT-formatted hard drives. However, I am only going to discuss GPT-formatted drives. There is no point in discussing obsolete technology unless a <a href='/expertArticles/'>legal firm is paying me for my time</a>. </p> <!-- endregion --> <!-- #region GPT Drives --> <h2 id="gpt">GPT Drives</h2> <p> The <a href='https://en.wikipedia.org/wiki/GUID_Partition_Table' target='_blank' rel="nofollow">GUID Partition Table (GPT)</a> is a standard for the layout of the partition tables on a physical hard disk. It forms a part of the <a href='https://en.wikipedia.org/wiki/UEFI' target='_blank' rel="nofollow">Unified Extensible Firmware Interface (UEFI) standard</a>, which Microsoft calls EFI. GPT allows for a maximum disk and partition size of 9.4 ZB (a zetabyte is a billion terabytes). </p> <p> The bootloader for GPT drives is stored in the EFI system partition, formatted as <code>FAT16</code> or <code>FAT32</code>, and resides at the beginning of the disk &mdash; <b>Partition 1</b> in the following diagram. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.easeus.com/partition-master/remove-gpt-disk-partition.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/gpt-disk-layout.cur" type="image/x-icon"> <img alt='From 'How to Remove, Delete or Format GPT Disk Partition'' class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/gpt-disk-layout.png" style='width: 100%; ' title='From 'How to Remove, Delete or Format GPT Disk Partition'' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.easeus.com/partition-master/remove-gpt-disk-partition.html" target='_blank' > From 'How to Remove, Delete or Format GPT Disk Partition' </a> </figcaption> </figure> </div> <p> As illustrated by the above diagram, drives contain a primary partition table, at least one partition, and a backup partition table. You could think of the two partition tables as bookends and the partitions as books between them, with the introductory book (the partition dedicated to containing the bootloader) in first place. </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/bookends.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/bookends.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/bookends.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/bookends.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/bookends.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/bookends.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/bookends.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/bookends.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/bookends.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/bookends.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/bookends.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/bookends.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/bookends.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/bookends.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/bookends.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/bookends.cur" type="image/x-icon"> <img class="imgImg " src="/av_studio/images/ableton/push/clone/bookends.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region Overview --> <h2 id="overview">Clonezilla Overview</h2> <p> Clonezilla can clone entire drives, including the two GPT partition tables that bookend all the partitions in between, or it can just clone specific partitions. </p> <p> The Clonezilla procedure is the same, no matter what size file system you start with. The process after using Clonezilla changes, depending on the scenario. To summarize: </p> <ol> <li> Once Clonezilla finishes cloning a drive, all of the partitions in the clone are identical to the partitions in the original drive; any extra storage capacity is not used. </li> <li> If the cloned drive is the same size as the original, you are done. </li> <li> <div class='imgWrapper imgFlex right' style='width: 25%; '> <a href='/blog/2024/06/12/supersize-fs.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/supersize_me.png" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/supersize_me.png" style='width: 100%; ' /> </picture> </a> </div> As I discuss in <a href='/blog/2024/06/12/supersize-fs.html'>Supersizing a Partition and Its File System</a>, tiny Linux file systems (those with 1K blocks) should be recreated larger, not enlarged, and the same is true for normal-sized file systems that are being migrated to much larger partitions. Normally, only one of the partitions on a drive should be enlarged, so this article makes that assumption. <br><br> To recreate a file system in a partition: <ol> <li> The file system in the partition being enlarged should be deleted. </li> <li> The partition should be enlarged. </li> <li> A new file system should be created in the enlarged partition. The creation process heavily optimizes the file system for the partition it is placed in. </li> <li> The files from the partition on the original drive should be copied to the new file system on the cloned drive. </li> </ol> </li> </ol> <!-- endregion --> <!-- #region Usage --> <h2 id="usage">Usage</h2> <p> The Clonezilla installation process depends on how you want to use it. I describe the installation processes separately for each scenario discussed in this article. </p> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/virtualbox_logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/virtualbox_logo.png" style='width: 100%; ' /> </picture> </div> <p> I would like to encourage you to get in the habit of running Clonezilla in a VirtualBox virtual machine, as I did when I wrote this article. Unless you want to clone the system drive in the machine that you prefer to run VirtualBox on, this is generally your best option for running Clonezilla. </p> <p> You have many other options as well. You can run Clonezilla from a normal Ubuntu system if you just want to clone a drive that is not mounted or if your file system supports snapshots. To clone a system disk that does not support snapshots, the drives on the disk will first have to be attached to the OS as a <code>/dev</code> device but not mounted, then the virtualized system should be booted, or an ISO system image on a CD / DVD / SSD should be booted. Once Ubuntu is running, Clonezilla can be run as a console text program in a terminal window. </p> <p> I was unable to use Clonezilla from WSL because I did not know how to map USB drives to the WSL VM so Clonezilla could work its magic. Maybe one day I will fight through the issues involved, but there is no reason for me to work that hard for no benefit. </p> <!-- endregion --> <!-- #region Cloning an Unmounted Drive --> <h2 id="unm">Cloning an Unmounted Drive</h2> <!-- #region Installation --> <h3 id="i2">Installation</h3> <p> If you want to clone unmounted drives, install Clonezilla on an Ubuntu system like this: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1d1a38dea115'><button class='copyBtn' data-clipboard-target='#id1d1a38dea115' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install clonezilla</pre> </div> <!-- endregion --> <p> Once installed, read the <code>clonezilla</code> help message: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id113e05638606'><button class='copyBtn' data-clipboard-target='#id113e05638606' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo clonezilla -h <span class='unselectable'>/usr/sbin/clonezilla: -h: invalid option Usage: Run clonezilla: /usr/sbin/clonezilla [OPTION] -l, --language INDEX Set the language to be shown by index number: [0|en_US.UTF-8]: English, [2|zh_TW.UTF-8]: Traditional Chinese (UTF-8, Unicode) - Taiwan [a|ask]: Prompt to ask the language index This option is for backward compatibility. It&#39;s recommended to use locales to assign that. -d0, --dialog Use dialog -d1, --Xdialog Use Xdialog -d2, --whiptail Use whiptail -d3, --gdialog Use gdialog -d4, --kdialog Use kdialog -k, --skip-ocs-prep-repo Skip preparing the clonezilla image home directory (assume it&#39;s ready), this is specially for device &lt;-&gt; image clone. -p, --postaction [choose|poweroff|reboot|command|CMD] When save/restoration finishs, choose action in the client, poweroff, reboot (default), in command prompt or run CMD -s, --skip-lite-menu Do not show live-server and lite-client in the dialog menu. Ex. /usr/sbin/clonezilla -l en </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Launch Clonezilla --> <h3 id="lc">Launch Clonezilla</h3> <p> Now launch <code>clonezilla</code>: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6bc8a7880b90'><button class='copyBtn' data-clipboard-target='#id6bc8a7880b90' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo clonezilla</pre> </div> <!-- endregion --> <p> If you are cloning an unmounted drive, you can skip the next section and continue to <a href='#rc'>Running Clonezilla</a>. </p> <!-- endregion --> <!-- endregion --> <!-- #region Cloning a System Drive --> <h2 id="sysclone">Cloning a System Drive</h2> <!-- #region implicit --> <p> <i>This section actually describes how to clone any Linux-compatible drive, not just system drives.</i> </p> <p> Generally speaking, a system cannot clone itself unless it has the ability to snapshot the drives that you want to back up. Without a snapshot, cloning a system drive requires Clonezilla to be booted, such that: </p> <ol> <li>The drive to be cloned is connected as a <code>/dev/</code> Linux device</li> <li>None of the file systems in any of the drive's partitions may be mounted</li> </ol> <!-- endregion --> <!-- #region Installation in a VirtualBox VM --> <h3 id="i2">Installation in a VirtualBox VM</h3> <p> Clonezilla is available as a package that includes a minimal Linux operating system. After the dedicated Linux OS boots, it runs Clonezilla. </p> <p> You can install Clonezilla on a CD, DVD, or SSD drive. It was more convenient for me to run Clonezilla in a VirtualBox virtual machine (VM) instead of running on bare metal using a CD or SSD drive. </p> <p> I downloaded the <a href='https://clonezilla.org/downloads/download.php?branch=stable' target='_blank' rel="nofollow">ISO image for standalone Clonezilla</a> for the <code>amd64</code> architecture. This will run on physical Windows desktops, Mac and Linux desktop computers that can boot from ISO images, CDs, DVDs and SSD drives. It will also run on a virtual 64-bit x86 computer. </p> <p> Then I started VirtualBox as described in my <a href='/blog/2024/06/11/virtualbox.html'>VirtualBox Setup</a> article and created a new virtual machine using the Clonezilla ISO image. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_1.png" style='width: 100%; ' /> </picture> </div> <p> Notice that Clonezilla does not require a virtual hard drive; it strictly runs from the downloaded ISO image. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_2.png" style='width: 100%; ' /> </picture> </div> <p> I launched the Clonezilla VM, and it instantly booted the Linux setup program. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_3.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_3.png" style='width: 100%; ' /> </picture> </div> <p> The Linux setup program asked me to select the keyboard layout I was using, and I pressed <kbd>Enter</kbd> to accept the default <code>en_US</code> layout. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_4.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_4.png" style='width: 100%; ' /> </picture> </div> <p> Yes, I am sure. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_5.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_5.png" style='width: 100%; ' /> </picture> </div> <p> Once Linux is configured, it is time to run the Clonezilla program. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_6.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- endregion --> <!-- #region Running Clonezilla --> <h2 id="rc">Running Clonezilla</h2> <p> Clonezilla uses different background colors when running from a command line, and when running from an ISO image. Otherwise, the program is exactly the same. </p> <p> The next two screenshots show the difference between Clonezilla running from a command-line, and when running from an ISO image. Both of the screens accomplish the same thing: defining the mode of operation. </p> <div class="rounded shadow" style="border: thin solid grey; padding: 1em; margin-bottom: 1em;"> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.cur" type="image/x-icon"> <img class="imgImg " src="/av_studio/images/ableton/push/clone/clonezilla_7_standalone.png" style='width: 100%; ' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_6b.cur" type="image/x-icon"> <img class="imgImg " src="/av_studio/images/ableton/push/clone/clonezilla_6b.png" style='width: 100%; ' /> </picture> </div> </div> <p> I wanted to copy from one physical disk to another, so I selected the second choice, <code>device-device</code>, using the up- and down-arrow keys, then pressing <kbd>Enter</kbd>. </p> <p> In order for the VM to 'see' the NVMe drives, I right-clicked on the little rocket icon at the bottom of the VirtualBox window. This displayed all the USB devices attached to the Windows computer. I then enabled the two <b>Sabrent [2001]</b> USB devices, which made them exclusively available to the VM running Clonezilla. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_7.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_7.png" style='width: 100%; ' /> </picture> </div> <p> Now I selected <code>disk_to_local_disk</code> and pressed the <kbd>Enter</kbd> key. </p> <p> After several long minutes, I was presented with the two USB drives and was asked to select the source drive to clone, which was of course the 256 GB NVMe. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_8.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_8.png" style='width: 100%; ' /> </picture> </div> <P> After pressing the <kbd>Enter</kbd> key, I had to wait a while once again while Clonezilla laboriously enumerated all available drives. Eventually, it displayed the 4 TB drive as the only possible candidate for the destination, which was correct. I pressed the <kbd>Enter</kbd> key once more. </P> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_9.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_9.png" style='width: 100%; ' /> </picture> </div> <p> Another long pause ensued, and then I was presented with this screen: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_10.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_10.png" style='width: 100%; ' /> </picture> </div> <p> I pressed <kbd>Enter</kbd> and soon saw: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_11.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_11.png" style='width: 100%; ' /> </picture> </div> <p> I just want to resize the <code>data</code> partition, so I selected the first option, <code>-k0</code>. I will resize the partition later using <code>gparted</code>. </p> <p> I will decide what to do later, so I picked the first option, <code>-p choose</code>. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_12.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_12.png" style='width: 100%; ' /> </picture> </div> <p> A helpful hit was displayed in case I need to run this command again. The information was presented in green text over a black background at the bottom of the window. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_13.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_13.png" style='width: 100%; ' /> </picture> </div> <p> The small blue LEDs on both Sabrent enclosures flashed for several minutes. Finally, I was asked twice if I wanted the contents of <code>sda</code> (the new 4 TB drive) to be overwritten. Yes! </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_14.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_14.png" style='width: 100%; ' /> </picture> </div> <p> <code>/dev/sda3</code> had a file with errors. I will run <code>fsck</code> on it shortly. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_15.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_15.png" style='width: 100%; ' /> </picture> </div> <p> I left Clonezilla running overnight. When I awoke, it had completed: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_16.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_16.png" style='width: 100%; ' /> </picture> </div> <p> I turned off the Clonezilla VM. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/clonezilla_17.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/clonezilla_17.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region Store the Original NVMe --> <h2 id="store">Store the Original NVMe</h2> <p> I unplugged the original 256 GB NVMe drive and placed it into an empty prescription bottle, then stored the bottle with my extra drives. Small spice bottles also work well for storing NVMe drives. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/clone/p3s_medicine_crop.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/clone/p3s_medicine_crop.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region Clone Verification --> <h2 id="verify">Clone Verification</h2> <p> Now we can verify that the cloned drive works by putting it into the system that the original drive came from and verifying that the system still works. </p> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> If the drive containing the clone has the same storage capacity as the original drive, you are done. </p> <!-- endregion --> <!-- #region Next: Resizing --> <h2 id="next">Next: Resizing</h2> <p> If the drive containing the clone is larger or smaller than the original drive, at least one of the partitions on the clone will need to be resized. Remove the cloned drive from the system, place it back into the Sabrent NVMe / USB enclosure, and reattach the enclosure to the computer for further work. Please continue on to read <a href='/blog/2024/06/13/supersize-fs.html'>Supersizing a Partition and Its File System</a> for more information. </p> <!-- endregion --> <!-- endregion --> VirtualBox 2024-06-11T00:00:00-04:00 https://mslinn.github.io/blog/2024/06/11/virtualbox <!-- #region intro --> <p> This article describes how I set up Ubuntu Desktop 24.04 in a VirtualBox VM. </p> <h2 id="miniseries">Mini-Series</h2> <p> This article can be read standalone; however, is part of a mini-series that discusses how to increase the storage capacity and performance of the Ableton Push 3 Standalone (P3S). If you own a P3S, or are thinking of purchasing one, you might want to first <a href='/av_studio/570-ableton-push-standalone.html'>read my review</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='/av_studio/570-ableton-push-standalone.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/av_studio/images/ableton/push/p3s_top.svg" type="image/svg"> <!---<source srcset="/av_studio/images/ableton/push/p3s_top.avif" type="image/avif">--> <source srcset="/av_studio/images/ableton/push/p3s_top.webp" type="image/webp"> <source srcset="/av_studio/images/ableton/push/p3s_top.apng" type="image/apng"> <source srcset="/av_studio/images/ableton/push/p3s_top.png" type="image/png"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.jfif" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjpeg" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.pjp" type="image/jpeg"> <source srcset="/av_studio/images/ableton/push/p3s_top.gif" type="image/gif"> <source srcset="/av_studio/images/ableton/push/p3s_top.tif" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.tiff" type="image/tiff"> <source srcset="/av_studio/images/ableton/push/p3s_top.bmp" type="image/bmp"> <source srcset="/av_studio/images/ableton/push/p3s_top.ico" type="image/x-icon"> <source srcset="/av_studio/images/ableton/push/p3s_top.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/av_studio/images/ableton/push/p3s_top.png" style='width: 100%; ' /> </picture> </a> </div> <p> The articles in this mini-series are: </p> <ol> <li><a href='/av_studio/570-ableton-push-nvme.html'>Ableton Push 3 NVMe Replacement</a></li> <li><a href='/blog/2024/07/05/cooling.html'>Thermal Control</a></li> <li><a href='/blog/2024/06/11/virtualbox.html'>VirtualBox</a></li> <li><a href='/blog/2024/06/12/clonezilla.html'>Clonezilla</a></li> <li><a href='/blog/2024/06/12/supersize-fs.html'>Supersizing a Partition and Its File System</a></li> </ol> <!-- endregion --> <!-- #region VirtualBox --> <h2 id="vb">VirtualBox</h2> <p> VirtualBox allows you to run another operating system within your computer&rsquo;s operating system. VirtualBox is available for Windows, Macintosh, Linux and other OSes. It can virtualize Windows, Linux, <a href='https://forums.virtualbox.org/viewtopic.php?t=92649&sid=b459106f6da250c0d79e573e3ee823b7' target='_blank' rel="nofollow">macOS</a>, Solaris, and the BSD UNIX work-alikes. </p> <p> I often use an Ubuntu Desktop instance in a <a href='https://www.virtualbox.org/' target='_blank' rel="nofollow">VirtualBox</a> VM. It is my default Ubuntu instance for general-purpose work. </p> <!-- endregion --> <!-- #region Terminology --> <h2 id="terminology">Terminology</h2> <dl> <dt>Host</dt> <dd>This is the OS of the physical computer on which Oracle VM VirtualBox was installed.</dd> <dt>Guest</dt> <dd>This is the OS of the physical computer on which Oracle VM VirtualBox was installed.</dd> <dt>Virtual machine (VM)</dt> <dd> This is the special environment that VirtualBox creates for your guest OS while it is running. In other words, you run your guest OS in a VM. </dd> </dl> <!-- endregion --> <!-- #region Installation --> <h2 id="install">Installation</h2> <p> I installed VirtualBox and the <a href='https://blogs.oracle.com/virtualization/post/friday-spotlight-virtualbox-extension-pack-guest-additions' target='_blank' rel="nofollow">VirtualBox Extension Pack</a> according to <a href='https://www.virtualbox.org/manual/ch04.html' target='_blank' rel="nofollow">these in&shy;struc&shy;tions</a>. </p> <p> I then installed Ubuntu Desktop 24.04 as a guest in a new VirtualBox VM. The steps are: </p> <ol> <li>Download the <a href='https://ubuntu.com/download/desktop' target='_blank' rel="nofollow">Ubuntu Desktop 24.04</a> ISO file.</li> <li>Open VirtualBox and click on the <kbd>New</kbd> button to create a new virtual machine.</li> <li>Name your virtual machine and select <b>Linux</b> as the type and <b>Ubuntu (64-bit)</b> as the version.</li> <li>Allocate at least 2 GB of RAM to the virtual machine; 4 GB is better.</li> <li>Choose <b>Create a virtual hard disk now</b> and click <kbd>Create</kbd>.</li> <li>Select <b>VDI (VirtualBox Disk Image)</b> as the hard disk file type.</li> </ol> 7 . Choose " Dynamically allocated " for the storage type . 8 . Allocate at least 20 GB of storage space for the virtual machine . 9 . Click on the " Start " button to launch the virtual machine . 10 . In the " Select start - up disk " window , click on the folder icon and browse for the Ubuntu 24.04 ISO file you downloaded . 11 . Click " Start " to begin the installation process . 12 . Follow the on - screen instructions to complete the installation . <!-- endregion --> <!-- #region VirtualBox Guest Additions --> <h2 id="vbga">VirtualBox Guest Additions</h2> <p> A third item needs to be installed in the guest OS, the VirtualBox Guest Additions. It is included in the VirtualBox download and provides: </p> <ul> <li>Seamless mouse support</li> <li>Shared folders</li> <li>Better video support</li> <li>Seamless windows</li> <li>Generic host/guest communication channels</li> <li>Time synchronization</li> <li>Shared clipboard</li> <li>Automated logins</li> </ul> <p> For Windows, the VirtualBox Guest Additions are installed at <code>%ProgramFiles%\Oracle\VirtualBox\VBoxGuestAdditions.iso</code>. For Mac OS X hosts, this file is provided in the VirtualBox application bundle. </p> <!-- endregion --> <!-- #region Installation --> <h3 id="vgai">Installation</h3> <p> Before installing the VirtualBox Guest Additions in a guest Linux instance, you should install <code>gcc</code>, <code>make</code> and <code>perl</code>. Launch the virtual machine containing the guest Ubuntu image and enter the following at a shell prompt: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id88015c08c3ea'><button class='copyBtn' data-clipboard-target='#id88015c08c3ea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install build-essential gcc make perl dkms</pre> </div> <!-- endregion --> <p> Now reboot the guest Ubuntu instance. </p> <p> Each time a VirtualBox Ubuntu instance boots, the VirtualBox Guest Additions CD-ROM image automounts at <code>/media/$USER/VBox_GAs_7.0.18/</code>. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb3f47fb8f32'><button class='copyBtn' data-clipboard-target='#idcb3f47fb8f32' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls /media/$USER/VBox_GAs_7.0.18/ <span class='unselectable'>AUTORUN.INF runasroot.sh VBoxSolarisAdditions.pkg autorun.sh TRANS.TBL VBoxWindowsAdditions-amd64.exe cert VBoxDarwinAdditions.pkg VBoxWindowsAdditions.exe NT3x VBoxDarwinAdditionsUninstall.tool VBoxWindowsAdditions-x86.exe OS2 VBoxLinuxAdditions.run windows11-bypass.reg </span><br></pre> </div> <!-- endregion --> <p> To install the <a href='https://www.virtualbox.org/manual/ch04.html' target='_blank' rel="nofollow">VirtualBox Guest Additions</a> in the guest Ubuntu instance, run <code>VBoxLinuxAdditions.run</code>, and then reboot, as follows: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7a8832e08439'><button class='copyBtn' data-clipboard-target='#id7a8832e08439' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo /media/$USER/VBox_GAs_7.0.18/VBoxLinuxAdditions.run <span class='unselectable'>Verifying archive integrity... 100% MD5 checksums are OK. All good. Uncompressing VirtualBox 7.0.18 Guest Additions for Linux 100% VirtualBox Guest Additions installer Removing installed version 7.0.18 of VirtualBox Guest Additions... update-initramfs: Generating /boot/initrd.img-6.8.0-35-generic Copying additional installer modules ... Installing additional modules ... VirtualBox Guest Additions: Starting. VirtualBox Guest Additions: Setting up modules VirtualBox Guest Additions: Building the VirtualBox Guest Additions kernel modules. This may take a while. VirtualBox Guest Additions: To build modules for other installed kernels, run VirtualBox Guest Additions: /sbin/rcvboxadd quicksetup <version> VirtualBox Guest Additions: or VirtualBox Guest Additions: /sbin/rcvboxadd quicksetup all VirtualBox Guest Additions: Building the modules for kernel 6.8.0-35-generic. update-initramfs: Generating /boot/initrd.img-6.8.0-35-generic </span><br> <span class='unselectable'>$ </span>sudo reboot</pre> </div> <!-- endregion --> <!-- endregion --> Rust 2024-05-30T00:00:00-04:00 https://mslinn.github.io/blog/2024/05/30/rust <!-- #region intro --><p> Rust is a low-level programming language done right, using modern tools with good documentation and extensive support. </p> <p> I have a lot of <a href='/resume/bio.html'>experience</a> with the Scala and Ruby languages, as well as dozens of other computer languages. I believe that Rust has adopted the best parts of the Scala and Ruby philosophies and melded the syntaxes together nicely. For example, all of these languages provide <a href='https://rust-book.cs.brown.edu/ch03-03-how-functions-work.html#statements-and-expressions' target='_blank' rel="nofollow">expressions</a>, <a href='https://doc.rust-lang.org/book/ch13-01-closures.html' target='_blank' rel="nofollow">closures</a>, <a href='https://doc.rust-lang.org/book/ch05-00-structs.html' target='_blank' rel="nofollow">structs</a> (value classes) and <a href='https://doc.rust-lang.org/book/ch10-00-generics.html' target='_blank' rel="nofollow">generics</a>. </p> <p> The package management is a bit like Ruby&rsquo;s <code>gem</code>s. <a href='https://crates.io/' target='_blank' rel="nofollow">Cargo crates</a> and <a href='https://rubygems.org/' target='_blank' rel="nofollow">Ruby gems</a> are provided as source code and compiled on demand. </p> <p> Rust has many of the Scala features I enjoy the most: </p> <ul> <li> <a href='https://doc.rust-lang.org/book/ch17-00-oop.html' target='_blank' rel="nofollow">Object-oriented</a> and <a href='https://doc.rust-lang.org/book/ch13-00-functional-features.html' target='_blank' rel="nofollow">functional programming</a> styles are both supported. </li> <li> <a href='https://doc.rust-lang.org/book/ch18-00-patterns.html' target='_blank' rel="nofollow">Pattern matching</a>. </li> <li><a href='https://doc.rust-lang.org/book/ch19-03-advanced-traits.html' target='_blank' rel="nofollow">Traits.</a></li> <li><a href='https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(algebraic_data_type)#Rust' target='_blank' rel="nofollow">Algebraic type system.</a></li> </ul> <p> What sets Rust apart, however, is: </p> <ol> <li><a href='https://doc.rust-lang.org/book/ch09-00-error-handling.html' target='_blank' rel="nofollow">Error handling</a>.</li> <li> <a href='https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html' target='_blank' rel="nofollow">Data ownership</a> (borrow checker) and <a href='https://doc.rust-lang.org/book/ch15-00-smart-pointers.html' target='_blank' rel="nofollow">smart pointers</a>. </li> <li>Data has <a href='https://doc.rust-lang.org/book/ch10-00-generics.html' target='_blank' rel="nofollow">lifetimes</a>.</li> <li>Stable, yet advanced compile-time metaprogramming (<a href='https://doc.rust-lang.org/book/ch19-06-macros.html' target='_blank' rel="nofollow">macros</a>).</li> <li>No runtime types.</li> <li><a href='https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html' target='_blank' rel="nofollow">Unsafe system.</a></li> <li> Memory safety without a garbage collector; eal-time behavior is deterministic. Real-time audio/video applications are therefore possible. </li> </ol> <p> <a href='https://rust-lang.org'><code>rust-lang.org</code></a> is the home of the Rust language. </p> <!-- endregion --> <!-- #region Background --> <h2 id="background">Background</h2> <p> Rust was initially released in May 2015 by <a href='https://www.crunchbase.com/person/graydon-hoare' target='_blank' rel="nofollow">Graydon Hoare</a> (no relation to <a href='https://en.wikipedia.org/wiki/Tony_Hoare' target='_blank' rel="nofollow">Tony Hoare</a>). Graydon had created the <a href='https://www.swift.org/' target='_blank' rel="nofollow">Swift language</a> used by Apple the previous year. <a href='https://v4.chriskrycho.com/rust-and-swift.html' target='_blank' rel="nofollow">Chris Krycho</a> has written a detailed comparison of the Swift and Rust languages. </p> <p> <a href='https://www.thurrott.com/windows/282471/microsoft-is-rewriting-parts-of-the-windows-kernel-in-rust' target='_blank' rel="nofollow">Microsoft vice president David Weston said</a> on Apr 29, 2023 that Microsoft had rewritten 36,000 lines of code in the Windows kernel in Rust, in addition to another 152,000 lines of code it had written for a proof of concept involving the DirectWrite Core library. The performance is purportedly excellent, with no regressions compared to the old C++ code. </p> <p> Linus Torvalds accepts contributions for Linux written in Rust, and the Rust infrastructure now present in most Linux installations is mature. Linux 6.8, released on March 10, 2024, was the first version of Linux to include a <a href='https://www.phoronix.com/news/Linux-6.8-Rust-PHY-Driver' target='_blank' rel="nofollow">device driver written in Rust</a>, replacing an older network device driver written in C. </p> <p> The <a href='https://www.reddit.com/r/rust/comments/1bxppdr/todays_xkcd_comic_is_a_physics_game_built_with/' target='_blank' rel="nofollow">XKCD Rust API</a> looks like a fun way to blow off an afternoon. </p> <p> If you need help, see the <code>#beginners</code> <a href='https://discord.gg/rust-lang' target='_blank' rel="nofollow">Rust Discord channel</a> and the <a href='https://users.rust-lang.org/' target='_blank' rel="nofollow">Rust Users Forum</a>. </p> <!-- endregion --> <!-- #region Bindings To Other Languages --> <h2 id="bind">Bindings To Other Languages</h2><ul> <li> <b>Ruby / Rust bindings</b> &mdash; <a href='https://crates.io/crates/magnus' target='_blank' rel="nofollow">Magnus</a> allows you to write Ruby gems in Rust, and call Ruby code from Rust code. </li> <li> <b>Python / Rust bindings.</b> &mdash; <a href='https://www.infoworld.com/article/3664124/how-to-use-rust-with-python-and-python-with-rust.html' target='_blank' rel="nofollow">Py03</a> allows you to mix Rust with Python. Enjoy Python&rsquo;s convenience with Rust&rsquo;s speed. </li> <li> <a href='https://docs.rust-embedded.org/book/interoperability/c-with-rust.html' target='_blank' rel="nofollow">Calling C from Rust</a>. </li> <li> <a href='https://developers.redhat.com/articles/2022/09/05/how-create-c-binding-rust-library' target='_blank' rel="nofollow">How to create C binding for a Rust library</a>: Rust can generate C dynamic libraries (<code>.so</code> files) as well as static libraries (<code>.a</code> files), which can be easily wrapped in Go bindings and Python bindings and used in code written in those languages. </li> <li> <a href='https://github.com/rust-lang/rust-bindgen' target='_blank' rel="nofollow"><code>bindgen</code></a> automatically generates Rust FFI bindings to C (and some C++) libraries. </li> <li> <a href='https://github.com/giovanniberti/robusta' target='_blank' rel="nofollow"><code>Robusta</code></a> — easy interoperability between Rust and Java. This library provides a procedural macro to make it easier to write JNI-compatible code in Rust and automatically convert Rust input and output types. </li> <li> <a href='https://stackoverflow.com/questions/31666936/execute-a-shell-command' target='_blank' rel="nofollow">Execute a shell command from Rust.</a> </li> </ul> <!-- endregion --> <!-- #region rustup --> <h2 id="rustup" class="clear">rustup</h2> <!-- #region implicit --> <p> <code>Rustup</code> is an installer for the Rust systems programming language. </p> <!-- endregion --> <h3 id="wslup">WSL / Ubuntu</h3> <p> For WSL / Ubuntu you can easily install <code>rustup</code>, then install the Rust toolchain with the following commands: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id512a9ef19b32'><button class='copyBtn' data-clipboard-target='#id512a9ef19b32' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install rustup<br> <span class='unselectable'>$ </span>rustup default stable <span class='unselectable'>info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu' info: latest update on 2024-05-02, rust version 1.78.0 (9b00956e5 2024-04-29) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' info: downloading component 'rust-std' info: downloading component 'rustc' 63.7 MiB / 63.7 MiB (100 %) 38.2 MiB/s in 1s ETA: 0s info: downloading component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' info: installing component 'rust-std' 24.3 MiB / 24.3 MiB (100 %) 23.8 MiB/s in 1s ETA: 0s info: installing component 'rustc' 63.7 MiB / 63.7 MiB (100 %) 20.5 MiB/s in 2s ETA: 0s info: installing component 'rustfmt' info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'<br> <span style='color: green;'>stable-x86_64-unknown-linux-gnu installed</span> - rustc 1.78.0 (9b00956e5 2024-04-29) </span></pre> </div> <!-- endregion --> <!-- #region Other OSes --> <h3 id="ruother">Other OSes</h3> <p> Installation instructions for your computer's OS appear when you visit <a href='https://rustup.rs'><code>rustup.rs</code></a>. </p> <p> If you want to customize your setup, you need to know the host triple for your system. A &lsquo;host triple&rsquo; identifies the computer architecture and OS to install to. In truth, the value is no longer a triple but a <a href='https://docs.rs/target-lexicon/latest/target_lexicon/struct.Triple.html' target='_blank' rel="nofollow">quintic value</a>. </p> <p> For WSL / Ubuntu, the host triple is <code>x86_64-unknown-linux-gnu</code>. For Mac, the host triple is <code>x86_64-apple-darwin</code>. The <code>toolchain</code> string consists of the <a href='https://rust-lang.github.io/rustup/concepts/channels.html' target='_blank' rel="nofollow">channel prefix</a>, followed by the host triple; the channels are <code>stable</code>, <code>beta</code> and <code>nightly</code>. Thus, the complete toolchain strings for the <code>stable</code> channel are: </p> <dl> <dt>WSL / Ubuntu</dt> <dd><code>stable-x86_64-unknown-linux-gnu</code></dd> <dt>Mac</dt> <dd><code>stable-x86_64-apple-darwin</code></dd> </ul> <p> This is how I ran the <code>rustup</code> installer. Notice that I specified the default, option 1 - standard installation because the default values are optimal. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id849f1d3f8700'><button class='copyBtn' data-clipboard-target='#id849f1d3f8700' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh <span class='unselectable'>info: downloading installer<br/> Welcome to Rust!<br/> This will download and install the official compiler for the Rust programming language, and its package manager, Cargo.<br/> Rustup metadata and toolchains will be installed into the Rustup home directory, located at:<br/> /home/mslinn/.rustup<br/> This can be modified with the RUSTUP_HOME environment variable.<br/> The Cargo home directory is located at:<br/> /home/mslinn/.cargo<br/> This can be modified with the CARGO_HOME environment variable.<br/> The cargo, rustc, rustup and other commands will be added to Cargo&#39;s bin directory, located at:<br/> /home/mslinn/.cargo/bin<br/> This path will then be added to your PATH environment variable by modifying the profile files located at:<br/> /home/mslinn/.profile /home/mslinn/.bashrc<br/> You can uninstall at any time with rustup self uninstall and these changes will be reverted.<br/> Current installation options:<br/> default host triple: x86_64-unknown-linux-gnu default toolchain: stable (default) profile: default modify PATH variable: yes<br/> 1) Proceed with standard installation (default - just press enter) 2) Customize installation 3) Cancel installation &gt; </span><kbd>Enter</kbd><br/> <span class='unselectable'>info: profile set to &#39;default&#39; info: setting default host triple to x86_64-unknown-linux-gnu info: syncing channel updates for &#39;stable-x86_64-unknown-linux-gnu&#39; info: latest update on 2024-05-02, rust version 1.78.0 (9b00956e5 2024-04-29) info: downloading component &#39;cargo&#39; info: downloading component &#39;clippy&#39; info: downloading component &#39;rust-docs&#39; 15.1 MiB / 15.1 MiB (100 %) 9.6 MiB/s in 2s ETA: 0s info: downloading component &#39;rust-mingw&#39; info: downloading component &#39;rust-std&#39; 24.6 MiB / 24.6 MiB (100 %) 18.3 MiB/s in 1s ETA: 0s info: downloading component &#39;rustc&#39; 73.8 MiB / 73.8 MiB (100 %) 18.1 MiB/s in 4s ETA: 0s info: downloading component &#39;rustfmt&#39; info: installing component &#39;cargo&#39; info: installing component &#39;clippy&#39; info: installing component &#39;rust-docs&#39; 15.1 MiB / 15.1 MiB (100 %) 3.6 MiB/s in 3s ETA: 0s info: installing component &#39;rust-mingw&#39; info: installing component &#39;rust-std&#39; 24.6 MiB / 24.6 MiB (100 %) 10.9 MiB/s in 2s ETA: 0s info: installing component &#39;rustc&#39; 73.8 MiB / 73.8 MiB (100 %) 10.8 MiB/s in 7s ETA: 0s info: installing component &#39;rustfmt&#39; info: default toolchain set to &#39;stable-x86_64-unknown-linux-gnu&#39;<br/> stable-x86_64-unknown-linux-gnu installed - (rustc does not exist)<br/> Rust is installed now. Great!<br/> To get started you may need to restart your current shell. This would reload your PATH environment variable to include Cargo&#39;s bin directory ($HOME/.cargo/bin).<br/> To configure your current shell, you need to source the corresponding env file under $HOME/.cargo.<br/> This is usually done by running one of the following (note the leading DOT): . &quot;$HOME/.cargo/env&quot; # For sh/bash/zsh/ash/dash/pdksh source &quot;$HOME/.cargo/env.fish&quot; # For fish</span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region All OSes --> <h3>All OSes</h3> <p> The <code>rustup</code> installation program modifies both <code>~/.bashrc</code> and <code>~/.profile</code>, which is redundant. The following line was appended to both files. I deleted it from <code>~/.profile</code>. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Extra line appended to ~/.profile and ~/.bashrc</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id666e6e437db6'><button class='copyBtn' data-clipboard-target='#id666e6e437db6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>. "$HOME/.cargo/env"</pre> </div> <!-- endregion --> <p> Now I reloaded the shell: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9b3fd87e6660'><button class='copyBtn' data-clipboard-target='#id9b3fd87e6660' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>source ~/.bashrc</pre> </div> <!-- endregion --> <p> When <code>rustup</code> runs, it creates a directory called <code>~/.rustup/</code>: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd38569539f42'><button class='copyBtn' data-clipboard-target='#idd38569539f42' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tree -d -L 3 ~/.rustup/ <span class='unselectable'>/home/mslinn/.rustup/ ├── downloads ├── tmp ├── toolchains │   └── stable-x86_64-unknown-linux-gnu │   ├── bin │   ├── etc │   ├── lib │   ├── libexec │   └── share └── update-hashes<br> 11 directories </span></pre> </div> <!-- endregion --> <p> <code>~/.rustup/settings.toml</code> is an important configuration file for Rust: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.rustup/settings.toml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7e0912473452'><button class='copyBtn' data-clipboard-target='#id7e0912473452' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>default_toolchain = "stable-x86_64-unknown-linux-gnu" profile = "default" version = "12"<br> [overrides]</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region Hello, World! --> <h2 id="hw" class="clear">Hello, World!</h2> <div class='imgWrapper imgFlex right' style='width: 220px; '> <a href='/blog/2023/05/12/c.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/k&r.svg" type="image/svg"> <!---<source srcset="/blog/c/k&r.avif" type="image/avif">--> <source srcset="/blog/c/k&r.webp" type="image/webp"> <source srcset="/blog/c/k&r.apng" type="image/apng"> <source srcset="/blog/c/k&r.png" type="image/png"> <source srcset="/blog/c/k&r.jpg" type="image/jpeg"> <source srcset="/blog/c/k&r.jpeg" type="image/jpeg"> <source srcset="/blog/c/k&r.jfif" type="image/jpeg"> <source srcset="/blog/c/k&r.pjpeg" type="image/jpeg"> <source srcset="/blog/c/k&r.pjp" type="image/jpeg"> <source srcset="/blog/c/k&r.gif" type="image/gif"> <source srcset="/blog/c/k&r.tif" type="image/tiff"> <source srcset="/blog/c/k&r.tiff" type="image/tiff"> <source srcset="/blog/c/k&r.bmp" type="image/bmp"> <source srcset="/blog/c/k&r.ico" type="image/x-icon"> <source srcset="/blog/c/k&r.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/c/k&r.png" style='width: 100%; ' /> </picture> </a> </div> <p> <a href='https://en.wikipedia.org/wiki/%22Hello,_World!%22_program' target='_blank' rel="nofollow">&ldquo;Hello, world!&rdquo;</a> was the output of the first C program in the seminal book <a href='https://en.wikipedia.org/wiki/The_C_Programming_Language' target='_blank' rel="nofollow">The C Programming Language</a>, written by Brian Kernighan and Dennis Ritchie and published in 1983. This book is often called &ldquo;the K&amp;R book&rdquo;. </p> <p> Here is the program, in all its glory: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>hello_world.c</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id53bcfc044bbb'><button class='copyBtn' data-clipboard-target='#id53bcfc044bbb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>main() { printf("Hello, world!"); }</pre> </div> <!-- endregion --> <p> <a href='/blog/2023/05/12/c.html'>I learned C from the K&R book</a>; the language and its libraries were small and simple, so it could be mastered in only a few weeks. </p> <p> As my first Rust program, I decided to write a <code>hello_world</code> equivalent program. As you can see, the Rust version looks almost identical to the C version: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>hello_world.rs</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd450f5e2c2ed'><button class='copyBtn' data-clipboard-target='#idd450f5e2c2ed' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>fn main() { println!("Hello, World!"); }</pre> </div> <!-- endregion --> <p> The <a href='https://doc.rust-lang.org/book' target='_blank' rel="nofollow">Rust Programming Language</a> online book is a great place to start learning Rust. </p> <p class="alert rounded shadow"> I did not need to write the above <b>Hello, World!</b> Rust program&ndash;<a href='https://doc.rust-lang.org/stable/cargo/' target='_blank' rel="nofollow">Cargo always writes this short program</a> when creating a new Rust project! This is what I actually typed to write the program: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2f36cafa6137'><button class='copyBtn' data-clipboard-target='#id2f36cafa6137' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cargo new hello_world <span class='unselectable'><span style='color: green;'>Creating</span> binary (application) `hello_world` package <span style='color: green;'>note</span>: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html </span><br> <span class='unselectable'>$ </span>cd hello_world<br></pre> </div> <!-- endregion --> <aside> <label>Aside - Saving Some Typing</label> <p> The above commands have some redundancy; the project name, <code>hello_world</code>, is typed twice. We can use a Bash history word designator to remove the redundancy. The token <code class='bg_yellow'>!:2</code> <a href='https://www.baeldung.com/linux/bash-recall-previous-command#1-understanding-word-designators-in-bashs-history-expansion' target='_blank' rel="nofollow">expands to the second argument of the previous command</a>. Here is how we can rewrite the above commands without redundancy: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8341038efa2c'><button class='copyBtn' data-clipboard-target='#id8341038efa2c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cargo new hello_world<br> <span class='unselectable'>$ </span>cd <span class='bg_yellow'>!:2</span></pre> </div> <!-- endregion --> <p> We can combine these two lines into a one-line command list using the <a href='https://www.gnu.org/software/bash/manual/html_node/Lists.html' target='_blank' rel="nofollow"><code>&amp;&amp;</code> list operator</a>: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide81747448a92'><button class='copyBtn' data-clipboard-target='#ide81747448a92' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cargo new hello_world && cd !:2</pre> </div> <!-- endregion --> </aside> <p> Now let&rsquo;s see what cargo generated in the new <code>hello_world</code> directory structure that it created: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd34e97a1fa05'><button class='copyBtn' data-clipboard-target='#idd34e97a1fa05' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tree -a -L 1 <span class='unselectable'>. ├── .git ├── .gitignore ├── Cargo.toml └── src </span><br> <span class='unselectable'>$ </span>cat Cargo.toml <span class='unselectable'>[package] name = "hello_world" version = "0.1.0" edition = "2021"<br> [dependencies] </span></pre> </div> <!-- endregion --> <p> Every Rust project generated by Cargo starts as the <b>Hello, World!</b> program. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id124f07bbba11'><button class='copyBtn' data-clipboard-target='#id124f07bbba11' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cat src/main.rs <span class='unselectable'>fn main() { println!("Hello, world!"); } </span></pre> </div> <!-- endregion --> <p> Now let&rsquo;s run the program: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7cd88c6519bf'><button class='copyBtn' data-clipboard-target='#id7cd88c6519bf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cargo run <span class='unselectable'>Compiling hello_world v0.1.0 (/mnt/c/work/rust/hello_world) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.64s Running `target/debug/hello_world` Hello, world! </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Visual Studio Code --> <h2 id="vscode">Visual Studio Code</h2> <p> IDEs had not been invented yet when I first learned C. I am very happy that they were, and today my IDE of choice is the F/OSS <a href='/blog/2021/03/22/vscode-notes.html'>Visual Studio Code</a> from Microsoft. JetBrains also makes excellent IDEs, and their Rust IDE, <a href='https://www.jetbrains.com/rust/' target='_blank' rel="nofollow">RustRover</a>, is free for non-commercial use. I find that JetBrains IDEs generally offer more capable refactoring than Visual Studio Code; however, JetBrains IDEs do not handle <a href='/blog/2018/08/20/wsl-revisited.html'>WSL</a> as well as Visual Studio Code does. </p> <div class='imgWrapper imgFlex right' style='width: 110px; '> <a href='https://marketplace.visualstudio.com/items?itemName=Primer4128.rust-analyzer-fork' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/rust/rust_analyzer.svg" type="image/svg"> <!---<source srcset="/blog/images/rust/rust_analyzer.avif" type="image/avif">--> <source srcset="/blog/images/rust/rust_analyzer.webp" type="image/webp"> <source srcset="/blog/images/rust/rust_analyzer.apng" type="image/apng"> <source srcset="/blog/images/rust/rust_analyzer.png" type="image/png"> <source srcset="/blog/images/rust/rust_analyzer.jpg" type="image/jpeg"> <source srcset="/blog/images/rust/rust_analyzer.jpeg" type="image/jpeg"> <source srcset="/blog/images/rust/rust_analyzer.jfif" type="image/jpeg"> <source srcset="/blog/images/rust/rust_analyzer.pjpeg" type="image/jpeg"> <source srcset="/blog/images/rust/rust_analyzer.pjp" type="image/jpeg"> <source srcset="/blog/images/rust/rust_analyzer.gif" type="image/gif"> <source srcset="/blog/images/rust/rust_analyzer.tif" type="image/tiff"> <source srcset="/blog/images/rust/rust_analyzer.tiff" type="image/tiff"> <source srcset="/blog/images/rust/rust_analyzer.bmp" type="image/bmp"> <source srcset="/blog/images/rust/rust_analyzer.ico" type="image/x-icon"> <source srcset="/blog/images/rust/rust_analyzer.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/rust/rust_analyzer.png" style='width: 100%; ' /> </picture> </a> </div> <p> The Visual Studio Code documentation has a section entitled <a href='https://code.visualstudio.com/docs/languages/rust' target='_blank' rel="nofollow">Rust in Visual Studio Code</a>. It discusses the <a href='https://marketplace.visualstudio.com/items?itemName=Primer4128.rust-analyzer-fork' target='_blank' rel="nofollow">Rust Analyzer plugin</a>. <a href='https://rust-analyzer.github.io/manual.html#vs-code-2' target='_blank' rel="nofollow">Read the fine manual.</a> <a href='https://rust-analyzer.github.io/' target='_blank' rel="nofollow">More documentation.</a> </p> <p> There is not much to say about Visual Studio Code and the Rust Analyzer plugin; they just work as they should. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/rust/vscode_rust.svg" type="image/svg"> <!---<source srcset="/blog/images/rust/vscode_rust.avif" type="image/avif">--> <source srcset="/blog/images/rust/vscode_rust.webp" type="image/webp"> <source srcset="/blog/images/rust/vscode_rust.apng" type="image/apng"> <source srcset="/blog/images/rust/vscode_rust.png" type="image/png"> <source srcset="/blog/images/rust/vscode_rust.jpg" type="image/jpeg"> <source srcset="/blog/images/rust/vscode_rust.jpeg" type="image/jpeg"> <source srcset="/blog/images/rust/vscode_rust.jfif" type="image/jpeg"> <source srcset="/blog/images/rust/vscode_rust.pjpeg" type="image/jpeg"> <source srcset="/blog/images/rust/vscode_rust.pjp" type="image/jpeg"> <source srcset="/blog/images/rust/vscode_rust.gif" type="image/gif"> <source srcset="/blog/images/rust/vscode_rust.tif" type="image/tiff"> <source srcset="/blog/images/rust/vscode_rust.tiff" type="image/tiff"> <source srcset="/blog/images/rust/vscode_rust.bmp" type="image/bmp"> <source srcset="/blog/images/rust/vscode_rust.ico" type="image/x-icon"> <source srcset="/blog/images/rust/vscode_rust.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/rust/vscode_rust.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --><!-- #region Videos --> <h2 id="videos">Videos</h2> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/u0VotuGzD_w" frameborder="0" allowfullscreen></iframe></div> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/CJtvnepMVAU" frameborder="0" allowfullscreen></iframe></div> <!-- endregion --> Product Development Collaboration 2024-02-18T00:00:00-05:00 https://mslinn.github.io/blog/2024/02/18/collaboration <!-- #region intro --> <p> For product development, the goal of effective collaboration is to fully engage all parties. Good product-oriented collaborators maintain a flow of information, ideas and commensurate action. Hesitation is minimized. Feedback is rapid. Reactions are prompt, yet measured through constant practice. </p> <!-- endregion --> <!-- #region Product Collaborator Checklist --> <h2 id="check">Product Collaborator Checklist</h2> <p> The following criteria are a guide to effective collaboration candidates for product development. </p> <!-- #region Desirable Traits --> <h3 id="destr">Desirable Traits</h3> <ul> <li> Strong interest in optimizing the architecture and engineering processes. </li> <li> A focus on the contributing to the desired results for the team, which are: reliably providing an easily understood and valuable benefit to a well-defined customer base. </li> <li> Manage the difference between desired outcomes and actual outcomes. </li> <li> Strive for process and product consistency. </li> <li> Willing to expend the overhead of time and energy required for effective communication: <ul> <li>Driving their activities from a Kanban-style status board</li> <li>Planning interactions with others, minimizing impulsive chatter</li> <li>Preparing and distributing notes before meetings</li> <li>Describing alternatives for decisions</li> <li>Documenting the rationale for design decisions</li> <li>Summarizing discussions in project documentation</li> <li>Contributing to an up-to-date product roadmap</li> </ul> </li> </ul> <!-- endregion --> <!-- #region Undesirable Traits --> <h3 id="undestr">Undesirable Traits</h3> <p> People with the following traits disqualify themselves from being a member of the core team because they are unable to consistently contribute effectively to the ever-evolving vision for the project. </p> <ul> <li> "Guns for hire" just want to get paid for time spent; they have no vested interest in the long-term outcome. They are tools to be exploited, not valuable resources to be cultivated. The best use of a mercenary is to give them orders, supervise them, learn from them, and then dispose of them as soon as possible. </li> <li> "Putting in time" demonstrates a lack of personal engagement. Those people require constant supervision, and the cost of the supervision must be subtracted from any value they might provide when computing their net contribution. </li> </ul> <!-- endregion --> <!-- #region Software Architecture and Engineering Processes --> <h2 id="defs">Software Architecture and Engineering Processes</h2> <p> ChatGPT gave me the following response when I asked, &ldquo;Compare and contrast the processes of architecture and engineering for software&rdquo;. I like the quality of this response, and provide it verbatim because it exactly matches my understanding of those terms. </p> <p> When it comes to software development, there are parallels between the processes of architecture and engineering, but there are also significant differences due to the intangible and rapidly evolving nature of software. Let's compare and contrast the processes of software architecture and engineering: </p> <h3 id="pf">Purpose and Focus</h3> <p> Software Architecture: Similar to architectural design in physical structures, software architecture focuses on designing the overall structure and organization of a software system. It involves making high-level decisions about the system's components, their interactions, and the principles that guide their design. </p> <p> Software Engineering: Software engineering encompasses the systematic approach to developing software solutions. It involves various disciplines such as requirements analysis, design, coding, testing, deployment, and maintenance. The focus is on creating reliable, efficient, and maintainable software systems. </p> <h3 id="dp">Design Process</h3> <p> Software Architecture: The architectural design process involves defining the system's key components, interfaces, and their interactions. Architectural patterns, such as layered architecture, microservices, or event-driven architecture, are often used to guide the design. Considerations include scalability, performance, security, and maintainability. </p> <p> Software Engineering: Software engineering encompasses the entire software development lifecycle (SDLC). It begins with requirements gathering and analysis, followed by design, implementation (coding), testing, deployment, and maintenance. Various methodologies like Waterfall, Agile, and DevOps guide this process. </p> <h3 id="ct"> Creativity vs. Technicality</h3> <p> Software Architecture: Software architects must balance creative design decisions with technical constraints. They need to envision the overall structure of the system and choose appropriate technologies and patterns to achieve the desired functionality and quality attributes. </p> <p> Software Engineering: Software engineers focus on implementing the design created by architects. While creativity is still important, engineers primarily deal with the technical aspects of coding, testing, and debugging. They translate the architectural design into working software components. </p> <h3 id="coll">Collaboration</h3> <p> Software Architecture: Collaboration between architects, stakeholders, and development teams is crucial to ensure that architectural decisions align with business goals and technical requirements. Architects often communicate their designs through diagrams, documentation, and discussions. </p> <p> Software Engineering: Software engineers collaborate closely with other team members, including architects, designers, testers, and project managers. They work together to implement features, resolve technical challenges, and deliver software increments according to project timelines. </p> <h3>Regulatory and Environmental Considerations</h3> <p> Software Architecture: Architectural decisions may need to comply with regulatory standards, industry best practices, and security guidelines. Architects must consider factors like data privacy, compliance requirements (such as GDPR or HIPAA), and interoperability with other systems. </p> <p> Software Engineering: Engineers implement features and functionalities while adhering to coding standards, security protocols, and testing practices. They ensure that the software meets quality standards, performance targets, and regulatory requirements specified by the architecture and project stakeholders. </p> <p> In summary, while both software architecture and engineering involve designing and building software systems, they have distinct focuses and processes. Architecture concentrates on high-level design decisions and system structure, while engineering encompasses the entire software development lifecycle, from requirements to deployment. Collaboration and communication are essential in both disciplines to deliver successful software solutions.</p> </p> <!-- endregion --> <!-- #region Collaboration --> <h2 id="collabl">Collaboration</h2> <p> ChatGPT defined collaboration as the following. Again, I very much like the response, so I provide it verbatim. </p> <p class="quote"> Collaboration refers to the process of individuals or groups working together to achieve a common goal or objective. It involves the sharing of ideas, resources, responsibilities, and tasks in a coordinated manner to accomplish tasks more effectively and efficiently than if done individually. <br><br> Collaboration often emphasizes communication, cooperation, and mutual respect among participants. It can occur within various contexts, including businesses, organizations, teams, communities, and even across different disciplines or sectors. <br><br> Successful collaboration typically results in synergy, where the combined efforts of the participants produce outcomes that exceed what could be achieved by any individual or subgroup working alone. </p> <p> Following is the response when I asked "what is required for effective collaboration in a new software project?" I <span class="bg_yellow">highlighted</span> a passage that is important, and <s>struck out</s> portions that I disagree with. </p> <p> Effective collaboration in a new software project requires several key elements: </p> <ol> <li> <b>Clear Goals and Objectives</b>: Establishing clear, well-defined goals and objectives for the project ensures that all collaborators understand what they are working towards and why. </li> <li> <b>Communication Channels</b>: Implementing reliable communication channels is essential for keeping all team members informed and facilitating the exchange of ideas, updates, and feedback. This can include tools like instant messaging, email, project management software, or regular team meetings. </li> <li> <b>Defined Roles and Responsibilities</b>: Clearly defining roles and responsibilities for each team member helps avoid confusion and ensures that tasks are completed efficiently. This includes designating a project manager, developers, testers, designers, and other relevant roles. </li> <li> <b>Collaboration Tools</b>: Utilizing collaboration tools such as version control systems (e.g., Git), issue tracking systems <s>(e.g., Jira)</s>, document sharing platforms (e.g., Google Drive), and collaborative coding environments (e.g., GitHub) can streamline the development process and enhance teamwork. </li> <li class="bg_yellow"> <b>Regular Feedback and Reviews</b>: Encouraging open and constructive feedback throughout the project lifecycle allows team members to address issues promptly, make necessary adjustments, and continuously improve the software. </li> <li class="bg_yellow"> <b>Flexibility and Adaptability</b>: Remaining flexible and adaptable to changes in requirements, timelines, and priorities is crucial for navigating unforeseen challenges and ensuring the project stays on track. </li> <li> <b>Respect and Trust</b>: Cultivating an environment of respect and trust among team members fosters collaboration and encourages open communication, creativity, and innovation. </li> <li class="bg_yellow"> <b>Documentation</b>: Maintaining thorough documentation of project requirements, design decisions, codebase, and processes helps ensure continuity and facilitates knowledge transfer among team members. </li> <li> <b>Regular Progress Tracking</b>: Implementing mechanisms to track progress, <s>such as sprint planning, daily stand-up meetings, and</s> regular status reports, enables the team to monitor project milestones, identify potential bottlenecks, and make necessary adjustments to stay on schedule. </li> <li> <b>Quality Assurance and Testing</b>: Integrating quality assurance processes and testing procedures into the development workflow ensures that the software meets the specified requirements, functions as intended, and delivers value to end-users. </li> </ol> <p> By incorporating these elements into the collaborative process, teams can increase the likelihood of success in their new software projects. </p> <!-- endregion --> <!-- #region Design Decisions --> <h2 id="dd">Design Decisions</h2> <p> I asked ChatGPT "how would someone document design decisions in a software project?" The highlighted portion is covered in more detail later in this article. </p> <p> Documenting design decisions in a software project is essential for ensuring clarity, facilitating collaboration, and providing a reference for future development. Here's a structured approach to documenting design decisions: </p> <ol> <li> <b>Establish a Design Document</b>: Create a designated document or section within project documentation specifically for recording design decisions. This could be a separate document, a section in the project wiki, or part of the version control repository. </li> <li> <b>Provide Context</b>: Start by providing context for the decision. Explain the problem or requirement that the design decision aims to address. Include any relevant background information, constraints, or considerations that influenced the decision-making process. </li> <li class="bg_yellow"> <b>Describe Alternatives</b>: Outline the various alternatives considered during the decision-making process. This could include different approaches, technologies, architectures, or design patterns. Briefly discuss the pros and cons of each alternative. </li> <li> <b>Justify the Chosen Solution</b>: Explain why the chosen solution was selected over the alternatives. Highlight the reasons, benefits, and trade-offs associated with the chosen approach. Consider factors such as performance, scalability, maintainability, ease of implementation, and alignment with project goals. </li> <li> <b>Detail the Design</b>: Provide a detailed description of the chosen design solution. Break down the design into its constituent components, modules, or layers. Use diagrams, flowcharts, UML diagrams, or other visual aids to illustrate the architecture, structure, and relationships between components. </li> <li> <b>Document Dependencies and Interactions</b>: Document dependencies between different components or modules within the design. Describe how these components interact with each other and with external systems or services. Identify any interfaces, APIs, or protocols used for communication. </li> <li> <b>Address Risks and Mitigations</b>: Identify potential risks or challenges associated with the chosen design. Discuss strategies for mitigating these risks and ensuring the robustness and resilience of the design solution. </li> <li> <b>Include Implementation Details</b>: If applicable, include implementation details or guidelines for developers to follow when implementing the design. This could include coding standards, best practices, design patterns, or frameworks to be used. </li> <li> <b>Update and Maintain</b>: Keep the design document up to date as the project evolves. Update it to reflect any changes, refinements, or new decisions made during the development process. Ensure that the document remains a reliable reference for developers and stakeholders. </li> <li> <b>Review and Iterate</b>: Encourage team members to review the design document periodically and provide feedback. Iterate on the design as necessary based on feedback, changes in requirements, or lessons learned from implementation. </li> </ol> <p> By following these steps, you can effectively document design decisions in a software project, providing a comprehensive and structured reference for developers, stakeholders, and future contributors. </p> <!-- endregion --> <!-- #region Comparative Product Matrix --> <h2 id="mat">Comparative Product Matrix</h2> <p> Comparative product matrices are useful for guiding and documenting design decisions. Search for them when you need to identify pre-existing solutions to a problem, and generate them when considering the implementation of various approaches. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://github.com/snowfort-ai/awesome-llm-webapps?tab=readme-ov-file#project-table' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/comprative_product_matrix.svg" type="image/svg"> <!---<source srcset="/blog/images/comprative_product_matrix.avif" type="image/avif">--> <source srcset="/blog/images/comprative_product_matrix.webp" type="image/webp"> <source srcset="/blog/images/comprative_product_matrix.apng" type="image/apng"> <source srcset="/blog/images/comprative_product_matrix.png" type="image/png"> <source srcset="/blog/images/comprative_product_matrix.jpg" type="image/jpeg"> <source srcset="/blog/images/comprative_product_matrix.jpeg" type="image/jpeg"> <source srcset="/blog/images/comprative_product_matrix.jfif" type="image/jpeg"> <source srcset="/blog/images/comprative_product_matrix.pjpeg" type="image/jpeg"> <source srcset="/blog/images/comprative_product_matrix.pjp" type="image/jpeg"> <source srcset="/blog/images/comprative_product_matrix.gif" type="image/gif"> <source srcset="/blog/images/comprative_product_matrix.tif" type="image/tiff"> <source srcset="/blog/images/comprative_product_matrix.tiff" type="image/tiff"> <source srcset="/blog/images/comprative_product_matrix.bmp" type="image/bmp"> <source srcset="/blog/images/comprative_product_matrix.ico" type="image/x-icon"> <source srcset="/blog/images/comprative_product_matrix.cur" type="image/x-icon"> <img alt='Comparative product matrix' class="imgImg " src="/blog/images/comprative_product_matrix.png" style='width: 100%; ' title='Comparative product matrix' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://github.com/snowfort-ai/awesome-llm-webapps?tab=readme-ov-file#project-table" target='_blank' > Comparative product matrix </a> </figcaption> </figure> </div> <p> Comparative product matrices take time to read and to prepare, but they help guide the team to the best technical decisions. Without them, you are just hoping to get lucky. </p><!-- endregion --> Expanding the WSL virtual hard drive 2024-02-12T00:00:00-05:00 https://mslinn.github.io/blog/2024/02/12/wsl-expand <!-- #region intro --> <p> I ran out of room on my WSL2 virtual hard drive. <a href='https://learn.microsoft.com/en-us/windows/wsl/disk-space#how-to-expand-the-size-of-your-wsl-2-virtual-hard-disk' target='_blank' rel="nofollow">Microsoft&rsquo;s instructions</a> worked perfectly to increase the virtual drive size. </p> <!-- endregion --> <!-- #region Shut Down WSL --> <h2 id="stop">Shut Down WSL</h2> <p> The following command work from WSL bash, and cmd, and PowerShell: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id988932281415'><button class='copyBtn' data-clipboard-target='#id988932281415' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>wsl.exe --shutdown</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Expand VHD With Diskpart --> <h2 id="Diskpart">Expand VHD With Diskpart</h2> <p style="text-align: left;"> The virtual disk was in <code>%LocalAppData%\<wbr>Packages\<wbr>CanonicalGroupLimited.<wbr>UbuntuonWindows_<wbr>79rhkp1fndgsc\<wbr>LocalState\<wbr>ext4.vhdx</code> </p> <p> Open a <code>cmd.exe</code> window with admininstrator privileges and run <code>diskpart</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>diskpart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id26f24fbf8e6c'><button class='copyBtn' data-clipboard-target='#id26f24fbf8e6c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>Select vdisk file="%LocalAppData%\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\ext4.vhdx"<br> <span class='unselectable'>DISKPART> </span>detail vdisk <span class='unselectable'>Device type ID: 0 (Unknown) Vendor ID: {00000000-0000-0000-0000-000000000000} (Unknown) State: Added Virtual size: 256 GB Physical size: 247 GB Filename: C:\Users\Mike Slinn\AppData\Local\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\ext4.vhdx Is Child: No Parent Filename: Associated disk#: Not found. </span></pre> </div> <!-- endregion --> <p> The virtual disk was 256 GB (256,000 MB). I want it to be twice as big: 512,000 MB. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>diskpart (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id12ba6a19f9a4'><button class='copyBtn' data-clipboard-target='#id12ba6a19f9a4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>expand vdisk maximum=512000<br> <span class='unselectable'>100 percent completed<br> DiskPart successfully expanded the virtual disk file. </span> <span class='unselectable'>DISKPART> </span>exit<br> <span class='unselectable'>Leaving DiskPart... </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Restart WSL and resize2fs --> <h2 id="restart">Restart WSL and Run <span class="code">resize2fs</span></h2> <p> Now I restarted WSL from a <code>cmd</code> window. The following command work from WSL bash, and cmd, and PowerShell: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1b9f4e058769'><button class='copyBtn' data-clipboard-target='#id1b9f4e058769' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>wsl.exe</pre> </div> <!-- endregion --> <p> Now tell Linux to use all available space: </p> <!-- #region --> <div class="jekyll_pre" style='margin-bottom: 1em;'> <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2db1a8fb44af'><button class='copyBtn' data-clipboard-target='#id2db1a8fb44af' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo resize2fs /dev/sdc 512000M <span class='unselectable'>resize2fs 1.47.0 (5-Feb-2023) Filesystem at /dev/sdc is mounted on /; on-line resizing required old_desc_blocks = 32, new_desc_blocks = 63 The filesystem on /dev/sdc is now 131072000 (4k) blocks long. </span></pre> </div> <!-- endregion --> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> Done! </p> <!-- endregion --> Installing JDK 17 on Ubuntu 2023-09-28T00:00:00-04:00 https://mslinn.github.io/blog/2023/09/28/jdk <!-- #region install --> <p> The Java Development Kit (JDK) version 17 is compatible with v11 and v8. Installing the JDK also installs the JRE, as you can see from the highlighted dependency shown below: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id92d2b91af8a8'><button class='copyBtn' data-clipboard-target='#id92d2b91af8a8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>apt show -a openjdk-17-jdk <span class='unselectable'>Package: openjdk-17-jdk Version: 17.0.8.1+1~us1-0ubuntu1~23.04 Priority: optional Section: java Source: openjdk-17 Origin: Ubuntu Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Original-Maintainer: OpenJDK Team <openjdk-17@packages.debian.org> Bugs: https://bugs.launchpad.net/ubuntu/+filebug Installed-Size: 1544 kB Provides: java-compiler, java-sdk (= 17), java10-sdk, java11-sdk, java12-sdk, java13-sdk, java14-sdk, java15-sdk, java16-sdk, java17-sdk, java2-sdk, java5-sdk, java6-sdk, java7-sdk, java8-sdk, java9-sdk Depends: <span class="bg_yellow">openjdk-17-jre (= 17.0.8.1+1~us1-0ubuntu1~23.04)</span>, openjdk-17-jdk-headless (= 17.0.8.1+1~us1-0ubuntu1~23.04), libc6 (>= 2.34), zlib1g (>= 1:1.1.4) Recommends: libxt-dev Suggests: openjdk-17-demo, openjdk-17-source, visualvm Homepage: https://openjdk.java.net/ Download-Size: 1486 kB APT-Sources: http://archive.ubuntu.com/ubuntu lunar-updates/main amd64 Packages Description: OpenJDK Development Kit (JDK) OpenJDK is a development environment for building applications, applets, and components using the Java programming language.<br> Package: openjdk-17-jdk Version: 17.0.6+10-1ubuntu2 Priority: optional Section: java Source: openjdk-17 Origin: Ubuntu Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Original-Maintainer: OpenJDK Team <openjdk-17@packages.debian.org> Bugs: https://bugs.launchpad.net/ubuntu/+filebug Installed-Size: 4732 kB Provides: java-compiler, java-sdk, java10-sdk, java11-sdk, java12-sdk, java13-sdk, java14-sdk, java15-sdk, java16-sdk, java17-sdk, java2-sdk, java5-sdk, java6-sdk, java7-sdk, java8-sdk, java9-sdk Depends: openjdk-17-jre (= 17.0.6+10-1ubuntu2), openjdk-17-jdk-headless (= 17.0.6+10-1ubuntu2), libc6 (>= 2.34) Recommends: libxt-dev Suggests: openjdk-17-demo, openjdk-17-source, visualvm Homepage: https://openjdk.java.net/ Download-Size: 4585 kB APT-Sources: http://archive.ubuntu.com/ubuntu lunar/main amd64 Packages Description: OpenJDK Development Kit (JDK) OpenJDK is a development environment for building applications, applets, and components using the Java programming language. </span></pre> </div> <!-- endregion --> <p> Install OpenJDK 17 and its dependencies: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd0faa463ac6e'><button class='copyBtn' data-clipboard-target='#idd0faa463ac6e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install openjdk-17-jdk</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region set default java --> <h2 id="def">Setting the Default Java Version</h2> <p> The man page for <code>update-java-alternatives</code> is: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id407c8dc58f78'><button class='copyBtn' data-clipboard-target='#id407c8dc58f78' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man update-java-alternatives <span class='unselectable'>UPDATE-JAVA-ALTERNATIVESystem Manager&#39;s MUPDATE-JAVA-ALTERNATIVES(8)<br/> NAME update-java-alternatives - update alternatives for jre/sdk installations<br/> SYNOPSIS update-java-alternatives [--jre] [--plugin] [-v|--verbose] -l|--list [&lt;jname&gt;] -s|--set &lt;jname&gt; -a|--auto -h|-?|--help<br/> DESCRIPTION update-java-alternatives updates all alternatives belonging to one runtime or development kit for the Java language. A package does provide these information of it&#39;s alternatives in /usr/lib/jvm/.&lt;jname&gt;.jinfo.<br/> OPTIONS -l|--list [&lt;jname&gt;] List all installed packages (or just &lt;jname&gt;) provid&#8208; ing information to set a bunch of java alternatives. Verbose output shows each alternative provided by the packages.<br/> -a|--auto Switch all alternatives of registered jre/sdk instal&#8208; lations to automatic mode.<br/> -s|--set &lt;jname&gt; Set all alternatives of the registered jre/sdk instal&#8208; lation to the program path provided by the &lt;jname&gt; in&#8208; stallation.<br/> --jre Limit the actions to alternatives belong to a runtime environment, not a development kit.<br/> --jre-headless Limit the actions to alternatives belong to the head&#8208; less part of a runtime environment.<br/> --plugin Limit the actions to alternatives providing browser plugins.<br/> -h|--help Display a help message.<br/> -v|--verbose Verbose output.<br/> FILES /usr/lib/jvm/.*.jinfo A text file describing a jre/sdk installation. Con&#8208; sists of some variables of the form &lt;var&gt;=&lt;value&gt; and a list of alternatives of the form jre|jdk &lt;name&gt; &lt;path&gt;.<br/> AUTHOR update-java-alternatives and this manual page was written by Matthias Klose &lt;doko@ubuntu.com&gt;.<br/> May 2006 UPDATE-JAVA-ALTERNATIVES(8) </span></pre> </div> <!-- endregion --> <p> The above man page neglects to say that a package version&rsquo;s priority is set from its version number. Thus a newer version would normally have a higher priority. To get a list of the installed Java versions, type: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id88bbf5a91fe3'><button class='copyBtn' data-clipboard-target='#id88bbf5a91fe3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo update-java-alternatives --list <span class='unselectable'>java-1.11.0-openjdk-amd64 1111 /usr/lib/jvm/java-1.11.0-openjdk-amd64 java-1.17.0-openjdk-amd64 1711 /usr/lib/jvm/java-1.17.0-openjdk-amd64 </span></pre> </div> <!-- endregion --> <p> As you can see, my machine had Java 11 and 17 installed. To set the newest version to be the default, type: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id24087ff23099'><button class='copyBtn' data-clipboard-target='#id24087ff23099' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo update-java-alternatives -a <span class='unselectable'>$ </span>java -version <span class='unselectable'>openjdk version "17.0.8.1" 2023-08-24 OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu123.04) OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu123.04, mixed mode, sharing) </span></pre> </div> <!-- endregion --> <!-- endregion --> C++ Boost library 2023-09-14T00:00:00-04:00 https://mslinn.github.io/blog/2023/09/14/boost <!-- #region intro --> <h2 id="about">About Boost</h2> <p> <a href='www.boost.org'>Boost</a> is a general-purpose open source library of utility functions for C++. The <a href='https://www.boost.org/doc/libs/?view=condensed' target='_blank' rel="nofollow">list of categories of fuctionality</a> is formidable. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://www.boost.org/doc/libs/?view=condensed' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/boostCategories.svg" type="image/svg"> <!---<source srcset="/blog/c/boostCategories.avif" type="image/avif">--> <source srcset="/blog/c/boostCategories.webp" type="image/webp"> <source srcset="/blog/c/boostCategories.apng" type="image/apng"> <source srcset="/blog/c/boostCategories.png" type="image/png"> <source srcset="/blog/c/boostCategories.jpg" type="image/jpeg"> <source srcset="/blog/c/boostCategories.jpeg" type="image/jpeg"> <source srcset="/blog/c/boostCategories.jfif" type="image/jpeg"> <source srcset="/blog/c/boostCategories.pjpeg" type="image/jpeg"> <source srcset="/blog/c/boostCategories.pjp" type="image/jpeg"> <source srcset="/blog/c/boostCategories.gif" type="image/gif"> <source srcset="/blog/c/boostCategories.tif" type="image/tiff"> <source srcset="/blog/c/boostCategories.tiff" type="image/tiff"> <source srcset="/blog/c/boostCategories.bmp" type="image/bmp"> <source srcset="/blog/c/boostCategories.ico" type="image/x-icon"> <source srcset="/blog/c/boostCategories.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/c/boostCategories.png" style='width: 100%; ' /> </picture> </a> </div> <p> Boost is used in many <a href='https://www.boost.org/users/uses_shrink.html' target='_blank' rel="nofollow">commercial products</a>, <a href='https://www.boost.org/users/uses_inhouse.html' target='_blank' rel="nofollow">in-house projects</a>, and <a href='https://www.boost.org/users/uses_open.html' target='_blank' rel="nofollow">open-source projects</a>. </p> <p> Ostensibly a C++ library for wide usage, it has become an techical underpinning for Python; that is why Boost documentation also contains information about Python. </p> <h2 id="start">Getting Started</h2> <p> I started by reading the online <a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/index.html' target='_blank' rel="nofollow">Getting Started</a> guide. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/index.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/boost_getting_started.svg" type="image/svg"> <!---<source srcset="/blog/c/boost_getting_started.avif" type="image/avif">--> <source srcset="/blog/c/boost_getting_started.webp" type="image/webp"> <source srcset="/blog/c/boost_getting_started.apng" type="image/apng"> <source srcset="/blog/c/boost_getting_started.png" type="image/png"> <source srcset="/blog/c/boost_getting_started.jpg" type="image/jpeg"> <source srcset="/blog/c/boost_getting_started.jpeg" type="image/jpeg"> <source srcset="/blog/c/boost_getting_started.jfif" type="image/jpeg"> <source srcset="/blog/c/boost_getting_started.pjpeg" type="image/jpeg"> <source srcset="/blog/c/boost_getting_started.pjp" type="image/jpeg"> <source srcset="/blog/c/boost_getting_started.gif" type="image/gif"> <source srcset="/blog/c/boost_getting_started.tif" type="image/tiff"> <source srcset="/blog/c/boost_getting_started.tiff" type="image/tiff"> <source srcset="/blog/c/boost_getting_started.bmp" type="image/bmp"> <source srcset="/blog/c/boost_getting_started.ico" type="image/x-icon"> <source srcset="/blog/c/boost_getting_started.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/c/boost_getting_started.png" style='width: 100%; ' /> </picture> </a> </div> <p> For most Boost functionality, only headers are required, and libraries need not be built. The exceptions are described <a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/unix-variants.html#header-only-libraries' target='_blank' rel="nofollow">here</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/unix-variants.html#header-only-libraries' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/boost_header_only.svg" type="image/svg"> <!---<source srcset="/blog/c/boost_header_only.avif" type="image/avif">--> <source srcset="/blog/c/boost_header_only.webp" type="image/webp"> <source srcset="/blog/c/boost_header_only.apng" type="image/apng"> <source srcset="/blog/c/boost_header_only.png" type="image/png"> <source srcset="/blog/c/boost_header_only.jpg" type="image/jpeg"> <source srcset="/blog/c/boost_header_only.jpeg" type="image/jpeg"> <source srcset="/blog/c/boost_header_only.jfif" type="image/jpeg"> <source srcset="/blog/c/boost_header_only.pjpeg" type="image/jpeg"> <source srcset="/blog/c/boost_header_only.pjp" type="image/jpeg"> <source srcset="/blog/c/boost_header_only.gif" type="image/gif"> <source srcset="/blog/c/boost_header_only.tif" type="image/tiff"> <source srcset="/blog/c/boost_header_only.tiff" type="image/tiff"> <source srcset="/blog/c/boost_header_only.bmp" type="image/bmp"> <source srcset="/blog/c/boost_header_only.ico" type="image/x-icon"> <source srcset="/blog/c/boost_header_only.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/c/boost_header_only.png" style='width: 100%; ' /> </picture> </a> </div> <p> <a href='https://theboostcpplibraries.com/' target='_blank' rel="nofollow">The Boost C++ Libraries</a> is a good free online book, and you can order non-free printed copies. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://theBoostCppLibraries.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/the_boost_cpp_libraries.svg" type="image/svg"> <!---<source srcset="/blog/c/the_boost_cpp_libraries.avif" type="image/avif">--> <source srcset="/blog/c/the_boost_cpp_libraries.webp" type="image/webp"> <source srcset="/blog/c/the_boost_cpp_libraries.apng" type="image/apng"> <source srcset="/blog/c/the_boost_cpp_libraries.png" type="image/png"> <source srcset="/blog/c/the_boost_cpp_libraries.jpg" type="image/jpeg"> <source srcset="/blog/c/the_boost_cpp_libraries.jpeg" type="image/jpeg"> <source srcset="/blog/c/the_boost_cpp_libraries.jfif" type="image/jpeg"> <source srcset="/blog/c/the_boost_cpp_libraries.pjpeg" type="image/jpeg"> <source srcset="/blog/c/the_boost_cpp_libraries.pjp" type="image/jpeg"> <source srcset="/blog/c/the_boost_cpp_libraries.gif" type="image/gif"> <source srcset="/blog/c/the_boost_cpp_libraries.tif" type="image/tiff"> <source srcset="/blog/c/the_boost_cpp_libraries.tiff" type="image/tiff"> <source srcset="/blog/c/the_boost_cpp_libraries.bmp" type="image/bmp"> <source srcset="/blog/c/the_boost_cpp_libraries.ico" type="image/x-icon"> <source srcset="/blog/c/the_boost_cpp_libraries.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/c/the_boost_cpp_libraries.png" style='width: 100%; ' /> </picture> </a> </div> <!-- endregion --> <!-- #region --> <h2 id="install">Ubuntu Installation</h2> <h3 id="boost_ver">Discover the Latest Boost Version Number</h3> <p> The latest version of Boost is shown on <a href='https://https://www.boost.org/users/history/?ref=learnubuntu.com' target='_blank' rel="nofollow"><code>https://www.boost.org/users/history/?ref=learnubuntu.com</code></a>: </p> <p> Let&rsquo;s scrape the lastest version number from the above web page: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddec8f31dc94a'><button class='copyBtn' data-clipboard-target='#iddec8f31dc94a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install libxml2-utils <span class='unselectable'>$ </span>VER=$( wget -q -O - https://www.boost.org/users/history/?ref=learnubuntu.com | \ xmllint --html --xpath '//*[@id="intro"]/div/h2[1]/a[2]' - 2>/dev/null | \ grep -oEi 'Version ([0-9].)*' | \ cut -d' ' -f 2 ) <span class='unselectable'>$ </span>echo $VER <span class='unselectable'>1.83 </span></pre> </div> <!-- endregion --> <h3 id="dl">Download Source &amp; Dependencies</h3> <p> Download and unpack the library source to a new directory within your home directory as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8eca2362b9f'><button class='copyBtn' data-clipboard-target='#idc8eca2362b9f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd <span class='unselectable'>$ </span>V_R=`echo $VER | tr . _` <span class='unselectable'>$ </span>echo $V_R <span class='unselectable'>1_83 </span> <span class='unselectable'>$ </span>NAME=boost_${V_R}_0 <span class='unselectable'>$ </span>echo $NAME <span class='unselectable'>boost_1_83_0 </span> <span class='unselectable'>$ </span>wget -O $NAME.tar.gz \ https://sourceforge.net/projects/boost/files/boost/$VER.0/$NAME.tar.gz/download <span class='unselectable'>$ </span>tar xzvf $NAME.tar.gz <span class='unselectable'>$ </span>cd $NAME/</pre> </div> <!-- endregion --> <p> You should now have the following files and directories: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7edb2c95c4de'><button class='copyBtn' data-clipboard-target='#id7edb2c95c4de' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72 <span class='unselectable'>INSTALL boost/ boostcpp.jam index.htm rst.css Jamroot boost-build.jam bootstrap.bat index.html status/ LICENSE_1_0.txt boost.css bootstrap.sh* libs/ tools/ README.md boost.png doc/ more/ </span></pre> </div> <!-- endregion --> <p> Install the required dependencies for building Boost. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5c27496ded60'><button class='copyBtn' data-clipboard-target='#id5c27496ded60' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt-get update <span class='unselectable'>$ </span>yes | sudo apt install autotools-dev build-essential g++ \ libbz2-dev libicu-dev python3-dev</pre> </div> <!-- endregion --> <h3 id="bootstrap">Build Installer Using <span class="code">Bootstrap</span></h3> <p> Two scripts for building the installation program Boost are provided: <code>bootstrap.sh</code> (for Bash) and <code>bootstrap.bat</code> (for Windows/DOS). By default the scripts build all possible static/shared debug/release Boost libraries. Another default is that the script uses all the CPU cores your computer has for the build process. Even using all the cores on a fast laptop, the build might take 10 minutes. </p> <p> Here is the <code>bootstrap.sh</code> help message: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id802a9a76f6a4'><button class='copyBtn' data-clipboard-target='#id802a9a76f6a4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh -h <span class='unselectable'>`./bootstrap.sh\&#39; builds the Boost build system B2 and prepares Boost for building. This includes setting defaults in the project-config.jam which you can adjust prior to invoking B2.<br/> Usage: ./bootstrap.sh [OPTION]...<br/> Defaults for the options are specified in brackets.<br/> Configuration: -h, --help display this help and exit --with-bjam=BJAM use existing Boost.Jam executable (bjam) [automatically built] --with-toolset=TOOLSET use specific TOOLSET to build B2 and as default for building Boost [automatically detected] --show-libraries show the set of libraries that require build and installation steps (i.e., those libraries that can be used with --with-libraries or --without-libraries), then exit --with-libraries=list build only a particular set of libraries, describing using either a comma-separated list of library names or &quot;all&quot; [all] --without-libraries=list build all libraries except the ones listed [] --with-icu enable Unicode/ICU support in Regex [automatically detected] --without-icu disable Unicode/ICU support in Regex --with-icu=DIR specify the root of the ICU library installation and enable Unicode/ICU support in Regex [automatically detected] --with-python=PYTHON specify the Python executable [python] --with-python-root=DIR specify the root of the Python installation [automatically detected] --with-python-version=X.Y specify the Python version as X.Y [automatically detected]<br/> Installation directories: --prefix=PREFIX install Boost into the given PREFIX [/usr/local] --exec-prefix=EPREFIX install Boost binaries into the given EPREFIX [PREFIX]<br/> More precise control over installation directories: --libdir=DIR install libraries here [EPREFIX/lib] --includedir=DIR install headers here [PREFIX/include] </span></pre> </div> <p> <code>Bootstrap.sh</code> compiles and links a build program called <code>b2</code>, which by default installs all of Boost into <code>/usr/<wbr>local/</code>. To be more specific, the default location for Boost include files to be installed into is <code>/usr/<wbr>local/<wbr>include/<wbr>boost/</code>, and Boost libraries are installed by default into <code>/usr/<wbr>local/<wbr>lib/</code>. </p> <!-- endregion --> <p> Create (and recreate) the <code>b2</code> installation program for Boost for <i>each</i> <a href='/blog/2021/04/09/python-venvs.html'>Python virtual environment</a>. By default, <code>bootstrap.sh</code> uses the currently active Python virtual environment, like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7b0c299f79fd'><button class='copyBtn' data-clipboard-target='#id7b0c299f79fd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh</pre> </div> <p> Following is how to build <code>b2</code> for the virtual environment at <code>~/venv/blah/</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idec4ce1be1364'><button class='copyBtn' data-clipboard-target='#idec4ce1be1364' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh --with-python-root=~/venv/blah <span class='unselectable'>Building B2 engine..<br/> ### ### ### Using &#39;gcc&#39; toolset. ### ###<br/> g++ (Ubuntu 12.3.0-1ubuntu1~23.04) 12.3.0 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.<br/><br/> ### ###<br/> &gt; g++ -x c++ -std=c++11 -O2 -s -DNDEBUG builtins.cpp class.cpp command.cpp compile.cpp constants.cpp cwd.cpp debug.cpp debugger.cpp execcmd.cpp execnt.cpp execunix.cpp filesys.cpp filent.cpp fileunix.cpp frames.cpp function.cpp glob.cpp hash.cpp hcache.cpp hdrmacro.cpp headers.cpp jam_strings.cpp jam.cpp jamgram.cpp lists.cpp make.cpp make1.cpp md5.cpp mem.cpp modules.cpp native.cpp object.cpp option.cpp output.cpp parse.cpp pathnt.cpp pathsys.cpp pathunix.cpp regexp.cpp rules.cpp scan.cpp search.cpp startup.cpp subst.cpp sysinfo.cpp timestamp.cpp variable.cpp w32_getreg.cpp modules/order.cpp modules/path.cpp modules/property-set.cpp modules/regex.cpp modules/sequence.cpp modules/set.cpp -o b2 tools/build/src/engine/b2 Detecting Python version... 3.11 Unicode/ICU support for Boost.Regex?... /usr Backing up existing B2 configuration in project-config.jam.2 Generating B2 configuration in project-config.jam for gcc...<br/> Bootstrapping is done. To build, run:<br/> ./b2<br/> To generate header files, run:<br/> ./b2 headers<br/> The configuration generated uses gcc to build by default. If that is unintended either use the --with-toolset option or adjust configuration, by editing &#39;project-config.jam&#39;.<br/> Further information:<br/> - Command line help: ./b2 --help<br/> - Getting started guide: http://www.boost.org/more/getting_started/unix-variants.html<br/> - B2 documentation: http://www.boost.org/build/ </span></pre> </div> <!-- endregion --> <h3 id="b2">Running <span class="code">B2</span>, the Boost Installer</h3> <p> Following is the help message for the newly created <code>b2</code> Boost installation program. As you can see, the defaults specified when creating the <code>b2</code> script can be overidden. Note that not all of the available options are described in this help message; for example, the <code>-j</code> option is only described in the <a href='https://www.boost.org/doc/libs/1_83_0/tools/build/doc/html/index.html' target='_blank' rel="nofollow">online help for <code>b2</code></a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5e0107fc8899'><button class='copyBtn' data-clipboard-target='#id5e0107fc8899' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./b2 --help <span class='unselectable'>B2 4.10-git<br/> Project-specific help:<br/> Project has jamfile at Jamroot<br/> Usage:<br/> b2 [options] [properties] [install|stage]<br/> Builds and installs Boost.<br/> Targets and Related Options:<br/> install Install headers and compiled library files to the ======= configured locations (below).<br/> --prefix=&lt;PREFIX&gt; Install architecture independent files here. Default: C:\Boost on Windows Default: /usr/local on Unix, Linux, etc.<br/> --exec-prefix=&lt;EPREFIX&gt; Install architecture dependent files here. Default: &lt;PREFIX&gt;<br/> --libdir=&lt;LIBDIR&gt; Install library files here. Default: &lt;EPREFIX&gt;/lib<br/> --includedir=&lt;HDRDIR&gt; Install header files here. Default: &lt;PREFIX&gt;/include<br/> --cmakedir=&lt;CMAKEDIR&gt; Install CMake configuration files here. Default: &lt;LIBDIR&gt;/cmake<br/> --no-cmake-config Do not install CMake configuration files.<br/> stage Build and install only compiled library files to the ===== stage directory.<br/> --stagedir=&lt;STAGEDIR&gt; Install library files here Default: ./stage<br/> Other Options:<br/> --build-type=&lt;type&gt; Build the specified pre-defined set of variations of the libraries. Note, that which variants get built depends on what each library supports.<br/> -- minimal -- (default) Builds a minimal set of variants. On Windows, these are static multithreaded libraries in debug and release modes, using shared runtime. On Linux, these are static and shared multithreaded libraries in release mode.<br/> -- complete -- Build all possible variations.<br/> --build-dir=DIR Build in this location instead of building within the distribution tree. Recommended!<br/> --show-libraries Display the list of Boost libraries that require build and installation steps, and then exit.<br/> --layout=&lt;layout&gt; Determine whether to choose library names and header locations such that multiple versions of Boost or multiple compilers can be used on the same system.<br/> -- versioned -- Names of boost binaries include the Boost version number, name and version of the compiler and encoded build properties. Boost headers are installed in a subdirectory of &lt;HDRDIR&gt; whose name contains the Boost version number.<br/> -- tagged -- Names of boost binaries include the encoded build properties such as variant and threading, but do not including compiler name and version, or Boost version. This option is useful if you build several variants of Boost, using the same compiler.<br/> -- system -- Binaries names do not include the Boost version number or the name and version number of the compiler. Boost headers are installed directly into &lt;HDRDIR&gt;. This option is intended for system integrators building distribution packages.<br/> The default value is &#39;versioned&#39; on Windows, and &#39;system&#39; on Unix.<br/> --buildid=ID Add the specified ID to the name of built libraries. The default is to not add anything.<br/> --python-buildid=ID Add the specified ID to the name of built libraries that depend on Python. The default is to not add anything. This ID is added in addition to --buildid.<br/> --help This message.<br/> --with-&lt;library&gt; Build and install the specified &lt;library&gt;. If this option is used, only libraries specified using this option will be built.<br/> --without-&lt;library&gt; Do not build, stage, or install the specified &lt;library&gt;. By default, all libraries are built.<br/> Properties:<br/> toolset=toolset Indicate the toolset to build with.<br/> variant=debug|release Select the build variant<br/> link=static|shared Whether to build static or shared libraries<br/> threading=single|multi Whether to build single or multithreaded binaries<br/> runtime-link=static|shared Whether to link to static or shared C and C++ runtime.<br/><br/> General command line usage:<br/> b2 [options] [properties] [targets]<br/> Options, properties and targets can be specified in any order.<br/> Important Options:<br/> * --clean Remove targets instead of building * -a Rebuild everything * -n Don&#39;t execute the commands, only print them * -d+2 Show commands as they are executed * -d0 Suppress all informational messages * -q Stop at first error * --reconfigure Rerun all configuration checks * --durations[=N] Report top N targets by execution time * --debug-configuration Diagnose configuration * --debug-building Report which targets are built with what properties * --debug-generator Diagnose generator search/execution<br/> Further Help:<br/> The following options can be used to obtain additional documentation.<br/> * --help-options Print more obscure command line options. * --help-internal B2 implementation details. * --help-doc-options Implementation details doc formatting.<br/> ...found 1 target... </span></pre> </div> <!-- endregion --> <p> The following runs the newly created <code>b2</code> program, which builds and installs Boost using all CPU cores. </p> <ol> <li> The Python version is stored into an environment variable called <code>PVER</code>. </li> <li> The <code>CPLUS_INCLUDE_PATH</code> environment variable is pointed at the location of the Python 3 include files. <code>PVER</code> was used to set the proper value for <code>CPLUS_INCLUDE_PATH</code>. </li> <li> <code>B2</code> generates screen upon screen of compiler warnings. The <code>-d 1</code> option suppresses them. </li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id706b0f713da9'><button class='copyBtn' data-clipboard-target='#id706b0f713da9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>PVER="$(python --version | cut -d' ' -f 2 | grep -oEi '(3.[0-9]*)')" <span class='unselectable'>$ </span>echo $PVER <span class='unselectable'>3.11 </span> <span class='unselectable'>$ </span>sudo CPLUS_INCLUDE_PATH=/usr/include/python$PVER ./b2 -d 1 install <span class='unselectable'>Performing configuration checks<br/> - default address-model : 64-bit (cached) [1] - default architecture : x86 (cached) [1] - compiler supports SSE2 : yes (cached) [2] - compiler supports SSE4.1 : yes (cached) [2] - has std::atomic_ref : no (cached) [2] - has -Wl,--no-undefined : yes (cached) [2] - has statx : yes (cached) [2] - has init_priority attribute : yes (cached) [2] - has stat::st_blksize : yes (cached) [2] - has stat::st_mtim : yes (cached) [2] - has stat::st_mtimensec : no (cached) [2] - has stat::st_mtimespec : no (cached) [2] - has stat::st_birthtim : no (cached) [2] - has stat::st_birthtimensec : no (cached) [2] - has stat::st_birthtimespec : no (cached) [2] - has fdopendir(O_NOFOLLOW) : yes (cached) [2] - has dirent::d_type : yes (cached) [2] - has POSIX *at APIs : yes (cached) [2] - cxx11_auto_declarations : yes (cached) [2] - cxx11_constexpr : yes (cached) [2] - cxx11_defaulted_functions : yes (cached) [2] - cxx11_final : yes (cached) [2] - cxx11_hdr_mutex : yes (cached) [2] - cxx11_hdr_tuple : yes (cached) [2] - cxx11_lambdas : yes (cached) [2] - cxx11_noexcept : yes (cached) [2] - cxx11_nullptr : yes (cached) [2] - cxx11_rvalue_references : yes (cached) [2] - cxx11_template_aliases : yes (cached) [2] - cxx11_thread_local : yes (cached) [2] - cxx11_variadic_templates : yes (cached) [2] - has_icu builds : yes (cached) [2] warning: Graph library does not contain MPI-based parallel components. note: to enable them, add &quot;using mpi ;&quot; to your user-config.jam. note: to suppress this message, pass &quot;--without-graph_parallel&quot; to bjam. - zlib : yes (cached) - bzip2 : yes (cached) - lzma : yes (cached) - zstd : yes (cached) - has_lzma_cputhreads builds : yes (cached) [2] - cxx11_decltype : yes (cached) [2] - cxx11_basic_alignas : yes (cached) [2] - iconv (libc) : yes (cached) [2] - icu : yes (cached) [2] - cxx11_defaulted_moves : yes (cached) [2] - cxx11_hdr_functional : yes (cached) [2] - cxx11_hdr_type_traits : yes (cached) [2] - cxx11_override : yes (cached) [2] - cxx11_range_based_for : yes (cached) [2] - cxx11_scoped_enums : yes (cached) [2] - cxx11_smart_ptr : yes (cached) [2] - cxx11_static_assert : yes (cached) [2] - lockfree boost::atomic_flag : yes (cached) [2] - native atomic int32 supported : yes (cached) [2] - native syslog supported : yes (cached) [2] - pthread supports robust mutexes : yes (cached) [2] - compiler supports SSSE3 : yes (cached) [2] - compiler supports AVX2 : yes (cached) [2] - gcc visibility : yes (cached) [2] - sfinae_expr : yes (cached) [2] - cxx11_unified_initialization_syntax : yes (cached) [2] - cxx11_hdr_initializer_list : yes (cached) [2] - cxx11_hdr_chrono : yes (cached) [2] - cxx11_numeric_limits : yes (cached) [2] - cxx11_hdr_array : yes (cached) [2] - cxx11_hdr_atomic : yes (cached) [2] - cxx11_allocator : yes (cached) [2] - cxx11_explicit_conversion_operators : yes (cached) [2] - long double support : yes (cached) [2] warning: skipping optional Message Passing Interface (MPI) library. note: to enable MPI support, add &quot;using mpi ;&quot; to user-config.jam. note: to suppress this message, pass &quot;--without-mpi&quot; to bjam. note: otherwise, you can safely ignore this message. - cxx11_char16_t : yes (cached) [2] - cxx11_char32_t : yes (cached) [2] - Has Large File Support : yes (cached) [2] - Has attribute init_priority : yes (cached) [2] - libbacktrace builds : yes (cached) [2] - addr2line builds : yes (cached) [2] - WinDbg builds : no (cached) [2] - WinDbg builds : no (cached) [3] - WinDbgCached builds : no (cached) [2] - WinDbgCached builds : no (cached) [3] - BOOST_COMP_GNUC &gt;= 4.3.0 : yes (cached) [2] - BOOST_COMP_GNUC &gt;= 4.3.0 : yes (cached) [4] - cxx11_hdr_thread : yes (cached) [2] - cxx11_hdr_regex : yes (cached) [2] - compiler supports SSE2 : yes (cached) [4] - compiler supports SSE4.1 : yes (cached) [4] - has std::atomic_ref : no (cached) [4] - has statx : yes (cached) [4] - has init_priority attribute : yes (cached) [4] - has stat::st_blksize : yes (cached) [4] - has stat::st_mtim : yes (cached) [4] - has stat::st_mtimensec : no (cached) [4] - has stat::st_mtimespec : no (cached) [4] - has stat::st_birthtim : no (cached) [4] - has stat::st_birthtimensec : no (cached) [4] - has stat::st_birthtimespec : no (cached) [4] - has fdopendir(O_NOFOLLOW) : yes (cached) [4] - has dirent::d_type : yes (cached) [4] - has POSIX *at APIs : yes (cached) [4] - cxx11_auto_declarations : yes (cached) [4] - cxx11_constexpr : yes (cached) [4] - cxx11_defaulted_functions : yes (cached) [4] - cxx11_final : yes (cached) [4] - cxx11_hdr_mutex : yes (cached) [4] - cxx11_hdr_tuple : yes (cached) [4] - cxx11_lambdas : yes (cached) [4] - cxx11_noexcept : yes (cached) [4] - cxx11_nullptr : yes (cached) [4] - cxx11_rvalue_references : yes (cached) [4] - cxx11_template_aliases : yes (cached) [4] - cxx11_thread_local : yes (cached) [4] - cxx11_variadic_templates : yes (cached) [4] - has_icu builds : yes (cached) [4] - zlib : yes (cached) [5] - bzip2 : yes (cached) [5] - lzma : yes (cached) [5] - zstd : yes (cached) [5] - has_lzma_cputhreads builds : yes (cached) [4] - cxx11_decltype : yes (cached) [4] - cxx11_basic_alignas : yes (cached) [4] - iconv (libc) : yes (cached) [4] - icu : yes (cached) [4] - cxx11_defaulted_moves : yes (cached) [4] - cxx11_hdr_functional : yes (cached) [4] - cxx11_hdr_type_traits : yes (cached) [4] - cxx11_override : yes (cached) [4] - cxx11_range_based_for : yes (cached) [4] - cxx11_scoped_enums : yes (cached) [4] - cxx11_smart_ptr : yes (cached) [4] - cxx11_static_assert : yes (cached) [4] - lockfree boost::atomic_flag : yes (cached) [4] - native atomic int32 supported : yes (cached) [4] - native syslog supported : yes (cached) [4] - pthread supports robust mutexes : yes (cached) [4] - compiler supports SSSE3 : yes (cached) [4] - compiler supports AVX2 : yes (cached) [4] - gcc visibility : yes (cached) [4] - sfinae_expr : yes (cached) [4] - cxx11_unified_initialization_syntax : yes (cached) [4] - cxx11_hdr_initializer_list : yes (cached) [4] - cxx11_hdr_chrono : yes (cached) [4] - cxx11_numeric_limits : yes (cached) [4] - cxx11_hdr_array : yes (cached) [4] - cxx11_hdr_atomic : yes (cached) [4] - cxx11_allocator : yes (cached) [4] - cxx11_explicit_conversion_operators : yes (cached) [4] - long double support : yes (cached) [4] - cxx11_char16_t : yes (cached) [4] - cxx11_char32_t : yes (cached) [4] - Has Large File Support : yes (cached) [4] - Has attribute init_priority : yes (cached) [4] - libbacktrace builds : yes (cached) [4] - addr2line builds : yes (cached) [4] - WinDbg builds : no (cached) [4] - WinDbg builds : no (cached) [6] - WinDbgCached builds : no (cached) [4] - WinDbgCached builds : no (cached) [6] - cxx11_hdr_thread : yes (cached) [4] - cxx11_hdr_regex : yes (cached) [4]<br/> [1] gcc-12 [2] gcc-12/release/python-3.11/threading-multi/visibility-hidden [3] gcc-12/release/build-no/python-3.11/threading-multi/visibility-hidden [4] gcc-12/release/link-static/python-3.11/threading-multi/visibility-hidden [5] link-static [6] gcc-12/release/build-no/link-static/python-3.11/threading-multi/visibility-hidden<br/> Component configuration:<br/> - atomic : building - chrono : building - container : building - context : building - contract : building - coroutine : building - date_time : building - exception : building - fiber : building - filesystem : building - graph : building - graph_parallel : building - headers : building - iostreams : building - json : building - locale : building - log : building - math : building - mpi : building - nowide : building - program_options : building - python : building - random : building - regex : building - serialization : building - stacktrace : building - system : building - test : building - thread : building - timer : building - type_erasure : building - url : building - wave : building<br/> ...patience... ...patience... ...patience... ...patience... ...patience... ...patience... ...patience... ...found 52031 targets... </span></pre> </div> <!-- endregion --> <p> The <code>b2</code> installation program stored the following include files in <code>/usr/<wbr>local/<wbr>include/<wbr>boost/</code> </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c80d47bb68c'><button class='copyBtn' data-clipboard-target='#id3c80d47bb68c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72 /usr/local/include/boost/ <span class='unselectable'>accumulators/ make_unique.hpp algorithm/ math/ align/ math_fwd.hpp align.hpp mem_fn.hpp aligned_storage.hpp memory_order.hpp any/ metaparse/ any.hpp metaparse.hpp archive/ move/ array.hpp mp11/ asio/ mp11.hpp asio.hpp mpi/ assert/ mpi.hpp assert.hpp mpl/ assign/ msm/ assign.hpp multi_array/ atomic/ multi_array.hpp atomic.hpp multi_index/ beast/ multi_index_container.hpp beast.hpp multi_index_container_fwd.hpp bimap/ multiprecision/ bimap.hpp mysql/ bind/ mysql.hpp bind.hpp next_prior.hpp blank.hpp non_type.hpp blank_fwd.hpp noncopyable.hpp call_traits.hpp nondet_random.hpp callable_traits/ none.hpp callable_traits.hpp none_t.hpp cast.hpp nowide/ cerrno.hpp numeric/ checked_delete.hpp operators.hpp chrono/ operators_v1.hpp chrono.hpp optional/ circular_buffer/ optional.hpp circular_buffer.hpp outcome/ circular_buffer_fwd.hpp outcome.hpp compat/ parameter/ compatibility/ parameter.hpp compressed_pair.hpp pending/ compute/ pfr/ compute.hpp pfr.hpp concept/ phoenix/ concept_archetype.hpp phoenix.hpp concept_check/ pointee.hpp concept_check.hpp pointer_cast.hpp config/ pointer_to_other.hpp config.hpp poly_collection/ container/ polygon/ container_hash/ polymorphic_cast.hpp context/ polymorphic_pointer_cast.hpp contract/ pool/ contract.hpp predef/ contract_macro.hpp predef.h convert/ preprocessor/ convert.hpp preprocessor.hpp core/ process/ coroutine/ process.hpp coroutine2/ program_options/ crc.hpp program_options.hpp cregex.hpp progress.hpp cstdfloat.hpp property_map/ cstdint.hpp property_tree/ cstdlib.hpp proto/ current_function.hpp ptr_container/ cxx11_char_types.hpp python/ date_time/ python.hpp date_time.hpp qvm/ describe/ qvm.hpp describe.hpp qvm_lite.hpp detail/ random/ dll/ random.hpp dll.hpp range/ dynamic_bitset/ range.hpp dynamic_bitset.hpp ratio/ dynamic_bitset_fwd.hpp ratio.hpp enable_shared_from_this.hpp rational.hpp endian/ ref.hpp endian.hpp regex/ exception/ regex.h exception_ptr.hpp regex.hpp fiber/ regex_fwd.hpp filesystem/ safe_numerics/ filesystem.hpp scope_exit.hpp flyweight/ scoped_array.hpp flyweight.hpp scoped_ptr.hpp foreach.hpp serialization/ foreach_fwd.hpp shared_array.hpp format/ shared_container_iterator.hpp format.hpp shared_ptr.hpp function/ signals2/ function.hpp signals2.hpp function_equal.hpp smart_ptr/ function_output_iterator.hpp smart_ptr.hpp function_types/ sort/ functional/ spirit/ functional.hpp spirit.hpp fusion/ stacktrace/ generator_iterator.hpp stacktrace.hpp geometry/ statechart/ geometry.hpp static_assert.hpp get_pointer.hpp static_string/ gil/ static_string.hpp gil.hpp stl_interfaces/ graph/ swap.hpp hana/ system/ hana.hpp system.hpp heap/ test/ histogram/ thread/ histogram.hpp thread.hpp hof/ throw_exception.hpp hof.hpp timer/ icl/ timer.hpp implicit_cast.hpp token_functions.hpp indirect_reference.hpp token_iterator.hpp integer/ tokenizer.hpp integer.hpp tti/ integer_fwd.hpp tuple/ integer_traits.hpp type.hpp interprocess/ type_erasure/ intrusive/ type_index/ intrusive_ptr.hpp type_index.hpp io/ type_traits/ io_fwd.hpp type_traits.hpp iostreams/ typeof/ is_placeholder.hpp units/ iterator/ unordered/ iterator.hpp unordered_map.hpp iterator_adaptors.hpp unordered_set.hpp json/ url/ json.hpp url.hpp lambda/ utility/ lambda2/ utility.hpp lambda2.hpp uuid/ leaf/ variant/ leaf.hpp variant.hpp lexical_cast/ variant2/ lexical_cast.hpp variant2.hpp limits.hpp version.hpp local_function/ visit_each.hpp local_function.hpp vmd/ locale/ wave/ locale.hpp wave.hpp lockfree/ weak_ptr.hpp log/ winapi/ logic/ xpressive/ make_default.hpp yap/ make_shared.hpp </span></pre> </div> <!-- endregion --> <p> The <code>b2</code> installation program stored the following libraries in <code>/usr/<wbr>local/<wbr>lib/</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0caeac51e248'><button class='copyBtn' data-clipboard-target='#id0caeac51e248' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72 /usr/local/lib <span class='unselectable'>cmake/ libboost_atomic.a libboost_atomic.so@ libboost_atomic.so.1.83.0* libboost_chrono.a libboost_chrono.so@ libboost_chrono.so.1.83.0* libboost_container.a libboost_container.so@ libboost_container.so.1.83.0* libboost_context.a libboost_context.so@ libboost_context.so.1.83.0* libboost_contract.a libboost_contract.so@ libboost_contract.so.1.83.0* libboost_coroutine.a libboost_coroutine.so@ libboost_coroutine.so.1.83.0* libboost_date_time.a libboost_date_time.so@ libboost_date_time.so.1.83.0* libboost_exception.a libboost_fiber.a libboost_fiber.so@ libboost_fiber.so.1.83.0* libboost_filesystem.a libboost_filesystem.so@ libboost_filesystem.so.1.83.0* libboost_graph.a libboost_graph.so@ libboost_graph.so.1.83.0* libboost_iostreams.a libboost_iostreams.so@ libboost_iostreams.so.1.83.0* libboost_json.a libboost_json.so@ libboost_json.so.1.83.0* libboost_locale.a libboost_locale.so@ libboost_locale.so.1.83.0* libboost_log.a libboost_log.so@ libboost_log.so.1.83.0* libboost_log_setup.a libboost_log_setup.so@ libboost_log_setup.so.1.83.0* libboost_math_c99.a libboost_math_c99.so@ libboost_math_c99.so.1.83.0* libboost_math_c99f.a libboost_math_c99f.so@ libboost_math_c99f.so.1.83.0* libboost_math_c99l.a libboost_math_c99l.so@ libboost_math_c99l.so.1.83.0* libboost_math_tr1.a libboost_math_tr1.so@ libboost_math_tr1.so.1.83.0* libboost_math_tr1f.a libboost_math_tr1f.so@ libboost_math_tr1f.so.1.83.0* libboost_math_tr1l.a libboost_math_tr1l.so@ libboost_math_tr1l.so.1.83.0* libboost_nowide.a libboost_nowide.so@ libboost_nowide.so.1.83.0* libboost_prg_exec_monitor.a libboost_prg_exec_monitor.so@ libboost_prg_exec_monitor.so.1.83.0* libboost_program_options.a libboost_program_options.so@ libboost_program_options.so.1.83.0* libboost_random.a libboost_random.so@ libboost_random.so.1.83.0* libboost_regex.a libboost_regex.so@ libboost_regex.so.1.83.0* libboost_serialization.a libboost_serialization.so@ libboost_serialization.so.1.83.0* libboost_stacktrace_addr2line.a libboost_stacktrace_addr2line.so@ libboost_stacktrace_addr2line.so.1.83.0* libboost_stacktrace_backtrace.a libboost_stacktrace_backtrace.so@ libboost_stacktrace_backtrace.so.1.83.0* libboost_stacktrace_basic.a libboost_stacktrace_basic.so@ libboost_stacktrace_basic.so.1.83.0* libboost_stacktrace_noop.a libboost_stacktrace_noop.so@ libboost_stacktrace_noop.so.1.83.0* libboost_system.a libboost_system.so@ libboost_system.so.1.83.0* libboost_test_exec_monitor.a libboost_thread.a libboost_thread.so@ libboost_thread.so.1.83.0* libboost_timer.a libboost_timer.so@ libboost_timer.so.1.83.0* libboost_type_erasure.a libboost_type_erasure.so@ libboost_type_erasure.so.1.83.0* libboost_unit_test_framework.a libboost_unit_test_framework.so@ libboost_unit_test_framework.so.1.83.0* libboost_url.a libboost_url.so@ libboost_url.so.1.83.0* libboost_wave.a libboost_wave.so@ libboost_wave.so.1.83.0* libboost_wserialization.a libboost_wserialization.so@ libboost_wserialization.so.1.83.0* node_modules/ python3.11/ </span></pre> </div> <!-- endregion --> <h3 id="load_path">Verify Boost System Libraries</h3> <p> The <code>ldconfig</code> command manages Linux system libraries. Here is the help message: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1aa06b6bedd6'><button class='copyBtn' data-clipboard-target='#id1aa06b6bedd6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man ldconfig <span class='unselectable'>ldconfig(8) System Manager&#39;s Manual ldconfig(8)<br/> NAME ldconfig - configure dynamic linker run-time bindings<br/> SYNOPSIS /sbin/ldconfig [-nNvVX] [-C cache] [-f conf] [-r root] directory ...<br/> /sbin/ldconfig -l [-v] library ...<br/> /sbin/ldconfig -p<br/> DESCRIPTION ldconfig creates the necessary links and cache to the most re&#8208; cent shared libraries found in the directories specified on the command line, in the file /etc/ld.so.conf, and in the trusted directories, /lib and /usr/lib. On some 64-bit architectures such as x86-64, /lib and /usr/lib are the trusted directories for 32-bit libraries, while /lib64 and /usr/lib64 are used for 64-bit libraries.<br/> The cache is used by the run-time linker, ld.so or ld-linux.so. ldconfig checks the header and filenames of the libraries it encounters when determining which versions should have their links updated. ldconfig should normally be run by the supe&#8208; ruser as it may require write permission on some root owned di&#8208; rectories and files.<br/> ldconfig will look only at files that are named lib*.so* (for regular shared objects) or ld-*.so* (for the dynamic loader it&#8208; self). Other files will be ignored. Also, ldconfig expects a certain pattern to how the symbolic links are set up, like this example, where the middle file (libfoo.so.1 here) is the SONAME for the library:<br/> libfoo.so -&gt; libfoo.so.1 -&gt; libfoo.so.1.12<br/> Failure to follow this pattern may result in compatibility is&#8208; sues after an upgrade.<br/> OPTIONS -c fmt --format=fmt (Since glibc 2.2) Use cache format fmt, which is one of old, new, or compat. Since glibc 2.32, the default is new. Before that, it was compat.<br/> -C cache Use cache instead of /etc/ld.so.cache.<br/> -f conf Use conf instead of /etc/ld.so.conf.<br/> -i --ignore-aux-cache (Since glibc 2.7) Ignore auxiliary cache file.<br/> -l (Since glibc 2.2) Interpret each operand as a libary name and configure its links. Intended for use only by experts.<br/> -n Process only the directories specified on the command line; don&#39;t process the trusted directories, nor those specified in /etc/ld.so.conf. Implies -N.<br/> -N Don&#39;t rebuild the cache. Unless -X is also specified, links are still updated.<br/> -p --print-cache Print the lists of directories and candidate libraries stored in the current cache.<br/> -r root Change to and use root as the root directory.<br/> -v --verbose Verbose mode. Print current version number, the name of each directory as it is scanned, and any links that are created. Overrides quiet mode.<br/> -V --version Print program version.<br/> -X Don&#39;t update links. Unless -N is also specified, the cache is still rebuilt.<br/> FILES /lib/ld.so is the run-time linker/loader. /etc/ld.so.conf contains a list of directories, one per line, in which to search for libraries. /etc/ld.so.cache contains an ordered list of libraries found in the di&#8208; rectories specified in /etc/ld.so.conf, as well as those found in the trusted directories.<br/> SEE ALSO ldd(1), ld.so(8)<br/> Linux man-pages 6.03 2023-01-07 ldconfig(8) </span></pre> </div> <!-- endregion --> <p> The Boost installation procedure automatically causes the newly built Boost libraries to be added to the configured system libraries when <code>bootstrap.sh</code> is not invoked with a target path, for example by specifying the <code>--with-python-root</code> option. </p> <p> To manually add the newly built Boost libraries to the system load path, use <code>ldconfig</code>. The following adds the newly compiled and installed Boost libraries in <code>/usr/local/lib/</code> (and any other libraries that might happen to be in that directory) to the <code>ld.so</code> cache. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id01f12b563fb1'><button class='copyBtn' data-clipboard-target='#id01f12b563fb1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ldconfig -n /usr/local/lib</pre> </div> <!-- endregion --> <p> We can verify that the library cache now contains the newly built Boost libraries by using <code>ldconfig</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf927a37e1aa2'><button class='copyBtn' data-clipboard-target='#idf927a37e1aa2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ldconfig -p | grep boost <span class='unselectable'>libboost_wserialization.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_wserialization.so.1.83.0 libboost_wserialization.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_wserialization.so libboost_wave.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_wave.so.1.83.0 libboost_wave.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_wave.so libboost_url.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_url.so.1.83.0 libboost_url.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_url.so libboost_unit_test_framework.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_unit_test_framework.so.1.83.0 libboost_unit_test_framework.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_unit_test_framework.so libboost_type_erasure.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_type_erasure.so.1.83.0 libboost_type_erasure.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_type_erasure.so libboost_timer.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_timer.so.1.83.0 libboost_timer.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_timer.so libboost_thread.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_thread.so.1.83.0 libboost_thread.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_thread.so libboost_system.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_system.so.1.83.0 libboost_system.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_system.so libboost_stacktrace_noop.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_noop.so.1.83.0 libboost_stacktrace_noop.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_noop.so libboost_stacktrace_basic.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_basic.so.1.83.0 libboost_stacktrace_basic.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_basic.so libboost_stacktrace_backtrace.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_backtrace.so.1.83.0 libboost_stacktrace_backtrace.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_backtrace.so libboost_stacktrace_addr2line.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_addr2line.so.1.83.0 libboost_stacktrace_addr2line.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_stacktrace_addr2line.so libboost_serialization.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_serialization.so.1.83.0 libboost_serialization.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_serialization.so libboost_regex.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_regex.so.1.83.0 libboost_regex.so.1.74.0 (libc6,x86-64) =&gt; /lib/x86_64-linux-gnu/libboost_regex.so.1.74.0 libboost_regex.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_regex.so libboost_random.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_random.so.1.83.0 libboost_random.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_random.so libboost_python311.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_python311.so.1.83.0 libboost_python311.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_python311.so libboost_program_options.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_program_options.so.1.83.0 libboost_program_options.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_program_options.so libboost_prg_exec_monitor.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_prg_exec_monitor.so.1.83.0 libboost_prg_exec_monitor.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_prg_exec_monitor.so libboost_nowide.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_nowide.so.1.83.0 libboost_nowide.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_nowide.so libboost_math_tr1l.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1l.so.1.83.0 libboost_math_tr1l.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1l.so libboost_math_tr1f.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1f.so.1.83.0 libboost_math_tr1f.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1f.so libboost_math_tr1.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1.so.1.83.0 libboost_math_tr1.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_tr1.so libboost_math_c99l.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99l.so.1.83.0 libboost_math_c99l.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99l.so libboost_math_c99f.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99f.so.1.83.0 libboost_math_c99f.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99f.so libboost_math_c99.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99.so.1.83.0 libboost_math_c99.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_math_c99.so libboost_log_setup.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_log_setup.so.1.83.0 libboost_log_setup.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_log_setup.so libboost_log.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_log.so.1.83.0 libboost_log.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_log.so libboost_locale.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_locale.so.1.83.0 libboost_locale.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_locale.so libboost_json.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_json.so.1.83.0 libboost_json.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_json.so libboost_iostreams.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_iostreams.so.1.83.0 libboost_iostreams.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_iostreams.so libboost_graph.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_graph.so.1.83.0 libboost_graph.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_graph.so libboost_filesystem.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_filesystem.so.1.83.0 libboost_filesystem.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_filesystem.so libboost_fiber.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_fiber.so.1.83.0 libboost_fiber.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_fiber.so libboost_date_time.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_date_time.so.1.83.0 libboost_date_time.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_date_time.so libboost_coroutine.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_coroutine.so.1.83.0 libboost_coroutine.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_coroutine.so libboost_contract.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_contract.so.1.83.0 libboost_contract.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_contract.so libboost_context.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_context.so.1.83.0 libboost_context.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_context.so libboost_container.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_container.so.1.83.0 libboost_container.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_container.so libboost_chrono.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_chrono.so.1.83.0 libboost_chrono.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_chrono.so libboost_atomic.so.1.83.0 (libc6,x86-64) =&gt; /usr/local/lib/libboost_atomic.so.1.83.0 libboost_atomic.so (libc6,x86-64) =&gt; /usr/local/lib/libboost_atomic.so </span></pre> </div> <!-- endregion --> <p> As a further check, examine the full path of an arbitrary include file (<code>src.hpp</code>) and list the Boost libraries: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddab3c9a57e74'><button class='copyBtn' data-clipboard-target='#iddab3c9a57e74' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>locate src.hpp <span class='unselectable'>/usr/include/boost/asio/impl/src.hpp /usr/include/boost/asio/ssl/impl/src.hpp /usr/include/boost/beast/src.hpp </span> <span class='unselectable'>$ </span>find /usr/local/lib/ -iname *boost* <span class='unselectable'>/usr/local/lib/libboost_math_tr1f.a /usr/local/lib/libboost_log_setup.so /usr/local/lib/libboost_system.so.1.83.0 /usr/local/lib/libboost_system.so /usr/local/lib/libboost_serialization.a /usr/local/lib/libboost_context.a /usr/local/lib/libboost_nowide.so /usr/local/lib/libboost_contract.so.1.83.0 /usr/local/lib/libboost_prg_exec_monitor.a /usr/local/lib/libboost_stacktrace_backtrace.so /usr/local/lib/libboost_iostreams.so /usr/local/lib/libboost_math_c99.so.1.83.0 /usr/local/lib/libboost_stacktrace_addr2line.so /usr/local/lib/libboost_locale.so.1.83.0 /usr/local/lib/libboost_math_c99l.so.1.83.0 /usr/local/lib/libboost_timer.a /usr/local/lib/libboost_atomic.so.1.83.0 /usr/local/lib/libboost_url.so.1.83.0 /usr/local/lib/libboost_math_c99.a /usr/local/lib/libboost_date_time.so /usr/local/lib/libboost_fiber.a /usr/local/lib/libboost_filesystem.so.1.83.0 /usr/local/lib/libboost_date_time.a /usr/local/lib/libboost_log_setup.a /usr/local/lib/libboost_type_erasure.so /usr/local/lib/libboost_json.so.1.83.0 /usr/local/lib/libboost_wave.so.1.83.0 /usr/local/lib/libboost_unit_test_framework.so /usr/local/lib/libboost_type_erasure.so.1.83.0 /usr/local/lib/libboost_random.so.1.83.0 /usr/local/lib/libboost_filesystem.so /usr/local/lib/libboost_stacktrace_basic.a /usr/local/lib/libboost_math_c99.so /usr/local/lib/libboost_regex.a /usr/local/lib/libboost_coroutine.so.1.83.0 /usr/local/lib/libboost_filesystem.a /usr/local/lib/libboost_date_time.so.1.83.0 /usr/local/lib/libboost_wave.a /usr/local/lib/libboost_regex.so /usr/local/lib/libboost_container.a /usr/local/lib/libboost_locale.a /usr/local/lib/libboost_graph.so /usr/local/lib/libboost_graph.so.1.83.0 /usr/local/lib/libboost_math_tr1f.so.1.83.0 /usr/local/lib/libboost_math_tr1.so.1.83.0 /usr/local/lib/libboost_program_options.so.1.83.0 /usr/local/lib/libboost_iostreams.so.1.83.0 /usr/local/lib/libboost_math_c99f.so.1.83.0 /usr/local/lib/libboost_nowide.so.1.83.0 /usr/local/lib/libboost_thread.so.1.83.0 /usr/local/lib/libboost_unit_test_framework.a /usr/local/lib/cmake/boost_math_c99f-1.83.0 /usr/local/lib/cmake/boost_math_c99f-1.83.0/libboost_math_c99f-variant-shared.cmake /usr/local/lib/cmake/boost_math_c99f-1.83.0/boost_math_c99f-config.cmake /usr/local/lib/cmake/boost_math_c99f-1.83.0/libboost_math_c99f-variant-static.cmake /usr/local/lib/cmake/boost_math_c99f-1.83.0/boost_math_c99f-config-version.cmake /usr/local/lib/cmake/boost_test_exec_monitor-1.83.0 /usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/libboost_test_exec_monitor-variant-static.cmake /usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/libboost_test_exec_monitor-variant-shared.cmake /usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/boost_test_exec_monitor-config.cmake /usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/boost_test_exec_monitor-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0 /usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/libboost_stacktrace_backtrace-variant-shared.cmake /usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/boost_stacktrace_backtrace-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/libboost_stacktrace_backtrace-variant-static.cmake /usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/boost_stacktrace_backtrace-config.cmake /usr/local/lib/cmake/boost_wave-1.83.0 /usr/local/lib/cmake/boost_wave-1.83.0/libboost_wave-variant-shared.cmake /usr/local/lib/cmake/boost_wave-1.83.0/boost_wave-config-version.cmake /usr/local/lib/cmake/boost_wave-1.83.0/libboost_wave-variant-static.cmake /usr/local/lib/cmake/boost_wave-1.83.0/boost_wave-config.cmake /usr/local/lib/cmake/boost_log-1.83.0 /usr/local/lib/cmake/boost_log-1.83.0/libboost_log-variant-shared.cmake /usr/local/lib/cmake/boost_log-1.83.0/boost_log-config.cmake /usr/local/lib/cmake/boost_log-1.83.0/libboost_log-variant-static.cmake /usr/local/lib/cmake/boost_log-1.83.0/boost_log-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_basic-1.83.0 /usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/boost_stacktrace_basic-config.cmake /usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/boost_stacktrace_basic-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/libboost_stacktrace_basic-variant-static.cmake /usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/libboost_stacktrace_basic-variant-shared.cmake /usr/local/lib/cmake/boost_coroutine-1.83.0 /usr/local/lib/cmake/boost_coroutine-1.83.0/boost_coroutine-config-version.cmake /usr/local/lib/cmake/boost_coroutine-1.83.0/boost_coroutine-config.cmake /usr/local/lib/cmake/boost_coroutine-1.83.0/libboost_coroutine-variant-shared.cmake /usr/local/lib/cmake/boost_coroutine-1.83.0/libboost_coroutine-variant-static.cmake /usr/local/lib/cmake/boost_timer-1.83.0 /usr/local/lib/cmake/boost_timer-1.83.0/boost_timer-config-version.cmake /usr/local/lib/cmake/boost_timer-1.83.0/boost_timer-config.cmake /usr/local/lib/cmake/boost_timer-1.83.0/libboost_timer-variant-shared.cmake /usr/local/lib/cmake/boost_timer-1.83.0/libboost_timer-variant-static.cmake /usr/local/lib/cmake/boost_program_options-1.83.0 /usr/local/lib/cmake/boost_program_options-1.83.0/boost_program_options-config.cmake /usr/local/lib/cmake/boost_program_options-1.83.0/libboost_program_options-variant-static.cmake /usr/local/lib/cmake/boost_program_options-1.83.0/libboost_program_options-variant-shared.cmake /usr/local/lib/cmake/boost_program_options-1.83.0/boost_program_options-config-version.cmake /usr/local/lib/cmake/boost_math_tr1f-1.83.0 /usr/local/lib/cmake/boost_math_tr1f-1.83.0/libboost_math_tr1f-variant-static.cmake /usr/local/lib/cmake/boost_math_tr1f-1.83.0/boost_math_tr1f-config.cmake /usr/local/lib/cmake/boost_math_tr1f-1.83.0/boost_math_tr1f-config-version.cmake /usr/local/lib/cmake/boost_math_tr1f-1.83.0/libboost_math_tr1f-variant-shared.cmake /usr/local/lib/cmake/boost_date_time-1.83.0 /usr/local/lib/cmake/boost_date_time-1.83.0/libboost_date_time-variant-static.cmake /usr/local/lib/cmake/boost_date_time-1.83.0/libboost_date_time-variant-shared.cmake /usr/local/lib/cmake/boost_date_time-1.83.0/boost_date_time-config.cmake /usr/local/lib/cmake/boost_date_time-1.83.0/boost_date_time-config-version.cmake /usr/local/lib/cmake/boost_math_c99l-1.83.0 /usr/local/lib/cmake/boost_math_c99l-1.83.0/libboost_math_c99l-variant-static.cmake /usr/local/lib/cmake/boost_math_c99l-1.83.0/libboost_math_c99l-variant-shared.cmake /usr/local/lib/cmake/boost_math_c99l-1.83.0/boost_math_c99l-config.cmake /usr/local/lib/cmake/boost_math_c99l-1.83.0/boost_math_c99l-config-version.cmake /usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0 /usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/libboost_prg_exec_monitor-variant-shared.cmake /usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/libboost_prg_exec_monitor-variant-static.cmake /usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/boost_prg_exec_monitor-config.cmake /usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/boost_prg_exec_monitor-config-version.cmake /usr/local/lib/cmake/boost_wserialization-1.83.0 /usr/local/lib/cmake/boost_wserialization-1.83.0/libboost_wserialization-variant-shared.cmake /usr/local/lib/cmake/boost_wserialization-1.83.0/boost_wserialization-config-version.cmake /usr/local/lib/cmake/boost_wserialization-1.83.0/libboost_wserialization-variant-static.cmake /usr/local/lib/cmake/boost_wserialization-1.83.0/boost_wserialization-config.cmake /usr/local/lib/cmake/boost_serialization-1.83.0 /usr/local/lib/cmake/boost_serialization-1.83.0/boost_serialization-config.cmake /usr/local/lib/cmake/boost_serialization-1.83.0/libboost_serialization-variant-static.cmake /usr/local/lib/cmake/boost_serialization-1.83.0/boost_serialization-config-version.cmake /usr/local/lib/cmake/boost_serialization-1.83.0/libboost_serialization-variant-shared.cmake /usr/local/lib/cmake/boost_random-1.83.0 /usr/local/lib/cmake/boost_random-1.83.0/boost_random-config.cmake /usr/local/lib/cmake/boost_random-1.83.0/libboost_random-variant-static.cmake /usr/local/lib/cmake/boost_random-1.83.0/libboost_random-variant-shared.cmake /usr/local/lib/cmake/boost_random-1.83.0/boost_random-config-version.cmake /usr/local/lib/cmake/boost_log_setup-1.83.0 /usr/local/lib/cmake/boost_log_setup-1.83.0/boost_log_setup-config-version.cmake /usr/local/lib/cmake/boost_log_setup-1.83.0/libboost_log_setup-variant-shared.cmake /usr/local/lib/cmake/boost_log_setup-1.83.0/libboost_log_setup-variant-static.cmake /usr/local/lib/cmake/boost_log_setup-1.83.0/boost_log_setup-config.cmake /usr/local/lib/cmake/boost_exception-1.83.0 /usr/local/lib/cmake/boost_exception-1.83.0/boost_exception-config.cmake /usr/local/lib/cmake/boost_exception-1.83.0/boost_exception-config-version.cmake /usr/local/lib/cmake/boost_system-1.83.0 /usr/local/lib/cmake/boost_system-1.83.0/libboost_system-variant-shared.cmake /usr/local/lib/cmake/boost_system-1.83.0/boost_system-config-version.cmake /usr/local/lib/cmake/boost_system-1.83.0/libboost_system-variant-static.cmake /usr/local/lib/cmake/boost_system-1.83.0/boost_system-config.cmake /usr/local/lib/cmake/boost_python-1.83.0 /usr/local/lib/cmake/boost_python-1.83.0/boost_python-config.cmake /usr/local/lib/cmake/boost_python-1.83.0/boost_python-config-version.cmake /usr/local/lib/cmake/boost_thread-1.83.0 /usr/local/lib/cmake/boost_thread-1.83.0/libboost_thread-variant-shared.cmake /usr/local/lib/cmake/boost_thread-1.83.0/boost_thread-config.cmake /usr/local/lib/cmake/boost_thread-1.83.0/boost_thread-config-version.cmake /usr/local/lib/cmake/boost_thread-1.83.0/libboost_thread-variant-static.cmake /usr/local/lib/cmake/boost_json-1.83.0 /usr/local/lib/cmake/boost_json-1.83.0/libboost_json-variant-shared.cmake /usr/local/lib/cmake/boost_json-1.83.0/libboost_json-variant-static.cmake /usr/local/lib/cmake/boost_json-1.83.0/boost_json-config.cmake /usr/local/lib/cmake/boost_json-1.83.0/boost_json-config-version.cmake /usr/local/lib/cmake/boost_iostreams-1.83.0 /usr/local/lib/cmake/boost_iostreams-1.83.0/libboost_iostreams-variant-shared.cmake /usr/local/lib/cmake/boost_iostreams-1.83.0/boost_iostreams-config.cmake /usr/local/lib/cmake/boost_iostreams-1.83.0/libboost_iostreams-variant-static.cmake /usr/local/lib/cmake/boost_iostreams-1.83.0/boost_iostreams-config-version.cmake /usr/local/lib/cmake/boost_math_c99-1.83.0 /usr/local/lib/cmake/boost_math_c99-1.83.0/libboost_math_c99-variant-static.cmake /usr/local/lib/cmake/boost_math_c99-1.83.0/boost_math_c99-config-version.cmake /usr/local/lib/cmake/boost_math_c99-1.83.0/libboost_math_c99-variant-shared.cmake /usr/local/lib/cmake/boost_math_c99-1.83.0/boost_math_c99-config.cmake /usr/local/lib/cmake/boost_unit_test_framework-1.83.0 /usr/local/lib/cmake/boost_unit_test_framework-1.83.0/libboost_unit_test_framework-variant-static.cmake /usr/local/lib/cmake/boost_unit_test_framework-1.83.0/boost_unit_test_framework-config-version.cmake /usr/local/lib/cmake/boost_unit_test_framework-1.83.0/libboost_unit_test_framework-variant-shared.cmake /usr/local/lib/cmake/boost_unit_test_framework-1.83.0/boost_unit_test_framework-config.cmake /usr/local/lib/cmake/boost_filesystem-1.83.0 /usr/local/lib/cmake/boost_filesystem-1.83.0/libboost_filesystem-variant-static.cmake /usr/local/lib/cmake/boost_filesystem-1.83.0/boost_filesystem-config.cmake /usr/local/lib/cmake/boost_filesystem-1.83.0/boost_filesystem-config-version.cmake /usr/local/lib/cmake/boost_filesystem-1.83.0/libboost_filesystem-variant-shared.cmake /usr/local/lib/cmake/Boost-1.83.0 /usr/local/lib/cmake/Boost-1.83.0/BoostConfigVersion.cmake /usr/local/lib/cmake/Boost-1.83.0/BoostConfig.cmake /usr/local/lib/cmake/boost_headers-1.83.0 /usr/local/lib/cmake/boost_headers-1.83.0/boost_headers-config.cmake /usr/local/lib/cmake/boost_headers-1.83.0/boost_headers-config-version.cmake /usr/local/lib/cmake/boost_nowide-1.83.0 /usr/local/lib/cmake/boost_nowide-1.83.0/libboost_nowide-variant-static.cmake /usr/local/lib/cmake/boost_nowide-1.83.0/libboost_nowide-variant-shared.cmake /usr/local/lib/cmake/boost_nowide-1.83.0/boost_nowide-config.cmake /usr/local/lib/cmake/boost_nowide-1.83.0/boost_nowide-config-version.cmake /usr/local/lib/cmake/boost_atomic-1.83.0 /usr/local/lib/cmake/boost_atomic-1.83.0/boost_atomic-config-version.cmake /usr/local/lib/cmake/boost_atomic-1.83.0/boost_atomic-config.cmake /usr/local/lib/cmake/boost_atomic-1.83.0/libboost_atomic-variant-shared.cmake /usr/local/lib/cmake/boost_atomic-1.83.0/libboost_atomic-variant-static.cmake /usr/local/lib/cmake/boost_graph-1.83.0 /usr/local/lib/cmake/boost_graph-1.83.0/boost_graph-config-version.cmake /usr/local/lib/cmake/boost_graph-1.83.0/libboost_graph-variant-static.cmake /usr/local/lib/cmake/boost_graph-1.83.0/libboost_graph-variant-shared.cmake /usr/local/lib/cmake/boost_graph-1.83.0/boost_graph-config.cmake /usr/local/lib/cmake/boost_math_tr1-1.83.0 /usr/local/lib/cmake/boost_math_tr1-1.83.0/boost_math_tr1-config-version.cmake /usr/local/lib/cmake/boost_math_tr1-1.83.0/libboost_math_tr1-variant-static.cmake /usr/local/lib/cmake/boost_math_tr1-1.83.0/libboost_math_tr1-variant-shared.cmake /usr/local/lib/cmake/boost_math_tr1-1.83.0/boost_math_tr1-config.cmake /usr/local/lib/cmake/boost_graph_parallel-1.83.0 /usr/local/lib/cmake/boost_graph_parallel-1.83.0/boost_graph_parallel-config-version.cmake /usr/local/lib/cmake/boost_graph_parallel-1.83.0/boost_graph_parallel-config.cmake /usr/local/lib/cmake/boost_stacktrace_noop-1.83.0 /usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/libboost_stacktrace_noop-variant-static.cmake /usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/boost_stacktrace_noop-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/boost_stacktrace_noop-config.cmake /usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/libboost_stacktrace_noop-variant-shared.cmake /usr/local/lib/cmake/boost_mpi-1.83.0 /usr/local/lib/cmake/boost_mpi-1.83.0/boost_mpi-config-version.cmake /usr/local/lib/cmake/boost_mpi-1.83.0/boost_mpi-config.cmake /usr/local/lib/cmake/boost_type_erasure-1.83.0 /usr/local/lib/cmake/boost_type_erasure-1.83.0/libboost_type_erasure-variant-shared.cmake /usr/local/lib/cmake/boost_type_erasure-1.83.0/libboost_type_erasure-variant-static.cmake /usr/local/lib/cmake/boost_type_erasure-1.83.0/boost_type_erasure-config-version.cmake /usr/local/lib/cmake/boost_type_erasure-1.83.0/boost_type_erasure-config.cmake /usr/local/lib/cmake/boost_chrono-1.83.0 /usr/local/lib/cmake/boost_chrono-1.83.0/boost_chrono-config-version.cmake /usr/local/lib/cmake/boost_chrono-1.83.0/libboost_chrono-variant-static.cmake /usr/local/lib/cmake/boost_chrono-1.83.0/libboost_chrono-variant-shared.cmake /usr/local/lib/cmake/boost_chrono-1.83.0/boost_chrono-config.cmake /usr/local/lib/cmake/boost_math_tr1l-1.83.0 /usr/local/lib/cmake/boost_math_tr1l-1.83.0/libboost_math_tr1l-variant-shared.cmake /usr/local/lib/cmake/boost_math_tr1l-1.83.0/boost_math_tr1l-config.cmake /usr/local/lib/cmake/boost_math_tr1l-1.83.0/boost_math_tr1l-config-version.cmake /usr/local/lib/cmake/boost_math_tr1l-1.83.0/libboost_math_tr1l-variant-static.cmake /usr/local/lib/cmake/boost_contract-1.83.0 /usr/local/lib/cmake/boost_contract-1.83.0/libboost_contract-variant-shared.cmake /usr/local/lib/cmake/boost_contract-1.83.0/boost_contract-config-version.cmake /usr/local/lib/cmake/boost_contract-1.83.0/boost_contract-config.cmake /usr/local/lib/cmake/boost_contract-1.83.0/libboost_contract-variant-static.cmake /usr/local/lib/cmake/boost_fiber-1.83.0 /usr/local/lib/cmake/boost_fiber-1.83.0/libboost_fiber-variant-static.cmake /usr/local/lib/cmake/boost_fiber-1.83.0/boost_fiber-config-version.cmake /usr/local/lib/cmake/boost_fiber-1.83.0/libboost_fiber-variant-shared.cmake /usr/local/lib/cmake/boost_fiber-1.83.0/boost_fiber-config.cmake /usr/local/lib/cmake/BoostDetectToolset-1.83.0.cmake /usr/local/lib/cmake/boost_context-1.83.0 /usr/local/lib/cmake/boost_context-1.83.0/boost_context-config.cmake /usr/local/lib/cmake/boost_context-1.83.0/libboost_context-variant-shared.cmake /usr/local/lib/cmake/boost_context-1.83.0/libboost_context-variant-static.cmake /usr/local/lib/cmake/boost_context-1.83.0/boost_context-config-version.cmake /usr/local/lib/cmake/boost_container-1.83.0 /usr/local/lib/cmake/boost_container-1.83.0/libboost_container-variant-shared.cmake /usr/local/lib/cmake/boost_container-1.83.0/boost_container-config-version.cmake /usr/local/lib/cmake/boost_container-1.83.0/boost_container-config.cmake /usr/local/lib/cmake/boost_container-1.83.0/libboost_container-variant-static.cmake /usr/local/lib/cmake/boost_regex-1.83.0 /usr/local/lib/cmake/boost_regex-1.83.0/libboost_regex-variant-shared.cmake /usr/local/lib/cmake/boost_regex-1.83.0/boost_regex-config.cmake /usr/local/lib/cmake/boost_regex-1.83.0/libboost_regex-variant-static.cmake /usr/local/lib/cmake/boost_regex-1.83.0/boost_regex-config-version.cmake /usr/local/lib/cmake/boost_url-1.83.0 /usr/local/lib/cmake/boost_url-1.83.0/boost_url-config-version.cmake /usr/local/lib/cmake/boost_url-1.83.0/libboost_url-variant-static.cmake /usr/local/lib/cmake/boost_url-1.83.0/libboost_url-variant-shared.cmake /usr/local/lib/cmake/boost_url-1.83.0/boost_url-config.cmake /usr/local/lib/cmake/boost_locale-1.83.0 /usr/local/lib/cmake/boost_locale-1.83.0/libboost_locale-variant-static.cmake /usr/local/lib/cmake/boost_locale-1.83.0/boost_locale-config.cmake /usr/local/lib/cmake/boost_locale-1.83.0/libboost_locale-variant-shared.cmake /usr/local/lib/cmake/boost_locale-1.83.0/boost_locale-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0 /usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/libboost_stacktrace_addr2line-variant-static.cmake /usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/libboost_stacktrace_addr2line-variant-shared.cmake /usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/boost_stacktrace_addr2line-config-version.cmake /usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/boost_stacktrace_addr2line-config.cmake /usr/local/lib/libboost_fiber.so.1.83.0 /usr/local/lib/libboost_chrono.a /usr/local/lib/libboost_serialization.so /usr/local/lib/libboost_fiber.so /usr/local/lib/libboost_wserialization.a /usr/local/lib/libboost_unit_test_framework.so.1.83.0 /usr/local/lib/libboost_stacktrace_noop.a /usr/local/lib/libboost_iostreams.a /usr/local/lib/libboost_math_c99f.a /usr/local/lib/libboost_exception.a /usr/local/lib/libboost_type_erasure.a /usr/local/lib/libboost_thread.a /usr/local/lib/libboost_chrono.so.1.83.0 /usr/local/lib/libboost_math_c99l.so /usr/local/lib/libboost_log_setup.so.1.83.0 /usr/local/lib/libboost_stacktrace_addr2line.a /usr/local/lib/libboost_container.so.1.83.0 /usr/local/lib/libboost_context.so /usr/local/lib/libboost_program_options.so /usr/local/lib/libboost_stacktrace_basic.so /usr/local/lib/libboost_graph.a /usr/local/lib/libboost_prg_exec_monitor.so.1.83.0 /usr/local/lib/libboost_atomic.a /usr/local/lib/libboost_math_tr1l.a /usr/local/lib/libboost_log.so.1.83.0 /usr/local/lib/libboost_random.a /usr/local/lib/libboost_math_tr1f.so /usr/local/lib/libboost_contract.a /usr/local/lib/libboost_atomic.so /usr/local/lib/libboost_serialization.so.1.83.0 /usr/local/lib/libboost_thread.so /usr/local/lib/libboost_wave.so /usr/local/lib/libboost_program_options.a /usr/local/lib/libboost_context.so.1.83.0 /usr/local/lib/libboost_coroutine.a /usr/local/lib/libboost_json.a /usr/local/lib/libboost_stacktrace_noop.so.1.83.0 /usr/local/lib/libboost_wserialization.so.1.83.0 /usr/local/lib/libboost_math_tr1.a /usr/local/lib/libboost_chrono.so /usr/local/lib/libboost_stacktrace_backtrace.a /usr/local/lib/libboost_stacktrace_addr2line.so.1.83.0 /usr/local/lib/libboost_stacktrace_basic.so.1.83.0 /usr/local/lib/libboost_math_c99l.a /usr/local/lib/libboost_log.so /usr/local/lib/libboost_log.a /usr/local/lib/libboost_math_tr1l.so /usr/local/lib/libboost_container.so /usr/local/lib/libboost_coroutine.so /usr/local/lib/libboost_prg_exec_monitor.so /usr/local/lib/libboost_nowide.a /usr/local/lib/libboost_url.so /usr/local/lib/libboost_url.a /usr/local/lib/libboost_math_c99f.so /usr/local/lib/libboost_stacktrace_backtrace.so.1.83.0 /usr/local/lib/libboost_test_exec_monitor.a /usr/local/lib/libboost_json.so /usr/local/lib/libboost_wserialization.so /usr/local/lib/libboost_system.a /usr/local/lib/libboost_regex.so.1.83.0 /usr/local/lib/libboost_contract.so /usr/local/lib/libboost_stacktrace_noop.so /usr/local/lib/libboost_locale.so /usr/local/lib/libboost_math_tr1.so /usr/local/lib/libboost_timer.so.1.83.0 /usr/local/lib/libboost_random.so /usr/local/lib/libboost_math_tr1l.so.1.83.0 /usr/local/lib/libboost_timer.so </span></pre> </div> <!-- endregion --> <!-- #region clean up --> <h3 id="clean">Clean Up</h3> <p> Many Ubuntu/Debian packages are left behind by the Boost installation program. You can view them as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id11112e805073'><button class='copyBtn' data-clipboard-target='#id11112e805073' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>apt --dry-run autoremove | grep -Po '^Remv \K[^ ]+' <span class='unselectable'>libboost-mpi-python-dev libboost-mpi-python1.74-dev libboost-mpi-python1.74.0 mpi-default-bin libboost-mpi-dev libboost-mpi1.74-dev mpi-default-dev libopenmpi-dev openmpi-bin libcoarrays-openmpi-dev libopenmpi3 libucx0 libfabric1 ibverbs-providers libboost-atomic-dev libboost-coroutine-dev libboost-coroutine1.74-dev libboost-fiber-dev libboost-fiber1.74-dev libboost-context1.74-dev libboost-type-erasure-dev libboost-type-erasure1.74-dev libboost-thread1.74-dev libboost-log-dev libboost-log1.74-dev libboost-atomic1.74-dev libboost-atomic1.74.0 libboost-chrono-dev libboost-timer-dev libboost-timer1.74-dev libboost-chrono1.74-dev libboost-timer1.74.0 libboost-chrono1.74.0 libboost-container-dev libboost-container1.74-dev libboost-container1.74.0 libboost-context-dev libboost-fiber1.74.0 libboost-coroutine1.74.0 libboost-context1.74.0 libboost-date-time-dev libboost-date-time1.74-dev libboost-date-time1.74.0 libboost-dev libboost-exception-dev libboost-exception1.74-dev libboost-filesystem-dev libboost-wave-dev libboost-wave1.74-dev libboost-filesystem1.74-dev libboost-graph-dev libboost-graph-parallel-dev libboost-graph-parallel1.74-dev libboost-graph-parallel1.74.0 libboost-graph1.74-dev libboost-graph1.74.0 libboost-iostreams-dev libboost-iostreams1.74-dev libboost-locale-dev libboost-locale1.74-dev libboost-log1.74.0 libboost-math-dev libboost-math1.74-dev libboost-math1.74.0 libboost-mpi1.74.0 libboost-nowide-dev libboost-nowide1.74-dev libboost-nowide1.74.0 libboost-numpy-dev libboost-numpy1.74-dev libboost-numpy1.74.0 libboost-program-options-dev libboost-program-options1.74-dev libboost-program-options1.74.0 libboost-python-dev libboost-python1.74-dev libboost-python1.74.0 libboost-random-dev libboost-random1.74-dev libboost-random1.74.0 libboost-regex-dev libboost-regex1.74-dev libboost-serialization-dev libboost-serialization1.74-dev libboost-serialization1.74.0 libboost-stacktrace-dev libboost-stacktrace1.74-dev libboost-stacktrace1.74.0 libboost-system-dev libboost-system1.74-dev libboost-system1.74.0 libboost-test-dev libboost-test1.74-dev libboost-test1.74.0 libboost-thread-dev libboost-tools-dev libboost-type-erasure1.74.0 libboost-wave1.74.0 libboost1.74-dev libboost1.74-tools-dev libcaf-openmpi-3 libpmix-dev libevent-dev libevent-extra-2.1-7 libevent-openssl-2.1-7 libpmix2 libevent-pthreads-2.1-7 libhwloc-dev libhwloc-plugins libhwloc15 libibverbs-dev librdmacm1 libibverbs1 libjs-jquery-ui libmunge2 libnl-route-3-dev libnl-3-dev libnuma-dev libpsm-infinipath1 libpsm2-2 openmpi-common </span></pre> </div> <!-- endregion --> <p> Remove the packages as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0882dbe262e7'><button class='copyBtn' data-clipboard-target='#id0882dbe262e7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt autoremove <span class='unselectable'>Reading package lists... Done Building dependency tree... Done Reading state information... Done The following packages will be REMOVED: ibverbs-providers libboost-atomic-dev libboost-atomic1.74-dev libboost-atomic1.74.0 libboost-chrono-dev libboost-chrono1.74-dev libboost-chrono1.74.0 libboost-container-dev libboost-container1.74-dev libboost-container1.74.0 libboost-context-dev libboost-context1.74-dev libboost-context1.74.0 libboost-coroutine-dev libboost-coroutine1.74-dev libboost-coroutine1.74.0 libboost-date-time-dev libboost-date-time1.74-dev libboost-date-time1.74.0 libboost-dev libboost-exception-dev libboost-exception1.74-dev libboost-fiber-dev libboost-fiber1.74-dev libboost-fiber1.74.0 libboost-filesystem-dev libboost-filesystem1.74-dev libboost-graph-dev libboost-graph-parallel-dev libboost-graph-parallel1.74-dev libboost-graph-parallel1.74.0 libboost-graph1.74-dev libboost-graph1.74.0 libboost-iostreams-dev libboost-iostreams1.74-dev libboost-locale-dev libboost-locale1.74-dev libboost-log-dev libboost-log1.74-dev libboost-log1.74.0 libboost-math-dev libboost-math1.74-dev libboost-math1.74.0 libboost-mpi-dev libboost-mpi-python-dev libboost-mpi-python1.74-dev libboost-mpi-python1.74.0 libboost-mpi1.74-dev libboost-mpi1.74.0 libboost-nowide-dev libboost-nowide1.74-dev libboost-nowide1.74.0 libboost-numpy-dev libboost-numpy1.74-dev libboost-numpy1.74.0 libboost-program-options-dev libboost-program-options1.74-dev libboost-program-options1.74.0 libboost-python-dev libboost-python1.74-dev libboost-python1.74.0 libboost-random-dev libboost-random1.74-dev libboost-random1.74.0 libboost-regex-dev libboost-regex1.74-dev libboost-serialization-dev libboost-serialization1.74-dev libboost-serialization1.74.0 libboost-stacktrace-dev libboost-stacktrace1.74-dev libboost-stacktrace1.74.0 libboost-system-dev libboost-system1.74-dev libboost-system1.74.0 libboost-test-dev libboost-test1.74-dev libboost-test1.74.0 libboost-thread-dev libboost-thread1.74-dev libboost-timer-dev libboost-timer1.74-dev libboost-timer1.74.0 libboost-tools-dev libboost-type-erasure-dev libboost-type-erasure1.74-dev libboost-type-erasure1.74.0 libboost-wave-dev libboost-wave1.74-dev libboost-wave1.74.0 libboost1.74-dev libboost1.74-tools-dev libcaf-openmpi-3 libcoarrays-openmpi-dev libevent-dev libevent-extra-2.1-7 libevent-openssl-2.1-7 libevent-pthreads-2.1-7 libfabric1 libhwloc-dev libhwloc-plugins libhwloc15 libibverbs-dev libibverbs1 libjs-jquery-ui libmunge2 libnl-3-dev libnl-route-3-dev libnuma-dev libopenmpi-dev libopenmpi3 libpmix-dev libpmix2 libpsm-infinipath1 libpsm2-2 librdmacm1 libucx0 mpi-default-bin mpi-default-dev openmpi-bin openmpi-common 0 upgraded, 0 newly installed, 121 to remove and 4 not upgraded. After this operation, 413 MB disk space will be freed. (Reading database ... 399799 files and directories currently installed.) Removing libboost-mpi-python-dev (1.74.0.3ubuntu7) ... Removing libboost-mpi-python1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-mpi-python1.74.0 (1.74.0-18.1ubuntu3) ... Removing mpi-default-bin (1.14) ... Removing libboost-mpi-dev (1.74.0.3ubuntu7) ... Removing libboost-mpi1.74-dev (1.74.0-18.1ubuntu3) ... Removing mpi-default-dev (1.14) ... Removing libopenmpi-dev:amd64 (4.1.4-3ubuntu2) ... Removing libcoarrays-openmpi-dev:amd64 (2.10.1-1) ... update-alternatives: warning: alternative /usr/bin/caf.openmpi (part of link group caf) doesn't exist; removing from list of alternatives update-alternatives: warning: /etc/alternatives/caf is dangling; it will be updated with best choice Removing libboost-atomic-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-coroutine-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-coroutine1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-fiber-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-fiber1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-type-erasure-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-type-erasure1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-log-dev (1.74.0.3ubuntu7) ... Removing libboost-log1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-chrono-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-timer-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-timer1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-timer1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-container-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-container1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-container1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-context-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-fiber1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-coroutine1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-date-time-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-exception-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-exception1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-filesystem-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-wave-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-wave1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-filesystem1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-graph-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-graph-parallel-dev (1.74.0.3ubuntu7) ... Removing libboost-graph-parallel1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-graph-parallel1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-graph1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-graph1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-iostreams-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-iostreams1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-locale-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-locale1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-log1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-math-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-math1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-math1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-mpi1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-nowide-dev (1.74.0.3ubuntu7) ... Removing libboost-nowide1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-nowide1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-numpy-dev (1.74.0.3ubuntu7) ... Removing libboost-numpy1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-numpy1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-program-options-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-program-options1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-program-options1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-python-dev (1.74.0.3ubuntu7) ... Removing libboost-python1.74-dev (1.74.0-18.1ubuntu3) ... Removing libboost-python1.74.0 (1.74.0-18.1ubuntu3) ... Removing libboost-random-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-random1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-random1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-regex-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-regex1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-serialization-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-stacktrace-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-stacktrace1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-stacktrace1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-system-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-test-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-test1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-test1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-thread-dev:amd64 (1.74.0.3ubuntu7) ... Removing libboost-tools-dev (1.74.0.3ubuntu7) ... Removing libboost-type-erasure1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-wave1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost1.74-tools-dev (1.74.0-18.1ubuntu3) ... Removing libcaf-openmpi-3:amd64 (2.10.1-1) ... Removing libpmix-dev:amd64 (4.2.2-1) ... Removing libevent-dev (2.1.12-stable-8ubuntu3) ... Removing libevent-extra-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ... Removing libevent-openssl-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ... Removing libhwloc-dev:amd64 (2.9.0-1) ... Removing libibverbs-dev:amd64 (44.0-2) ... Removing libjs-jquery-ui (1.13.2+dfsg-1) ... Removing libnl-route-3-dev:amd64 (3.7.0-0.2) ... Removing libnl-3-dev:amd64 (3.7.0-0.2) ... Removing libnuma-dev:amd64 (2.0.16-1) ... Removing openmpi-bin (4.1.4-3ubuntu2) ... Removing libopenmpi3:amd64 (4.1.4-3ubuntu2) ... Removing libucx0:amd64 (1.13.1-1) ... Removing libfabric1:amd64 (1.17.0-3) ... Removing ibverbs-providers:amd64 (44.0-2) ... Removing libboost-context1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-thread1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-atomic1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-atomic1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-chrono1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-chrono1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-context1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-date-time1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-date-time1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-serialization1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-serialization1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-system1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost-system1.74.0:amd64 (1.74.0-18.1ubuntu3) ... Removing libboost1.74-dev:amd64 (1.74.0-18.1ubuntu3) ... Removing libpmix2:amd64 (4.2.2-1) ... Removing libevent-pthreads-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ... Removing libhwloc-plugins:amd64 (2.9.0-1) ... Removing libhwloc15:amd64 (2.9.0-1) ... Removing librdmacm1:amd64 (44.0-2) ... Removing libibverbs1:amd64 (44.0-2) ... Removing libmunge2 (0.5.15-2) ... Removing libpsm-infinipath1 (3.3+20.604758e7-6.2) ... update-alternatives: warning: alternative /usr/lib/libpsm1/libpsm_infinipath.so.1.16 (part of link group libpsm_infinipath.so.1) doesn't exist; removing from list of alternatives update-alternatives: warning: /etc/alternatives/libpsm_infinipath.so.1 is dangling; it will be updated with best choice Removing libpsm2-2 (11.2.185-2) ... Removing openmpi-common (4.1.4-3ubuntu2) ... Processing triggers for man-db (2.11.2-1) ... Processing triggers for libc-bin (2.37-0ubuntu2) ... /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region conclusion --> <h2 id="go">Go Forth and Get Boosted</h2> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> Boost usage as a C++ general-pupose library is widespread throughout a diverse range of industries and requirements. </p> <!-- endregion --> PDF Manipulation 2023-09-08T00:00:00-04:00 https://mslinn.github.io/blog/2023/09/08/pdftk <!-- #region intro --> <h2 id="install">Installation</h2> <p> The <a href='https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/' target='_blank' rel="nofollow"><code>pdftk</code> website</a> discusses <code>pdktk</code> (a F/OSS command-line program), a Windows GUI, and mentions the book <a href='https://github.com/InspectorDidi/Hacking-Books/blob/master/PDF%20Hacks.pdf' target='_blank' rel="nofollow"><b>PDF Hacks</b></a>. </p> <p> An alternative free GUI is <a href='https://portableapps.com/apps/office/pdftk_builder_portable' target='_blank' rel="nofollow">PDFTK Builder</a>. </p> <p> Other command-line utilties for manipulating PDF documents are: </p> <ul> <li><a href='https://en.wikipedia.org/wiki/Poppler_(software)' target='_blank' rel="nofollow">Poppler</a> (GPL)</li> <li><a href='https://qpdf.readthedocs.io/en/stable/index.html' target='_blank' rel="nofollow">QPDF</a></li> </ul> <p> Install the command-line <code>pdftk</code> program on Ubuntu: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1d8e368a347c'><button class='copyBtn' data-clipboard-target='#id1d8e368a347c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install pdftk</pre> </div> <!-- endregion --> <p> This is the <code>pdftk</code> help text: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd7a4cc09b312'><button class='copyBtn' data-clipboard-target='#idd7a4cc09b312' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man pdftk <span class='unselectable'>pdftk.pdftk-java(1) General Commands Manual pdftk.pdftk-java(1)<br/> NAME pdftk - A handy tool for manipulating PDF<br/> SYNOPSIS pdftk &lt;input PDF files | - | PROMPT&gt; [ input_pw &lt;input PDF owner passwords | PROMPT&gt; ] [ &lt;operation&gt; &lt;operation arguments&gt; ] [ output &lt;output filename | - | PROMPT&gt; ] [ encrypt_40bit | encrypt_128bit | encrypt_aes128 ] [ allow &lt;permissions&gt; ] [ owner_pw &lt;owner password | PROMPT&gt; ] [ user_pw &lt;user password | PROMPT&gt; ] [ flatten ] [ need_appearances ] [ compress | uncompress ] [ keep_first_id | keep_final_id ] [ drop_xfa ] [ drop_xmp ] [ replacement_font &lt;font name&gt; ] [ verbose ] [ dont_ask | do_ask ] Where: &lt;operation&gt; may be empty, or: [ cat | shuffle | burst | rotate | generate_fdf | fill_form | background | multibackground | stamp | multistamp | dump_data | dump_data_utf8 | dump_data_fields | dump_data_fields_utf8 | dump_data_annots | update_info | update_info_utf8 | attach_files | unpack_files ]<br/> For Complete Help: pdftk --help<br/> DESCRIPTION If PDF is electronic paper, then pdftk is an electronic staple- remover, hole-punch, binder, secret-decoder-ring, and X-Ray- glasses. Pdftk is a simple tool for doing everyday things with PDF documents. Use it to:<br/> * Merge PDF Documents or Collate PDF Page Scans * Split PDF Pages into a New Document * Rotate PDF Documents or Pages * Decrypt Input as Necessary (Password Required) * Encrypt Output as Desired * Fill PDF Forms with X/FDF Data and/or Flatten Forms * Generate FDF Data Stencils from PDF Forms * Apply a Background Watermark or a Foreground Stamp * Report PDF Metrics, Bookmarks and Metadata * Add/Update PDF Metrics, Bookmarks or Metadata * Attach Files to PDF Pages or the PDF Document * Unpack PDF Attachments * Burst a PDF Document into Single Pages * Uncompress and Re-Compress Page Streams * Repair Corrupted PDF (Where Possible)<br/> OPTIONS A summary of options is included below.<br/> --help, -h Show this summary of options.<br/> &lt;input PDF files | - | PROMPT&gt; A list of the input PDF files. If you plan to combine these PDFs (without using handles) then list files in the order you want them combined. Use - to pass a sin&#8208; gle PDF into pdftk via stdin. Input files can be asso&#8208; ciated with handles, where a handle is one or more up&#8208; per-case letters:<br/> &lt;input PDF handle&gt;=&lt;input PDF filename&gt;<br/> Handles are often omitted. They are useful when speci&#8208; fying PDF passwords or page ranges, later.<br/> For example: A=input1.pdf QT=input2.pdf M=input3.pdf<br/> [input_pw &lt;input PDF owner passwords | PROMPT&gt;] Input PDF owner passwords, if necessary, are associated with files by using their handles:<br/> &lt;input PDF handle&gt;=&lt;input PDF file owner password&gt;<br/> If handles are not given, then passwords are associated with input files by order.<br/> Most pdftk features require that encrypted input PDF are accompanied by the ~owner~ password. If the input PDF has no owner password, then the user password must be given, instead. If the input PDF has no passwords, then no password should be given.<br/> When running in do_ask mode, pdftk will prompt you for a password if the supplied password is incorrect or none was given.<br/> [&lt;operation&gt; &lt;operation arguments&gt;] Available operations are: cat, shuffle, burst, rotate, generate_fdf, fill_form, background, multibackground, stamp, multistamp, dump_data, dump_data_utf8, dump_data_fields, dump_data_fields_utf8, dump_data_an&#8208; nots, update_info, update_info_utf8, attach_files, un&#8208; pack_files. Some operations takes additional arguments, described below.<br/> If this optional argument is omitted, then pdftk runs in &#39;filter&#39; mode. Filter mode takes only one PDF input and creates a new PDF after applying all of the output op&#8208; tions, like encryption and compression.<br/> cat [&lt;page ranges&gt;] Assembles (catenates) pages from input PDFs to create a new PDF. Use cat to merge PDF pages or to split PDF pages from documents. You can also use it to rotate PDF pages. Page order in the new PDF is specified by the order of the given page ranges. Page ranges are described like this:<br/> &lt;input PDF handle&gt;[&lt;begin page number&gt;[-&lt;end page number&gt;[&lt;qualifier&gt;]]][&lt;page rotation&gt;]<br/> Where the handle identifies one of the input PDF files, and the beginning and ending page numbers are one-based references to pages in the PDF file. The qualifier can be even, odd, or ~, and the page rota&#8208; tion can be north, south, east, west, left, right, or down.<br/> If a PDF handle is given but no pages are specified, then the entire PDF is used. If no pages are speci&#8208; fied for any of the input PDFs, then the input PDFs&#39; bookmarks are also merged and included in the output.<br/> If the handle is omitted from the page range, then the pages are taken from the first input PDF.<br/> The even qualifier causes pdftk to use only the even- numbered PDF pages, so 1-6even yields pages 2, 4 and 6 in that order. 6-1even yields pages 6, 4 and 2 in that order.<br/> The odd qualifier works similarly to the even.<br/> Pages can be subtracted from a page range using the ~ qualifier followed by a page range. For instance, 1-20~5-6 and 1-20~5~6 are equivalent to 1-4 7-20, and ~5 yields all pages except page 5. Depending on your shell, you may need to quote this argument because of the ~ at the beginning.<br/> The page rotation setting can cause pdftk to rotate pages and documents. Each option sets the page rota&#8208; tion as follows (in degrees): north: 0, east: 90, south: 180, west: 270, left: -90, right: +90, down: +180. left, right, and down make relative adjustments to a page&#39;s rotation.<br/> If no arguments are passed to cat, then pdftk com&#8208; bines all input PDFs in the order they were given to create the output.<br/> NOTES: * &lt;end page number&gt; may be less than &lt;begin page num&#8208; ber&gt;. * The keyword end may be used to reference the final page of a document instead of a page number. * Reference a single page by omitting the ending page number. * The handle may be used alone to represent the en&#8208; tire PDF document, e.g., B1-end is the same as B. * You can reference page numbers in reverse order by prefixing them with the letter r. For example, page r1 is the last page of the document, r2 is the next- to-last page of the document, and rend is the first page of the document. You can use this prefix in ranges, too, for example r3-r1 is the last three pages of a PDF.<br/> Page Range Examples without Handles: 1\-endeast &ndash; rotate entire document 90 degrees 5 11 20 &ndash; take single pages from input PDF 5-25oddwest &ndash; take odd pages in range, rotate 90 de&#8208; grees 6-1 &ndash; reverse pages in range from input PDF<br/> Page Range Examples Using Handles: Say A=in1.pdf B=in2.pdf, then: A1-21 &ndash; take range from in1.pdf Bend-1odd &ndash; take all odd pages from in2.pdf in re&#8208; verse order A72 &ndash; take a single page from in1.pdf A1-21 Beven A72 &ndash; assemble pages from both in1.pdf and in2.pdf Awest &ndash; rotate entire in1.pdf document 90 degrees B &ndash; use all of in2.pdf A2-30evenleft &ndash; take the even pages from the range, remove 90 degrees from each page&#39;s rotation A A &ndash; catenate in1.pdf with in1.pdf Aevenwest Aoddeast &ndash; apply rotations to even pages, odd pages from in1.pdf Awest Bwest Bdown &ndash; catenate rotated documents<br/> shuffle [&lt;page ranges&gt;] Collates pages from input PDFs to create a new PDF. Works like the cat operation except that it takes one page at a time from each page range to assemble the output PDF. If one range runs out of pages, it con&#8208; tinues with the remaining ranges. Ranges can use all of the features described above for cat, like reverse page ranges, multiple ranges from a single PDF, and page rotation. This feature was designed to help collate PDF pages after scanning paper documents.<br/> burst Splits a single input PDF document into individual pages. Also creates a report named doc_data.txt which is the same as the output from dump_data. The output section can contain a printf-styled format string to name these pages. For example, if you want pages named page_01.pdf, page_02.pdf, etc., pass output page_%02d.pdf to pdftk. If the pattern is omitted, then a default pattern g_%04d.pdf is appended and produces pages named pg_0001.pdf, pg_0002.pdf, etc. Encryption can be applied to the output by appending output options such as owner_pw, e.g.:<br/> pdftk in.pdf burst owner_pw foopass<br/> rotate [&lt;page ranges&gt;] Takes a single input PDF and rotates just the speci&#8208; fied pages. All other pages remain unchanged. The page order remains unchanged. Specify the pages to rotate using the same notation as you would with cat, except you omit the pages that you aren&#39;t rotating:<br/> [&lt;begin page number&gt;[-&lt;end page number&gt;[&lt;quali&#8208; fier&gt;]]][&lt;page rotation&gt;]<br/> The qualifier can be even or odd, and the page rota&#8208; tion can be north, south, east, west, left, right, or down.<br/> Each option sets the page rotation as follows (in de&#8208; grees): north: 0, east: 90, south: 180, west: 270, left: -90, right: +90, down: +180. left, right, and down make relative adjustments to a page&#39;s rotation.<br/> The given order of the pages doesn&#39;t change the page order in the output.<br/> generate_fdf Reads a single input PDF file and generates an FDF file suitable for fill_form out of it to the given output filename or (if no output is given) to stdout. Does not create a new PDF.<br/> fill_form &lt;FDF data filename | XFDF data filename | - | PROMPT&gt; Fills the single input PDF&#39;s form fields with the data from an FDF file, XFDF file or stdin. Enter the data filename after fill_form, or use - to pass the data via stdin, like so:<br/> pdftk form.pdf fill_form data.fdf output form.filled.pdf<br/> If the input FDF file includes Rich Text formatted data in addition to plain text, then the Rich Text data is packed into the form fields as well as the plain text. Pdftk also sets a flag that cues Reader/Acrobat to generate new field appearances based on the Rich Text data. So when the user opens the PDF, the viewer will create the Rich Text appear&#8208; ance on the spot. If the user&#39;s PDF viewer does not support Rich Text, then the user will see the plain text data instead. If you flatten this form before Acrobat has a chance to create (and save) new field appearances, then the plain text field data is what you&#39;ll see.<br/> Also see the flatten, need_appearances, and replace&#8208; ment_font options.<br/> background &lt;background PDF filename | - | PROMPT&gt; Applies a PDF watermark to the background of a single input PDF. Pass the background PDF&#39;s filename after background like so:<br/> pdftk in.pdf background back.pdf output out.pdf<br/> Pdftk uses only the first page from the background PDF and applies it to every page of the input PDF. This page is scaled and rotated as needed to fit the input page. You can use - to pass a background PDF into pdftk via stdin.<br/> If the input PDF does not have a transparent back&#8208; ground (such as a PDF created from page scans) then the resulting background won&#39;t be visible &ndash; use the stamp operation instead.<br/> multibackground &lt;background PDF filename | - | PROMPT&gt; Same as the background operation, but applies each page of the background PDF to the corresponding page of the input PDF. If the input PDF has more pages than the stamp PDF, then the final stamp page is re&#8208; peated across these remaining pages in the input PDF.<br/> stamp &lt;stamp PDF filename | - | PROMPT&gt; This behaves just like the background operation ex&#8208; cept it overlays the stamp PDF page on top of the in&#8208; put PDF document&#39;s pages. This works best if the stamp PDF page has a transparent background.<br/> multistamp &lt;stamp PDF filename | - | PROMPT&gt; Same as the stamp operation, but applies each page of the background PDF to the corresponding page of the input PDF. If the input PDF has more pages than the stamp PDF, then the final stamp page is repeated across these remaining pages in the input PDF.<br/> dump_data Reads a single input PDF file and reports its meta&#8208; data, bookmarks (a/k/a outlines), page metrics (me&#8208; dia, rotation and labels), data embedded by STAMPtk (see STAMPtk&#39;s embed option) and other data to the given output filename or (if no output is given) to stdout. Non-ASCII characters are encoded as XML nu&#8208; merical entities. Does not create a new PDF.<br/> dump_data_utf8 Same as dump_data except that the output is encoded as UTF-8.<br/> dump_data_fields Reads a single input PDF file and reports form field statistics to the given output filename or (if no output is given) to stdout. Non-ASCII characters are encoded as XML numerical entities. Does not create a new PDF.<br/> dump_data_fields_utf8 Same as dump_data_fields except that the output is encoded as UTF-8.<br/> dump_data_annots This operation currently reports only link annota&#8208; tions. Reads a single input PDF file and reports an&#8208; notation information to the given output filename or (if no output is given) to stdout. Non-ASCII charac&#8208; ters are encoded as XML numerical entities. Does not create a new PDF.<br/> update_info &lt;info data filename | - | PROMPT&gt; Changes the bookmarks, page labels, page sizes, page rotations, and metadata in a single PDF&#39;s Info dic&#8208; tionary to match the input data file. The input data file uses the same syntax as the output from dump_data. Non-ASCII characters should be encoded as XML numerical entities.<br/> This operation does not change the metadata stored in the PDF&#39;s XMP stream, if it has one. (For this reason you should include a ModDate entry in your updated info with a current date/timestamp, format: D:YYYYM&#8208; MDDHHmmSS, e.g. D:201307241346 &ndash; omitted data after YYYY revert to default values.)<br/> For example:<br/> pdftk in.pdf update_info in.info output out.pdf<br/> update_info_utf8 &lt;info data filename | - | PROMPT&gt; Same as update_info except that the input is encoded as UTF-8.<br/> attach_files &lt;attachment filenames | PROMPT&gt; [to_page &lt;page number | PROMPT&gt; | relation &lt;relationship&gt;] Packs arbitrary files into a PDF using PDF&#39;s file at&#8208; tachment features. More than one attachment may be listed after attach_files. Attachments are added at the document level unless the optional to_page option is given, in which case the files are attached to the given page number (the first page is 1, the final page is end). Attachments at the document level may be tagged with a relationship among Source, Data, Al&#8208; ternative, Supplement, and Unspecified (default).<br/> For example:<br/> pdftk in.pdf attach_files table1.html table2.html to_page 6 output out.pdf<br/> pdftk in.pdf attach_files in.tex relation Source out&#8208; put out.pdf<br/> unpack_files Copies all of the attachments from the input PDF into the current folder or to an output directory given after output. For example:<br/> pdftk report.pdf unpack_files output ~/atts/<br/> or, interactively:<br/> pdftk report.pdf unpack_files output PROMPT<br/> [output &lt;output filename | - | PROMPT&gt;] The output PDF filename may not be set to the name of an input filename. Use - to output to stdout. When using the dump_data operation, use output to set the name of the output data file. When using the unpack_files opera&#8208; tion, use output to set the name of an output directory. When using the burst operation, you can use output to control the resulting PDF page filenames (described above).<br/> [encrypt_40bit | encrypt_128bit | encrypt_aes128] If an output PDF user or owner password is given, the output PDF encryption algorithm defaults to AES-128. The weaker RC4 40-bit and RC4 128-bit algorithms can be cho&#8208; sen by specifying encrypt_40bit or encrypt_128bit (dis&#8208; couraged).<br/> [allow &lt;permissions&gt;] Permissions are applied to the output PDF only if an en&#8208; cryption strength is specified or an owner or user pass&#8208; word is given. If permissions are not specified, they default to &#39;none,&#39; which means all of the following fea&#8208; tures are disabled.<br/> The permissions section may include one or more of the following features:<br/> Printing Top Quality Printing<br/> DegradedPrinting Lower Quality Printing<br/> ModifyContents Also allows Assembly<br/> Assembly<br/> CopyContents Also allows ScreenReaders<br/> ScreenReaders<br/> ModifyAnnotations Also allows FillIn<br/> FillIn<br/> AllFeatures Allows the user to perform all of the above, and top quality printing.<br/> [owner_pw &lt;owner password | PROMPT&gt;]<br/> [user_pw &lt;user password | PROMPT&gt;] If an encryption strength is given but no passwords are supplied, then the owner and user passwords remain empty, which means that the resulting PDF may be opened and its security parameters altered by anybody.<br/> [compress | uncompress] These are only useful when you want to edit PDF code in a text editor like vim or emacs. Remove PDF page stream compression by applying the uncompress filter. Use the compress filter to restore compression.<br/> [flatten] Use this option to merge an input PDF&#39;s interactive form fields (and their data) with the PDF&#39;s pages. Only one input PDF may be given. Sometimes used with the fill_form operation.<br/> [need_appearances] Sets a flag that cues Reader/Acrobat to generate new field appearances based on the form field values. Use this when filling a form with non-ASCII text to ensure the best presentation in Adobe Reader or Acrobat. It won&#39;t work when combined with the flatten option.<br/> [replacement_font &lt;font name&gt;] Use the specified font to display text in form fields. This option is useful when filling a form with non-ASCII text that is not supported by the fonts included in the input PDF. font name may be either the file name or the family name of a font, but using a file name is more re&#8208; liable. Currently only TrueType fonts with Unicode text are supported.<br/> [keep_first_id | keep_final_id] When combining pages from multiple PDFs, use one of these options to copy the document ID from either the first or final input document into the new output PDF. Otherwise pdftk creates a new document ID for the output PDF. When no operation is given, pdftk always uses the ID from the (single) input PDF.<br/> [drop_xfa] If your input PDF is a form created using Acrobat 7 or Adobe Designer, then it probably has XFA data. Filling such a form using pdftk yields a PDF with data that fails to display in Acrobat 7 (and 6?). The workaround solution is to remove the form&#39;s XFA data, either before you fill the form using pdftk or at the time you fill the form. Using this option causes pdftk to omit the XFA data from the output PDF form.<br/> This option is only useful when running pdftk on a sin&#8208; gle input PDF. When assembling a PDF from multiple in&#8208; puts using pdftk, any XFA data in the input is automati&#8208; cally omitted.<br/> [drop_xmp] Many PDFs store document metadata using both an Info dictionary (old school) and an XMP stream (new school). Pdftk&#39;s update_info operation can update the Info dic&#8208; tionary, but not the XMP stream. The proper remedy for this is to include a ModDate entry in your updated info with a current date/timestamp. The date/timestamp format is: D:YYYYMMDDHHmmSS, e.g. D:201307241346 &ndash; omitted data after YYYY revert to default values. This newer ModDate should cue PDF viewers that the Info metadata is more current than the XMP data.<br/> Alternatively, you might prefer to remove the XMP stream from the PDF altogether &ndash; that&#39;s what this option does. Note that objects inside the PDF might have their own, separate XMP metadata streams, and that drop_xmp does not remove those. It only removes the PDF&#39;s document- level XMP stream.<br/> [verbose] By default, pdftk runs quietly. Append verbose to the end and it will speak up.<br/> [dont_ask | do_ask] Depending on the compile-time settings (see ASK_ABOUT_WARNINGS), pdftk might prompt you for further input when it encounters a problem, such as a bad pass&#8208; word. Override this default behavior by adding dont_ask (so pdftk won&#39;t ask you what to do) or do_ask (so pdftk will ask you what to do).<br/> When running in dont_ask mode, pdftk will over-write files with its output without notice.<br/> EXAMPLES Collate scanned pages pdftk A=even.pdf B=odd.pdf shuffle A B output collated.pdf or if odd.pdf is in reverse order: pdftk A=even.pdf B=odd.pdf shuffle A Bend-1 output col&#8208; lated.pdf<br/> The following examples use actual passwords as command line pa&#8208; rameters, which is discouraged (see the SECURITY CONSIDERATIONS section).<br/> Decrypt a PDF pdftk secured.pdf input_pw foopass output unsecured.pdf<br/> Encrypt a PDF using AES-128 (the default), withhold all permis&#8208; sions (the default) pdftk 1.pdf output 1.128.pdf owner_pw foopass<br/> Same as above, except password &#39;baz&#39; must also be used to open output PDF pdftk 1.pdf output 1.128.pdf owner_pw foo user_pw baz<br/> Same as above, except printing is allowed (once the PDF is open) pdftk 1.pdf output 1.128.pdf owner_pw foo user_pw baz allow printing<br/> Apply RCA 40-bit encryption to output, revoking all permissions (the default). Set the owner PW to &#39;foopass&#39;. pdftk 1.pdf 2.pdf cat output 3.pdf encrypt_40bit owner_pw foopass<br/> Join two files, one of which requires the password &#39;foopass&#39;. The output is not encrypted. pdftk A=secured.pdf 2.pdf input_pw A=foopass cat output 3.pdf<br/> Join in1.pdf and in2.pdf into a new PDF, out1.pdf pdftk in1.pdf in2.pdf cat output out1.pdf or (using handles): pdftk A=in1.pdf B=in2.pdf cat A B output out1.pdf or (using wildcards): pdftk *.pdf cat output combined.pdf<br/> Remove page 13 from in1.pdf to create out1.pdf pdftk in.pdf cat 1-12 14-end output out1.pdf or: pdftk A=in1.pdf cat A1-12 A14-end output out1.pdf<br/> Uncompress PDF page streams for editing the PDF in a text edi&#8208; tor (e.g., vim, emacs) pdftk doc.pdf output doc.unc.pdf uncompress<br/> Repair a PDF&#39;s corrupted XREF table and stream lengths, if pos&#8208; sible pdftk broken.pdf output fixed.pdf<br/> Burst a single PDF document into pages and dump its data to doc_data.txt pdftk in.pdf burst<br/> Burst a single PDF document into encrypted pages. Allow low- quality printing pdftk in.pdf burst owner_pw foopass allow DegradedPrinting<br/> Write a report on PDF document metadata and bookmarks to re&#8208; port.txt pdftk in.pdf dump_data output report.txt<br/> Rotate the first PDF page to 90 degrees clockwise pdftk in.pdf cat 1east 2-end output out.pdf<br/> Rotate an entire PDF document to 180 degrees pdftk in.pdf cat 1-endsouth output out.pdf<br/> NOTES This is a port of pdftk to java. See https://gitlab.com/pdftk-java/pdftk The original program can be found at www.pdftk.com<br/> AUTHOR Original author of pdftk is Sid Steward (sid.steward at pdflabs dot com).<br/> SECURITY CONSIDERATIONS Passing a password as a command line parameter is insecure be&#8208; cause it can get saved into the shell&#39;s history and be accessi&#8208; ble by other users via /proc. Use the keyword PROMPT and input any passwords via standard input instead.<br/> December 7, 2020 pdftk.pdftk-java(1) </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region OCR --> <h2 id="ocr">OCR</h2> <p> <a href='https://https://tools.pdf24.org/en/ocr-pdf' target='_blank' rel="nofollow"><code>https://tools.pdf24.org/en/ocr-pdf</code></a> provides fast, free OCR, without ads. </p> <p> This website works well, and offers a total of 30 PDF operations. The other operations of <a href='https://https://tools.pdf24.org/' target='_blank' rel="nofollow"><code>https://tools.pdf24.org/</code></a> include merging, splitting, compressing, editing, signing, redacting, watermarking, locking, unlocking, etc. </p> <!-- endregion --> <!-- #region ops --> <h2 id="ops">PdfTk Operations</h2> <!-- #region rotate one page --> <h3 id="2south">Rotate page 2 only</h3> <p> Given <code>my_file.pdf</code>, a 2-page document: </p> <ol> <li>Do not transform page 1 (keep it as-is)</li> <li>Rotate page 2 only</li> <li>Save as <code>my_file_fixed.pdf</code></li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99ecec3271a3'><button class='copyBtn' data-clipboard-target='#id99ecec3271a3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk my_file.pdf cat 1 2south output my_file_fixed.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region merge --> <h3 id="merge">Merge PDFs</h3> <p> Concatenate <code>file_a.pdf</code>, <code>file_b.pdf</code> and <code>file_c.pdf</code>, then save as <code>big_file.pdf</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id72bc247be635'><button class='copyBtn' data-clipboard-target='#id72bc247be635' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk file_a.pdf file_b.pdf file_c.pdf cat output big_file.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region assemble --> <h3 id="assemble">Assemble Book Excerpt</h3> <p> If you need to create multiple excerpts of a book, one way to proceed is to: </p> <ol> <li> Create a PDF containing 3 pages: the front cover, the page containing the <a href='https://en.wikipedia.org/wiki/Edition_notice' target='_blank' rel="nofollow">edition notice</a>, and the back cover. Let's call this PDF file <code>book_wrapper.pdf</code>. </li> <li> Scan each of the excerpts into separate PDFs. Let's call these PDF files <code>book_excerpt_raw_1.pdf</code>, <code>book_excerpt_raw_2.pdf</code>, etc. </li> <li> Wrap the each excerpt within the front cover and edition notice, and the back cover. To do that, concatenate the first 2 pages of <code>book_wrapper.pdf</code>, <code>book_excerpt_raw_N.pdf</code> and the last page of <code>book_wrapper.pdf</code>, then save as <code>book_excerpt_N.pdf</code>. Following is an example of how to do that for 3 excerpts: </li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida5104c759b2a'><button class='copyBtn' data-clipboard-target='#ida5104c759b2a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \ book_excerpt_raw_1.pdf \ book_wrapper.pdf 3 \ cat output book_excerpt_1.pdf <span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \ book_excerpt_raw_2.pdf \ book_wrapper.pdf 3 \ cat output book_excerpt_2.pdf <span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \ book_excerpt_raw_3.pdf \ book_wrapper.pdf 3 \ cat output book_excerpt_3.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region cut --> <h3 id="cut">Cut a large PDF into 2 Parts</h3> <p> Cut <code>bigfile.pdf</code> into two parts: </p> <ol> <li><code>bigfile_part1.pdf</code> (containing pages 1-150)</li> <li><code>bigfile_part2.pdf</code> (containing pages 151-350)</li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id819933c9dad2'><button class='copyBtn' data-clipboard-target='#id819933c9dad2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat 1-150 output bigfile_part1.pdf <span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat 151-end output bigfile_part2.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Move Pages --> <h2 id="double">Interleaving Double-Sided Originals</h2> <p> If you scan a double-sided document, you might end up with the odd-numbered pages in one PDF document, and the even-numbered pages in another. </p> <p> A problem that you will encouter is that when you turn over the document to scan the even-numbered pages, they will be in reverse order because the pages will be scanned from the back to the front of the document. The following examples show two ways of combining the odd- and even-numbered pages in the proper order. </p> <h3 id="manual_reverse">Manually Reversing Even-Numbered Pages</h3> <p> This example shows how to interleave the pages in both documents to create <code>complete.pdf</code>. </p> <ol> <li>Scan the paper document from front to back, so only the odd pages are saved as <code>odd.pdf</code>.</li> <li>Manually reverse the order of the pages in the paper document.</li> <li>Turn the paper document over.</li> <li>Scan the even pages into <code>even.pdf</code>.</li> <li>Run the following incantation:</li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6c1ea6abbdde'><button class='copyBtn' data-clipboard-target='#id6c1ea6abbdde' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=odd.pdf B=even.pdf shuffle A B output complete.pdf<br> <span class='unselectable'>$ </span>rm even.pdf odd.pdf</pre> </div> <!-- endregion --> <h3 id="auto_reverse">Automatically Reversing Even-Numbered Pages</h3> <p> This example does not require you to manually re-order the pages, because <code>pdftk</code> can perform that task. </p> <ol> <li>Scan the document from front to back, so only the odd pages are saved as <code>odd.pdf</code>.</li> <li>Turn over the paper document.</li> <li>Scan the even pages from back to front into <code>even_reversed.pdf</code>.</li> <li>Run the following incantation:</li> </ol> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id70fad7d434c9'><button class='copyBtn' data-clipboard-target='#id70fad7d434c9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk even_reversed.pdf cat end-1 output even.pdf<br> <span class='unselectable'>$ </span>pdftk A=odd.pdf B=even.pdf shuffle A B output complete.pdf<br> <span class='unselectable'>$ </span>rm even.pdf even_reversed.pdf odd.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region delete 1 page --> <h3 id="cut">Delete a Page</h3> <p> Delete page 69 from <code>bigfile.pdf</code> and save as <code>smaller.pdf</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idddb55b401c08'><button class='copyBtn' data-clipboard-target='#idddb55b401c08' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat ~69 output smaller.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region delete several pages --> <h3 id="cut">Delete Several Pages</h3> <p> Delete pages 69-96 from <code>bigfile.pdf</code> and save as <code>smaller.pdf</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3cab440ad67d'><button class='copyBtn' data-clipboard-target='#id3cab440ad67d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat ~69-96 output smaller.pdf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region remove password --> <h2 id="pwd">Remove Password</h2> <p> Assuming <code>protected.pdf</code> is encrypted with password <code>myPwd</code>, make an unencrypted copy and save as <code>output.pdf</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbf565ac6b560'><button class='copyBtn' data-clipboard-target='#idbf565ac6b560' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk protected.pdf input_pw myPwd output output.pdf</pre> </div> <!-- endregion --> <p> You might need to enclose the password within single quotes. </p> <!-- endregion --> <!-- endregion --> <!-- #region Edit Contents --> <h2 id="edit">Edit Contents</h2> <p> Use LibreOffice Writer to edit the PDF, then export as PDF. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id90bc096dac60'><button class='copyBtn' data-clipboard-target='#id90bc096dac60' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>libreoffice --writer my_file.pdf</pre> </div> <!-- endregion --> <!-- endregion --> Pytest and Visual Studio Code 2023-08-20T00:00:00-04:00 https://mslinn.github.io/blog/2023/08/20/pytest <!-- #region intro --> <p> Python has several well-known testing frameworks. Popular choices for functional and unit testing include <a href='https://docs.pytest.org' target='_blank' rel="nofollow"><code>pytest</code></a>, the <a href='https://robotframework.org/' target='_blank' rel="nofollow">Robot framework</a>, and <a href='https://docs.python.org/3/library/unittest.html' target='_blank' rel="nofollow"><code>unittest</code></a>. <a href='https://github.com/gabrielfalcao/lettuce' target='_blank' rel="nofollow">Lettuce</a> and <a href='https://behave.readthedocs.io/en/latest/' target='_blank' rel="nofollow">Behave</a> are popular for behavior-driven testing. </p> <p> I decided to use <code>pytest</code> for a recent Python project. </p> <p> I found that <code>pytest</code>&rsquo;s test discovery mechanism was finicky, and demanded that Python packages and modules be set up before tests could be run. This is in sharp contrast to writing unit tests for Ruby, for example, using <a href='https://rspec.info/' target='_blank' rel="nofollow"><code>rspec</code></a>, because Ruby modules are so much more flexible, forgiving and malleable. </p> <p> However, getting <code>pytest</code> to work in Visual Studio Code was nearly impossible. Microsoft's documentation leaves a lot unsaid. I share what I learned, and how I made things work in this article. </p> <!-- endregion --> <!-- #region pytest --> <h2 id="pytest">Pytest</h2> <p> You can run <code>pytest</code> from the command line 3 ways. </p> <!-- #region pytest command --> <h3 id="cmd"><span class="code">Pytest</span> Command</h3> <p> The most common way to run <code>pytest</code> is to type the <code>pytest</code> command, like the following, which runs all tests: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc351e360982a'><button class='copyBtn' data-clipboard-target='#idc351e360982a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region module --> <h3 id="module"><span class="code">Pytest</span> Module</h3> <p> The second way to run <code>pytest</code> is to invoke the <code>pytest</code> Python module, as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id41e16a8a34bf'><button class='copyBtn' data-clipboard-target='#id41e16a8a34bf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python -m pytest</pre> </div> <!-- endregion --> <p> Running <code>pytest</code> as a Python module is nearly the same as running the <code>pytest</code> command, except that running it as a module adds the current directory to <code>sys.path</code>, which is standard python behavior, and can be helpful. </p> <p> I had best results running <code>pytest</code> tests from Visual Studio Code when <code>pytest</code> was invoked as a module. </p> <!-- endregion --> <!-- #region calling from python --> <h3 id="pgm">Calling <span class="code">Pytest</span> from Python</h3> <p> Your code can <a href='https://docs.pytest.org/en/7.1.x/how-to/usage.html#calling-pytest-from-python-code' target='_blank' rel="nofollow">call <code>pytest</code></a>. Lots of interesting and advanced development setups could be crafted using this approach. Baby steps. </p> <!-- endregion --> <!-- #region help --> <h3 id="help">Help Message</h3> <p> The help message for <code>pytest</code> is: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1dc0565a8edb'><button class='copyBtn' data-clipboard-target='#id1dc0565a8edb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest -h <span class='unselectable'>usage: pytest [options] [file_or_dir] [file_or_dir] [...]<br/> positional arguments: file_or_dir<br/> general: -k EXPRESSION Only run tests which match the given substring expression. An expression is a Python evaluatable expression where all names are substring-matched against test names and their parent classes. Example: -k &#39;test_method or test_other&#39; matches all test functions and classes whose name contains &#39;test_method&#39; or &#39;test_other&#39;, while -k &#39;not test_method&#39; matches those that don&#39;t contain &#39;test_method&#39; in their names. -k &#39;not test_method and not test_other&#39; will eliminate the matches. Additionally keywords are matched to classes and functions containing extra names in their &#39;extra_keyword_matches&#39; set, as well as functions which have names assigned directly to them. The matching is case-insensitive. -m MARKEXPR Only run tests matching given mark expression. For example: -m &#39;mark1 and not mark2&#39;. --markers show markers (builtin, plugin and per-project ones). -x, --exitfirst Exit instantly on first error or failed test --fixtures, --funcargs Show available fixtures, sorted by plugin appearance (fixtures with leading &#39;_&#39; are only shown with &#39;-v&#39;) --fixtures-per-test Show fixtures per test --pdb Start the interactive Python debugger on errors or KeyboardInterrupt --pdbcls=modulename:classname Specify a custom interactive Python debugger for use with --pdb.For example: --pdbcls=IPython.terminal.debugger:TerminalPdb --trace Immediately break when running each test --capture=method Per-test capturing method: one of fd|sys|no|tee-sys -s Shortcut for --capture=no --runxfail Report the results of xfail tests as if they were not marked --lf, --last-failed Rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first Run all tests, but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown. --nf, --new-first Run tests from new files first, then the rest of the tests sorted by file mtime --cache-show=[CACHESHOW] Show cache contents, don&#39;t perform collection or tests. Optional argument: glob (default: &#39;*&#39;). --cache-clear Remove all cache contents at start of test run --lfnf={all,none}, --last-failed-no-failures={all,none} Which tests to run with no previously (known) failures --sw, --stepwise Exit on test failure and continue from last failing test next time --sw-skip, --stepwise-skip Ignore the first failing test but stop on the next failing test. Implicitly enables --stepwise.<br/> Reporting: --durations=N Show N slowest setup/test durations (N=0 for all) --durations-min=N Minimal duration in seconds for inclusion in slowest list. Default: 0.005. -v, --verbose Increase verbosity --no-header Disable header --no-summary Disable summary -q, --quiet Decrease verbosity --verbosity=VERBOSE Set verbosity. Default: 0. -r chars Show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), &#39;N&#39; can be used to reset the list. (default: &#39;fE&#39;). --disable-warnings, --disable-pytest-warnings Disable warnings summary -l, --showlocals Show locals in tracebacks (disabled by default) --no-showlocals Hide locals in tracebacks (negate --showlocals passed through addopts) --tb=style Traceback print mode (auto/long/short/line/native/no) --show-capture={no,stdout,stderr,log,all} Controls how captured stdout/stderr/log is shown on failed tests. Default: all. --full-trace Don&#39;t cut any tracebacks (default is to cut) --color=color Color terminal output (yes/no/auto) --code-highlight={yes,no} Whether code should be highlighted (only if --color is also enabled). Default: yes. --pastebin=mode Send failed|all info to bpaste.net pastebin service --junit-xml=path Create junit-xml style report file at given path --junit-prefix=str Prepend prefix to classnames in junit-xml output<br/> pytest-warnings: -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS Set which warnings to report, see -W option of Python itself --maxfail=num Exit after first num failures or errors --strict-config Any warnings encountered while parsing the `pytest` section of the configuration file raise errors --strict-markers Markers not registered in the `markers` section of the configuration file raise errors --strict (Deprecated) alias to --strict-markers -c FILE, --config-file=FILE Load configuration from `FILE` instead of trying to locate one of the implicit configuration files. --continue-on-collection-errors Force test execution even if collection errors occur --rootdir=ROOTDIR Define root directory for tests. Can be relative path: &#39;root_dir&#39;, &#39;./root_dir&#39;, &#39;root_dir/another_dir/&#39;; absolute path: &#39;/home/user/root_dir&#39;; path with variables: &#39;$HOME/root_dir&#39;.<br/> collection: --collect-only, --co Only collect tests, don&#39;t execute them --pyargs Try to interpret all arguments as Python packages --ignore=path Ignore path during collection (multi-allowed) --ignore-glob=path Ignore path pattern during collection (multi-allowed) --deselect=nodeid_prefix Deselect item (via node id prefix) during collection (multi-allowed) --confcutdir=dir Only load conftest.py&#39;s relative to specified dir --noconftest Don&#39;t load any conftest.py files --keep-duplicates Keep duplicate tests --collect-in-virtualenv Don&#39;t ignore tests in a local virtualenv directory --import-mode={prepend,append,importlib} Prepend/append to sys.path when importing test modules and conftest files. Default: prepend. --doctest-modules Run doctests in all .py modules --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} Choose another output format for diffs on doctest failure --doctest-glob=pat Doctests file matching pattern, default: test*.txt --doctest-ignore-import-errors Ignore doctest ImportErrors --doctest-continue-on-failure For a given doctest, continue to run after the first failure<br/> test session debugging and configuration: --basetemp=dir Base temporary directory for this test run. (Warning: this directory is removed if it exists.) -V, --version Display pytest version and information about plugins. When given twice, also display information about plugins. -h, --help Show help message and configuration info -p name Early-load given plugin module name or entry point (multi-allowed). To avoid loading of plugins, use the `no:` prefix, e.g. `no:doctest`. --trace-config Trace considerations of conftest.py files --debug=[DEBUG_FILE_NAME] Store internal tracing debug information in this log file. This file is opened with &#39;w&#39; and truncated as a result, care advised. Default: pytestdebug.log. -o OVERRIDE_INI, --override-ini=OVERRIDE_INI Override ini option with &quot;option=value&quot; style, e.g. `-o xfail_strict=True -o cache_dir=cache`. --assert=MODE Control assertion debugging tools. &#39;plain&#39; performs no assertion debugging. &#39;rewrite&#39; (the default) rewrites assert statements in test modules on import to provide assert expression information. --setup-only Only setup fixtures, do not execute tests --setup-show Show setup of fixtures while executing tests --setup-plan Show what fixtures and tests would be executed but don&#39;t execute anything<br/> logging: --log-level=LEVEL Level of messages to catch/display. Not set by default, so it depends on the root/parent log handler&#39;s effective level, where it is &quot;WARNING&quot; by default. --log-format=LOG_FORMAT Log format used by the logging module --log-date-format=LOG_DATE_FORMAT Log date format used by the logging module --log-cli-level=LOG_CLI_LEVEL CLI logging level --log-cli-format=LOG_CLI_FORMAT Log format used by the logging module --log-cli-date-format=LOG_CLI_DATE_FORMAT Log date format used by the logging module --log-file=LOG_FILE Path to a file when logging will be written to --log-file-level=LOG_FILE_LEVEL Log file logging level --log-file-format=LOG_FILE_FORMAT Log format used by the logging module --log-file-date-format=LOG_FILE_DATE_FORMAT Log date format used by the logging module --log-auto-indent=LOG_AUTO_INDENT Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer. --log-disable=LOGGER_DISABLE Disable a logger by name. Can be passed multiple times.<br/> [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:<br/> markers (linelist): Markers for test functions empty_parameter_set_mark (string): Default marker for empty parametersets norecursedirs (args): Directory patterns to avoid for recursion testpaths (args): Directories to search for tests when no files or directories are given on the command line filterwarnings (linelist): Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. usefixtures (args): List of default fixtures to be used with this project python_files (args): Glob-style file patterns for Python test module discovery python_classes (args): Prefixes or glob names for Python test class discovery python_functions (args): Prefixes or glob names for Python test function and method discovery disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool): Disable string escape non-ASCII characters, might cause unwanted side effects(use at your own risk) console_output_style (string): Console output: &quot;classic&quot;, or with additional progress information (&quot;progress&quot; (percentage) | &quot;count&quot; | &quot;progress-even-when-capture-no&quot; (forces progress even when capture=no) xfail_strict (bool): Default for the strict parameter of xfail markers when not given explicitly (default: False) tmp_path_retention_count (string): How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`. tmp_path_retention_policy (string): Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. (all/failed/none) enable_assertion_pass_hook (bool): Enables the pytest_assertion_pass hook. Make sure to delete any previously generated pyc cache files. junit_suite_name (string): Test suite name for JUnit report junit_logging (string): Write captured log messages to JUnit report: one of no|log|system-out|system-err|out-err|all junit_log_passing_tests (bool): Capture log information for passing tests to JUnit report: junit_duration_report (string): Duration time to report: one of total|call junit_family (string): Emit XML for schema: one of legacy|xunit1|xunit2 doctest_optionflags (args): Option flags for doctests doctest_encoding (string): Encoding used for doctest files cache_dir (string): Cache directory path log_level (string): Default value for --log-level log_format (string): Default value for --log-format log_date_format (string): Default value for --log-date-format log_cli (bool): Enable log display during test run (also known as &quot;live logging&quot;) log_cli_level (string): Default value for --log-cli-level log_cli_format (string): Default value for --log-cli-format log_cli_date_format (string): Default value for --log-cli-date-format log_file (string): Default value for --log-file log_file_level (string): Default value for --log-file-level log_file_format (string): Default value for --log-file-format log_file_date_format (string): Default value for --log-file-date-format log_auto_indent (string): Default value for --log-auto-indent pythonpath (paths): Add paths to sys.path faulthandler_timeout (string): Dump the traceback of all threads if a test takes more than TIMEOUT seconds to finish addopts (args): Extra command line options minversion (string): Minimally required pytest version required_plugins (args): Plugins that must be present for pytest to run<br/> Environment variables: PYTEST_ADDOPTS Extra command line options PYTEST_PLUGINS Comma-separated plugins to load during startup PYTEST_DISABLE_PLUGIN_AUTOLOAD Set to disable plugin auto-loading PYTEST_DEBUG Set to enable debug tracing of pytest&#39;s internals<br/><br/> to see available markers type: pytest --markers to see available fixtures type: pytest --fixtures (shown according to specified file_or_dir or current dir if not specified; fixtures with leading &#39;_&#39; are only shown with the &#39;-v&#39; option </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region helpful options --> <h3 id="options">Helpful Options</h3> <p> It is handy to run new and failing tests, but to skip previously passing tests. The <code>&#8209;&#8209;nf</code> and <code>&#8209;&#8209;lf</code> options, respectively, invoke that mode of behavior: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5bfda46a8f31'><button class='copyBtn' data-clipboard-target='#id5bfda46a8f31' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest --nf --lf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region setup --> <h2 id="setup">Setup</h2> <!-- #region config --> <h3 id="config">Configuration</h3> <p> <code>Pytest</code> needed to know that the source code for my Python project is stored in the <code>src</code> directory. I created <code>pyproject.toml</code> as follows, which configured <code>pytest</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>pyproject.toml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7268f7a3c501'><button class='copyBtn' data-clipboard-target='#id7268f7a3c501' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[tool.pytest.ini_options] addopts = [ "--import-mode=importlib", ] pythonpath = "src"</pre> </div> <!-- endregion --> <p> The way in which <code>pytest</code> discovers tests is <a href='https://docs.pytest.org/en/7.1.x/explanation/pythonpath.htm' target='_blank' rel="nofollow">complex</a>. <a href='https://www.merriam-webster.com/dictionary/TL%3BDR' target='_blank' rel="nofollow">TL;DR</a>: specify the newest and best mechanism for test discovery by using the <code>&#8209;&#8209;import-mode=importlib</code> option as shown above. </p> <p> To always run <code>pytest</code> with the <code>&#8209;&#8209;nf</code> and <code>&#8209;&#8209;lf</code> options, add those options to <code>pyproject.toml</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>pyproject.toml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id95b59f9d92f0'><button class='copyBtn' data-clipboard-target='#id95b59f9d92f0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[tool.pytest.ini_options] addopts = [ "--import-mode=importlib", <span class='bg_yellow'>"--lf",</span> <span class='bg_yellow'>"--nf",</span> ] pythonpath = "src"</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Test Directories --> <h3 id="dirs">Test Directories</h3> <p> Flexibility generally adds complexity. <code>Pytest</code> allows for a lot of flexibility regarding the location of where tests can reside. Unfortunately, Python&rsquo;s package and module mechanism in combination with the <code>pytest</code> discovery mechanism for tests can lead to a frustrating experience when setting up a new project. </p> <p> You can read about <a href='https://pytest.org/en/7.4.x/explanation/goodpractices.html' target='_blank' rel="nofollow">good practices for <code>pytest</code></a>. Those recommendations are intimidating for first-time <code>pytest</code> programmers. I did not follow all of them. </p> <p> I found the simplest approach was to create a subdirectory for the bulk of the Python project code. If a file called <code>__init__.py</code> is created in that directory (use <a href='https://man7.org/linux/man-pages/man1/touch.1.html' target='_blank' rel="nofollow"><code>touch</code></a> for this) then the subdirectory becomes a submodule of your Python program. For this type of <code>pytest</code> setup, each submodule needs a <code>tests</code> directory. </p> <p> Here is how you could use <code>touch</code> to define an imaginary subdirectory/submodule called <code>my_submodule</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfedccaa8f852'><button class='copyBtn' data-clipboard-target='#idfedccaa8f852' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir -p src/my_submodule/<br> <span class='unselectable'>$ </span>touch src/my_submodule/__init__.py</pre> </div> <!-- endregion --> <p> My project structure was as follows. <a href='https://linux.die.net/man/1/tree' target='_blank' rel="nofollow"><code>Tree</code> docs are here.</a> </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>pytest project structure</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer tree' id='idcf7895192dcd'><button class='copyBtn' data-clipboard-target='#idcf7895192dcd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tree -I .git -I __pycache__ <span class='unselectable'>. ├── README.md ├── dl ├── dl.config ├── pyproject.toml ├── requirements.txt └── src ├── __main__.py └── dl ├── __init__.py ├── argument_parse.py ├── dl_config.py ├── media_file.py ├── remote.py ├── tests │   ├── __init__.py │   ├── test_dl_config.py │   └── test_uti.py └── util.py 3 directories, 15 files </span></pre> </div> <!-- endregion --> <p> <code>src/dl/tests/__init__.py</code> was an empty marker file, but the <code>___init__.py</code> file in the <a href='https://docs.python.org/3/tutorial/modules.html' target='_blank' rel="nofollow">parent module</a> defined <code>import</code>s for the submodule: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>src/dl/__init__.py</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7c62c625663f'><button class='copyBtn' data-clipboard-target='#id7c62c625663f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>from dl.argument_parse import ArgParse from dl.dl_config import DLConfig from dl.media_file import MediaFile from dl.remote import Remote from dl.util import *</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region sample test --> <h2 id="test">Sample Test</h2> <p> Here is a sample <code>pytest</code> file, containing 2 unit tests. It is simple and clear. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>src/dl/tests/test_util.py</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf606292360e5'><button class='copyBtn' data-clipboard-target='#idf606292360e5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>import pytest from pathlib import Path from dl import samba_mount, samba_parse class TestUtil: def test_samba_parse(self): remote_drive, local_path = samba_parse('e:/media/renders') assert remote_drive=='e' assert local_path=='/media/renders' def test_samba_mount(self): samba_root = samba_mount('bear', 'e', False) assert samba_root==Path('/mnt/bear/e') assert samba_root.is_mount()</pre> </div> <!-- endregion --> <h2 id="rap">Secret Sauce</h2> <p> If you have been paying attention, you should realize that one option for running new and failing tests is to type the following from the command line. We will use this method of invocation in the next section to integrate with Visual Studio Code. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id250a2c7972a4'><button class='copyBtn' data-clipboard-target='#id250a2c7972a4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python -m pytest --nf --lf</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region vscode --> <h2 id="vscode">Visual Studio Code Integration</h2> <p> Visual Studio Code has a Testing explorer and a Run and Debug explorer. I find the existance of two very similar explorers annoying, because you often want to debug a test. Yes, you can do that by pushing the right button in the Testing Explorer ... if it works. This did not work for me. </p> <p> <a href='https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-extensions-pack' target='_blank' rel="nofollow">Shopify Ruby</a>, the latest and greatest Ruby extension for Visual Studio Code, uses the Run and Debug explorer to run and debug tests as well as entire programs. This makes sense to me. </p> <p> <a href='https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance' target='_blank' rel="nofollow">Microsoft Pylance</a> (for Python) follows a different convention, and uses the Run and Debug explorer to run and debug Python programs. You must use the Testing explorer for running or debugging Python unit tests. Annoying. </p> <p> In the Testing explorer, I was able to see my <code>dl</code> module, but no tests were discovered. This was really annoying, and this is where more documentation from Microsoft would have been helpful. After wasting lots of time trying to solve this problem, I discovered a solution. </p> <h3 id="">Invoke <span class="code">Pytest</span> As a Module from VSCode</h3> <p> Run and Debug configurations that invoke the <code>pytest</code> module work perfectly. Here is an example <code>.vscode/launch.json</code> that demonstrates this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>.vscode/launch.json</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id00959361c0b4'><button class='copyBtn' data-clipboard-target='#id00959361c0b4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "version": "0.2.0", "configurations": [ { "name": "PyTest All", "type": "python", "request": "launch", "module": "pytest", "justMyCode": true }, { "args": ["--nf", "--lf"], "name": "PyTest New and Failing", "type": "python", "request": "launch", "module": "pytest", "justMyCode": true }, ] }</pre> </div> <!-- endregion --> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> You can set breakpoints in your code and debug your tests this way. </p> <!-- endregion --> C and C++ Online and On Ubuntu 2023-05-12T00:00:00-04:00 https://mslinn.github.io/blog/2023/05/12/c <!-- #region intro --> <div class="one_column right quartersize"> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='/blog/c/Lattice_C_8086_1982.pdf' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/lattice_c.svg" type="image/svg"> <!---<source srcset="/blog/c/lattice_c.avif" type="image/avif">--> <source srcset="/blog/c/lattice_c.webp" type="image/webp"> <source srcset="/blog/c/lattice_c.apng" type="image/apng"> <source srcset="/blog/c/lattice_c.png" type="image/png"> <source srcset="/blog/c/lattice_c.jpg" type="image/jpeg"> <source srcset="/blog/c/lattice_c.jpeg" type="image/jpeg"> <source srcset="/blog/c/lattice_c.jfif" type="image/jpeg"> <source srcset="/blog/c/lattice_c.pjpeg" type="image/jpeg"> <source srcset="/blog/c/lattice_c.pjp" type="image/jpeg"> <source srcset="/blog/c/lattice_c.gif" type="image/gif"> <source srcset="/blog/c/lattice_c.tif" type="image/tiff"> <source srcset="/blog/c/lattice_c.tiff" type="image/tiff"> <source srcset="/blog/c/lattice_c.bmp" type="image/bmp"> <source srcset="/blog/c/lattice_c.ico" type="image/x-icon"> <source srcset="/blog/c/lattice_c.cur" type="image/x-icon"> <img alt='Lattice C' class="imgImg rounded shadow" src="/blog/c/lattice_c.png" style='width: 100%; ' title='Lattice C' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="/blog/c/Lattice_C_8086_1982.pdf" target='_blank' > Lattice C </a> </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/c/k&r.svg" type="image/svg"> <!---<source srcset="/blog/c/k&r.avif" type="image/avif">--> <source srcset="/blog/c/k&r.webp" type="image/webp"> <source srcset="/blog/c/k&r.apng" type="image/apng"> <source srcset="/blog/c/k&r.png" type="image/png"> <source srcset="/blog/c/k&r.jpg" type="image/jpeg"> <source srcset="/blog/c/k&r.jpeg" type="image/jpeg"> <source srcset="/blog/c/k&r.jfif" type="image/jpeg"> <source srcset="/blog/c/k&r.pjpeg" type="image/jpeg"> <source srcset="/blog/c/k&r.pjp" type="image/jpeg"> <source srcset="/blog/c/k&r.gif" type="image/gif"> <source srcset="/blog/c/k&r.tif" type="image/tiff"> <source srcset="/blog/c/k&r.tiff" type="image/tiff"> <source srcset="/blog/c/k&r.bmp" type="image/bmp"> <source srcset="/blog/c/k&r.ico" type="image/x-icon"> <source srcset="/blog/c/k&r.cur" type="image/x-icon"> <img alt='K&R Book' class="imgImg rounded shadow" src="/blog/c/k&r.png" style='width: 100%; ' title='K&R Book' /> </picture> <figcaption class='imgFigCaption '> K&R Book </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://developerinsider.co/download-turbo-c-for-windows-7-8-8-1-and-windows-10-32-64-bit-full-screen/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/c/turbo_c_2.svg" type="image/svg"> <!---<source srcset="/blog/c/turbo_c_2.avif" type="image/avif">--> <source srcset="/blog/c/turbo_c_2.webp" type="image/webp"> <source srcset="/blog/c/turbo_c_2.apng" type="image/apng"> <source srcset="/blog/c/turbo_c_2.png" type="image/png"> <source srcset="/blog/c/turbo_c_2.jpg" type="image/jpeg"> <source srcset="/blog/c/turbo_c_2.jpeg" type="image/jpeg"> <source srcset="/blog/c/turbo_c_2.jfif" type="image/jpeg"> <source srcset="/blog/c/turbo_c_2.pjpeg" type="image/jpeg"> <source srcset="/blog/c/turbo_c_2.pjp" type="image/jpeg"> <source srcset="/blog/c/turbo_c_2.gif" type="image/gif"> <source srcset="/blog/c/turbo_c_2.tif" type="image/tiff"> <source srcset="/blog/c/turbo_c_2.tiff" type="image/tiff"> <source srcset="/blog/c/turbo_c_2.bmp" type="image/bmp"> <source srcset="/blog/c/turbo_c_2.ico" type="image/x-icon"> <source srcset="/blog/c/turbo_c_2.cur" type="image/x-icon"> <img alt='Borland Turbo C' class="imgImg rounded shadow" src="/blog/c/turbo_c_2.png" style='width: 100%; ' title='Borland Turbo C' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://developerinsider.co/download-turbo-c-for-windows-7-8-8-1-and-windows-10-32-64-bit-full-screen/" target='_blank' > Borland Turbo C </a> </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/zamples.svg" type="image/svg"> <!---<source srcset="/blog/images/zamples.avif" type="image/avif">--> <source srcset="/blog/images/zamples.webp" type="image/webp"> <source srcset="/blog/images/zamples.apng" type="image/apng"> <source srcset="/blog/images/zamples.png" type="image/png"> <source srcset="/blog/images/zamples.jpg" type="image/jpeg"> <source srcset="/blog/images/zamples.jpeg" type="image/jpeg"> <source srcset="/blog/images/zamples.jfif" type="image/jpeg"> <source srcset="/blog/images/zamples.pjpeg" type="image/jpeg"> <source srcset="/blog/images/zamples.pjp" type="image/jpeg"> <source srcset="/blog/images/zamples.gif" type="image/gif"> <source srcset="/blog/images/zamples.tif" type="image/tiff"> <source srcset="/blog/images/zamples.tiff" type="image/tiff"> <source srcset="/blog/images/zamples.bmp" type="image/bmp"> <source srcset="/blog/images/zamples.ico" type="image/x-icon"> <source srcset="/blog/images/zamples.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/zamples.png" style='width: 100%; ' /> </picture> </div> </div> <p> Recently I decided to brush up on my C programming. </p> <p> I first discovered C during a visit in 1978 to the <a href='https://cs.berkeley.edu/' target='_blank' rel="nofollow">Computer Science department of University of California, Berkeley</a>, where I also encounted UNIX for the first time. </p> <p> From about 1983 to 1985 I was the unofficial Canadian distributor of the <a href='/blog/c/Lattice_C_8086_1982.pdf'>Lattice C compiler</a>. I used that compiler for most of the programming work I was doing on PCs at the time. FedEx Express Canada did not come into being until 1987, UPS was even worse with the Canadian border than it is now, and Canada Post took forever to bring packages across the border. I bought in bulk at a discount, and sold at list price. Distribution is all about time and place. </p> <p> I also sold the first edition of the K&R book (&ldquo;The C Programming Language&rdquo;) via mail order, and I provided technical support, which brought in interesting consulting work. </p> <p> The Borland Turbo C compiler, released in 1990, was the original IDE. It was eventually replaced by Borland C++ compiler, which also compiled C. In 2005 I became the product marketing manager for most of Borland&rsquo;s compilers. One of my responsibilities was to give free copies to book authors and members of the trade press. Borland C++ shipped in a massive box that was responsible for a lot of dead trees. </p> <p> The C language has evolved since K&R days. The current and previous iterations are well summarized on <a href='https://en.cppreference.com/w/c/language' target='_blank' rel="nofollow"><code>cppreference.com</code></a>, which also provides an online C/C++ IDE for sample code. </p> <!-- endregion --> <!-- #region Revisions --> <h2 id="revisions">Revision History</h2> <p> 1978: <b>K&R book</b> first published. </p> <p> 1989: <b>C89</b>, also known as ANSI X3. 159-1989, or ANSI C. </p> <p>1995: <b>NA1</b> (aka C94 and C95) added support for wide-characters, wide-strings, and international keyboard layouts. </p> <p>1999: <b>C99</b> added restricted pointers, variable-length arrays, flexible array members, complex numbers support, type-generic math, <code>long long int</code>, extended identifiers, hexadecimal floating-point constants, compound literals, designated initializers, single line comments, extended integer type, the ability to mix declarations and code, variadic macros, new math functions, inline functions, boolean types, <code>_Pragma</code> and standard pragmas, and <code>VA_COPY</code>. C99 removed implicit function declaration. </p> <p>2011: <b>C11</b> added concurrency support, type-generic expressions, alignment-related facilities, static assertion, Unicode support, floating-point characteristic macros, no-return functions, anonymous structures and unions, bound-checking, and reentrancy functions. </p> <p>2018: <b>C18</b> addressed defects in C11 without introducing new language features. </p> <p>2023: The most recent publicly available draft of the next release, <a href='https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf' target='_blank' rel="nofollow"><b>C23</b></a>, was released on April 1, 2023. Unfortunately, that document was written for insiders, and is very difficult to read. Happily, JeanHeyd Meneide, the Project Editor for ISO/IEC JTC1 SC22 WG14 - Programming Languages, C, wrote an <a href='https://thephd.dev/c23-is-coming-here-is-what-is-on-the-menu' target='_blank' rel="nofollow">easy-to-read <wbr>summary of C23</a>. </p> <h2 id="linux">Linux Is Written in C</h2> <p> It has been <a href='https://www.zdnet.com/article/linus-torvalds-prepares-to-move-the-linux-kernel-to-modern-c/' target='_blank' rel="nofollow">reported</a> that Linux was originally written in C89, but Linux moved to C11 in 2022. I am curious as to why C18 is not used, since C18 just contains bug fixes for C11. Most of the Linux kernel code is written using the <a href='https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/C-Extensions.html#C-Extensions' target='_blank' rel="nofollow">GNU extensions of GCC</a>. </p> <!-- endregion --> <!-- #region online --> <h2 id="online">Online IDEs</h2> <p> I pioneered online programming with SMX Explorer in 1994, <a href='/resume/history/jspexplorer/developerWorks/'>JSP Explorer</a> in 2000, and supported most available languages in 2002 with <a href='/blog/2004/12/09/live-code-examples-for-j2se-6-mustang.html'>Zamples.com</a>. </p> <p> Now it seems that every day another online programming option becomes available. Many are free: </p> <ul> <li> <a href='https://coliru.stacked-crooked.com/' target='_blank' rel="nofollow">Coliru</a> is used by <code>cppreference.com</code>; <a href='https://github.com/StackedCrooked/coliru' target='_blank' rel="nofollow">GitHub project</a> </li> <li> <a href='https://github.com/features/codespaces' target='_blank' rel="nofollow">GitHub Codespaces</a> is tightly integrated with Microsoft Azure. </li> <li><a href='https://www.jetbrains.com/space/' target='_blank' rel="nofollow">JetBrains Space</a></li> <li> <a href='https://www.onlinegdb.com/online_c_compiler' target='_blank' rel="nofollow">OnlineGDB</a> claims to be to first Online IDE, but the <a href='https://web.archive.org/web/20160815000000*/OnlineGDB.com' target='_blank' rel="nofollow">WayBack Machine</a> shows that this site was created in 2016. </li> <li><a href='https://www.programiz.com/c-programming/online-compiler/' target='_blank' rel="nofollow">Programiz</a></li> <li><a href='https://www.tutorialspoint.com/compile_c_online.php' target='_blank' rel="nofollow">TutorialsPoint CodingGround</a></li> <li><a href='https://replit.com/languages/c' target='_blank' rel="nofollow">Replit</a></li> <li><a href='https://onecompiler.com/c' target='_blank' rel="nofollow">OneCompiler</a></li> <li>... and <a href='https://www.google.com/search?q=online+c+programming' target='_blank' rel="nofollow">many more</a> ... <a href='https://arne-mertz.de/2017/05/online-compilers/' target='_blank' rel="nofollow">even more!</a></li> </ul> <!-- endregion --> <!-- #region install --> <h2 id="install">Installing C/C++ on WSL/Ubuntu</h2> <p> Online programming is fine for learning, but I prefer to be independent and self-reliant whenever practical for doing actual work. </p> <h3 id="cli">Command-Line Toolchain</h3> <p> To program in C or C++ on Ubuntu using the traditional Linux command-line tool&shy;chain, install the <a href='https://packages.debian.org/sid/build-essential' target='_blank' rel="nofollow"><code>build-essential</code></a> Debian meta-package on Ubuntu. It includes the <a href='https://gcc.gnu.org/' target='_blank' rel="nofollow">GNU Compiler Collection (GCC)</a>, which is a collection of compilers and libraries for C, C++, Objective-C, Fortran, Ada, Go, and D programming languages. It also includes <a href='https://linux.die.net/man/1/make' target='_blank' rel="nofollow"><code>make</code></a> and <a href='https://packages.debian.org/sid/libc6.1-dev' target='_blank' rel="nofollow"><code>libc6.1-dev</code></a>, the GNU C development libraries and header files. </p> <p> You probably should also install <a href='https://www.sourceware.org/gdb/' target='_blank' rel="nofollow"><code>gdb</code></a>, the GNU Project Debugger, and the <a href='https://packages.debian.org/unstable/manpages-dev' target='_blank' rel="nofollow">manual pages</a> about using GNU/Linux for development. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id462bbbe634ed'><button class='copyBtn' data-clipboard-target='#id462bbbe634ed' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install build-essential gdb manpages-dev</pre> </div> <!-- endregion --> <p> Now you are able to compile and run C programs at the command line: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id08087c6ca8ce'><button class='copyBtn' data-clipboard-target='#id08087c6ca8ce' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir test && cd test <span class='unselectable'>$ </span>cat &gt;test.c &lt;&lt;EOF #include &lt;stdio.h&gt; int main(void) { printf("Hello, World!\n"); } EOF <span class='unselectable'>$ </span>gcc test.c -o test && ./test <span class='unselectable'>Hello, World! </span></pre> </div> <!-- endregion --> <h3 id="vscode">Visual Studio Code</h3> <p> Visual Studio Code is a high-quality (and free) IDE. To work with C and C++ in Visual Studio Code, install the <a href='https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools' target='_blank' rel="nofollow">Microsoft C/C++</a> extension. </p> <!-- endregion --> Don't Poke the Bear 2023-03-03T00:00:00-05:00 https://mslinn.github.io/blog/2023/03/03/lego-tm <!-- #region --> <h2 style="margin-bottom: 2em; margin-top: 2em;"> To the developers of the excellent &lsquo;<a href='https://github.com/go-acme/lego/' class='code>lego</span>' target='_blank' rel="nofollow"><span</a>&rsquo; GitHub project </h2> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://github.com/go-acme/lego' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ddns/lego-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/lego-logo.avif" type="image/avif">--> <source srcset="/blog/images/ddns/lego-logo.webp" type="image/webp"> <source srcset="/blog/images/ddns/lego-logo.apng" type="image/apng"> <source srcset="/blog/images/ddns/lego-logo.png" type="image/png"> <source srcset="/blog/images/ddns/lego-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.gif" type="image/gif"> <source srcset="/blog/images/ddns/lego-logo.tif" type="image/tiff"> <source srcset="/blog/images/ddns/lego-logo.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/lego-logo.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/lego-logo.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/lego-logo.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/ddns/lego-logo.png" style='width: 100%; ' /> </picture> </a> </div> <p> The name of this project is problematic, and you <a href='https://github.com/go-acme/lego/issues/470' target='_blank' rel="nofollow">publicly dismissed</a> the issue when it was brought to your attention. While ignorance of the law is no defense; demonstrably flaunting another party&rsquo;s rights never has positive consequences. </p> <p> <a href='https://www.lego.com/en-sg/service/help/fun-for-fans/behind-the-scenes/our-company/commercial-use-of-lego-products-bltb4ad5090ec5e4b08' target='_blank' rel="nofollow">The LEGO Group</a> has not taken legal action yet, but the laws of most countries (including the US) are that <a href='https://www.forbes.com/sites/oliverherzfeld/2013/02/28/failure-to-enforce-trademarks-if-you-snooze-do-you-lose/?sh=1d67ec326c22' target='_blank' rel="nofollow">if a trademark is not defended, then the trademark is at risk of being lost</a>. </p> <p> You should expect to receive a legal notice at some point. Not because the trademark owners are evil, but because they have a duty to their company, and the well-being of their employees, and a duty to their shareholders. </p> <p> Your disregard for the LEGO Group's rights constituted a real and present danger to them, their employees and shareholders. They are compelled to address threats against their well-being. </p> <div class='imgWrapper imgBlock right fullsize' style=' '> <figure> <a href='https://www.flickr.com/photos/angietigger/6894972004/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/bearpokelego.svg" type="image/svg"> <!---<source srcset="/blog/images/bearpokelego.avif" type="image/avif">--> <source srcset="/blog/images/bearpokelego.webp" type="image/webp"> <source srcset="/blog/images/bearpokelego.apng" type="image/apng"> <source srcset="/blog/images/bearpokelego.png" type="image/png"> <source srcset="/blog/images/bearpokelego.jpg" type="image/jpeg"> <source srcset="/blog/images/bearpokelego.jpeg" type="image/jpeg"> <source srcset="/blog/images/bearpokelego.jfif" type="image/jpeg"> <source srcset="/blog/images/bearpokelego.pjpeg" type="image/jpeg"> <source srcset="/blog/images/bearpokelego.pjp" type="image/jpeg"> <source srcset="/blog/images/bearpokelego.gif" type="image/gif"> <source srcset="/blog/images/bearpokelego.tif" type="image/tiff"> <source srcset="/blog/images/bearpokelego.tiff" type="image/tiff"> <source srcset="/blog/images/bearpokelego.bmp" type="image/bmp"> <source srcset="/blog/images/bearpokelego.ico" type="image/x-icon"> <source srcset="/blog/images/bearpokelego.cur" type="image/x-icon"> <img alt='&lsquo;lego bear&rsquo; by angie <br> CC BY-NC-SA 2.0' class="imgImg rounded shadow" src="/blog/images/bearpokelego.png" style='width: 100%; ' title='&lsquo;lego bear&rsquo; by angie <br> CC BY-NC-SA 2.0' /> </picture> </a> <figcaption class='imgFigCaption fullsize'> <a href="https://www.flickr.com/photos/angietigger/6894972004/" target='_blank' > &lsquo;lego bear&rsquo; by angie <br> CC BY-NC-SA 2.0 </a> </figcaption> </figure> </div> <h2 id="think">Think</h2> <p> Not only must a trademark holder defend their trademark, they must be seen to defend it. That means taking action that is publicly visible. For example, they might include GitHub and maybe even Microsoft in the legal action against you. </p> <div class="pullQuote"> No-one would have reason to defend you </div> <p> The LEGO Group would likely include GitHub/Microsoft in the case, even though they know that <a href='https://www.eff.org/issues/cda230' target='_blank' rel="nofollow">Section 230</a> would probably be found to apply (in due course), and GitHub/Microsoft would not be found to be liable &ndash; after they spent several million dollars in a successful legal defense. </p> <p> It is easy to understand the motivation for the LEGO Group deliberately &lsquo;losing&rsquo; at the attempt to ensnare GitHub/Microsoft in this court case. You would have caused GitHub/Microsoft an unnecessary and pointless waste of time and resources, and it would be shown that you knew better. Remember, you have already been publicly been warned that there would likely be consequences &ndash; yet you chose to ignore the warning. </p> <p> This would likely motivate GitHub/Microsoft to protect themselves against similar misguided behavior of other GitHub users. Restrictive policies and procedures would likely get implemented, and the entire world would know that the resulting degraded GitHub experience was your fault. You would be vilified by every other GitHub user. Not a great thing for your resume. </p> <p> ... And GitHub/Microsoft might ask you to pay their legal expenses. </p> <h2 id="family">Do You Have Families?</h2> <p> Please understand that because you are acting as an informal collection of individuals, everyone who ever contributed to this project is exposed to signficant financial risk. The LEGO Group would probably not attempt to narrow down the guilty individuals &ndash; identifying the guilty and assigning punishment would be the outcomes of the court case. Instead, they would likely include every contributor in the litigation; that means every committer. </p> <p> You are risking the financial well-being of your families ... just because you like a cute play on words? </p> <p class="pullQuoteFull"> Rational people would not expose themselves to significant risk without any prospect of signficant benefit, and when not faced with dire necessity. </p> <p> If you presently balk at paying a lawyer to advise you on this matter, that cost will be as nothing compared to the world of hurt you are likely to find yourselves in if you do not change the name of your project. </p> <p> From a technical aspect, you have done excellent work on your project, and you are freely providing the world a valuable benefit. Unfortunately, you trampled the rights of another in doing so... and for no reason. Please act on this before the inevitable negative consequences arise. Do not poke the bear because you are curious to see what it will do. </p> <div class='imgWrapper imgFlex right' style='width: 45%; '> <picture class='imgPicture'> <source srcset="/blog/images/bearpoke.svg" type="image/svg"> <!---<source srcset="/blog/images/bearpoke.avif" type="image/avif">--> <source srcset="/blog/images/bearpoke.webp" type="image/webp"> <source srcset="/blog/images/bearpoke.apng" type="image/apng"> <source srcset="/blog/images/bearpoke.png" type="image/png"> <source srcset="/blog/images/bearpoke.jpg" type="image/jpeg"> <source srcset="/blog/images/bearpoke.jpeg" type="image/jpeg"> <source srcset="/blog/images/bearpoke.jfif" type="image/jpeg"> <source srcset="/blog/images/bearpoke.pjpeg" type="image/jpeg"> <source srcset="/blog/images/bearpoke.pjp" type="image/jpeg"> <source srcset="/blog/images/bearpoke.gif" type="image/gif"> <source srcset="/blog/images/bearpoke.tif" type="image/tiff"> <source srcset="/blog/images/bearpoke.tiff" type="image/tiff"> <source srcset="/blog/images/bearpoke.bmp" type="image/bmp"> <source srcset="/blog/images/bearpoke.ico" type="image/x-icon"> <source srcset="/blog/images/bearpoke.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/bearpoke.png" style='width: 100%; ' /> </picture> </div> <p class="left alert rounded shadow" style="width: 50%;"> The wheels of corporate law grind slowly, and huge amounts of money are spent. According to the path you are now on, the committers of this project may be subjected to immense financial and mental burdens. Broken families and suicides may result. <br><br> Please heed this warning. If nothing bad has ever happened to you yet in life, rest assured that <a href='https://brewminate.com/clotho-lachesis-and-atropos-the-three-sisters-of-fate-in-ancient-greek-mythology/' target='_blank' rel="nofollow">Lachesis, Atropos and Clotho</a> will inevitably make themselves known. Do not let arrogance and pride ruin you. </p> <div class="clear pullQuoteFull" style="margin-top: 2.5em;"> I am trying to help you people.<br> Because you have done good work,<br> and it would be a shame to see you suffer. </div> <h2 id="disclaim" class="clear">Disclaimer</h2> <p> I have no relationship with the LEGO Group, or any individuals associated with them. Please consider this as a notice freely offered for public service. </p> <p> Although <a href='/softwareexpert/'>I have experience working as an expert witness in the US Federal legal system as a software expert</a>, I am not a lawyer and I have no legal training. However, I have seen corporate legal machinery at work many times, up close. I have spent thousands of hours working with corporate attorneys in the US and Europe. My clients have included Adobe, Amazon/AWS, Apple, and SAP. </p> <!-- endregion --> Letsencrypt/ACME Wildcard SSL Certificates by Lego 2023-03-02T00:00:00-05:00 https://mslinn.github.io/blog/2023/03/02/lego <style> .redButton { color:white; background-color:#e04433; padding: 2px; font-family: Helvetica, Arial, Sans-Serif; font-weight: bold; } </style> <!-- #region intro --> <div class='imgWrapper imgFlex right' style='width: 40%; '> <a href='https://letsencrypt.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ddns/letsencrypt-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/letsencrypt-logo.avif" type="image/avif">--> <source srcset="/blog/images/ddns/letsencrypt-logo.webp" type="image/webp"> <source srcset="/blog/images/ddns/letsencrypt-logo.apng" type="image/apng"> <source srcset="/blog/images/ddns/letsencrypt-logo.png" type="image/png"> <source srcset="/blog/images/ddns/letsencrypt-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.gif" type="image/gif"> <source srcset="/blog/images/ddns/letsencrypt-logo.tif" type="image/tiff"> <source srcset="/blog/images/ddns/letsencrypt-logo.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/letsencrypt-logo.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/letsencrypt-logo.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/letsencrypt-logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/ddns/letsencrypt-logo.png" style='width: 100%; ' /> </picture> </a> </div> <p> Letsencrypt certificates are only valid for <a href='https://letsencrypt.org/docs/faq/#what-is-the-lifetime-for-let-s-encrypt-certificates-for-how-long-are-they-valid' target='_blank' rel="nofollow">90 days</a>. It is common practice to renew them every 60 days, a task that one should automate when the website is first published. </p> <p> I wrote about <a href='/blog/2022/06/15/certbot.html'>setting up wildcard SSL certificates with Nginx</a> 9 months ago. That article included a demonstration of how to create an SSL certificate manually, but did not discuss how to maintain it. </p> <p> This article discusses how to automatically generate and maintain the SSL certificate using <a href='https://github.com/go-acme/lego' target='_blank' rel="nofollow"><code>lego</code></a>, a Letsencrypt/ACME client and library written in Go that has become popular since I last wrote about this topic. </p> <p> <code>Lego</code> handles many moving parts transparently. It is <i>much</i> simpler than using DNS delegates with <a href='https://github.com/joohoi/acme-dns' target='_blank' rel="nofollow"><code>acme-dns</code></a>. </p> <p> We&rsquo;ll start by briefly discussing some background information. </p> <!-- endregion --> <!-- #region Letsencrypt certbot and wildcard ssl certs --> <h2 id="certbot" class="clear">Letsencrypt&rsquo;s <span class="code">Certbot</span> and Wildcard SSL Certificates</h2> <p> You must prove to Letsencrypt that you control the DNS for a domain before it issues a wildcard SSL certificate for that domain. <a href='https://certbot.eff.org/' target='_blank' rel="nofollow">Letsencrypt&rsquo;s <code>certbot</code></a> currently uses the <a href='https://cert-manager.io/docs/configuration/acme/dns01/' target='_blank' rel="nofollow"><code>DNS-01</code> challenge</a> for this purpose. </p> <h3 id="DNS-01">DNS-01 Challenge</h3> <p> The DNS-01 challenge requires that DNS <code>TXT</code> records for the domain be created with specific values as part of the authentication mechanism. Several of these <code>TXT</code> records can co-exist among the DNS records for a domain. The name of the <code>TXT</code> records is always <code>_acme-challenge</code>. </p> <p> When <code>certbot</code> requests that a new wildcard SSL certificate be created by Letsencrypt, it sends DNS-01 <code>TXT</code> queries to the primary DNS. The Letsencrypt service verifies that the required <code>_acme-challenge</code> <code>TXT</code> records are available with the correct values. </p> <p> <a href='https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation' target='_blank' rel="nofollow">A Technical Deep Dive: Securing the Automation of ACME DNS Challenge Validation</a>, published by the Electronic Frontier Foundation, explains how this works in detail. </p> <p> <code>Certbot</code> plugins have been created for various DNS providers to make this process easier. <code>Lego</code> is a higher-level program that makes the entire process even easier. </p> <!-- endregion --> <!-- #region agenda --> <h2 id="agenda">Agenda</h2> <p> The high-level outline of the remainder of this blog is: </p> <ol> <li><a href='#go'>Install <code>go</code> language support.</a></li> <li><a href='#install'>Install <code>lego</code>.</a></li> <li><a href='#gen'>Generate the wildcard SSL certificate.</a></li> <li><a href='#provide'>Provide the certificate</a> to your production web server.</li> <li><a href='#restart'>Restart the webserver</a> and verify the new certificate is served.</li> <li><a href='#crontab'>Add a new entry to <code>crontab</code></a> to automate the SSL certificate generation.</li> </ol> <!-- endregion --> <!-- #region Install Go Language Support --> <h2 id="go">Install Go Language Support</h2> <p> <code>Lego</code> is written in Go. Ensure that <a href='https://go.dev/doc/install' target='_blank' rel="nofollow">Go language support</a> is installed. For Ubuntu Linux (and the default WSL distro), type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id04f07e188903'><button class='copyBtn' data-clipboard-target='#id04f07e188903' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install golang-go <span class='unselectable'>The following NEW packages will be installed: golang-1.18-go golang-1.18-src golang-go golang-src 0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded. Need to get 82.2 MB of archives. After this operation, 436 MB of additional disk space will be used. Do you want to continue? [Y/n] Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-1.18-src all 1.18.1-1ubuntu1 [16.2 MB] Get:2 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-1.18-go amd64 1.18.1-1ubuntu1 [66.0 MB] Get:3 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-src all 2:1.18~0ubuntu2 [4438 B] Get:4 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-go amd64 2:1.18~0ubuntu2 [41.8 kB] Fetched 82.2 MB in 2s (33.6 MB/s) Selecting previously unselected package golang-1.18-src. (Reading database ... 173763 files and directories currently installed.) Preparing to unpack .../golang-1.18-src_1.18.1-1ubuntu1_all.deb ... Unpacking golang-1.18-src (1.18.1-1ubuntu1) ... Selecting previously unselected package golang-1.18-go. Preparing to unpack .../golang-1.18-go_1.18.1-1ubuntu1_amd64.deb ... Unpacking golang-1.18-go (1.18.1-1ubuntu1) ... Selecting previously unselected package golang-src. Preparing to unpack .../golang-src_2%3a1.18~0ubuntu2_all.deb ... Unpacking golang-src (2:1.18~0ubuntu2) ... Selecting previously unselected package golang-go:amd64. Preparing to unpack .../golang-go_2%3a1.18~0ubuntu2_amd64.deb ... Unpacking golang-go:amd64 (2:1.18~0ubuntu2) ... Setting up golang-1.18-src (1.18.1-1ubuntu1) ... Setting up golang-src (2:1.18~0ubuntu2) ... Setting up golang-1.18-go (1.18.1-1ubuntu1) ... Setting up golang-go:amd64 (2:1.18~0ubuntu2) ... Processing triggers for man-db (2.10.2-1) ... Scanning processes... Scanning processor microcode... Scanning linux images... Failed to retrieve available kernel versions. Failed to check for processor microcode upgrades. No services need to be restarted. No containers need to be restarted. No user sessions are running outdated binaries. No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre> </div> <p> View the version of <code>go</code> that was installed: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5af120edbfc1'><button class='copyBtn' data-clipboard-target='#id5af120edbfc1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>go version <span class='unselectable'>go version go1.19.2 linux/amd64 </span></pre> </div> <!-- endregion --> <!-- #region Installing Lego --> <h2 id="install">Installing Lego</h2> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/ddns/lego-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/lego-logo.avif" type="image/avif">--> <source srcset="/blog/images/ddns/lego-logo.webp" type="image/webp"> <source srcset="/blog/images/ddns/lego-logo.apng" type="image/apng"> <source srcset="/blog/images/ddns/lego-logo.png" type="image/png"> <source srcset="/blog/images/ddns/lego-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/lego-logo.gif" type="image/gif"> <source srcset="/blog/images/ddns/lego-logo.tif" type="image/tiff"> <source srcset="/blog/images/ddns/lego-logo.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/lego-logo.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/lego-logo.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/lego-logo.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/ddns/lego-logo.png" style='width: 100%; ' /> </picture> </div> <p> The official <code>lego</code> installation instructions are <a href='https://go-acme.github.io/lego/installation/' target='_blank' rel="nofollow">here</a>. They do not mention Ubuntu instructions, but the <a href='https://ubuntu.pkgs.org/22.04/ubuntu-universe-amd64/lego_4.1.3-3ubuntu1_amd64.deb.html' target='_blank' rel="nofollow"><code>lego</code> package for Ubuntu</a> can be installed in the usual way: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id100276c3bc48'><button class='copyBtn' data-clipboard-target='#id100276c3bc48' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install lego <span class='unselectable'>Reading package lists... Done Building dependency tree... Done Reading state information... Done The following NEW packages will be installed: lego 0 upgraded, 1 newly installed, 0 to remove and 7 not upgraded. Need to get 5322 kB of archives. After this operation, 19.7 MB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 lego amd64 4.1.3-3ubuntu1.22.04.1 [5322 kB] Fetched 5322 kB in 1s (10.2 MB/s) Selecting previously unselected package lego. (Reading database ... 86044 files and directories currently installed.) Preparing to unpack .../lego_4.1.3-3ubuntu1.22.04.1_amd64.deb ... Unpacking lego (4.1.3-3ubuntu1.22.04.1) ... Setting up lego (4.1.3-3ubuntu1.22.04.1) ... </span></pre> </div> <p> Here is the <code>lego</code> help message: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb633b161deff'><button class='copyBtn' data-clipboard-target='#idb633b161deff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lego -h <span class='unselectable'>NAME: lego - Let&#39;s Encrypt client written in Go<br/> USAGE: lego [global options] command [command options] [arguments...]<br/> VERSION: dev<br/> COMMANDS: run Register an account, then create and install a certificate revoke Revoke a certificate renew Renew a certificate dnshelp Shows additional help for the &#39;--dns&#39; global option list Display certificates and accounts information. help, h Shows a list of commands or help for one command<br/> GLOBAL OPTIONS: --domains value, -d value Add a domain to the process. Can be specified multiple times. --server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: &quot;https://acme-v02.api.letsencrypt.org/directory&quot;) --accept-tos, -a By setting this flag to true you indicate that you accept the current Let&#39;s Encrypt terms of service. --email value, -m value Email used for registration and recovery contact. --csr value, -c value Certificate signing request filename, if an external CSR is to be used. --eab Use External Account Binding for account registration. Requires --kid and --hmac. --kid value Key identifier from External CA. Used for External Account Binding. --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384. (default: &quot;ec256&quot;) --filename value (deprecated) Filename of the generated certificate. --path value Directory to use for storing the data. (default: &quot;/mnt/_/work/lego/.lego&quot;) [$LEGO_PATH] --http Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges. --http.port value Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port. (default: &quot;:80&quot;) --http.proxy-header value Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy. (default: &quot;Host&quot;) --http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. --tls Use the TLS challenge to solve challenges. Can be mixed with other types of challenges. --tls.port value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port. (default: &quot;:443&quot;) --dns value Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run &#39;lego dnshelp&#39; for help on usage. --dns.disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers. --dns.resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google&#39;s DNS resolvers if the system&#39;s cannot be determined. --http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0) --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10) --pem Generate a .pem file by concatenating the .key and .crt files together. --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) --help, -h show help --version, -v print the version </span></pre> </div> <!-- endregion --> <!-- #region Generating the Wildcard SSL Certificate --> <h2 id="gen">Generating the Wildcard SSL Certificate</h2> <!-- #region /etc/environment --> <h3 id="env"><span class="code">/etc/environment</span></h3> <p> My projects are defined in a directory tree, which is pointed to by an environment variable called <code>work</code>. This is helpful when working across many machines, each of which has a different directory layout. </p> <p> I define the <code>work</code> environment variable in the <a href='https://askubuntu.com/a/866240/58760' target='_blank' rel="nofollow">system-wide <code>/etc/environment</code></a> file, where it affects all users, all scripts, and crontab entries: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/environment</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddcb971b652d7'><button class='copyBtn' data-clipboard-target='#iddcb971b652d7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin" work=/mnt/_/work sites=/mnt/_/www</pre> </div> <p> <code>$work</code> is used in the script shown <a href='#certGenScalaCourses'>below</a>, and that script will be launched via <code>crontab</code>, which is why <code>/etc/environment</code> is used instead of <code>$HOME/.bashrc</code>. </p> <p class="alert rounded shadow"> WSL does not run <code>systemd</code> services, and WSL2 only runs them if they are enabled. You can easily convert WSL instances into WSL2 instances. <br><br> Unless <code>systemd</code> starts automatically at boot time, <code>/etc/environment</code> will not be processed. <br><br> Read Microsoft&rsquo;s announcement to learn more: <a href='https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/' target='_blank' rel="nofollow"><code>Systemd</code> support is now available in WSL!</a> </p> <!-- endregion --> <!-- #region generation --> <h3 id="dir">Generation</h3> <p> When working with the <code>lego</code> CLI with DNS authentication, a directory is needed to hold all the files that are required. I created a directory for this purpose at <code>$work/lego</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaf1fb1022889'><button class='copyBtn' data-clipboard-target='#idaf1fb1022889' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir $work/lego<br> <span class='unselectable'>$ </span>cd $work/lego</pre> </div> <p> The <code>lego --dns namecheap</code> option connects to Namecheap, which is my DNS provider, and uses the Namecheap DNS for the <code>DNS-01</code> challenge. Lego supports <a href='https://go-acme.github.io/lego/dns/#dns-providers' target='_blank' rel="nofollow">many other DNS providers</a>. </p> <!-- #region Namecheap Users --> <div class="alert rounded shadow"> <h2>Namecheap Users</h2> <p> The Namecheap API documentation is <a href='https://www.namecheap.com/support/api/intro/' target='_blank' rel="nofollow">here</a>. For some reason, that page reloads to a different page, which is annoying. Press the <kbd>Esc</kbd> key right after the page opens, so you can read it. </p> <p> Please also read the <a href='https://www.namecheap.com/support/knowledgebase/article.aspx/9739/63/api-faq/#t' target='_blank' rel="nofollow">FAQ</a>.<br> Enable the Namecheap API <a href='https://ap.www.namecheap.com/settings/tools/apiaccess/' target='_blank' rel="nofollow">here</a>.<br> You could enable the Namecheap sandbox API <a href='https://ap.www.sandbox.namecheap.com/settings/tools/apiaccess/' target='_blank' rel="nofollow">here</a>. </p> <p> Here is a short script that displays your current public IP address: </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,whatismyip' download='whatismyip' title='Click on the file name to download the file'>whatismyip</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id5f6bcb411110"><button class='copyBtn' data-clipboard-target='#id5f6bcb411110'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash dig +short myip.opendns.com @resolver1.opendns.com </pre> <p style="margin-top: 1em;"> If your modem goes offline, Namecheap&rsquo;s DNS will assign a new IP address the next time the modem reconnects. That will cause the scripts shown below to stop working until you manually provide the new address to the Namecheap API. Annoying! Be sure to plug your modem into a UPS to maintain the IP address even during short outages, power brownouts and voltage dropouts. A sine-wave UPS is strongly preferred, since modems are sensitive to low-quality power. </p> <p> The Namecheap API needs 2 environment variables for authentication (<code>NAMECHEAP_API_USER</code> and <code>NAMECHEAP_API_KEY</code>). Here is how I defined those environment variables so they were available for the <code>lego</code> process launched by <code>sudo</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1b15bdaaa8b3'><button class='copyBtn' data-clipboard-target='#id1b15bdaaa8b3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export NAMECHEAP_API_USER=asdf<br> <span class='unselectable'>$ </span>export NAMECHEAP_API_KEY=asdfasdf</pre> </div> </div> <!-- endregion --> <p> To generate a wildcard certificate, you must specify the <code>--domains</code> option twice: once with the domain name, and once for all subdomains, like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id279781951fe7'><button class='copyBtn' data-clipboard-target='#id279781951fe7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lego \ --accept-tos \ --dns namecheap \ --domains="scalacourses.com" \ --domains="*.scalacourses.com" \ --email="mslinn@scalacourses.com" \ <span class="bg_yellow">run</span> <span class='unselectable'>2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: Obtaining bundled SAN certificate 2023/03/01 13:40:03 [INFO] [*.scalacourses.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/207409481036 2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: use dns-01 solver 2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: Preparing to solve DNS-01 2023/03/01 13:40:04 [INFO] [*.scalacourses.com] acme: Trying to solve DNS-01 2023/03/01 13:40:04 [INFO] [*.scalacourses.com] acme: Checking DNS record propagation using [172.19.176.1:53] 2023/03/01 13:40:19 [INFO] Wait for propagation [timeout: 1h0m0s, interval: 15s] 2023/03/01 13:40:20 [INFO] [*.scalacourses.com] acme: Waiting for DNS record propagation. 2023/03/01 13:40:35 [INFO] [*.scalacourses.com] acme: Waiting for DNS record propagation. 2023/03/01 13:40:56 [INFO] [*.scalacourses.com] The server validated our request 2023/03/01 13:40:56 [INFO] [*.scalacourses.com] acme: Cleaning DNS-01 challenge 2023/03/01 13:40:56 [INFO] [*.scalacourses.com] acme: Validations succeeded; requesting certificates 2023/03/01 13:40:57 [INFO] [*.scalacourses.com] Server responded with a certificate. </span></pre> </div> <!-- endregion --> <p> The wildcard SSL certificate and associated files are generated into the <code>.lego/certificates</code> directory. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id346663d00eba'><button class='copyBtn' data-clipboard-target='#id346663d00eba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -alF .lego/certificates <span class='unselectable'>total 12 drwx------ 1 mslinn mslinn 4096 Mar 1 13:40 ./ drwx------ 1 mslinn mslinn 4096 Mar 1 13:15 ../ -rw------- 1 mslinn mslinn 5325 Mar 1 13:40 scalacourses.com.crt -rw------- 1 mslinn mslinn 3751 Mar 1 13:40 scalacourses.com.issuer.crt -rw------- 1 mslinn mslinn 239 Mar 1 13:40 scalacourses.com.json -rw------- 1 mslinn mslinn 227 Mar 1 13:40 scalacourses.com.key </span></pre> </div> <!-- endregion --> <ul> <li> <code>scalacourses.com.crt</code> is the server certificate that nginx needs, already combined with the CA certificate. </li> <li><code>scalacourses.com.key</code> is the private key for the server certificate.</li> <li><code>scalacourses.com.issuer.crt</code> is the issuing Certificate Authority&apos;s certificate.</li> <li> <code>scalacourses.com.json</code> contains JSON encoded meta information. It looked like this: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>&dollar;work/lego/.lego/certificates/scalacourses.com.json</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc31a14271bc3'><button class='copyBtn' data-clipboard-target='#idc31a14271bc3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "domain": "*.scalacourses.com", "certUrl": "https://acme-v02.api.letsencrypt.org/acme/cert/3939493849340275024024", "certStableUrl": "https://acme-v02.api.letsencrypt.org/acme/cert/738378373923492384932874932" }</pre> </div> </li> </ul> <p> Using the value for <code>certStableUrl</code> in the above JSON file, we can view the generated (and combined) certificate that was saved on <code>letsencrypt.org</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id953c2f0e38e5'><button class='copyBtn' data-clipboard-target='#id953c2f0e38e5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>wget -O aw.crt \ https://acme-v02.api.letsencrypt.org/acme/cert/03b099f52b4841db17e035c5c2b390f0219b</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region certGenScalaCourses script --> <h2 id="certGenScalaCourses"><span class="code">certGenScalaCourses</span> Script</h2> <p> 60 days from now, when the certificate should be renewed, the last parameter passed to <code>lego</code> in the above command line should be changed from <code class="bg_yellow">run</code> to <code>renew</code>. Here is a bash script that you can edit for this purpose; at the end of this article this script is incorporated into a new <code>crontab</code> entry: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>&dollar;work/lego/certGenScalaCourses</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id290858708712'><button class='copyBtn' data-clipboard-target='#id290858708712' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash export NAMECHEAP_API_USER=asdf export NAMECHEAP_API_KEY=asdfasdf ACTION=run if [ -f .lego/certificates/scalacourses.com.crt ]; then ACTION=renew fi cd $work/lego lego \ --accept-tos \ --dns namecheap \ --domains="scalacourses.com" \ --domains="*.scalacourses.com" \ --email="mslinn@scalacourses.com" \ "$ACTION" sudo systemctl reload nginx</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Viewing a Certificate --> <h2 id="view">Viewing a Certificate</h2> <p> For Ubuntu 22.10, the filetype associations are defined in a hierarchy of files called <code>mimeapps.list</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id58a09354dec4'><button class='copyBtn' data-clipboard-target='#id58a09354dec4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>locate mimeapps.list <span class='unselectable'>~/.config/mimeapps.list ~/.local/share/applications/mimeapps.list /snap/core/14447/usr/share/applications/mimeapps.list /snap/core/14784/usr/share/applications/mimeapps.list /snap/core18/2679/usr/share/applications/mimeapps.list /snap/core18/2697/usr/share/applications/mimeapps.list /snap/core20/1778/usr/share/applications/mimeapps.list /snap/core20/1822/usr/share/applications/mimeapps.list /snap/core22/509/usr/share/applications/mimeapps.list /snap/core22/522/usr/share/applications/mimeapps.list /usr/share/gdm/greeter/applications/mimeapps.list </span></pre> </div> <!-- endregion --> <p> <code>/snap/<wbr>core22/<wbr>509/<wbr>usr/<wbr>share/<wbr>applications/<wbr>mimeapps.list</code> contains the default association for <code>*.crt</code> files: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/snap/core22/509/usr/share/applications/mimeapps.list</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3fc462b46752'><button class='copyBtn' data-clipboard-target='#id3fc462b46752' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[Default Applications] x-scheme-handler/http=xdg-open.desktop x-scheme-handler/https=xdg-open.desktop x-scheme-handler/mailto=xdg-open.desktop x-scheme-handler/help=xdg-open.desktop</pre> </div> <!-- endregion --> <p> The above associates a program called <a href='https://linux.die.net/man/1/xdg-open' target='_blank' rel="nofollow"><code>xdg-open</code></a> with CRT files. </p> <p> Double-clicking on a <code>crt</code> file displayed by an Ubuntu file manager such as Nautilus causes <code>xdg-open</code> to open the file. You can also use the command line to open the file in <code>xdg-open</code>. For example, typing the following causes <code>aw.crt</code> to open: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5c42f4b66ae9'><button class='copyBtn' data-clipboard-target='#id5c42f4b66ae9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>xdg-open aw.crt</pre> </div> <p> This is what <code>xdg-open</code> displays: </p> <div class='imgWrapper imgFlex center' style='width: 478px; '> <picture class='imgPicture'> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.avif" type="image/avif">--> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.webp" type="image/webp"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.apng" type="image/apng"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.png" type="image/png"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.gif" type="image/gif"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.tif" type="image/tiff"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/ddns/ubuntuViewFileOfSslCert_2.png" style='width: 100%; ' /> </picture> </div> <p> As you can see, this is a combined certificate, containing not just one certificate, but an entire certificate chain. Clicking on any of the red <kbd class="redButton">&gt; Details</kbd> buttons causes more information to be displayed about the corresponding certificate in the chain. </p> <!-- endregion --> <!-- #region warning --> <div class="alert rounded shadow"> <h2>Warning</h2> <p> If you forget to include the domain, and just specify the wildcard for subdomains, the certificate will not be valid for the domain. For example, if above I had only specified one <code>--domains</code> option, like this: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5d85cf48f1f2'><button class='copyBtn' data-clipboard-target='#id5d85cf48f1f2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>--domains="scalacourses.com"</pre> </div> ... instead of <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id35a77228aa6a'><button class='copyBtn' data-clipboard-target='#id35a77228aa6a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>--domains="scalacourses.com" \<br>--domains="*.scalacourses.com"</pre> </div> ... then that would be an error. You can notice your error three ways: </p> <ol> <li> The name of the generated certificate will start with an underscore (<code>_</code>), for example <code>_.scalacourses.<wbr>com.<wbr>crt</code>, instead of being named <code>scalacourses.<wbr>com.<wbr>crt</code>. </li> <li> When you examine the certificate, the domain listed will show the subdomain wildcard, as shown below, with a leading underscore asterisk (<code>*</code>).<br> <div class='imgWrapper imgFlex center' style='width: 478px; '> <picture class='imgPicture'> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.avif" type="image/avif">--> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.webp" type="image/webp"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.apng" type="image/apng"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.png" type="image/png"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.gif" type="image/gif"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.tif" type="image/tiff"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/ddns/ubuntuViewFileOfSslCert_1.png" style='width: 100%; margin-top: 2em;' /> </picture> </div> </li> <li style="margin-bottom: 0; padding-bottom: 0;"> When you open the certificate, the section entitled <b>Subject Alternative Names</b> will just contain the subdomain wildcard, like this:<br><br> <b>Subject Alternative Names</b><br> <code>DNS: *.scalacourses.com</code> <br><br>Or just the domain, without a wildcard:<br><br> <b>Subject Alternative Names</b><br> <code>DNS: scalacourses.com</code><br><br> Instead of both being present, like this:<br><br> <b>Subject Alternative Names</b><br> <code>DNS: *.scalacourses.com<br> DNS: scalacourses.com</code> </li> </ol> </div> <!-- endregion --> <!-- #region Provide the certificate to Nginx --> <h2 id="provide">Provide the certificate to Nginx</h2> <p> <a href='/blog/2022/07/08/reverse-proxy.html#vs'>Previously</a>, before working with <code>lego</code>, I used raw <code>certbot</code> to generate the SSL wildcard certificates. That process generated files in <code>~/.certbot/<wbr>scalacourses.com/<wbr>config/<wbr>live/<wbr>scalacourses.com</code>: </p> <ul> <li> <code>fullchain.pem</code> (the <a href='http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate' target='_blank' rel="nofollow"><code>ssl_certificate</code></a>) </li> <li> <code>privkey.pem</code> (the <a href='https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key' target='_blank' rel="nofollow"><code>ssl_certificate_key</code></a>) </li> </ul> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>From /etc/nginx/sites-enabled/scalacourses.com</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide9831d80fb5c'><button class='copyBtn' data-clipboard-target='#ide9831d80fb5c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>ssl_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/fullchain.pem; ssl_certificate_key /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/privkey.pem;</pre> </div> <p> Now I need to modify <code>/etc/<wbr>nginx/<wbr>sites-enabled/<wbr>scalacourses.com</code> to reference the files generated by <code>lego</code>: </p> <ul> <li> <code>scalacourses.com.crt</code> (the <a href='http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate' target='_blank' rel="nofollow"><code>ssl_certificate</code></a>) </li> <li> <code>scalacourses.com.key</code> (the <a href='https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key' target='_blank' rel="nofollow"><code>ssl_certificate_key</code></a>) </li> </ul> <p> If the webserver had access to the directory containing the new wildcard certificate, it would be simplest to provide the full path to the certificate and its key, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Modified portion of /etc/nginx/sites-enabled/scalacourses.com</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4f6e3f5063bf'><button class='copyBtn' data-clipboard-target='#id4f6e3f5063bf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>ssl_certificate /mnt/_/work/lego/.lego/certificates/scalacourses.com.crt; ssl_certificate_key /mnt/_/work/lego/.lego/certificates/scalacourses.com.key;</pre> </div> <!-- endregion --> <!-- #region Restart Nginx --> <h2 id="restart">Restart Nginx</h2> <p> I tested the <code>nginx</code> configuration: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id056394d39140'><button class='copyBtn' data-clipboard-target='#id056394d39140' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo nginx -t <span class='unselectable'>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful </span></pre> </div> <p> Then I reloaded nginx: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7e532439e07f'><button class='copyBtn' data-clipboard-target='#id7e532439e07f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl reload nginx</pre> </div> <p> The new SSL certificate needs to be checked to make sure it was installed properly. I just visually examine the start and expire dates: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id314152d2cbbc'><button class='copyBtn' data-clipboard-target='#id314152d2cbbc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -Lvs https://www.scalacourses.com \ 2>&1 1>/dev/null | \ grep '\(start\|expire\) date:' <span class='unselectable'>* <span style="color:red">start date</span>: Mar 1 17:40:56 2023 GMT * <span style="color:red">expire date</span>: May 30 17:40:55 2023 GMT </span></pre> </div> <span style='font-size: 3em;;'>&#x1F601;</span> <!-- endregion --> <!-- #region Add a New Entry to crontab --> <h2 id="crontab" class="clear">Add a New Entry to <span class="code">Crontab</span></h2> <p> Automating the regeneration of the SSL wildcard certificate is easy. Edit your <code>crontab</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id64e9fc5a05cc'><button class='copyBtn' data-clipboard-target='#id64e9fc5a05cc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>crontab -e</pre> </div> <p> Add the following reference to the <a href='#certGenScalaCourses'><code>certGenScalaCourses</code> script</a> that I gave you earlier to <code>crontab</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>crontab Entry</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd6fff2728511'><button class='copyBtn' data-clipboard-target='#idd6fff2728511' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Runs every 2 months / 60 days (more or less) 30 0 1 */2 * $work/lego/certGenScalaCourses</pre> </div> <p> The above only works because <a href='#env'>we defined the <code>work</code> environment variable</a> in <code>/etc/<wbr>environment</code>. </p> <!-- endregion --> <!-- #region checking with With A Web Browser --> <h2 id="chrome">Checking the Certificate With A Web Browser</h2> <p> Microsoft Windows caches SSL certificates, which can prevent your web browser from detecting newly updated SSL certificates. To clear the Windows SSL cache: </p> <ol> <li>Press the <kbd>Windows</kbd> key, type <code>Internet Options</code>, and press <kbd>Enter</kbd>.</li> <li>Select the <b>Content</b> tab.</li> <li>Click the <kbd>Clear SSL state</kbd> button.</li> <li>Click the <kbd>OK</kbd> button.</li> </ol> <!-- endregion --> HTML Hyphens 2023-01-23T00:00:00-05:00 https://mslinn.github.io/blog/2023/01/23/html-hyphen <!-- #region intro --> <p> Web browsers now have built-in automatic hyphenation support, but it is turned off by default for compatibity with older browsers. You can enable hyphenation with two words of CSS, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>your_stylesheet.css</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0459a764c5ea'><button class='copyBtn' data-clipboard-target='#id0459a764c5ea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>body { <span class='bg_yellow'>hyphens: auto;</span> }</pre> </div> <!-- endregion --> <!-- #region W3 CSS3 Hyphenation Standard --> <h2 id="w3c">W3 CSS3 Hyphenation Standard</h2> <div class='quote'> <div class='quoteText clearfix'> Hyphenation occurs when the line breaks at a valid hyphenation opportunity, which is a type of soft wrap opportunity that exists within a word where hyphenation is allowed. <br><br> In CSS, hyphenation opportunities are controlled with the <code>hyphens</code> property. CSS Text Level 3 does not define the exact rules for hyphenation; however, UAs are strongly encouraged to optimize their choice of break points and to chose language-appropriate hyphenation points. <div class="jekyll_pre" > <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4cfb9b7d5d91'><button class='copyBtn' data-clipboard-target='#id4cfb9b7d5d91' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Possible hyphens values: none | manual | auto Initial value: manual</pre> </div> </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://www.w3.org/TR/css-text-3/#hyphenation' rel='nofollow' target='_blank'>W3 CSS3 Standard</a></div> </div> <p> However, the W3 CSS3 hyphenation standard also says: </p> <div class='quote'> <div class='quoteText clearfix'> Correct automatic hyphenation requires a hyphenation resource appropriate to the language of the text being broken. The UA must therefore only automatically hyphenate text for which the content language is known and for which it has an appropriate hyphenation resource. <br><br> Authors should correctly tag their content’s language (e.g. using the HTML <code>lang</code> attribute or XML <code>xml:lang</code> attribute) in order to obtain correct automatic hyphenation. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://www.w3.org/TR/css-text-3/#hyphenation' rel='nofollow' target='_blank'>W3 CSS3 Standard</a></div> </div> <p> Let&rsquo;s put this into practice now. </p> <!-- endregion --> <!-- #region Your Website Stylesheet --> <h2 id="css">Your Website Stylesheet</h2> <p> You may not want certain passages to be hyphenated. Defining a CSS style that disables automatic hyphenation for portions of a document is easy; you can still use <code>&ampshy;</code> in those portions of the document to manually define optional hyphenation points. </p> <p> Here is a suggestion for your stylesheet, so it has the hyphenation support you need. Note that hypenation for <kbd>kbd</kbd> tags is completely disabled. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>your_stylesheet.css</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1d3516cf52d2'><button class='copyBtn' data-clipboard-target='#id1d3516cf52d2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>body, .hyphen { hyphens: auto; } .nohyphen { hyphens: manual; } kbd { hyphens: none; }</pre> </div> <!-- endregion --> <!-- #region Complete Example --> <h2 id="example">Complete Example</h2> <p> The following example enables automatic hyphenation of the entire HTML <code>body</code>, according to US English rules. The HTML document contains nested tags, some of which disable or enable automatic hyphenation. One line demonstrates manual hyphenation, through the use of <a href='https://developer.mozilla.org/en-US/docs/Web/CSS/hyphens' target='_blank' rel="nofollow"><code>&amp;shy;</code> entities</a>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>index.html</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' style='margin-bottom: 1em;' id='id54165fc62cff'><button class='copyBtn' data-clipboard-target='#id54165fc62cff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;html <span class="bg_yellow">lang="en-US"</span>> &lt;head> &lt;style> body, .hyphen { hyphens: auto; } .nohyphen { hyphens: manual; } &lt;/style> &lt;/head> &lt;body> &lt;p>This element is automatically hyphenated.&lt;/p> &lt;p <span class="bg_yellow">class="nohyphen"</span>>This element is not hyphenated.&lt;/p> &lt;ol <span class="bg_yellow">class="nohyphen"</span>> &lt;li>This elem<span class="bg_yellow">&amp;shy;</span>ent is manual<span class="bg_yellow">&amp;shy;</span>ly hyphen<span class="bg_yellow">&amp;shy;</span>ated.&lt;/li> &lt;li <span class="bg_yellow">class="hyphen"</span>>This element is automatically hyphenated.&lt;/li> &lt;/ol> &lt;/body> &lt;/html></pre> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p>Easy!</p> <!-- endregion --> <!-- #region Alternative Manual Hyphenation --> <h2 id="alt" class="clear">Alternative Manual Hyphenation</h2> <!-- #region implicit --> <p> Invisible hypenation is sometimes desirable. For example, if I have a long directory path, like <code>$HOME/.rbenv/versions/$RUBY_VERSION/lib/ruby/gems/$RUBY_VERSION/$GEM_NAME-$GEM_VERSION</code> I might want to tell the web browser where the string could be broken. </p> <p> You have two options &mdash; they are actually different syntaxes for the same <a href='https://en.wikipedia.org/wiki/Zero-width_space' target='_blank' rel="nofollow"><i>zero-width space</i></a>. </p> <!-- endregion --> <!-- #region Zero-Width Space HTML Entity --> <h3 id="alt">Zero-Width Space HTML Entity</h3> <p> <code>&amp;#8203;</code> is the HTML entity for a unicode character called the zero-width space (<a href='https://unicode-table.com/en/200B/' target='_blank' rel="nofollow"><code>ZWSP</code></a>). </p> <p> For example, <code>$HOME/.rbenv/<span class="bg_yellow">&amp;#8203;</span>versions/<span class="bg_yellow">&amp;#8203;</span>$RUBY_VERSION/<span class="bg_yellow">&amp;#8203;</span>lib/<span class="bg_yellow">&amp;#8203;</span>ruby/<span class="bg_yellow">&amp;#8203;</span>gems/<span class="bg_yellow">&amp;#8203;</span>$RUBY_VERSION/<span class="bg_yellow">&amp;#8203;</span>$GEM_NAME-<span class="bg_yellow">&amp;#8203;</span>$GEM_VERSION</code> <br><br>renders as:<br><br> <code>$HOME/.rbenv/versions/$RUBY_VERSION/&#8203;lib/&#8203;ruby/&#8203;gems/&#8203;$RUBY_VERSION/&#8203;$GEM_NAME-&#8203;$GEM_VERSION</code> </p> <!-- endregion --> <!-- #region Line Break Opportunity Tag --> <h3 id="alt">Line Break Opportunity Tag</h3> <p> You could also use the <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr' target='_blank' rel="nofollow">line break opportunity tag</a>, <code>&lt;wbr&gt;</code> . </p> <p> For example, <code>$HOME/.rbenv/<span class="bg_yellow">&lt;wbr&gt;</span>versions/<span class="bg_yellow">&lt;wbr&gt;</span>$RUBY_VERSION/<span class="bg_yellow">&lt;wbr&gt;</span>lib/<span class="bg_yellow">&lt;wbr&gt;</span>ruby/<span class="bg_yellow">&lt;wbr&gt;</span>gems/<span class="bg_yellow">&lt;wbr&gt;</span>$RUBY_VERSION/<span class="bg_yellow">&lt;wbr&gt;</span>$GEM_NAME-<span class="bg_yellow">&lt;wbr&gt;</span>$GEM_VERSION</code> <br><br>renders as:<br><br> <code>$HOME/.rbenv/versions/$RUBY_VERSION/<wbr>lib/<wbr>ruby/<wbr>gems/<wbr>$RUBY_VERSION/<wbr>$GEM_NAME-<wbr>$GEM_VERSION</code> </p> <!-- endregion --> <!-- #region Both Options Yield the Same Rendered Text --> <h3 id="same">Both Options Yield the Same Rendered Text</h3> <p> I prefer to use &lt;wbr&gt;. <a href='https://dictionary.cambridge.org/dictionary/english/ymmv' target='_blank' rel="nofollow">YMMV</a>. </p> <!-- endregion --> <!-- endregion --> <!-- #region Non-Breaking Hyphen --> <h2 id="nbh">Non-Breaking Hyphen</h2> <p> The non-breaking hyphen HTML entity (<code>&amp;#8209;</code>) displays a hyphen character that does not break. </p> <p> For CSS, use <code>content: "\2011"</code> to produce a non-breaking hyphen. </p> <!-- endregion --> A Curmudgeon’s Social Networking 2023-01-07T00:00:00-05:00 https://mslinn.github.io/blog/2023/01/07/curmudgeon <p> I am an unapologetic curmudgeon. I am not here to please anyone else, I am only here to earnestly be myself to the best of my ability. </p> <p> If we do not continuously demonstrate that we endeavor to treat each other right, then we should be dismissed and forgotten. </p> <p> Bullshit walks; money drives away, unsatisfied. </p> <p> Fairness and equity are essential in all things without reservation. Otherwise, go away, life is short and you are just noise. </p> <h2 id="social">Facebook and LinkedIn</h2> <p> Twitter is now a circus. I don't need or want another circus in my communication. Do not look for me on Twitter any longer. </p> <p> If someone connects with me on Facebook or LinkedIn, I expect them to interact with me in person, on the phone, and to want to engage with me. Failing that, I disconnect from them without hesitation. </p> <p> Yes, please connect with me. Tell me your truth. Listen to me. I will listen to you. </p> <h2 id="action">Action Over Empty Words</h2> <p> Even better, we might endeavor to do things together. Imagining, conceptualizing, and building something real with others gives me joy. I want to share that joy. </p> <p> People who talk about what &lsquo;others&rsquo; should do, but are unwilling to act themselves, are a waste of time. They irritate me. Only approach me if you are an action-oriented individual. </p> <div class='quote'> The most difficult thing is the decision to act, the rest is merely tenacity. <span class='quoteAttribution'> &nbsp;&ndash; Amelia Earhart </span> </div> <p> Peace and love. <span style="font-size: 36pt;"><a href='https://en.wiktionary.org/wiki/Sigma' target='_blank' rel="nofollow">&#963;</a></span> </p> Working With Volumes and Directories Under Ubuntu 2022-12-01T00:00:00-05:00 https://mslinn.github.io/blog/2022/12/01/ubuntu-files <p> I've been reorganizing directories across several volumes on an Ubuntu server. This article documents how I used the commands I found to be most useful: <a href='https://linux.die.net/man/1/rsync' target='_blank' rel="nofollow"><code>rsync</code></a>, <a href='https://linux.die.net/man/1/ncdu' target='_blank' rel="nofollow"><code>ncdu</code></a>, and <a href='https://linux.die.net/man/1/fdupes' target='_blank' rel="nofollow"><code>fdupes</code></a>. The latter two programs are not installed by default. You can install them this way: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb2596695b17'><button class='copyBtn' data-clipboard-target='#idcb2596695b17' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install fdupes ncdu</pre> </div> <h2 id="ncdu">Display Directory Sizes</h2> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcebdc6454c58'><button class='copyBtn' data-clipboard-target='#idcebdc6454c58' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ncdu /directory</pre> </div> <h2 id="fdupes">Find Duplicate Files</h2> <p> This displays duplicate files and interactively asks which duplicate files to delete. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id81f0acd39b2d'><button class='copyBtn' data-clipboard-target='#id81f0acd39b2d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>fdupes -r1Sd /directory</pre> </div> <h2 id="comp">Compare Two Folders For Missing Files</h2> <p> This tip was inspired by an answer on <a href='https://unix.stackexchange.com/questions/524074/compare-two-folders-for-missing-files' target='_blank' rel="nofollow"><code>unix.stackexchange.com</code></a>. </p> <h3 id="dry">Dry Run</h3> <p> The <code>-n</code> option displays the names of missing files in the destination directory, but makes no changes to the destination file system. If that option is not specified, then the missing files are copied. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id32149a0a63ee'><button class='copyBtn' data-clipboard-target='#id32149a0a63ee' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sync -ri --ignore-existing -n /srcDir<span class="bg_yellow">/</span> /destDir</pre> </div> <p> There are a few things to note: </p> <ul> <li>The first directory is the source, and it must end with a slash (<kbd class="bg_yellow">/</kbd>).</li> <li>The second directory is the target, and it must NOT end with a slash.</li> </ul> <h3 id="doit">Copy Missing Files</h3> <p> Simply do not provide the <code>-n</code> (dry run) option: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddc417371b07e'><button class='copyBtn' data-clipboard-target='#iddc417371b07e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>rsync -ri --ignore-existing /srcDir<span class="bg_yellow">/</span> /destDir</pre> </div> JiraCLI, a Feature-rich Interactive Jira Command Line 2022-08-12T00:00:00-04:00 https://mslinn.github.io/blog/2022/08/12/jiracli <!-- #region intro --> <p> When first released in 2002, <a href='https://www.atlassian.com/software/jira' target='_blank' rel="nofollow">Jira</a> was merely a bug tracker. Since then, it has gained features and is now used as a project management tool. Jira only provides a web user interface. </p> <p> <a href='https://github.com/ankitpokhrel/jira-cli' target='_blank' rel="nofollow">JiraCLI</a>, an independent F/OSS project, provides command-line explorers for Jira issues, epics, and sprints. </p> <!-- endregion --> <!-- #region Installation --> <h2 id="install">Installation</h2> <p> JiraCLI is a Go program, so it is quick and easy to install on Ubuntu once Go is installed. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6e239b5301ee'><button class='copyBtn' data-clipboard-target='#id6e239b5301ee' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install golang-go<br> <span class='unselectable'>$ </span>go install github.com/ankitpokhrel/jira-cli/cmd/jira@latest <span class='unselectable'>go: downloading github.com/ankitpokhrel/jira-cli v1.0.0 go: downloading github.com/kr/text v0.2.0 go: downloading github.com/spf13/cobra v1.5.0 go: downloading github.com/spf13/viper v1.12.0 go: downloading github.com/zalando/go-keyring v0.2.1 go: downloading github.com/briandowns/spinner v1.18.1 go: downloading github.com/fatih/color v1.13.0 go: downloading github.com/mitchellh/go-homedir v1.1.0 go: downloading github.com/AlecAivazis/survey/v2 v2.3.5 go: downloading github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 go: downloading github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.2 go: downloading github.com/spf13/pflag v1.0.5 go: downloading gopkg.in/yaml.v2 v2.4.0 go: downloading github.com/atotto/clipboard v0.1.4 go: downloading github.com/charmbracelet/glamour v0.5.0 go: downloading github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d go: downloading github.com/cli/safeexec v1.0.0 go: downloading github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 go: downloading github.com/kentaro-m/blackfriday-confluence v0.0.0-20220126124413-8e85477b49b3 go: downloading github.com/russross/blackfriday/v2 v2.1.0 go: downloading github.com/mattn/go-colorable v0.1.12 go: downloading github.com/mattn/go-isatty v0.0.14 go: downloading github.com/gdamore/tcell/v2 v2.5.1 go: downloading github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 go: downloading github.com/fsnotify/fsnotify v1.5.4 go: downloading github.com/mitchellh/mapstructure v1.5.0 go: downloading github.com/spf13/afero v1.8.2 go: downloading github.com/spf13/cast v1.5.0 go: downloading github.com/spf13/jwalterweatherman v1.1.0 go: downloading github.com/godbus/dbus/v5 v5.1.0 go: downloading golang.org/x/text v0.3.7 go: downloading golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 go: downloading golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c go: downloading github.com/subosito/gotenv v1.4.0 go: downloading github.com/hashicorp/hcl v1.0.0 go: downloading gopkg.in/ini.v1 v1.66.6 go: downloading github.com/magiconair/properties v1.8.6 go: downloading github.com/pelletier/go-toml/v2 v2.0.2 go: downloading gopkg.in/yaml.v3 v3.0.1 go: downloading github.com/gdamore/encoding v1.0.0 go: downloading github.com/pelletier/go-toml v1.9.5 go: downloading github.com/lucasb-eyer/go-colorful v1.2.0 go: downloading github.com/mattn/go-runewidth v0.0.13 go: downloading github.com/muesli/termenv v0.12.0 go: downloading github.com/yuin/goldmark v1.4.12 go: downloading github.com/yuin/goldmark-emoji v1.0.1 go: downloading github.com/rivo/uniseg v0.2.0 go: downloading github.com/alecthomas/chroma v0.10.0 go: downloading github.com/microcosm-cc/bluemonday v1.0.18 go: downloading github.com/muesli/reflow v0.3.0 go: downloading github.com/olekukonko/tablewriter v0.0.5 go: downloading github.com/aymerick/douceur v0.2.0 go: downloading golang.org/x/net v0.0.0-20220621193019-9d032be2e588 go: downloading github.com/dlclark/regexp2 v1.4.0 go: downloading github.com/gorilla/css v1.0.0 </span></pre> </div> <!-- endregion --> <p> Add <code>$HOME/go/bin</code> to the <code>PATH</code> so <code>jira</code> can be found: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0709912d084e'><button class='copyBtn' data-clipboard-target='#id0709912d084e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo 'PATH=$HOME/go/bin:$PATH' >> ~/.bashrc<br> <span class='unselectable'>$ </span>source ~/.bashrc<br> <span class='unselectable'>$ </span>which jira <span class='unselectable'>/home/mslinn/go/bin/jira </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Usage --> <h2 id="usage">Usage</h2> <p>This is the <code>jira </code> help message:</p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99b49393872d'><button class='copyBtn' data-clipboard-target='#id99b49393872d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira <span class='unselectable'>Interactive Jira CLI.<br/> USAGE jira [flags]<br/> MAIN COMMANDS board Board manages Jira boards in a project epic Epic manage epics in a project issue Issue manage issues in a project open Open issue in a browser project Project manages Jira projects sprint Sprint manage sprints in a project board<br/> OTHER COMMANDS completion Output shell completion code for the specified shell (bash or zsh) help Help about any command init Init initializes jira config man Help generate man(7) pages for Jira CLI. me Displays configured jira user version Print the app version information<br/> FLAGS -c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml) --debug Turn on debug output -h, --help help for jira -p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/> LEARN MORE Use &#39;jira &lt;command&gt; &lt;subcommand&gt; --help&#39; for more information about a command. </span></pre> </div> <!-- endregion --> <p> You need an Jira API token before you can use JiraCLI. Get it from <a href='https://id.atlassian.com/manage-profile/security/api-tokens' target='_blank' rel="nofollow">here</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/atlassian_api_token.svg" type="image/svg"> <!---<source srcset="/blog/images/atlassian_api_token.avif" type="image/avif">--> <source srcset="/blog/images/atlassian_api_token.webp" type="image/webp"> <source srcset="/blog/images/atlassian_api_token.apng" type="image/apng"> <source srcset="/blog/images/atlassian_api_token.png" type="image/png"> <source srcset="/blog/images/atlassian_api_token.jpg" type="image/jpeg"> <source srcset="/blog/images/atlassian_api_token.jpeg" type="image/jpeg"> <source srcset="/blog/images/atlassian_api_token.jfif" type="image/jpeg"> <source srcset="/blog/images/atlassian_api_token.pjpeg" type="image/jpeg"> <source srcset="/blog/images/atlassian_api_token.pjp" type="image/jpeg"> <source srcset="/blog/images/atlassian_api_token.gif" type="image/gif"> <source srcset="/blog/images/atlassian_api_token.tif" type="image/tiff"> <source srcset="/blog/images/atlassian_api_token.tiff" type="image/tiff"> <source srcset="/blog/images/atlassian_api_token.bmp" type="image/bmp"> <source srcset="/blog/images/atlassian_api_token.ico" type="image/x-icon"> <source srcset="/blog/images/atlassian_api_token.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/atlassian_api_token.png" style='width: 100%; ' /> </picture> </div> <p> You need to configure the program before you use it. Set the <code>JIRA_API_TOKEN</code> environment variable before running <code>jira init</code>. If you do not, then you will get an error like <code>Received unexpected response '401 Unauthorized' from jira.</code> in the next step. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7ee7a33a8688'><button class='copyBtn' data-clipboard-target='#id7ee7a33a8688' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export JIRA_API_TOKEN=asdfasdfasdf <span class='unselectable'>$ </span>jira init <span class='unselectable'>? Installation type: Cloud ? Link to Jira server: https://xxx.atlassian.net/ ? Login email: mslinn@mslinn.com ⠼ Verifying login details... ? Default project: My ? Default board: [Use arrows to move, type to filter, ? for more help] > [Search...] ---------- My Scrum Board None ✓ Configuration generated: /home/mslinn/.config/.jira/.config.yml </span></pre> </div> <!-- endregion --> <p> This is the configuration file that was generated: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/home/mslinn/.config/.jira/.config.yml/</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4c3e423d4223'><button class='copyBtn' data-clipboard-target='#id4c3e423d4223' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>board: id: 350 name: My Scrum Board type: scrum epic: name: customfield_10009 link: customfield_10008 installation: Cloud issue: fields: custom: - name: Epic Link key: customfield_10008 schema: datatype: any - name: Epic Name key: customfield_10009 schema: datatype: string types: - id: "3" name: Task handle: Task subtask: false - id: "5" name: Sub-task handle: Sub-task subtask: true - id: "7" name: Story handle: Story subtask: false - id: "1" name: Bug handle: Bug subtask: false - id: "6" name: Epic handle: Epic subtask: false login: mslinn@mslinn.com project: key: My type: classic server: https://xxx.atlassian.net</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Example Commands --> <h2 id="ex">Example Commands</h2> <p> Help for the <code>jira issue</code> subcommand: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id97f2869d87ca'><button class='copyBtn' data-clipboard-target='#id97f2869d87ca' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue -h <span class='unselectable'>Issue manage issues in a given project. See available commands below.<br/> USAGE jira issue [flags]<br/> MAIN COMMANDS assign Assign issue to a user clone Clone duplicates an issue comment Manage issue comments create Create an issue in a project delete Delete an issue edit Edit an issue in a project link Link connects two issues list List lists issues in a project move Transition an issue to a given state unlink Unlink disconnects two issues from each other view View displays contents of an issue worklog Manage issue worklog<br/> FLAGS -h, --help help for issue<br/> INHERITED FLAGS -c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml) --debug Turn on debug output -p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/> ALIASES issues<br/> LEARN MORE Use &#39;jira &lt;command&gt; &lt;subcommand&gt; --help&#39; for more information about a command. </span></pre> </div> <!-- endregion --> <p> Help for the <code>jira issue list</code> sub-subcommand: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0b6c84457049'><button class='copyBtn' data-clipboard-target='#id0b6c84457049' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list -h <span class='unselectable'>List lists issues in a given project.<br/> You can combine different flags to create a unique query. For instance,<br/> # Issues that are of high priority, is in progress, was created this month, and has given labels jira issue list -yHigh -s&quot;In Progress&quot; --created month -lbackend -l&quot;high prio&quot;<br/> Issues are displayed in an interactive list view by default. You can use a --plain flag to display output in a plain text mode. A --no-headers flag will hide the table headers in plain view. A --no-truncate flag will display all available fields in plain mode.<br/> USAGE jira issue list [flags]<br/> FLAGS -t, --type string Filter issues by type -R, --resolution string Filter issues by resolution type -s, --status string Filter issues by status -y, --priority string Filter issues by priority -r, --reporter string Filter issues by reporter (email or display name) -a, --assignee string Filter issues by assignee (email or display name) -C, --component string Filter issues by component -l, --label stringArray Filter issues by label -P, --parent string Filter issues by parent --history Issues you accessed recently -w, --watching Issues you are watching --created string Filter issues by created date Accepts: today, week, month, year, or a date in yyyy-mm-dd and yyyy/mm/dd format, or a period format using w = weeks, d = days, h = hours, m = minutes. eg: -10d Created filter will have precedence over created-after and created-before filter --updated string Filter issues by updated date Accepts: today, week, month, year, or a date in yyyy-mm-dd and yyyy/mm/dd format, or a period format using w = weeks, d = days, h = hours, m = minutes. eg: -10d Updated filter will have precedence over updated-after and updated-before filter --created-after string Filter by issues created after certain date --updated-after string Filter by issues updated after certain date --created-before string Filter by issues created before certain date --updated-before string Filter by issues updated before certain date -q, --jql string Run a raw JQL query in a given project context --order-by string Field to order the list with (default &quot;created&quot;) --reverse Reverse the display order (default &quot;DESC&quot;) --paginate string Paginate the result. Max 100 at a time, format: &lt;from&gt;:&lt;limit&gt; where &lt;from&gt; is optional (default &quot;0:100&quot;) --plain Display output in plain mode --no-headers Don&#39;t display table headers in plain mode. Works only with --plain --no-truncate Show all available columns in plain mode. Works only with --plain --columns string Comma separated list of columns to display in the plain mode. Accepts: TYPE, KEY, SUMMARY, STATUS, ASSIGNEE, REPORTER, PRIORITY, RESOLUTION, CREATED, UPDATED -h, --help help for list<br/> INHERITED FLAGS -c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml) --debug Turn on debug output -p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/> EXAMPLES $ jira issue list<br/> # Limit list to 20 items $ jira issue list --paginate 20<br/> # Get 50 items starting from 10 $ jira issue list --paginate 10:50<br/> # List issues in a plain table view without headers $ jira issue list --plain --no-headers<br/> # List some columns of the issue in a plain table view $ jira issue list --plain --columns key,assignee,status<br/> # List issues in a plain table view and show all fields $ jira issue list --plain --no-truncate<br/> # List issues of type &quot;Epic&quot; in status &quot;Done&quot; $ jira issue list -tEpic -sDone<br/> # List issues in status other than &quot;Open&quot; and is assigned to no one $ jira issue list -s~Open -ax<br/> # List issues from all projects $ jira issue list -q&quot;project IS NOT EMPTY&quot;<br/> ALIASES lists ls<br/> LEARN MORE Use &#39;jira &lt;command&gt; &lt;subcommand&gt; --help&#39; for more information about a command. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Issues Updated on a Certain Date --> <h3 id="updated">Issues Updated on a Certain Date</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5b977486b288'><button class='copyBtn' data-clipboard-target='#id5b977486b288' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list --updated 2021-11-17</pre> </div> <!-- endregion --> <!-- #region Issues Selected by Complex Criteria --> <h3 id="updated">Issues Selected by Complex Criteria</h3> <p> The following command will yield a list of high-priority issues, created this month, with status <code>To Do</code>, that are assigned to you, and have the label <code>backend</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id71e64a153b7c'><button class='copyBtn' data-clipboard-target='#id71e64a153b7c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list -yHigh -s"To Do" --created month -lbackend -a$(jira me)</pre> </div> <p> Output might look something like the following, which was redacted: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/jira_issues.svg" type="image/svg"> <!---<source srcset="/blog/images/jira_issues.avif" type="image/avif">--> <source srcset="/blog/images/jira_issues.webp" type="image/webp"> <source srcset="/blog/images/jira_issues.apng" type="image/apng"> <source srcset="/blog/images/jira_issues.png" type="image/png"> <source srcset="/blog/images/jira_issues.jpg" type="image/jpeg"> <source srcset="/blog/images/jira_issues.jpeg" type="image/jpeg"> <source srcset="/blog/images/jira_issues.jfif" type="image/jpeg"> <source srcset="/blog/images/jira_issues.pjpeg" type="image/jpeg"> <source srcset="/blog/images/jira_issues.pjp" type="image/jpeg"> <source srcset="/blog/images/jira_issues.gif" type="image/gif"> <source srcset="/blog/images/jira_issues.tif" type="image/tiff"> <source srcset="/blog/images/jira_issues.tiff" type="image/tiff"> <source srcset="/blog/images/jira_issues.bmp" type="image/bmp"> <source srcset="/blog/images/jira_issues.ico" type="image/x-icon"> <source srcset="/blog/images/jira_issues.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/jira_issues.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region Issue Ids Only --> <h3 id="ids"><!-- #region Issue Ids Only --></h3> <p> The <code>--plain</code> and <code>--no-headers</code> options are useful for driving scripts. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id39b0f34c5246'><button class='copyBtn' data-clipboard-target='#id39b0f34c5246' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list --updated 2021-11-17 --plain --no-headers --columns KEY</pre> </div> <!-- endregion --> <!-- #region View Issue --> <h3 id="detail">View Issue</h3> <p> The following returns the available information about an issue, in a plain-text format. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idff3fae787673'><button class='copyBtn' data-clipboard-target='#idff3fae787673' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue view ISSUE-1 --comments 9999 --plain</pre> </div> <!-- endregion --> <!-- #region Verdict --> <h2 id="verdict">Verdict</h2> <p> JiraCLI is useful project! <span style='font-size: 3em;;'>&#x1F601;</span> </p> <!-- endregion --> ImageMagick Slicing on Ubuntu/WSL 2022-07-28T00:00:00-04:00 https://mslinn.github.io/blog/2022/07/28/imagemagick-slicing <!-- #region intro --> <p> Lawyers like the Microsoft Office software suite; so when I am working on a court case as an expert, I endeavor to provide my clients with Word documents that contain necessary information. I like working in WSL/WSL2 because I can use Windows programs and Ubuntu programs together effectively. </p> <!-- endregion --> <!-- #region Grab Image, Then Slice --> <h2 id="steps">Grab Image, Then Slice</h2> <p> Recently, I used <a href='https://www.techsmith.com/screen-capture.html' target='_blank' rel="nofollow">SnagIt</a>, a Windows program, to capture large web pages as single images. Some of these images were quite tall. </p> <p> The CSS for the web pages made some content invisible on the printed page. Yes, I could have injected CSS using a Chrome plugin like <a href='https://chrome.google.com/webstore/detail/my-style/ljdhjpmbnkbengahefamnhmegbdifhlb' target='_blank' rel="nofollow">My Style</a> to ensure that all content will be printed, like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Injected Style</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id40e785a90a30'><button class='copyBtn' data-clipboard-target='#id40e785a90a30' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@media print { * { display: initial; visibility: visible; } }</pre> </div> <!-- endregion --> <p> I decided to use screen grabs, which would guarantee that the contents of my report would exactly match what had been displayed on the screen, without injecting anything into the web pages. </p> <p> <a href='https://imagemagick.org/index.php' target='_blank' rel="nofollow">ImageMagick</a> is preinstalled on Ubuntu Desktop. I used ImageMagick to slice the image captures into smaller page-sized images, so they could be inserted into a Word document. </p> <!-- endregion --> <!-- #region The Computer Worked Hard --> <h2 id="grunt">The Computer Worked Hard</h2> <p> Grabbing such large web pages was a lot of work for my desktop computer. The only programs active during the screen grab process were the Google Chrome browser and SnagIt. I found that 10GB RAM and 30% of the GPU capability (an NVidia GTX 1660 Super) was used. </p> <p> The screen grab failed if I did not start scrolling from the top of the web page; while it is possible to scrub up and down smaller web pages in order to grab portions of interest, this fails for large pages. </p> <p> I also found that scrolling too fast caused the screen grabbing process to fail. Clicking and holding the bottom scroll arrowhead at the bottom right of the screen seemed to result in a smooth and optimal scrolling speed. This meant that grabbing large web pages took a few minutes as the page slowly scrolled downward. </p> <!-- endregion --> <!-- #region Setting Up the Conversion --> <h2 id="setup">Setting Up the Conversion</h2> <p> The Word documents I usually work with are formatted for North American standards. This means one-inch margins on letter-sized paper (8.5" x 11"), which gives a working area of 6.5" x 9", yielding an aspect ratio of 0.72. </p> <p> The tall captured images needed to be sliced into rectangles that fit efficiently into Word documents. The computations are as follows. </p> <ol> <li> Determine the width of a screen grab and save it into <code>W</code>. The ImageMagick <code>identify</code> command does not provide a newline after its output, however I have inserted one for readability: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6cf36020dedb'><button class='copyBtn' data-clipboard-target='#id6cf36020dedb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>identify -ping -format '%w' ../IMG2005.png <span class='unselectable'>1536 </span> <span class='unselectable'>$ </span>export W="$( identify -ping -format '%w' ../IMG2005.png )"</pre> </div> </li> <li> Determine the height of a screen grab and save it into <code>H</code>: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idee7e75fa0c10'><button class='copyBtn' data-clipboard-target='#idee7e75fa0c10' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>identify -ping -format '%h' ../IMG2005.png <span class='unselectable'>$ </span>export H="$( identify -ping -format '%h' ../IMG2005.png )"</pre> </div> </li> <li> The width can be divided by the aspect ratio to obtain the desired height of each slice so they can be inserted optimally into the Word documents. I used the <a href='https://linux.die.net/man/1/bc' target='_blank' rel="nofollow"><code>bc</code> calculator</a> provided with <code>Bash</code> to divide <code>W / ASPECT_RATIO</code>. The <code>H2</code> integer variable contains the computed height for the images. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc1d3e4ae0a45'><button class='copyBtn' data-clipboard-target='#idc1d3e4ae0a45' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export ASPECT_RATIO=0.72 <span class='unselectable'>$ </span>export H2="$( echo "scale=0 ; $W / $ASPECT_RATIO" | bc )"</pre> </div> </li> <li> Now the image called <code>IMG2005.png</code> can be sliced using ImageMagick’s convert command. The slices are stored into a subdirectory called <code>slices</code>, with file names like <code>IMG2005-1.jpg</code>, <code>IMG2005-2.jpg</code>, etc. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida783665239e8'><button class='copyBtn' data-clipboard-target='#ida783665239e8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>convert IMG2005.png -crop ${W}x${H2} \ -quality 100% -scene 0 slices/IMG2005-%d.jpg</pre> </div> </li> </ol> <!-- endregion --> <!-- #region Automating the Conversion --> <h2 id="script">Automating the Conversion</h2> <p> I wrote the following bash script, which incorporates the above computations. It slices all the images in a directory and saves the results to a second directory. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>sliceImages</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id066d344bd35a'><button class='copyBtn' data-clipboard-target='#id066d344bd35a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash<br> function help { if [ "$1" ]; then echo "Error: $1"; fi echo " $(basename $0): slice all images in the given directory and place them into a specified directory, which will be created if required. " exit 1 }<br> function setup { export ASPECT_RATIO=0.72 export W="$( identify -ping -format '%w' "$1" )" export H="$( identify -ping -format '%h' "$1" )" export H2="$( echo "scale=0 ; $W / $ASPECT_RATIO" | bc )" }<br> function convert1 { FULLNAME=$(basename -- "$1") FILENAME="${FULLNAME%.*}" FILETYPE="${FULLNAME##*.}"<br> convert "$1" \ -crop "${W}x${H2}" \ -quality 100% \ -scene 0 \ "$DIR_OUTPUT/$FILENAME-%d.png" }<br><br> if [ -z "$1" ]; then help "No directory path for images to be converted was provided."; fi export DIR_INPUT="$( realpath $1 )"<br> if [ -z "$2" ]; then help "No directory path for the image slices to be saved into was provided."; fi export DIR_OUTPUT="$( realpath $2 )"<br> mkdir -p "$DIR_OUTPUT"<br> find $DIR_INPUT -type f -exec file --mime-type {} \+ | awk -F: '{if ($2 ~/image\//) print $1}' | while read FILE; do setup "$FILE" echo "Slicing $FILE into ${W}x${H2} pixels" convert1 "$FILE" done</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Overcoming ImageMagick Processing Limits --> <h2 id="limits">Overcoming ImageMagick Processing Limits</h2> <p> Some of the web pages that I needed to grab were quite long, which resulted in those images requiring more computational resources than the default Imagemagick configuration allows. This caused errors such as the following to appear: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6ba2e0f1e6cd'><button class='copyBtn' data-clipboard-target='#id6ba2e0f1e6cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>convert-im6.q16: no images defined `/mnt/c/images/slices/IMG1466-%d.png' @ error/convert.c/ConvertImageCommand/3229. convert-im6.q16: cache resources exhausted `/mnt/c/images/IMG1091.png' @ error/cache.c/OpenPixelCache/4095.</pre> </div> <p> Imagemagick defines computational resources limits in <code>/etc/ImageMagick-6/policy.xml</code>. The default maximum memory is 256 KB, the default maximum allowable height is 16,000 pixels (16KP), and the default maximum area is 128M pixels. These values are defined by the following entries: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3677bd04ceef'><button class='copyBtn' data-clipboard-target='#id3677bd04ceef' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;policy domain="resource" name="memory" value="256MiB"/> &lt;policy domain="resource" name="height" value="16KP"/> &lt;policy domain="resource" name="area" value="128MP"/></pre> </div> <p> I changed the maximum memory limit to 2 GB RAM, the maximum height limit to 10,000,000 pixels (10MP), and the maximum area limit to 2G pixels with these entries: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb0ff968771d6'><button class='copyBtn' data-clipboard-target='#idb0ff968771d6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;policy domain="resource" name="memory" value="2GiB"/> &lt;policy domain="resource" name="height" value="10MP"/> &lt;policy domain="resource" name="area" value="2GP"/></pre> </div> <p> Alternatively, I could have simply commented out the limits, as shown in highlighted text below. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0f8eb02892c0'><button class='copyBtn' data-clipboard-target='#id0f8eb02892c0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class="bg_yellow">&lt;!--</span> &lt;policy domain="resource" name="memory" value="256MiB"/> &lt;policy domain="resource" name="height" value="16KP"/> &lt;policy domain="resource" name="area" value="128MP"/> <span class="bg_yellow">--></span></pre> </div> <p> The largest web page to be sliced was converted to a very tall image, which was 83,703 pixels high. It was sliced into 40 images. </p> <!-- endregion --> <!-- #region Word Macro --> <h2 id="macro">Word Macro</h2> <p> A Word macro is also needed to insert the images into the currently open Word document in alphabetical order. I modified <a href='https://software-solutions-online.com/word-vba-insert-images/' target='_blank' rel="nofollow">this one</a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft Word Macro</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id867c8ba1e207'><button class='copyBtn' data-clipboard-target='#id867c8ba1e207' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Sub insertImages() Dim intResult As Integer Dim strPath As String Dim strFolderPath As String Dim objFSO As Object Dim objFolder As Object Dim objFile As Object Dim i As Integer<br> intResult = Application.FileDialog(msoFileDialogFolderPicker).Show 'Check if user canceled the dialog If intResult <> 0 Then 'dispaly message box strFolderPath = Application.FileDialog(msoFileDialogFolderPicker).SelectedItems(1) 'Create an instance of the FileSystemObject Set objFSO = CreateObject("Scripting.FileSystemObject") 'Get the folder object Set objFolder = objFSO.GetFolder(strFolderPath) i = 1 'loops through each file in the directory and prints their names and path For Each objFile In objFolder.Files 'get file path strPath = objFile.Path 'insert the image Selection.InlineShapes.AddPicture FileName:= _ strPath, LinkToFile:=False, _ SaveWithDocument:=True Next objFile End If End Sub</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Done! --> <h2 id="done">Done!</h2> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> Thanks to the above automation, I was able to deliver the Word documents containing the sliced web pages to my client soon after they were requested. </p> <!-- endregion --> Optimizing Plex Media Server on Ubuntu 2022-07-23T00:00:00-04:00 https://mslinn.github.io/blog/2022/07/23/optimizing-plex <p> <a href='https://www.plex.tv/media-server-downloads/#plex-media-server' target='_blank' rel="nofollow">Plex Media Server</a> is a free, full-featured media manager that is available for Linux, Windows, Mac, and is built into some NAS devices. I run it along with other processes on an Ubuntu server in my apartment. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://support.plex.tv/articles/200288286-what-is-plex/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/plex/plex-works-matrix-1.svg" type="image/svg"> <!---<source srcset="/blog/images/plex/plex-works-matrix-1.avif" type="image/avif">--> <source srcset="/blog/images/plex/plex-works-matrix-1.webp" type="image/webp"> <source srcset="/blog/images/plex/plex-works-matrix-1.apng" type="image/apng"> <source srcset="/blog/images/plex/plex-works-matrix-1.png" type="image/png"> <source srcset="/blog/images/plex/plex-works-matrix-1.jpg" type="image/jpeg"> <source srcset="/blog/images/plex/plex-works-matrix-1.jpeg" type="image/jpeg"> <source srcset="/blog/images/plex/plex-works-matrix-1.jfif" type="image/jpeg"> <source srcset="/blog/images/plex/plex-works-matrix-1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/plex/plex-works-matrix-1.pjp" type="image/jpeg"> <source srcset="/blog/images/plex/plex-works-matrix-1.gif" type="image/gif"> <source srcset="/blog/images/plex/plex-works-matrix-1.tif" type="image/tiff"> <source srcset="/blog/images/plex/plex-works-matrix-1.tiff" type="image/tiff"> <source srcset="/blog/images/plex/plex-works-matrix-1.bmp" type="image/bmp"> <source srcset="/blog/images/plex/plex-works-matrix-1.ico" type="image/x-icon"> <source srcset="/blog/images/plex/plex-works-matrix-1.cur" type="image/x-icon"> <img alt='From Plex.tv documentation' class="imgImg rounded shadow" src="/blog/images/plex/plex-works-matrix-1.png" style='width: 100%; ' title='From Plex.tv documentation' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://support.plex.tv/articles/200288286-what-is-plex/" target='_blank' > From Plex.tv documentation </a> </figcaption> </figure> </div> <p> We can learn a lot about the Plex Media Server processes using the information provided by <code>systemctl status</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id520a55696812'><button class='copyBtn' data-clipboard-target='#id520a55696812' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl status plexmediaserver <span class='unselectable'>● plexmediaserver.service - Plex Media Server Loaded: loaded (/lib/systemd/system/plexmediaserver.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2022-07-22 12:13:32 EDT; 23h ago Process: 2307 ExecStartPre=/bin/sh -c /usr/bin/test -d "${PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR}" || /bin/mkdir -p "${PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR}" (code=exited, status=0/SUCCESS) Main PID: 2329 (Plex Media Serv) Tasks: 45 (limit: 38369) Memory: 25.2G CPU: 42min 54.673s CGroup: /system.slice/plexmediaserver.service ├─2329 "/usr/lib/plexmediaserver/Plex Media Server" ├─3404 "Plex Plug-in [com.plexapp.system]" /usr/lib/plexmediaserver/Resources/Plug-ins-6b0e31a64/Framework.bundle/Contents/Resources/Versions/2/Python/bootstrap.py --server-version 1.27.1.5916-6b0e31a64 /usr/lib/plexmediaserver/Resources/Plug-ins-6b0e31a64/System.bundle └─3521 "/usr/lib/plexmediaserver/Plex Tuner Service" /usr/lib/plexmediaserver/Resources/Tuner/Private /usr/lib/plexmediaserver/Resources/Tuner/Shared 1.27.1.5916-6b0e31a64 32600 </span></pre> </div> <p> The above tells us that Plex Media Server runs under a <a href='https://man7.org/linux/man-pages/man7/cgroups.7.html' target='_blank' rel="nofollow"><code>cgroup</code></a>, which is &ldquo;a Linux kernel feature which allow processes to be organized into hierarchical groups whose usage of various types of resources can then be limited and monitored&rdquo;. The <code>cgroup</code> is defined in <code>/lib/systemd/system/plexmediaserver.service</code>. Here is mine: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/lib/systemd/system/plexmediaserver.service</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id60cfcf6a58d8'><button class='copyBtn' data-clipboard-target='#id60cfcf6a58d8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># DO NOT EDIT THIS FILE DIRECTLY! # # Plex Media Server's variables can be customized by creating an 'overide.conf' # file using 'systemctl edit plexmediaserver' which will create the following; # /etc/systemd/system/plexmediaserver.service.d/override.conf # # An example of the override.conf would be as follows if you wished to edit # your user, group, temp directory, or app support directory (without the leading #) # # [Service] # Environment="TMPDIR=/path/to/new/tmp" # Environment="PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/home/myusername/Library/Application Support" # User=myusername # Group=mygroup # [Unit] Description=Plex Media Server After=network.target network-online.target [Service] Environment="PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/var/lib/plexmediaserver/Library/Application Support" Environment=PLEX_MEDIA_SERVER_HOME=/usr/lib/plexmediaserver Environment=PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS=6 ExecStartPre=/bin/sh -c '/usr/bin/test -d "${PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR}" || /bin/mkdir -p "${PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR}"' ExecStart=/bin/sh -c '\ export PLEX_MEDIA_SERVER_INFO_VENDOR="$(grep ^NAME= /etc/os-release | awk -F= "{print \\$2}" | tr -d \\" )"; \ export PLEX_MEDIA_SERVER_INFO_DEVICE="PC"; \ export PLEX_MEDIA_SERVER_INFO_MODEL="$(uname -m)"; \ export PLEX_MEDIA_SERVER_INFO_PLATFORM_VERSION="$(grep ^VERSION= /etc/os-release | awk -F= "{print \\$2}" | tr -d \\" )"; \ exec "/usr/lib/plexmediaserver/Plex Media Server"' Type=simple User=plex Group=plex Restart=on-failure RestartSec=5 StartLimitInterval=60s StartLimitBurst=3 SyslogIdentifier=Plex Media Server StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target</pre> </div> <h2 id="dirs">Directories and Files</h2> <p> On Linux, Plex Media Server uses the following directories and files by default. These are defined by the <code>cgroup</code> information shown above. </p> <table> <tr> <td> Programs </td> <td> <code>/usr/lib/plexmediaserver/</code> </td> </tr> <tr> <td> Service Configuration </td> <td> <code>/usr/lib/plexmediaserver/lib/plexmediaserver.service</code> </td> </tr> <tr> <td> Content library metadata </td> <td> <code>/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/</code> </td> </tr> </table> <h2 id="priority">Adjusting CPU Priority</h2> <p> By default, the Plex processes run at nice factor 35 (priority 20), which is standard. When transcoding this means that most other processes, such as web servers, must wait. Transcoding can take a while, which means that the websites served from the same server time out. </p> <p> There are several ways to control how much CPU resource is allocated to a Linux process. I ran my Plex processes under <a href='https://linux.die.net/man/1/nice' target='_blank' rel="nofollow">nice</a> so other processes can continue unimpeded. <code>Nice</code> only throttles the process it manages when other processes request a time slice. If the system load is low then <code>nice</code> does not throttle the process that is supervises. </p> <p> All the Plex processes run under Linux as user <code>plex</code>, group <code>plex</code>. It is easy to change the priority of these processes using <code>nice</code>. The following changes all currently running Plex processes to priority 22:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf827cd80fd75'><button class='copyBtn' data-clipboard-target='#idf827cd80fd75' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo renice 22 -u plex</pre> </div> <p> I noticed that the video stream would stall at this priority, so I raised it by 10% (10% "less nice" equates to renicing by -2). <a href='https://www.tecmint.com/set-linux-process-priority-using-nice-and-renice-commands/' target='_blank' rel="nofollow">Aaron Kili</a> wrote a nice explanation of <code>renice</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2668f582e3da'><button class='copyBtn' data-clipboard-target='#id2668f582e3da' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo renice -2 -u plex</pre> </div> <p> One way to ensure that Plex Media Server always runs at normal priority is to add this to the root <code>crontab</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>sudo crontab -e</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id631b840ab3f3'><button class='copyBtn' data-clipboard-target='#id631b840ab3f3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@reboot sleep 60 && renice 18 -u plex</pre> </div> <h2 id="rygel">Rygel</h2> <p> While playing with Plex on Ubuntu I noticed that <a href='https://wiki.gnome.org/Projects/Rygel' target='_blank' rel="nofollow"><code>rygel</code></a> became active when Plex started transcoding. Coincidence? I did not know about <code>rygel</code> before. The <a href='https://github.com/GNOME/rygel' target='_blank' rel="nofollow">GitHub repo</a>. </p> Uncomplicated Firewall on Ubuntu 2022-07-16T00:00:00-04:00 https://mslinn.github.io/blog/2022/07/16/ufw <p> All websites, especially ecommerce sites, need to be secure. A properly set up firewall is an essential component for a secure server. </p> <p> Ubuntu 22.04 uses the <a href='https://wiki.ubuntu.com/UncomplicatedFirewall' target='_blank' rel="nofollow">Uncomplicated Firewall <code>ufw</code> firewall</a> frontend by default. <code>Ufw</code> has been provided for Ubuntu since v8.04 (Hardy Heron). </p> <h2 id="tldr">Quick Setup</h2> <p> Enable <code>ufw</code> as follows: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id61e077c79a88'><button class='copyBtn' data-clipboard-target='#id61e077c79a88' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ufw enable <span class='unselectable'>Firewall is active and enabled on system startup </span></pre> </div> <p> Enable the <a href='https://github.com/ageis/ufw-application-profiles' target='_blank' rel="nofollow"><code>ufw</code> application profiles</a> for <code>ssh</code> and nginx (HTTP/HTTPS) like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id48bbcc71b4fe'><button class='copyBtn' data-clipboard-target='#id48bbcc71b4fe' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ufw allow OpenSSH <span class='unselectable'>Output Rule added Rule added (v6) </span> <span class='unselectable'>$ </span>sudo ufw allow 'Nginx Full' <span class='unselectable'>Output Rule added Rule added (v6) </span></pre> </div> <h3 id="ufwDetails">Diving Deeper</h3> <p> The following is mostly true: </p> <div class='quote'> <div class='quoteText clearfix'> The default firewall on Ubuntu 22.04 Jammy Jellyfish is <code>ufw</code>, which is short for “uncomplicated firewall.” <code>Ufw</code> is a frontend for the typical Linux <code>iptables</code> commands, but it is developed in such a way that basic firewall tasks can be performed without the knowledge of <code>iptables</code>. <br><br> Additionally, <code>ufw</code> can be managed from a graphical interface. In this tutorial, you will learn how to enable and disable the <code>ufw</code> firewall on Ubuntu 22.04 Jammy Jellyfish from both command line and GUI. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://linuxconfig.org/how-to-enable-disable-firewall-on-ubuntu-22-04-lts-jammy-jellyfish-linux' rel='nofollow' target='_blank'>How to enable/disable firewall on Ubuntu 22.04 LTS Jammy Jellyfish Linux</a></div> </div> <p> The above makes no mention of how Ubuntu 22.04 replaced <code>iptables</code> with <code>nftables</code>, as described below. </p> <div class='quote'> <div class='quoteText clearfix'> <h2><span class="code">nftables</span> as the default firewall backend</h2> Firewalling on Linux consists of two components &ndash; the firewall mechanism within the Linux kernel, and the tools used to configure this from userspace. The Linux kernel has traditionally supported two different subsystems for firewall policies – <code>iptables</code> / <code>xtables</code> and the newer <code>nftables</code>. <br><br> <code>Nftables</code> brings significant benefits both in terms of performance and flexibility when creating and deploying firewall rules, particularly for dual stack IPv4/IPv6 systems. <br><br> The traditional <code>iptables</code> userspace management tool now configures the <code>nftables</code> kernel backend, whilst the new <code>nft</code> userspace tool is also present to allow the creation of more flexible rules not supported by the traditional iptables paradigm. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://ubuntu.com/blog/whats-new-in-security-for-ubuntu-22-04-lts/docs-internal-guid-117e0493-7fff-ea13-3537-bde70965f89d' rel='nofollow' target='_blank'>What’s new in Security for Ubuntu 22.04 LTS?</a></div> </div> <p> Digital Ocean has a <a href='https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-18-04' target='_blank' rel="nofollow">good <code>ufw</code> tutorial</a>. </p> Using Nginx As a Reverse Proxy With SSL 2022-07-08T00:00:00-04:00 https://mslinn.github.io/blog/2022/07/08/reverse-proxy <div class="right"> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.ScalaCourses.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/svg"> <!---<source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/avif">--> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/webp"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/apng"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/png"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/jpeg"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/jpeg"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/jpeg"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/jpeg"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/jpeg"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/gif"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/tiff"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/tiff"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/bmp"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/x-icon"> <source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/x-icon"> <img alt='ScalaCourses.com' class="imgImg rounded shadow" src="/assets/images/ScalaCoursesLogo207x207.png" style='width: 100%; ' title='ScalaCourses.com' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.ScalaCourses.com" target='_blank' > ScalaCourses.com </a> </figcaption> </figure> </div> </div> <p> Recently I <a href='https://mslinn.com/blog/2022/05/26/aws-hijacking.html' target='_blank' rel="nofollow">migrated</a> ScalaCourses.com from AWS EC2/S3/CloudFront to a server in my apartment, which has fiber optic internet service. The server&rsquo;s motherboard is an ASUS Sabertooth x79 with an Intel i7 4820, 32 GB DDR3 RAM, and a 4TB SATA SSD. This motherboard is unable to boot from NVMe drives, so SATA is necessary. At the time of this writing, the server runs Ubuntu 22.04. </p> <p> The backup server is currently being set up. I am repurposing an old Hackintosh as a fallback to the Ubuntu server. That motherboard is another ASUS Sabertooth x79, with 32 GB DDR3 RAM and two 2 TB SATA drives. </p> <h2 id="why">Why Am I Doing This?</h2> <p> Once again, I control my hardware, my software, my network, and all ancillary services. After several years of using PaaS vendor servers, I am now reverting to running ScalaCourses.com on my own hardware and software, using my own network. </p> <div class="pullQuote"> No longer will I subject myself to the unlimited financial liability that current PaaS vendors expose their customers to. </div> <h2 id="defs">Definitions</h2> <p> A <i>proxy</i> is a person or process serving as an authorized agent or substitute for another. In computer science, a more specific term is <i>forward proxy</i>. <br><br> A <i>proxy server</i> is a server process that acts as an intermediary between a client requesting a resource, and the process that provides the resource. <br><br> A <i>reverse proxy</i> is a process that sits in front of other processes, and forwards client requests to them. The term <i>forwarding process</i> is similar to <i>reverse proxy</i>. <br><br> The definitions for <i>proxy</i>, <i>forward proxy</i> and <i>reverse proxy</i> all sound identical. The key difference between a reverse proxy and a forward proxy is that a <b>forward proxy enables computers isolated on a private network</b> to connect to the public internet, while a <b>reverse proxy enables computers on the internet</b> to access a private subnet. </p> <h2 id="details">Dealing With Details</h2> <p> The old <a href='https://en.wikipedia.org/wiki/Pound_(networking)' target='_blank' rel="nofollow">Pound</a> v2.8-2 <a href='https://www.cloudflare.com/en-ca/learning/cdn/glossary/reverse-proxy/' target='_blank' rel="nofollow">reverse proxy</a> that was the front end for the old Play Framework app that runs <a href='https://scalacourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a> is no longer viable, and the new version 3 of Pound is incomplete. Depending on configuration, reverse proxies can provide extra security from external attacks on a website, decrypt https requests to http, and act as stream editors for the content. </p> <p> This site still uses AWS CloudFront, for the moment. I am researching alternatives, with the goal of closing my AWS account... unless they introduce a way to cap financial liability before I fully migrate off AWS. </p> <h2 id="vs">Apache httpd vs. Nginx</h2> <p> Two popular reverse proxies are <a href='https://httpd.apache.org/docs/trunk/mod/mod_proxy_http2.html' target='_blank' rel="nofollow">Apache <code>mod_proxy_http2</code></a> and <a href='https://www.nginx.com/' target='_blank' rel="nofollow">nginx</a>. Anything Pound can do, both nginx and Apache httpd/2 can do. While they both work well, Apache httpd has an older code base, in fact, many of my websites ran on Apache httpd at the turn of the century. </p> <p> Nginx is relatively newer than Apache httpd. It is performant, well supported, well documented, and widely available. I decided to use nginx as the reverse proxy because I had found that nginx worked well on previous projects. </p> <p> Installing nginx and the Letsencrypt software is easy on Debian distros such as Ubuntu: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf07bcb2b6795'><button class='copyBtn' data-clipboard-target='#idf07bcb2b6795' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install nginx certbot python3-certbot-nginx</pre> </div> <h2 id="vs">Verifying the Nginx Build</h2> <p> When acting as a proxy server, nginx requires the <code>http_sub_module</code> to translate HTTP content. This allows the website content to be stream edited, so various links and paths that are not translated properly can be fixed up. </p> <p> The nginx package provided by Ubuntu includes the <code>&#8209;&#8209;with&#8209;http_sub_module</code> option. You can verify that your instance of nginx was built with the <code>&#8209;&#8209;with&#8209;http_sub_module</code> option as follows: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd8ae66686e1f'><button class='copyBtn' data-clipboard-target='#idd8ae66686e1f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nginx -V <span class='unselectable'>nginx version: nginx/1.18.0 (Ubuntu) built with OpenSSL 3.0.2 15 Mar 2022 TLS SNI support enabled configure arguments: --with-cc-opt='-g -O2 -ffile-prefix-map=/build/nginx-9P0wNJ/nginx-1.18.0=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --add-dynamic-module=/build/nginx-9P0wNJ/nginx-1.18.0/debian/modules/http-geoip2 --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module <span class="bg_yellow">--with-http_sub_module</span> </span></pre> </div> <h2 id="vs">Nginx SSL Configuration</h2> <p> Two files are needed for Letsencrypt to be able to create SSL certificates for nginx: </p> <ol> <li> The contents of <a href='https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf' target='_blank' rel="nofollow"><code>/etc/letsencrypt/options-ssl-nginx.conf</code></a> need to be stored into <code>/etc/letsencrypt/options-ssl-nginx.conf</code>. I later discovered this file was also available locally at <code>/usr/lib/python3/dist-packages/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf</code>. </li> <li> The contents of <a href='https://github.com/certbot/certbot/blob/master/certbot/certbot/ssl-dhparams.pem' target='_blank' rel="nofollow"><code>/etc/letsencrypt/ssl-dhparams.pem</code></a> need to be stored into <code>/etc/letsencrypt/ssl-dhparams.pem</code>. I later discovered that this file was also available locally at <code>/usr/lib/python3/dist-packages/certbot/ssl-dhparams.pem</code>. </li> </ol> <h2 id="ssl">Making a Wildcard SSL Certificate</h2> <p> I knew an SSL wildcard certificate would be needed, so I made one using Letsencrypt. Please read <a href='/blog/2022/06/15/certbot.html'>Creating and Renewing Letsencrypt Wildcard SSL Certificates</a> for details. </p> <p class="alert rounded shadow"> Update &ndash; 8 months after writing this article, I wrote about a better way to generate wildcard SSL certificates: <a href='/blog/2023/03/02/lego.html'>Wildcard SSL Certificates for Let&apos;s Encrypt with Lego</a>. </p> <h2 id="vs">Defining the Nginx Website Reverse Proxy</h2> <p> Please see my article entitled <a href='/blog/2021/03/20/cors.html'>Cross-Origin Resource Sharing (CORS)</a> for a discussion of how to configure servers whose content needs to be proxied. </p> <p> I saved the following configuration in a new file called <code>/etc/nginx/sites-available/scalacourses.com</code>. Note that a single <code>server</code> block answers on ports 80 and 443, using IPv4 and IPv6, for SSL and non-SSL requests, for the apex domain (<code>scalacourses.com</code>) and the <code>www.scalacourses.com</code> subdomain. No redirects are used. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide567548f2ac4'><button class='copyBtn' data-clipboard-target='#ide567548f2ac4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; server_name scalacourses.com www.scalacourses.com; ssl_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/fullchain.pem; ssl_certificate_key /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/privkey.pem; ssl_trusted_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/chain.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; root /var/www/html; index index.html; # This gets served if the proxied website is down location / { proxy_pass http://localhost:9000/; proxy_set_header Accept-Encoding ""; # sub_filter requires this sub_filter 'https://localhost:9000/authenticate/' 'https://www.scalacourses.com/authenticate/'; sub_filter_once off; } }</pre> </div> <p> I disabled the <code>default</code> site: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id05a88420c306'><button class='copyBtn' data-clipboard-target='#id05a88420c306' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo rm /etc/nginx/sites-enabled/default</pre> </div> <p> I enabled the new <code>scalacourses.com</code> site: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2039b8d4a206'><button class='copyBtn' data-clipboard-target='#id2039b8d4a206' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ln /etc/nginx/sites-{available,enabled}/scalacourses.com</pre> </div> <p> The <code>nginx</code> configuration was tested for syntax: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc08000cf5366'><button class='copyBtn' data-clipboard-target='#idc08000cf5366' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo nginx -t <span class='unselectable'>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful </span></pre> </div> <p> The nginx configuration was reloaded: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1c6e28bae230'><button class='copyBtn' data-clipboard-target='#id1c6e28bae230' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl reload nginx</pre> </div> <h2 id="flush">Flush DNS Cache</h2> <p> Ensure that DNS requests receive up-to-date values by flushing the DNS cache. The following works on Ubuntu, but not when running on WSL/WSL2. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida18ec5797c26'><button class='copyBtn' data-clipboard-target='#ida18ec5797c26' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo resolvectl flush-caches</pre> </div> <p> The following works on Windows 10: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Command and PowerShell consoles</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9bf4dbfb491e'><button class='copyBtn' data-clipboard-target='#id9bf4dbfb491e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>ipconfig /flushdns</pre> </div> <h2 id="verify">Verifying nginx Works</h2> <p> I verified that <code>nginx</code> was listening on ports 80 and 443. I highlighted the <code>nginx</code> process number &ndash; you might need to scroll the output below to the right to see it. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2f23d39ce7e5'><button class='copyBtn' data-clipboard-target='#id2f23d39ce7e5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo netstat -tulpn | grep ':\(443\|80\)' <span class='unselectable'>tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN - tcp6 0 0 :::80 :::* LISTEN - tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN <span class="bg_yellow">87487</span>/nginx: master </span></pre> </div> <span> <p> The executable for process 87487 can be found by: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfbffac4a35bf'><button class='copyBtn' data-clipboard-target='#idfbffac4a35bf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -l /proc/87487/exe <span class='unselectable'>lrwxrwxrwx 1 mslinn mslinn 0 Jul 7 09:13 /proc/5166/exe -> <span class="bg_yellow">/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java*</span> </span></pre> </div> <p> If more detail is desired, get it from the process list. Enclosing a character of the process id within square brackets is an old trick for only showing the desired process. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1dd16ea84126'><button class='copyBtn' data-clipboard-target='#id1dd16ea84126' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ps aux | grep [8]7487 <span class='unselectable'>mslinn 87487 2.4 1.3 6596476 439456 ? Sl 09:13 0:20 java -Xms1024m -Xmx1024m -Dhttp.port=9000 /path/to/jar </span></pre> </div> <p> If there is any problem getting things to work, it is often helpful to monitor the logs as you click on the web pages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8593a33cd0f9'><button class='copyBtn' data-clipboard-target='#id8593a33cd0f9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo tail -f /var/log/nginx/*.log</pre> </div> <h2 id="persist">Finishing Up</h2> <p> Make nginx start each time the system starts as follows. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcd09d8be5e3f'><button class='copyBtn' data-clipboard-target='#idcd09d8be5e3f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl enable nginx <span class='unselectable'>Synchronizing state of nginx.service with SysV service script with /lib/systemd/systemd-sysv-install. Executing: /lib/systemd/systemd-sysv-install enable nginx </span> <span class='unselectable'>$ </span>sudo update-rc.d nginx defaults</pre> </div> <h3 id="performance">Performance Test</h3> <p> <a href='https://tools.keycdn.com/performance?url=https://www.scalacourses.com' target='_blank' rel="nofollow">KeyCDN</a> offfers free website performance statistics. The result column labeled <b>TTFB</b> means &ldquo;time to first byte&rdquo;, which is the length of time required for the website content to begin being received by a user's web browser. </p> <p> I am in Montreal, Canada. Response time for TTFB reported by KeyCDN varies, from a minimum of 68ms in New York, to a maximum of 815ms in Bangalore. This range of response times is typical for websites that have a centralized process. The speed of light is finite, after all. </p> <h3 id="monitor">Free Availability Monitoring</h3> <p> <a href='https://hetrixtools.com/' target='_blank' rel="nofollow">HetrixTools</a> offers free availability monitoring for up to 10 websites. It was quick and easy to set up. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/hetrix.svg" type="image/svg"> <!---<source srcset="/blog/images/hetrix.avif" type="image/avif">--> <source srcset="/blog/images/hetrix.webp" type="image/webp"> <source srcset="/blog/images/hetrix.apng" type="image/apng"> <source srcset="/blog/images/hetrix.png" type="image/png"> <source srcset="/blog/images/hetrix.jpg" type="image/jpeg"> <source srcset="/blog/images/hetrix.jpeg" type="image/jpeg"> <source srcset="/blog/images/hetrix.jfif" type="image/jpeg"> <source srcset="/blog/images/hetrix.pjpeg" type="image/jpeg"> <source srcset="/blog/images/hetrix.pjp" type="image/jpeg"> <source srcset="/blog/images/hetrix.gif" type="image/gif"> <source srcset="/blog/images/hetrix.tif" type="image/tiff"> <source srcset="/blog/images/hetrix.tiff" type="image/tiff"> <source srcset="/blog/images/hetrix.bmp" type="image/bmp"> <source srcset="/blog/images/hetrix.ico" type="image/x-icon"> <source srcset="/blog/images/hetrix.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/hetrix.png" style='width: 100%; ' /> </picture> </div> <h3 id="live">We Are Live!</h3> <p> <a href='https://www.scalacourses.com' target='_blank' rel="nofollow"><code>ScalaCourses.com</code></a> now serves Scala students from its newly refurbished server! <span style='font-size: 3em;;'>&#x1F601;</span> </p> Trialing mslinn.com on Linode Storage 2022-07-01T00:00:00-04:00 https://mslinn.github.io/blog/2022/07/01/trialing-linode-storage <!-- #region intro --> <p> This is another article in my ongoing saga of moving off AWS, which <a href='/blog/2022/05/26/aws-hijacking.html'>does not integrate security with real-time billing</a>. I recently learned this the hard way: when my AWS account was hijacked, in less than 15 minutes, a huge bill was incurred. </p> <div class="pullQuote"> &ldquo;Pay-as-you-go&rdquo; is shorthand for &ldquo;there is nothing you can do to limit your financial liability&rdquo; </div> <p> The world of pain that I experienced after the breach was inflicted by broken and wasteful AWS remedial processes, and an ineffective AWS management structure. This type of issue only is enabled because of deficiencies in the AWS architecture. Those AWS architectural deficiencies feel like the result of an exploitive mindset: </p> <div class='quote'> <div class='quoteText clearfix'> Unlimited financial liability is our customer&rsquo;s problem, not ours &ndash; as a result, exploits are quite profitable for us. </div><div class='quoteAttribution'> &nbsp;&ndash; From a mythical retrospective discussion at an AWS offsite. </div> </div> <div class='quote'> <div class='quoteText clearfix'> At this moment that feature of setting limits does not exist, Azure is not able to safeguard customers from unlimited financial liability. </div><div class='quoteAttribution'> &nbsp;&ndash; From an email sent to me from Microsoft Azure support staff on 2022-06-22. </div> </div> <!-- endregion --> <!-- #region Demand Limits to Financial Liability From PaaS Vendors --> <div class="alert rounded shadow"> <h2>Demand Limits to Financial Liability From PaaS Vendors</h2> <p> PaaS vendors currently provide accounts with all services ready to go, without limit. That is good for the vendor's bottom line, but highly dangerous for their customers. </p> <p> All PaaS customers should demand the ability for themselves to be able to set firm limits on budgeted expenses, along with the ability to deny all services not explicitly authorized. </p> <p> You can buy insurance against losses resulting from various calamities, but you cannot limit your financial liability with PaaS vendors. </p> <p> <span style="font-size:larger; font-weight: bold;">Yet.</span> </p> </div> <!-- endregion --> <!-- #region Website Hosting Market --> <h2 id="market">Website Hosting Market</h2> <p> Recently, I have spent a lot of time looking at options for hosting websites. I found the following types of products: </p> <style> .row th { text-align: right; } </style> <table class="table"> <tr> <td></td> <th>WordPress</th> <th>General Web Server</th> <th>VM</th> <th>S3 Compatible</th> </tr> <tr class="row"> <th>CDN</th> <td>No</td> <td>No</td> <td>No</td> <td>Yes</td> </tr> <tr class="row"> <th>Speed</th> <td>Slow to Medium</td> <td>Slow to Medium</td> <td>Slow to Fast</td> <td>Fast</td> </tr> <tr class="row"> <th>Reliability</th> <td>Fair</td> <td>Fair</td> <td>Depends on you</td> <td>Good</td> </tr> <tr class="row"> <th>Financial Liability</th> <td>Fixed monthly cost</td> <td>Fixed monthly cost</td> <td class="bg_yellow">Depends</td> <td class="bg_yellow">Unlimited</td> </tr> <tr class="row"> <th>Storage</th> <td>Small</td> <td>Small</td> <td>Depends</td> <td>Unlimited</td> </tr> <tr class="row"> <th>CLI & API</th> <td>No</td> <td>No</td> <td>Depends</td> <td>Yes</td> </tr> </table> <p> This website requires about 220 GB of storage. It has numerous images, in 2 versions: <a href='/blog/2020/08/15/converting-all-images-to-webp-format.html'><code>webp</code></a> and <code>png</code>. So for this website, &lsquo;small&rsquo; means less than 250 GB storage. </p> <p> I decided to give the S3-compatible product <a href='https://www.linode.com/docs/products/storage/object-storage/' target='_blank' rel="nofollow">Linode Storage</a> a try. </p> <!-- endregion --> <!-- #region Why Linode --> <h2 id="why">Why Linode?</h2> <p> First, the positive: <a href='https://www.linode.com/' target='_blank' rel="nofollow">Linode</a> is a pioneer in virtual computing. It has been on my short list of places for hosting my pet projects for many years. Prices have always been quite competitive, products solid, with good support ... and smart people have answered the phone when I have called. Being able to easily speak with a capable human provides huge value. </p> <p> On the other hand, as I shall demonstrate in this article, Linode&rsquo;s documentation presents as a significant barrier to customers considering adopting their services. I had to work a lot harder than I should have to get my evaluation website up and running. Hopefully, this document is complete enough, so others can follow along and host their static websites on Linode Storage. </p> <!-- endregion --> <!-- #region Acquired by Akamai --> <h2 id="akamai">Acquired by Akamai</h2> <p> <a href='https://www.akamai.com/newsroom/press-release/akamai-completes-acquisition-of-linode' target='_blank' rel="nofollow">Linode was acquired by Akamai</a> 3 months ago. Akamai is the original CDN, and their network is gigantic. </p> <p> Linode does not yet offer a CDN product, but the person I spoke to at Linode when I started writing this article suggested that a CDN product from Linode based on Akamai&rsquo;s network might be available soon. He also said that they were planning to working on a mechanism for limiting financial liability to their customers in 2022. </p> <p> This was music to my financially risk-averse ears! </p> <div class="notepaper shadow" style="width: 80%;"> <p> The remainder of this article will take you through all the steps necessary to: </p> <ol> <li>Install and configure software tools on a WSL / WSL2 / Ubuntu computer</li> <li>Make an S3-compatible bucket and set it up to hold a website</li> <li>Upload the website, and easily handle mimetype issues</li> <li>Generate and install a free 4096-bit SSL wildcard certificate</li> <li>Get a security rating from Qualys / SSL Labs</li> </ol> </div> <!-- endregion --> <!-- #region Trialing Linode Storage --> <h2 id="setup">Trialing Linode Storage</h2> <!-- #region Installing s3cmd --> <h3 id="s3cmd">Installing <span class="code">s3cmd</span></h3> <p> I installed the recommended S3-compatible command-line program <code>s3cmd</code> on WSL2 / Ubuntu like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfc42a7248726'><button class='copyBtn' data-clipboard-target='#idfc42a7248726' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install s3cmd <span class='unselectable'>Reading package lists... Done Building dependency tree... Done Reading state information... Done The following additional packages will be installed: python3-magic The following NEW packages will be installed: python3-magic s3cmd 0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded. Need to get 133 kB of archives. After this operation, 584 kB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-magic all 2:0.4.24-2 [12.6 kB] Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 s3cmd all 2.2.0-1 [120 kB] Fetched 133 kB in 0s (278 kB/s) Selecting previously unselected package python3-magic. (Reading database ... 169821 files and directories currently installed.) Preparing to unpack .../python3-magic_2%3a0.4.24-2_all.deb ... Unpacking python3-magic (2:0.4.24-2) ... Selecting previously unselected package s3cmd. Preparing to unpack .../archives/s3cmd_2.2.0-1_all.deb ... Unpacking s3cmd (2.2.0-1) ... Setting up python3-magic (2:0.4.24-2) ... Setting up s3cmd (2.2.0-1) ... Processing triggers for man-db (2.10.2-1) ... Scanning processes... Scanning processor microcode... Scanning linux images... Failed to retrieve available kernel versions. Failed to check for processor microcode upgrades. No services need to be restarted. No containers need to be restarted. No user sessions are running outdated binaries. No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre> </div> <!-- endregion --> <p> Here is the <code>s3cmd</code> help message: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7752b5b0d746'><button class='copyBtn' data-clipboard-target='#id7752b5b0d746' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd <span class='unselectable'>Usage: s3cmd [options] COMMAND [parameters]<br/> S3cmd is a tool for managing objects in Amazon S3 storage. It allows for making and removing &quot;buckets&quot; and uploading, downloading and removing &quot;objects&quot; from these buckets.<br/> Options: -h, --help show this help message and exit --configure Invoke interactive (re)configuration tool. Optionally use as &#39;--configure s3://some-bucket&#39; to test access to a specific bucket instead of attempting to list them all. -c FILE, --config=FILE Config file name. Defaults to $HOME/.s3cfg --dump-config Dump current configuration after parsing config files and command line options and exit. --access_key=ACCESS_KEY AWS Access Key --secret_key=SECRET_KEY AWS Secret Key --access_token=ACCESS_TOKEN AWS Access Token -n, --dry-run Only show what should be uploaded or downloaded but don&#39;t actually do it. May still perform S3 requests to get bucket listings and other information though (only for file transfer commands) -s, --ssl Use HTTPS connection when communicating with S3. (default) --no-ssl Don&#39;t use HTTPS. -e, --encrypt Encrypt files before uploading to S3. --no-encrypt Don&#39;t encrypt files. -f, --force Force overwrite and other dangerous operations. --continue Continue getting a partially downloaded file (only for [get] command). --continue-put Continue uploading partially uploaded files or multipart upload parts. Restarts parts/files that don&#39;t have matching size and md5. Skips files/parts that do. Note: md5sum checks are not always sufficient to check (part) file equality. Enable this at your own risk. --upload-id=UPLOAD_ID UploadId for Multipart Upload, in case you want continue an existing upload (equivalent to --continue- put) and there are multiple partial uploads. Use s3cmd multipart [URI] to see what UploadIds are associated with the given URI. --skip-existing Skip over files that exist at the destination (only for [get] and [sync] commands). -r, --recursive Recursive upload, download or removal. --check-md5 Check MD5 sums when comparing files for [sync]. (default) --no-check-md5 Do not check MD5 sums when comparing files for [sync]. Only size will be compared. May significantly speed up transfer but may also miss some changed files. -P, --acl-public Store objects with ACL allowing read for anyone. --acl-private Store objects with default ACL allowing access for you only. --acl-grant=PERMISSION:EMAIL or USER_CANONICAL_ID Grant stated permission to a given amazon user. Permission is one of: read, write, read_acp, write_acp, full_control, all --acl-revoke=PERMISSION:USER_CANONICAL_ID Revoke stated permission for a given amazon user. Permission is one of: read, write, read_acp, write_acp, full_control, all -D NUM, --restore-days=NUM Number of days to keep restored file available (only for &#39;restore&#39; command). Default is 1 day. --restore-priority=RESTORE_PRIORITY Priority for restoring files from S3 Glacier (only for &#39;restore&#39; command). Choices available: bulk, standard, expedited --delete-removed Delete destination objects with no corresponding source file [sync] --no-delete-removed Don&#39;t delete destination objects [sync] --delete-after Perform deletes AFTER new uploads when delete-removed is enabled [sync] --delay-updates *OBSOLETE* Put all updated files into place at end [sync] --max-delete=NUM Do not delete more than NUM files. [del] and [sync] --limit=NUM Limit number of objects returned in the response body (only for [ls] and [la] commands) --add-destination=ADDITIONAL_DESTINATIONS Additional destination for parallel uploads, in addition to last arg. May be repeated. --delete-after-fetch Delete remote objects after fetching to local file (only for [get] and [sync] commands). -p, --preserve Preserve filesystem attributes (mode, ownership, timestamps). Default for [sync] command. --no-preserve Don&#39;t store FS attributes --exclude=GLOB Filenames and paths matching GLOB will be excluded from sync --exclude-from=FILE Read --exclude GLOBs from FILE --rexclude=REGEXP Filenames and paths matching REGEXP (regular expression) will be excluded from sync --rexclude-from=FILE Read --rexclude REGEXPs from FILE --include=GLOB Filenames and paths matching GLOB will be included even if previously excluded by one of --(r)exclude(-from) patterns --include-from=FILE Read --include GLOBs from FILE --rinclude=REGEXP Same as --include but uses REGEXP (regular expression) instead of GLOB --rinclude-from=FILE Read --rinclude REGEXPs from FILE --files-from=FILE Read list of source-file names from FILE. Use - to read from stdin. --region=REGION, --bucket-location=REGION Region to create bucket in. As of now the regions are: us-east-1, us-west-1, us-west-2, eu-west-1, eu- central-1, ap-northeast-1, ap-southeast-1, ap- southeast-2, sa-east-1 --host=HOSTNAME HOSTNAME:PORT for S3 endpoint (default: s3.amazonaws.com, alternatives such as s3-eu- west-1.amazonaws.com). You should also set --host- bucket. --host-bucket=HOST_BUCKET DNS-style bucket+hostname:port template for accessing a bucket (default: %(bucket)s.s3.amazonaws.com) --reduced-redundancy, --rr Store object with &#39;Reduced redundancy&#39;. Lower per-GB price. [put, cp, mv] --no-reduced-redundancy, --no-rr Store object without &#39;Reduced redundancy&#39;. Higher per- GB price. [put, cp, mv] --storage-class=CLASS Store object with specified CLASS (STANDARD, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, GLACIER or DEEP_ARCHIVE). [put, cp, mv] --access-logging-target-prefix=LOG_TARGET_PREFIX Target prefix for access logs (S3 URI) (for [cfmodify] and [accesslog] commands) --no-access-logging Disable access logging (for [cfmodify] and [accesslog] commands) --default-mime-type=DEFAULT_MIME_TYPE Default MIME-type for stored objects. Application default is binary/octet-stream. -M, --guess-mime-type Guess MIME-type of files by their extension or mime magic. Fall back to default MIME-Type as specified by --default-mime-type option --no-guess-mime-type Don&#39;t guess MIME-type and use the default type instead. --no-mime-magic Don&#39;t use mime magic when guessing MIME-type. -m MIME/TYPE, --mime-type=MIME/TYPE Force MIME-type. Override both --default-mime-type and --guess-mime-type. --add-header=NAME:VALUE Add a given HTTP header to the upload request. Can be used multiple times. For instance set &#39;Expires&#39; or &#39;Cache-Control&#39; headers (or both) using this option. --remove-header=NAME Remove a given HTTP header. Can be used multiple times. For instance, remove &#39;Expires&#39; or &#39;Cache- Control&#39; headers (or both) using this option. [modify] --server-side-encryption Specifies that server-side encryption will be used when putting objects. [put, sync, cp, modify] --server-side-encryption-kms-id=KMS_KEY Specifies the key id used for server-side encryption with AWS KMS-Managed Keys (SSE-KMS) when putting objects. [put, sync, cp, modify] --encoding=ENCODING Override autodetected terminal and filesystem encoding (character set). Autodetected: UTF-8 --add-encoding-exts=EXTENSIONs Add encoding to these comma delimited extensions i.e. (css,js,html) when uploading to S3 ) --verbatim Use the S3 name as given on the command line. No pre- processing, encoding, etc. Use with caution! --disable-multipart Disable multipart upload on files bigger than --multipart-chunk-size-mb --multipart-chunk-size-mb=SIZE Size of each chunk of a multipart upload. Files bigger than SIZE are automatically uploaded as multithreaded- multipart, smaller files are uploaded using the traditional method. SIZE is in Mega-Bytes, default chunk size is 15MB, minimum allowed chunk size is 5MB, maximum is 5GB. --list-md5 Include MD5 sums in bucket listings (only for &#39;ls&#39; command). -H, --human-readable-sizes Print sizes in human readable form (eg 1kB instead of 1234). --ws-index=WEBSITE_INDEX Name of index-document (only for [ws-create] command) --ws-error=WEBSITE_ERROR Name of error-document (only for [ws-create] command) --expiry-date=EXPIRY_DATE Indicates when the expiration rule takes effect. (only for [expire] command) --expiry-days=EXPIRY_DAYS Indicates the number of days after object creation the expiration rule takes effect. (only for [expire] command) --expiry-prefix=EXPIRY_PREFIX Identifying one or more objects with the prefix to which the expiration rule applies. (only for [expire] command) --progress Display progress meter (default on TTY). --no-progress Don&#39;t display progress meter (default on non-TTY). --stats Give some file-transfer stats. --enable Enable given CloudFront distribution (only for [cfmodify] command) --disable Disable given CloudFront distribution (only for [cfmodify] command) --cf-invalidate Invalidate the uploaded filed in CloudFront. Also see [cfinval] command. --cf-invalidate-default-index When using Custom Origin and S3 static website, invalidate the default index file. --cf-no-invalidate-default-index-root When using Custom Origin and S3 static website, don&#39;t invalidate the path to the default index file. --cf-add-cname=CNAME Add given CNAME to a CloudFront distribution (only for [cfcreate] and [cfmodify] commands) --cf-remove-cname=CNAME Remove given CNAME from a CloudFront distribution (only for [cfmodify] command) --cf-comment=COMMENT Set COMMENT for a given CloudFront distribution (only for [cfcreate] and [cfmodify] commands) --cf-default-root-object=DEFAULT_ROOT_OBJECT Set the default root object to return when no object is specified in the URL. Use a relative path, i.e. default/index.html instead of /default/index.html or s3://bucket/default/index.html (only for [cfcreate] and [cfmodify] commands) -v, --verbose Enable verbose output. -d, --debug Enable debug output. --version Show s3cmd version (2.2.0) and exit. -F, --follow-symlinks Follow symbolic links as if they are regular files --cache-file=FILE Cache FILE containing local source MD5 values -q, --quiet Silence output on stdout --ca-certs=CA_CERTS_FILE Path to SSL CA certificate FILE (instead of system default) --ssl-cert=SSL_CLIENT_CERT_FILE Path to client own SSL certificate CRT_FILE --ssl-key=SSL_CLIENT_KEY_FILE Path to client own SSL certificate private key KEY_FILE --check-certificate Check SSL certificate validity --no-check-certificate Do not check SSL certificate validity --check-hostname Check SSL certificate hostname validity --no-check-hostname Do not check SSL certificate hostname validity --signature-v2 Use AWS Signature version 2 instead of newer signature methods. Helpful for S3-like systems that don&#39;t have AWS Signature v4 yet. --limit-rate=LIMITRATE Limit the upload or download speed to amount bytes per second. Amount may be expressed in bytes, kilobytes with the k suffix, or megabytes with the m suffix --no-connection-pooling Disable connection re-use --requester-pays Set the REQUESTER PAYS flag for operations -l, --long-listing Produce long listing [ls] --stop-on-error stop if error in transfer --content-disposition=CONTENT_DISPOSITION Provide a Content-Disposition for signed URLs, e.g., &quot;inline; filename=myvideo.mp4&quot; --content-type=CONTENT_TYPE Provide a Content-Type for signed URLs, e.g., &quot;video/mp4&quot;<br/> Commands: Make bucket s3cmd mb s3://BUCKET Remove bucket s3cmd rb s3://BUCKET List objects or buckets s3cmd ls [s3://BUCKET[/PREFIX]] List all object in all buckets s3cmd la Put file into bucket s3cmd put FILE [FILE...] s3://BUCKET[/PREFIX] Get file from bucket s3cmd get s3://BUCKET/OBJECT LOCAL_FILE Delete file from bucket s3cmd del s3://BUCKET/OBJECT Delete file from bucket (alias for del) s3cmd rm s3://BUCKET/OBJECT Restore file from Glacier storage s3cmd restore s3://BUCKET/OBJECT Synchronize a directory tree to S3 (checks files freshness using size and md5 checksum, unless overridden by options, see below) s3cmd sync LOCAL_DIR s3://BUCKET[/PREFIX] or s3://BUCKET[/PREFIX] LOCAL_DIR or s3://BUCKET[/PREFIX] s3://BUCKET[/PREFIX] Disk usage by buckets s3cmd du [s3://BUCKET[/PREFIX]] Get various information about Buckets or Files s3cmd info s3://BUCKET[/OBJECT] Copy object s3cmd cp s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2] Modify object metadata s3cmd modify s3://BUCKET1/OBJECT Move object s3cmd mv s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2] Modify Access control list for Bucket or Files s3cmd setacl s3://BUCKET[/OBJECT] Modify Bucket Policy s3cmd setpolicy FILE s3://BUCKET Delete Bucket Policy s3cmd delpolicy s3://BUCKET Modify Bucket CORS s3cmd setcors FILE s3://BUCKET Delete Bucket CORS s3cmd delcors s3://BUCKET Modify Bucket Requester Pays policy s3cmd payer s3://BUCKET Show multipart uploads s3cmd multipart s3://BUCKET [Id] Abort a multipart upload s3cmd abortmp s3://BUCKET/OBJECT Id List parts of a multipart upload s3cmd listmp s3://BUCKET/OBJECT Id Enable/disable bucket access logging s3cmd accesslog s3://BUCKET Sign arbitrary string using the secret key s3cmd sign STRING-TO-SIGN Sign an S3 URL to provide limited public access with expiry s3cmd signurl s3://BUCKET/OBJECT &lt;expiry_epoch|+expiry_offset&gt; Fix invalid file names in a bucket s3cmd fixbucket s3://BUCKET[/PREFIX] Create Website from bucket s3cmd ws-create s3://BUCKET Delete Website s3cmd ws-delete s3://BUCKET Info about Website s3cmd ws-info s3://BUCKET Set or delete expiration rule for the bucket s3cmd expire s3://BUCKET Upload a lifecycle policy for the bucket s3cmd setlifecycle FILE s3://BUCKET Get a lifecycle policy for the bucket s3cmd getlifecycle s3://BUCKET Remove a lifecycle policy for the bucket s3cmd dellifecycle s3://BUCKET List CloudFront distribution points s3cmd cflist Display CloudFront distribution point parameters s3cmd cfinfo [cf://DIST_ID] Create CloudFront distribution point s3cmd cfcreate s3://BUCKET Delete CloudFront distribution point s3cmd cfdelete cf://DIST_ID Change CloudFront distribution point parameters s3cmd cfmodify cf://DIST_ID Display CloudFront invalidation request(s) status s3cmd cfinvalinfo cf://DIST_ID[/INVAL_ID]<br/> For more information, updates and news, visit the s3cmd website: http://s3tools.org </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Signup --> <h3 id="signup">Signup</h3> <p> I went to the <a href='https://login.linode.com/signup' target='_blank' rel="nofollow">Linode signup page</a> and signed up with my email. Using third parties for authentication means they track you more easily, and introduces an unnecessary dependency. </p> <p> The email signup procedure requires a mobile phone for SMS-based MFA. I would rather use a <a href='https://datatracker.ietf.org/doc/html/rfc6238' target='_blank' rel="nofollow">TOTP authenticator app</a> instead of SMS for MFA. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/linode/signup.svg" type="image/svg"> <!---<source srcset="/blog/images/linode/signup.avif" type="image/avif">--> <source srcset="/blog/images/linode/signup.webp" type="image/webp"> <source srcset="/blog/images/linode/signup.apng" type="image/apng"> <source srcset="/blog/images/linode/signup.png" type="image/png"> <source srcset="/blog/images/linode/signup.jpg" type="image/jpeg"> <source srcset="/blog/images/linode/signup.jpeg" type="image/jpeg"> <source srcset="/blog/images/linode/signup.jfif" type="image/jpeg"> <source srcset="/blog/images/linode/signup.pjpeg" type="image/jpeg"> <source srcset="/blog/images/linode/signup.pjp" type="image/jpeg"> <source srcset="/blog/images/linode/signup.gif" type="image/gif"> <source srcset="/blog/images/linode/signup.tif" type="image/tiff"> <source srcset="/blog/images/linode/signup.tiff" type="image/tiff"> <source srcset="/blog/images/linode/signup.bmp" type="image/bmp"> <source srcset="/blog/images/linode/signup.ico" type="image/x-icon"> <source srcset="/blog/images/linode/signup.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/linode/signup.png" style='width: 100%; ' /> </picture> </div> <p> I want to ensure that I limit my financial liability. One way of protecting myself is to limit the maximum amount that can be charged to the payment mechanism. Because PayPal has no maximum transaction limit, it is not a suitable choice for a service with unlimited financial liability. However, credit cards can have transaction limits set, and Google Pay has the following limits: </p> <div class='quote'> If you <a href='https://support.google.com/googlepay/answer/10623602' target='_blank' rel="nofollow">set up your Google Pay balance to make contactless payments</a>, there are some transaction limits: <br><br> Maximum single transaction amount: $2,000 USD.<br> Daily maximum total transaction amount: $2,500 USD.<br> Up to 15 transactions per day.<br> Additional limits on the dollar amount or frequency of transactions may be imposed in accordance with the Google Pay Terms of Service. <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://support.google.com/googlepay/answer/10187490#:~:text=If%20you%20set%20up%20your,to%2015%20transactions%20per%20day.' rel='nofollow' target='_blank'>Google Pay Help</a></span> </div> <p> <a href='https://support.google.com/googlepay/answer/10187490' target='_blank' rel="nofollow">Here</a> are additional limits for Google Pay. Because of prior good experiences with how credit card processors handled fraud, I decided to use a credit card with a low limit instead of Google Pay. </p> <!-- endregion --> <!-- #region Generating Linode Access Keys --> <h3 id="keys">Generating Linode Access Keys</h3> <p> I followed the <a href='https://www.linode.com/docs/products/storage/object-storage/guides/access-keys/' target='_blank' rel="nofollow">directions</a>: </p> <ol> <li>Logged into the <a href='https://cloud.linode.com/' target='_blank' rel="nofollow">Cloud Manager</a>.</li> <li>Selected the <b>Object Storage</b> menu item in the sidebar and clicked on the <b>Access Keys</b> tab.</li> <li>Clicked on the <b>Create Access</b> Key button, which displays the <b>Create Access Key</b> panel.</li> <li>Typed in the name <code>mslinn</code> as the label for the new access key.</li> <li>Clicked the <kbd>Submit</kbd> button to create the access key.</li> <li>The new access key and its secret key are displayed. This is the only time that the secret key is visible. I stored the access key and the secret key in my password manager, <a href='https://lastpass.com'><code>lastpass.com</code></a>.</li> </ol> <!-- endregion --> <!-- #region Configuring s3cmd --> <h3 id="s3cmd_cfg">Configuring <span class="code">s3cmd</span></h3> <p> <code>s3cmd</code> is a Python program that works with all S3-compatible APIs, such as those provided by AWS, Linode Storage, Google Cloud Storage and DreamHost DreamObjects. I intend to use its <a href='https://s3tools.org/s3cmd-sync' target='_blank' rel="nofollow"><code>sync</code></a> subcommand to synchronize the <code>www.mslinn.com</code> bucket contents in Linode Storage with the most recently generated version of this website by Jekyll. </p> <p> <code>s3cmd</code> needs to be <a href='https://s3tools.org/kb/item14.htm' target='_blank' rel="nofollow">configured</a> for <a href='https://www.linode.com/docs/products/storage/object-storage/guides/s3cmd' target='_blank' rel="nofollow">Linode</a> before it can be used. Some configuration settings for Linode are non-obvious. The following should work for most users, with the caution that the access key and secret key are of course unique for every user. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id40766f8caf1a'><button class='copyBtn' data-clipboard-target='#id40766f8caf1a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd --configure <span class='unselectable'><br>Enter new values or accept defaults in brackets with Enter. Refer to user manual for detailed description of all options.<br/> Access key and Secret key are your identifiers for Amazon S3. Leave them empty for using the env variables. Access Key [asdfasdf]: </span>asdfasdfasdf <span class='unselectable'>Secret Key [asdfasdfasdf]: </span>asdfasdfasdf <span class='unselectable'>Default Region [US]:<br/> Use &quot;s3.amazonaws.com&quot; for S3 Endpoint and not modify it to the target Amazon S3. S3 Endpoint [s3.amazonaws.com]: </span>us-east-1.linodeobjects.com<br/> <span class='unselectable'>Use &quot;%(bucket)s.s3.amazonaws.com&quot; to the target Amazon S3. &quot;%(bucket)s&quot; and &quot;%(location)s&quot; vars can be used if the target S3 system supports dns based buckets. DNS-style bucket+hostname:port template for accessing a bucket [%(bucket)s.s3.amazonaws.com]: </span>%(bucket)s.us-east-1.linodeobjects.com<br/> <span class='unselectable'>Encryption password is used to protect your files from reading by unauthorized persons while in transfer to S3 Encryption password: Path to GPG program [/usr/bin/gpg]:<br/> When using secure HTTPS protocol all communication with Amazon S3 servers is protected from 3rd party eavesdropping. This method is slower than plain HTTP, and can only be proxied with Python 2.7 or newer Use HTTPS protocol [Yes]:<br/> On some networks all internet access must go through a HTTP proxy. Try setting it here if you can&#39;t connect to S3 directly HTTP Proxy server name:<br/> New settings: Access Key: </span>asdfasdfasdf <span class='unselectable'>Secret Key: </span>asdfasdfasdf <span class='unselectable'>Default Region: US S3 Endpoint: </span>https://us-east-1.linodeobjects.com <span class='unselectable'>DNS-style bucket+hostname:port template for accessing a bucket: </span>%(bucket)s.us-east-1.linodeobjects.com <span class='unselectable'>Encryption password: Path to GPG program: /usr/bin/gpg Use HTTPS protocol: True HTTP Proxy server name: HTTP Proxy server port: 0<br/> Test access with supplied credentials? [Y/n] n Save settings? [y/N] y Configuration saved to '/home/mslinn/.s3cfg' </span></pre> </div> <!-- endregion --> <p> I was unable to successfully test access. Two different errors appeared at different times: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id80e3280176e1'><button class='copyBtn' data-clipboard-target='#id80e3280176e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Please wait, attempting to list all buckets... ERROR: Test failed: 403 (SignatureDoesNotMatch) ERROR: Test failed: [Errno -2] Name or service not known<br/></pre> </div> <!-- endregion --> <p> Eventually I saved the configuration without testing as shown above. This created a file called <code>$HOME/.s3cfg</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.s3cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idca7f8f7699f8'><button class='copyBtn' data-clipboard-target='#idca7f8f7699f8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[default] access_key = asdfasdfasdf access_token = add_encoding_exts = add_headers = bucket_location = US ca_certs_file = cache_file = check_ssl_certificate = True check_ssl_hostname = True cloudfront_host = cloudfront.amazonaws.com connection_max_age = 5 connection_pooling = True content_disposition = content_type = default_mime_type = binary/octet-stream delay_updates = False delete_after = False delete_after_fetch = False delete_removed = False dry_run = False enable_multipart = True encoding = UTF-8 encrypt = False expiry_date = expiry_days = expiry_prefix = follow_symlinks = False force = False get_continue = False gpg_command = /usr/bin/gpg gpg_decrypt = %(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s gpg_encrypt = %(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s gpg_passphrase = guess_mime_type = True host_base = us-east-1.linodeobjects.com host_bucket = %(bucket)s.us-east-1.linodeobjects.com human_readablesizes = False invalidate_default_index_on_cf = False invalidate_default_index_root_on_cf = True invalidate_on_cf = False kms_key = limit = -1 limitrate = 0 list_md5 = False log_target_prefix = long_listing = False max_delete = -1 mime_type = multipart_chunksize_mb = 15 multipart_copy_chunksize_mb = 1024 multipart_max_chunks = 10000 preserve_attrs = True progress_meter = True proxy_host = proxy_port = 0 public_url_use_https = False put_continue = False recursive = False recv_chunk = 65536 reduced_redundancy = False requester_pays = False restore_days = 1 restore_priority = Standard secret_key = asdfasdfasdf send_chunk = 65536 server_side_encryption = False signature_v2 = False signurl_use_https = False simpledb_host = sdb.amazonaws.com skip_existing = False socket_timeout = 300 ssl_client_cert_file = ssl_client_key_file = stats = False stop_on_error = False storage_class = throttle_max = 100 upload_id = urlencoding_mode = normal use_http_expect = False use_https = True use_mime_magic = True verbosity = INFO website_endpoint = http://%(bucket)s.s3-website-%(location)s.amazonaws.com/ website_error = website_index = index.html</pre> </div> <!-- endregion --> <p> According to the docs (<a href='https://www.linode.com/docs/products/storage/object-storage/guides/urls/' target='_blank' rel="nofollow">Access Buckets and Files through URLs</a>), the configuration file needs <code>website_endpoint: http://%(bucket)s.website-[cluster-url]/</code>. However, this is an error; instead of <code>cluster-url</code>, which might be <code>https://us-east-1.linodeobjects.com</code>, <code>cluster-id</code> should be used, which for me was simply <code>us-east-1</code>. <span class="bg_yellow">Thus the endpoint for me, and everyone with a bucket at <code>us-east-1</code> in Newark, New Jersey should be:<br> <code>http://%(bucket)s.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com/</code></span> </p> <p> After editing the file to manually change the value of <code>website_endpoint</code>, I tried to list the buckets, and that worked: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaf0bf69cce13'><button class='copyBtn' data-clipboard-target='#idaf0bf69cce13' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd ls <span class='unselectable'>2022-07-01 17:32 s3://mslinn </span></pre> </div> <!-- endregion --> <!-- #region linode-cli --> <h3 id="linode-cli"><span class="code">linode-cli</span></h3> <p> Next I tried <a href='https://github.com/linode/linode-cli' target='_blank' rel="nofollow"><code>linode-cli</code></a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5d4b366de886'><button class='copyBtn' data-clipboard-target='#id5d4b366de886' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pip install linode-cli <span class='unselectable'>Collecting linode-cli Downloading linode_cli-5.21.0-py2.py3-none-any.whl (204 kB) &#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473; 204.2/204.2 kB 4.8 MB/s eta 0:00:00 Collecting terminaltables Downloading terminaltables-3.1.10-py2.py3-none-any.whl (15 kB) Collecting PyYAML Downloading PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (682 kB) &#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473;&#9473; 682.2/682.2 kB 16.8 MB/s eta 0:00:00 Requirement already satisfied: requests in /home/mslinn/venv/default/lib/python3.10/site-packages (from linode-cli) (2.28.0) Requirement already satisfied: charset-normalizer~=2.0.0 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests-&gt;linode-cli) (2.0.12) Requirement already satisfied: idna&lt;4,&gt;=2.5 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests-&gt;linode-cli) (3.3) Requirement already satisfied: urllib3&lt;1.27,&gt;=1.21.1 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests-&gt;linode-cli) (1.26.9) Requirement already satisfied: certifi&gt;=2017.4.17 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests-&gt;linode-cli) (2022.6.15) Installing collected packages: terminaltables, PyYAML, linode-cli Successfully installed PyYAML-6.0 linode-cli-5.21.0 terminaltables-3.1.10 </span></pre> </div> <!-- endregion --> <p> The documentation fails to mention that the first time <code>linode-cli</code> is executed, and every time it is invoked with the <code>configure</code> subcommand, it attempts to open a web browser. WSL and WSL2 will not respond appropriately unless an X client is already running, for example <a href='https://apps.microsoft.com/store/detail/gwsl/9NL6KD1H33V3' target='_blank' rel="nofollow">GWSL</a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida01963a98c74'><button class='copyBtn' data-clipboard-target='#ida01963a98c74' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>linode-cli configure <span class='unselectable'><br>Welcome to the Linode CLI. This will walk you through some initial setup.<br/> The CLI will use its web-based authentication to log you in. If you prefer to supply a Personal Access Token, use <code>linode-cli configure --token</code><br/> Press enter to continue. This will open a browser and proceed with authentication. A browser should open directing you to this URL to authenticate:<br/> https://login.linode.com/oauth/authorize?client_id=asdfasdfasdf&amp;response_type=token&amp;scopes=*&amp;redirect_uri=http://localhost:45789<br/> If you are not automatically directed there, please copy/paste the link into your browser to continue..<br/><br/> Configuring mslinn<br/><br/> Default Region for operations. Choices are: 1 - ap-west 2 - ca-central 3 - ap-southeast 4 - us-central 5 - us-west 6 - us-southeast 7 - us-east 8 - eu-west 9 - ap-south 10 - eu-central 11 - ap-northeast<br/> Default Region (Optional): 7<br/> Default Type of Linode to deploy. Choices are: 1 - g6-nanode-1 2 - g6-standard-1 3 - g6-standard-2 4 - g6-standard-4 5 - g6-standard-6 6 - g6-standard-8 7 - g6-standard-16 8 - g6-standard-20 9 - g6-standard-24 10 - g6-standard-32 11 - g7-highmem-1 12 - g7-highmem-2 13 - g7-highmem-4 14 - g7-highmem-8 15 - g7-highmem-16 16 - g6-dedicated-2 17 - g6-dedicated-4 18 - g6-dedicated-8 19 - g6-dedicated-16 20 - g6-dedicated-32 21 - g6-dedicated-48 22 - g6-dedicated-50 23 - g6-dedicated-56 24 - g6-dedicated-64 25 - g1-gpu-rtx6000-1 26 - g1-gpu-rtx6000-2 27 - g1-gpu-rtx6000-3 28 - g1-gpu-rtx6000-4<br/> Default Type of Linode (Optional):<br/> Default Image to deploy to new Linodes. Choices are: 1 - linode/almalinux8 2 - linode/almalinux9 3 - linode/alpine3.12 4 - linode/alpine3.13 5 - linode/alpine3.14 6 - linode/alpine3.15 7 - linode/alpine3.16 8 - linode/arch 9 - linode/centos7 10 - linode/centos-stream8 11 - linode/centos-stream9 12 - linode/debian10 13 - linode/debian11 14 - linode/debian9 15 - linode/fedora34 16 - linode/fedora35 17 - linode/fedora36 18 - linode/gentoo 19 - linode/kali 20 - linode/debian11-kube-v1.20.15 21 - linode/debian9-kube-v1.20.7 22 - linode/debian9-kube-v1.21.1 23 - linode/debian11-kube-v1.21.12 24 - linode/debian9-kube-v1.22.2 25 - linode/debian11-kube-v1.22.9 26 - linode/debian11-kube-v1.23.6 27 - linode/opensuse15.3 28 - linode/opensuse15.4 29 - linode/rocky8 30 - linode/slackware14.2 31 - linode/slackware15.0 32 - linode/ubuntu16.04lts 33 - linode/ubuntu18.04 34 - linode/ubuntu20.04 35 - linode/ubuntu21.10 36 - linode/ubuntu22.04 37 - linode/centos8 38 - linode/slackware14.1 39 - linode/ubuntu21.04<br/> Default Image (Optional): Active user is now mslinn<br/> Config written to /home/mslinn/.config/linode-cli </span></pre> </div> <!-- endregion --> <p> <code>linode-cli</code> configuration created this file: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.config/linode-cli</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idde92716e4bd1'><button class='copyBtn' data-clipboard-target='#idde92716e4bd1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[DEFAULT] default-user = mslinn [mslinn] token = asdfasdfasdfasdfasdfasdf region = us-east</pre> </div> <!-- endregion --> <p> Right after I did the above, I was surprised to get the following email from Linode: </p> <div class="quote"> Hello,<br><br> This is a notification to inform you that a device has been trusted to skip authentication on your Linode Account (mslinn). The request came from the following IP address: 70.53.179.203. <br><br> The device will not be prompted for a username or password for 30 days. <br><br> If this action did not originate from you, we recommend logging in and changing your password immediately. Also, as an extra layer of security, we highly recommend enabling two-factor authentication on your account. Please see the link below for more information on how to enable two-factor authentication: <br><br> <a href='https://www.linode.com/docs/security/linode-manager-security-controls/#enable-two-factor-authentication' target='_blank' rel="nofollow">Two-Factor Authentication</a> <br><br> If you have any questions or concerns, please do not hesitate to contact us 24/7 by opening a support ticket from within the Linode Manager, giving us a call, or emailing <a href='mailto:support@linode.com'>support@linode.com</a>. <br><br> <a href='https://www.linode.com/contact' target='_blank' rel="nofollow">Contact</a> <br><br> Thank you,<br> The Linode.com Team<br> <br><br> <a href='mailto:support@linode.com'>Contact Us</a> </div> <p class="alert rounded shadow"> I began to suspect that Linode&rsquo;s documentation had major deficiencies at this point. Instead of merely configuring a command-line client, my IP was whitelisted unexpectedly. I have nothing polite to say about this. </p> <p> After poking around a bit, I discovered my <a href='https://cloud.linode.com/profile/tokens' target='_blank' rel="nofollow">API Tokens page</a> on Linode. It seems that the <code>linode-cli</code> created a personal access token with a label containing the name of the computer used: <code>Linode CLI @ camille</code>. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/linode/apiTokens.svg" type="image/svg"> <!---<source srcset="/blog/images/linode/apiTokens.avif" type="image/avif">--> <source srcset="/blog/images/linode/apiTokens.webp" type="image/webp"> <source srcset="/blog/images/linode/apiTokens.apng" type="image/apng"> <source srcset="/blog/images/linode/apiTokens.png" type="image/png"> <source srcset="/blog/images/linode/apiTokens.jpg" type="image/jpeg"> <source srcset="/blog/images/linode/apiTokens.jpeg" type="image/jpeg"> <source srcset="/blog/images/linode/apiTokens.jfif" type="image/jpeg"> <source srcset="/blog/images/linode/apiTokens.pjpeg" type="image/jpeg"> <source srcset="/blog/images/linode/apiTokens.pjp" type="image/jpeg"> <source srcset="/blog/images/linode/apiTokens.gif" type="image/gif"> <source srcset="/blog/images/linode/apiTokens.tif" type="image/tiff"> <source srcset="/blog/images/linode/apiTokens.tiff" type="image/tiff"> <source srcset="/blog/images/linode/apiTokens.bmp" type="image/bmp"> <source srcset="/blog/images/linode/apiTokens.ico" type="image/x-icon"> <source srcset="/blog/images/linode/apiTokens.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/linode/apiTokens.png" style='width: 100%; ' /> </picture> </div> <p> Clicking on the <kbd>View Scopes</kbd> button above displays the permissions granted to the token: </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/linode/cliPermissions.svg" type="image/svg"> <!---<source srcset="/blog/images/linode/cliPermissions.avif" type="image/avif">--> <source srcset="/blog/images/linode/cliPermissions.webp" type="image/webp"> <source srcset="/blog/images/linode/cliPermissions.apng" type="image/apng"> <source srcset="/blog/images/linode/cliPermissions.png" type="image/png"> <source srcset="/blog/images/linode/cliPermissions.jpg" type="image/jpeg"> <source srcset="/blog/images/linode/cliPermissions.jpeg" type="image/jpeg"> <source srcset="/blog/images/linode/cliPermissions.jfif" type="image/jpeg"> <source srcset="/blog/images/linode/cliPermissions.pjpeg" type="image/jpeg"> <source srcset="/blog/images/linode/cliPermissions.pjp" type="image/jpeg"> <source srcset="/blog/images/linode/cliPermissions.gif" type="image/gif"> <source srcset="/blog/images/linode/cliPermissions.tif" type="image/tiff"> <source srcset="/blog/images/linode/cliPermissions.tiff" type="image/tiff"> <source srcset="/blog/images/linode/cliPermissions.bmp" type="image/bmp"> <source srcset="/blog/images/linode/cliPermissions.ico" type="image/x-icon"> <source srcset="/blog/images/linode/cliPermissions.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/linode/cliPermissions.png" style='width: 100%; ' /> </picture> </div> <p class="alert rounded shadow"> This personal access token is all-powerful. The <code>linode-cli</code> quietly created a personal access token with all permissions enabled, and did not warn the user, and did not say how to reduce or limit the permissions. <br><br> Security is enhanced when the minimum permission necessary is provided to accomplish necessary tasks. Instead, this token silently maximizes the financial risk to the person or entity who pays for the account. </p> <!-- endregion --> <!-- #region Creating the Website Bucket --> <h3 id="bucket">Creating the Website Bucket</h3> <p class="alert rounded shadow"> The name of the bucket must exactly match the name of the subdomain that the website is served from. If you try to serve content from a misnamed bucket, you will not be able to apply an SSL certificate; instead, the certificate will be issued by <code>linodeobjects.com</code>. </p> <p> I wanted to test using the subdomain <code>linode.mslinn.com</code>, and then if all was well, switch <code>www.mslinn.com</code> over to Linode Storage. Because buckets cannot be renamed, I had to make 2 buckets with identical contents, one called <code>linode.mslinn.com</code> and the other called <code>www.mslinn.com</code>. If I decide to stay with Linode, I will delete the bucket I am using for testing, called <code>linode.mslinn.com</code>, and run this website from the <code>www.mslinn.com</code> bucket. </p> <p> I created a website-enabled bucket called <code>www.mslinn.com</code> as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id93cc26ed7418'><button class='copyBtn' data-clipboard-target='#id93cc26ed7418' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd mb --acl-public s3://www.mslinn.com <span class='unselectable'>Bucket 's3://www.mslinn.com/' created </span> <span class='unselectable'>$ </span>s3cmd ls <span class='unselectable'>2022-07-01 17:32 s3://www.mslinn.com </span> <span class='unselectable'>$ </span>s3cmd ws-create \ --ws-index=index.html \ --ws-error=404.html \ s3://www.mslinn.com <span class='unselectable'>Bucket 's3://www.mslinn.com/': website configuration created. </span></pre> </div> <!-- endregion --> <p> The <code>s3cmd ws-info</code> subcommand displays the Linode Object Storage bucket URL as the <b>Website endpoint</b>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9fec3e31baba'><button class='copyBtn' data-clipboard-target='#id9fec3e31baba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd ws-info s3://www.mslinn.com <span class='unselectable'>Bucket s3://www.mslinn.com/: Website configuration Website endpoint: http://www.mslinn.com.website-us-east-1.linodeobjects.com/ Index document: index.html Error document: 404.html </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Defining a CNAME for the Bucket --> <h3 id="subdomain">Defining a CNAME for the Bucket</h3> <p> Although Linode provides a <a href='https://www.linode.com/docs/guides/dns-manager/' target='_blank' rel="nofollow">DNS Manager</a>, while I am testing out Linode I wanted to continue using Namecheap for DNS, so I navigated to <a href='https://ap.www.namecheap.com/Domains/DomainControlPanel/mslinn.com/advancedns'><code>ap.www.namecheap.com/Domains/DomainControlPanel/mslinn.com/advancedns</code></a> and created CNAMEs like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id91b42ddfca9d'><button class='copyBtn' data-clipboard-target='#id91b42ddfca9d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>linode.mslinn.com CNAME linode.mslinn.com.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com www.mslinn.com CNAME www.mslinn.com.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com</pre> </div> <p class="alert rounded shadow"> Warning: if the <code>CNAME</code> does not point to a URL with the word <code>website</code> in it, for example <code>www.mslinn.com.us-east-1.linodeobjects.com</code>, the default index file and the error file will not automatically be served when expected. </p> <!-- endregion --> <!-- #region Making a Free SSL Certificate --> <h3 id="cert">Making a Free SSL Certificate</h3> <p> <a href='https://www.ssllabs.com/ssltest/' target='_blank' rel="nofollow">Qualys / SSL Labs</a> rates the SSL security provided by AWS CloudFront using the AWS-generated SSL certificates with a &ldquo;B&rdquo; grade. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/sslReport.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/sslReport.avif" type="image/avif">--> <source srcset="/blog/images/aws/sslReport.webp" type="image/webp"> <source srcset="/blog/images/aws/sslReport.apng" type="image/apng"> <source srcset="/blog/images/aws/sslReport.png" type="image/png"> <source srcset="/blog/images/aws/sslReport.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/sslReport.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/sslReport.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/sslReport.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/sslReport.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/sslReport.gif" type="image/gif"> <source srcset="/blog/images/aws/sslReport.tif" type="image/tiff"> <source srcset="/blog/images/aws/sslReport.tiff" type="image/tiff"> <source srcset="/blog/images/aws/sslReport.bmp" type="image/bmp"> <source srcset="/blog/images/aws/sslReport.ico" type="image/x-icon"> <source srcset="/blog/images/aws/sslReport.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/sslReport.png" style='width: 100%; ' /> </picture> </div> <p> Lets see if Linode Object Storage can host a more secure website when provided with a 4096-bit certificate generated by <code>certbot</code>/<code>letsencrypt</code>. The conclusion of this article will reveal the answer. </p> <p> Please read <a href='/blog/2022/06/15/certbot.html'>Creating and Renewing Letsencrypt Wildcard SSL Certificates</a> for details. </p> <!-- endregion --> <!-- #region Linode DNS Authentication --> <h4 id="linodeDns">Linode DNS Authentication</h4> <p> Eventually, if I stick with Linode, I could use the <a href='https://certbot-dns-linode.readthedocs.io/en/stable/' target='_blank' rel="nofollow"><code>certbot</code> DNS plugin for Linode</a>, but I will not use it right now because I am trialing Linode. Use of this plugin requires a configuration file containing Linode API credentials, obtained from your Linode account&rsquo;s <a href='https://cloud.linode.com/profile/tokens' target='_blank' rel="nofollow">Applications &amp; API Tokens</a> page. I stored the credentials in <code>~/.certbot/linode.ini</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.certbot/linode.ini</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id46238c5d1254'><button class='copyBtn' data-clipboard-target='#id46238c5d1254' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Linode API credentials used by Certbot dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64 dns_linode_version = [<blank>|3|4]</pre> </div> <!-- endregion --> <p> Here is how to create an SSL certificate <code>certbot</code> using Linode DNS authentication: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id50a2e4cb99b8'><button class='copyBtn' data-clipboard-target='#id50a2e4cb99b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><!-- #region --> <span class='unselectable'>$ </span>certbot certonly \ --dns-linode \ --dns-linode-credentials ~/.certbot/linode.ini \ --rsa-key-size 4096 \ -d mslinn.com \ -d *.mslinn.com \ --config-dir ~/.certbot/mslinn.com/config \ --logs-dir ~/.certbot/mslinn.com/logs \ --work-dir ~/.certbot/mslinn.com/work</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region Installing the Certificate --> <h2 id="certUp">Installing the Certificate</h2> <!-- #region implicit --> <p> Now I created 2 certificates: one with the full chain of responsibility (<code>fullchain.pem</code>) and one containing my private key (<code>privkey.pem</code>). I used bash environment variables called <code>CERT_DIR</code>, <code>CERT</code> and <code>KEY</code> to make the incantation convenient to type in. <code>CERT</code> and <code>KEY</code> contain the contents of the files <code>fullchain.pem</code> and <code>privkey.pem</code>, respectively. As you can see, the result was a valid (wildcard) SSL certificate. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbceade70e74b'><button class='copyBtn' data-clipboard-target='#idbceade70e74b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>CERT_DIR=/home/mslinn/.certbot/mslinn.com/config/live/mslinn.com <span class='unselectable'>$ </span>CERT="$( cat $CERT_DIR/fullchain.pem )" <span class='unselectable'>$ </span>KEY="$( cat $CERT_DIR/privkey.pem )" <span class='unselectable'>$ </span>linode-cli object-storage ssl-upload \ us-east-1 www.mslinn.com \ --certificate "$CERT" \ --private_key "$KEY" <div class="tree"><span class='unselectable'>┌──────┐ │ ssl │ ├──────┤ │ True │ └──────┘ </span></div></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Syncing the Website Bucket --> <h3 id="sync">Syncing the Website Bucket</h3> <p> I wrote a bash script to build this Jekyll-generated website. Jekyll places the generated website in a directory called <code>_site</code>. I <a href='https://s3tools.org/s3cmd-sync' target='_blank' rel="nofollow">synchronized</a> the contents of the freshly generated <code>mslinn.com</code> website, stored in <code>_site/</code>, with the bucket as follows. Note the trailing slash on <code>_site<span class="bg_yellow">/</span></code>, <a href='https://s3tools.org/s3cmd-sync' target='_blank' rel="nofollow">it is significant</a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id29b926d5b010'><button class='copyBtn' data-clipboard-target='#id29b926d5b010' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd sync \ --acl-public --delete-removed --guess-mime-type --quiet \ _site<span class="bg_yellow">/</span> \ s3://www.mslinn.com <span class='unselectable'>INFO: No cache file found, creating it. INFO: Compiling list of local files... INFO: Running stat() and reading/calculating MD5 values on 2157 files, this may take some time... INFO: [1000/2157] INFO: [2000/2157] INFO: Retrieving list of remote files for s3://www.mslinn.com/_site ... INFO: Found 2157 local files, 0 remote files INFO: Verifying attributes... INFO: Summary: 2137 local files to upload, 20 files to remote copy, 0 remote files to delete ... lots more output ... Done. Uploaded 536630474 bytes in 153.6 seconds, 3.33 MB/s. </span></pre> </div> <!-- endregion --> <p> Originally, I did not provide the <code>--acl-public</code> option to <code>s3cmd sync</code>. That meant the files in the bucket were all private &ndash; the opposite of what is required for a website. Here is the incantation for making all the files in the bucket publicly readable: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id125df05d7716'><button class='copyBtn' data-clipboard-target='#id125df05d7716' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd setacl s3://www.mslinn.com/ \ --acl-public --recursive --quiet</pre> </div> <p> Linode&rsquo;s S3 implementation faithfully mirrors a problem that AWS S3 has. CSS files are automatically assigned the MIME type <code>text/plain</code>, instead of <code>text/css</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id596d587ce77b'><button class='copyBtn' data-clipboard-target='#id596d587ce77b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -sI http://linode.mslinn.com/assets/css/style.css | \ grep Content-Type <span class='unselectable'>Content-Type: text/plain </span></pre> </div> <p> I re-uploaded the CSS files with the proper mime type using this incantation: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd453e2c4b03c'><button class='copyBtn' data-clipboard-target='#idd453e2c4b03c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd _site <span class='unselectable'>$ </span>find . -name *.css -exec \ s3cmd put -P --mime-type=text/css {} s3://www.mslinn.com/{} \;</pre> </div> <p> It should be possible to provide a MIME-type mapping somehow! Linode, if you are reading this, please provide an extension to the S3 functionality, so customers could go beyond AWS's bugs. </p> <!-- endregion --> <!-- endregion --> <!-- #region The Result --> <h2 id="result">The Result</h2> <p> Success! The same content can be fetched with 3 different URLs: </p> <ul> <li> <a href='https://https://linode.mslinn.com' target='_blank' rel="nofollow"><code>https://linode.mslinn.com</code></a> uses the free wildcard certificate that was created above. </li> <li> <a href='http://linode.mslinn.com.website-us-east-1.linodeobjects.com' target='_blank' rel="nofollow"><code>http://linode.mslinn.com.website-us-east-1.linodeobjects.com</code></a> works without an SSL certificate. </li> <li> <a href='https://linode.mslinn.com.website-us-east-1.linodeobjects.com' target='_blank' rel="nofollow"><code>https://linode.mslinn.com.website-us-east-1.linodeobjects.com</code></a> uses a certificate from <code>us-east-1.linodeobjects.com</code>, which shows as an error, as it should. The error would be prevented if the certificate was a wildcard certificate issued by <code>linodeobjects.com</code> instead. </li> </ul> <p> Qualys / SSL Labs rated the site on Linode Storage with the Let&rsquo;s Encrypt SSL certificate: and gave it the same rating as when the site was hosted on AWS S3 / CloudFront. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/linode/sslReportLinode.svg" type="image/svg"> <!---<source srcset="/blog/images/linode/sslReportLinode.avif" type="image/avif">--> <source srcset="/blog/images/linode/sslReportLinode.webp" type="image/webp"> <source srcset="/blog/images/linode/sslReportLinode.apng" type="image/apng"> <source srcset="/blog/images/linode/sslReportLinode.png" type="image/png"> <source srcset="/blog/images/linode/sslReportLinode.jpg" type="image/jpeg"> <source srcset="/blog/images/linode/sslReportLinode.jpeg" type="image/jpeg"> <source srcset="/blog/images/linode/sslReportLinode.jfif" type="image/jpeg"> <source srcset="/blog/images/linode/sslReportLinode.pjpeg" type="image/jpeg"> <source srcset="/blog/images/linode/sslReportLinode.pjp" type="image/jpeg"> <source srcset="/blog/images/linode/sslReportLinode.gif" type="image/gif"> <source srcset="/blog/images/linode/sslReportLinode.tif" type="image/tiff"> <source srcset="/blog/images/linode/sslReportLinode.tiff" type="image/tiff"> <source srcset="/blog/images/linode/sslReportLinode.bmp" type="image/bmp"> <source srcset="/blog/images/linode/sslReportLinode.ico" type="image/x-icon"> <source srcset="/blog/images/linode/sslReportLinode.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/linode/sslReportLinode.png" style='width: 100%; ' /> </picture> </div> <p> BTW, you can test the date range of a certificate with this incantation: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7f23d65f98f5'><button class='copyBtn' data-clipboard-target='#id7f23d65f98f5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://linode.mslinn.com -vI --stderr - | grep 'date:' <span class='unselectable'>* start date: Nov 25 17:46:07 2022 GMT * expire date: Feb 23 17:46:06 2023 GMT </span></pre> </div> <p> The site feels quite responsive. <span style='font-size: 3em;;'>&#x1F601;</span> </p> <!-- endregion --> Considering Microsoft Azure for Static Websites 2022-06-20T00:00:00-04:00 https://mslinn.github.io/blog/2022/06/20/azure <p> After realizing that <a href='/blog/2022/06/02/azure-security.html'>AWS does not integrate security with real-time billing</a>, and being presented with a huge bill after my account was hijacked, I decided to see if Microsoft Azure offered me better financial security. </p> <p> In particular, I am looking for two things: </p> <ol> <li> <b>A more secure authentication mechanism</b> &ndash; If AWS credentials are compromised, they can be used from anywhere in the world. </li> <li> <b>Restricting scope and scale of authorized services</b> &ndash; With AWS, if an IAM user has a role that allows EC2 instances to be launched, any number of any size EC2 instances can be launched. There is no way to cap the number or type of EC2 instances, and AWS real-time billing is not integrated with the authentication mechanism. Furthermore, AWS has a peculiar set of busy work tasks for victims of account hijacking that appears to be targeted more towards increasing support revenue than address root issues. This means that the bad guys have no limit to the financial damage they can incur on their victims. </li> </ol> <p class="alert rounded shadow"> For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc: &ldquo;pay-as-you-go&rdquo; is shorthand for &ldquo;there is nothing you can do to limit your financial liability&rdquo;. </p> <p> I set out to discover if Azure also suffers from these same problems. </p> <h2 id="policies_roles">Azure Roles and Policies</h2> <div class='quote'> <h2>Policies</h2> <p> <a href='https://docs.microsoft.com/en-us/azure/governance/policy/overview' target='_blank' rel="nofollow">Azure policies</a> can be used to define the desired behavior for your organization's <a href='https://docs.microsoft.com/en-us/azure/virtual-machines/windows/policy' target='_blank' rel="nofollow">Windows VMs</a> and <a href='https://docs.microsoft.com/en-us/azure/virtual-machines/linux/policy' target='_blank' rel="nofollow">Linux VMs</a>. By using policies, an organization can enforce various conventions and rules throughout the enterprise. Enforcement of the desired behavior can help mitigate risk while contributing to the success of the organization. </p> <h3>Azure role-based access control</h3> <p> Using <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/overview' target='_blank' rel="nofollow">Azure role-based access control (Azure RBAC)</a>, you can segregate duties within your team and grant only the amount of access to users on your VM that they need to perform their jobs. Instead of giving everybody unrestricted permissions on the VM, you can allow only certain actions. You can configure access control for the VM in the <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal' target='_blank' rel="nofollow">Azure portal</a>, using the <a href='https://docs.microsoft.com/en-us/cli/azure/role' target='_blank' rel="nofollow">Azure CLI</a>, or <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-powershell' target='_blank' rel="nofollow">Azure PowerShell</a>. </p> <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://docs.microsoft.com/en-us/azure/virtual-machines/security-policy' rel='nofollow' target='_blank'>Azure Virtual Machines Documentation</a></span> </div> <p> Ekran Systems publishes a <a href='https://www.ekransystem.com/en/blog/rbac-vs-abac' target='_blank' rel="nofollow">good description</a> of various flavors of RBAC, and the more complex ABAC. Okta, Inc. also publishes a <a href='https://www.okta.com/identity-101/role-based-access-control-vs-attribute-based-access-control/' target='_blank' rel="nofollow">good overview of RBAC and ABAC</a>. I have no connection with either company. </p> <p> I soon discovered the following passage in the Azure documentation, which appeared to exactly match the second item in the wish list at the top of this article: </p> <div class='quote'> Azure RBAC focuses on managing user <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations' target='_blank' rel="nofollow">actions</a> at different scopes. If control of an action is required, then Azure RBAC is the correct tool to use. Even if an individual has access to perform an action, if the result is a non-compliant resource, Azure Policy still blocks the create or update. <br><br> The combination of Azure RBAC and Azure Policy provides full scope control in Azure. <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://docs.microsoft.com/en-us/azure/governance/policy/overview#azure-policy-and-azure-rbac' rel='nofollow' target='_blank'>Azure documentation</a></span> </div> <p class="alert rounded shadow"> <a href='https://docs.aws.amazon.com/prescriptive-guidance/latest/saas-multitenant-api-access-authorization/access-control-types.html' target='_blank' rel="nofollow">AWS RBAC</a> does not provide functionality equivalent to that provided by Microsoft Azure RBAC plus Azure Policy, even when combined with other AWS functionality. </p> <h2 id="rbacAgain">No Limits On Other Azure Services</h2> <p> The financial limitations that Azure allows you to impose are only available for virtual machines. Users are subject to unlimited financial liability from other Azure services. It seems I have <strike>two</strike>three choices: </p> <ol> <li>Figure out how to set up RBAC and accept that other services might run up a huge bill.</li> <li>Look for a fixed-price hosting service</li> <li>Give <a href="#spendLimit">Azure Spending Limit</a> a try</li> </ol> <p> Before I sign off today, I encountered a blocking issue with Azure that I'd like to mention. </p> <h2 id="ad">Active Directory?</h2> <p> I want to update my website by running a command-line command that runs something analogous to <a href='https://linux.die.net/man/1/rsync' target='_blank' rel="nofollow"><code>rsync</code></a>. I spent quite some time looking for how this might be done with Azure. </p> <p> Before long, it seemed that Active Directory was the best way forward, however setting it up is daunting. Apparently a <a href='https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals' target='_blank' rel="nofollow">service principal</a> is better suited for scripts that run on-premises, like my home office. I got the impression that I need to register an app; unsure what app might be required for hosting a web site on Azure Blob Storage with Azure CDN. </p> <p> These Active Directory docs look like a lot of abstract, generalized information that is way more complex than I need. I found a <a href='https://docs.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory' target='_blank' rel="nofollow">streamlined article</a> for my use case. Not sure what this means: &ldquo;Only storage accounts created with the Azure Resource Manager deployment model support Azure AD authorization.&rdquo; This streamlined document is not very streamlined. For me, this issue is a significant barrier to adoption. </p> <h2 id="spendLimit">Update: Azure Spending Limit</h2> <p> I just bumped into an article entitled <a href='https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/spending-limit' target='_blank' rel="nofollow">Azure spending limit</a>. Perhaps this might be useful. I tried to find out more from Microsoft, but at first they said I would have to subscribe to an expensive support plan before anyone would speak to me. That is not how I wish to proceed. Later, I opened a ticket inquiring about Azure customers facing unlimited financial liability. </p> <h2>Update 2022-06-29</h2> <p> The ticket was escalated twice, then Microsoft sent me the following official response (emphasis is mine): </p> <div class='quote'> Correct, at this moment that feature of setting limits does not exist, <b class="bg_yellow">Azure is not able to safeguard customers from unlimited financial liability</b>. <br><br> We are a support team that we provide help with current account or billing issues, in addition to that, I would like to mention that Azure products are constantly being updated, also Azure is constantly improving to offer better services to our customers. For this reason, I would like to invite you to leave your feedback in the Azure feedback forum (<a href='https://feedback.azure.com/d365community/forum/79b1327d-d925-ec11-b6e6-000d3a4f06a4' target='_blank' rel="nofollow">General Feedback · Community (azure.com)</a>), so your feedback is forwarded through the right channel and to the Microsoft internal resources that work on it. </div> <p> I responded by indicating that I would close my Azure account. </p> <p class="alert rounded shadow"> While the practice of imposing unlimited financial liability on customers is common today for PaaS vendors, it is unnecessary and predatory. If in the future I need to use a product or service that incurs unlimited financial liability for a client, I will require the client to accept the liability. </p> Creating and Renewing Letsencrypt Wildcard SSL Certificates 2022-06-15T00:00:00-04:00 https://mslinn.github.io/blog/2022/06/15/certbot <!-- #region intro --> <p> The process of making wildcard SSL certificates can be hard to get right at first. However, I use wildcard SSL certificates for most of my internet domains, so it is important to me. This article distills the essence of what I have found to be important when creating these certificates. </p> <p class="alert rounded shadow"> Update &ndash; 9 months after writing this article, I wrote about a better way to generate wildcard SSL certificates: <a href='/blog/2023/03/02/lego.html'>Wildcard SSL Certificates for Letsencrypt with Lego</a>. </p> <!-- endregion --> <!-- #region cert --> <div class='imgWrapper imgFlex right quartersize' style=' '> <a href='https://letsencrypt.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ddns/letsencrypt-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/letsencrypt-logo.avif" type="image/avif">--> <source srcset="/blog/images/ddns/letsencrypt-logo.webp" type="image/webp"> <source srcset="/blog/images/ddns/letsencrypt-logo.apng" type="image/apng"> <source srcset="/blog/images/ddns/letsencrypt-logo.png" type="image/png"> <source srcset="/blog/images/ddns/letsencrypt-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/letsencrypt-logo.gif" type="image/gif"> <source srcset="/blog/images/ddns/letsencrypt-logo.tif" type="image/tiff"> <source srcset="/blog/images/ddns/letsencrypt-logo.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/letsencrypt-logo.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/letsencrypt-logo.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/letsencrypt-logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/ddns/letsencrypt-logo.png" style='width: 100%; ' /> </picture> </a> </div> <h2 id="cert">Making a Free Wildcard SSL Certificate</h2> <p> <a href='https://certbot.eff.org/' target='_blank' rel="nofollow"><code>Certbot</code></a> powers <a href='https://www.eff.org/' target='_blank' rel="nofollow">EFF</a>&rsquo;s <a href='https://letsencrypt.org/' target='_blank' rel="nofollow">Letsencrypt</a> capability. Source code is on <a href='https://github.com/certbot/certbot' target='_blank' rel="nofollow">GitHub</a>. </p> <p> Install <code>certbot</code> on Debian distros such as Ubuntu as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable clear' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8595aad28014'><button class='copyBtn' data-clipboard-target='#id8595aad28014' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install certbot <span class='unselectable'>Preparing to unpack .../06-python3-zope.event_4.4-3_all.deb ... Unpacking python3-zope.event (4.4-3) ... Selecting previously unselected package python3-zope.component. Preparing to unpack .../07-python3-zope.component_4.3.0-3_all.deb ... Unpacking python3-zope.component (4.3.0-3) ... Selecting previously unselected package python3-certbot. Preparing to unpack .../08-python3-certbot_1.21.0-1build1_all.deb ... Unpacking python3-certbot (1.21.0-1build1) ... Selecting previously unselected package python3-icu. Preparing to unpack .../09-python3-icu_2.8.1-0ubuntu2_amd64.deb ... Unpacking python3-icu (2.8.1-0ubuntu2) ... Selecting previously unselected package certbot. Preparing to unpack .../10-certbot_1.21.0-1build1_all.deb ... Unpacking certbot (1.21.0-1build1) ... Setting up python3-configargparse (1.5.3-1) ... Setting up python3-requests-toolbelt (0.9.1-1) ... Setting up python3-parsedatetime (2.6-2) ... Setting up python3-icu (2.8.1-0ubuntu2) ... Setting up python3-zope.event (4.4-3) ... Setting up python3-zope.hookable (5.1.0-1build1) ... Setting up python3-josepy (1.10.0-1) ... Setting up python3-zope.component (4.3.0-3) ... Setting up python3-acme (1.21.0-1) ... Setting up python3-certbot (1.21.0-1build1) ... Setting up certbot (1.21.0-1build1) ... Created symlink /etc/systemd/system/timers.target.wants/certbot.timer → /lib/systemd/system/certbot.timer. Processing triggers for man-db (2.10.2-1) ... Scanning processes... Scanning processor microcode... Scanning linux images... Failed to retrieve available kernel versions. Failed to check for processor microcode upgrades. No services need to be restarted. No containers need to be restarted. No user sessions are running outdated binaries. No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre> </div> <!-- endregion --> <p> <code>certbot</code> has 2 subcommands of interest: <code>certonly</code> (used when creating a certificate for the first time), and <code>renew</code> (used when updating a pre-existing certificate). The creation process requires the user to do things before it can complete, so it can only be run interactively. </p> <p class="alert shadow rounded"> The files and directories creating by the process of creating a new SSL certificate should not be deleted. Contrary to what the Letsencrypt <code>certbot</code> documentation says, I have found that these files and directories allow the renewal process to proceed without requiring user interaction, so it can be scripted. </p> <!-- endregion --> <!-- #region help --> <h2 id="help"><span class='code'>Certbot</span> Help Messages</h2> <p> Here is the help message for the <code>certbot certonly</code> subcommand: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb35cae2e3bdf'><button class='copyBtn' data-clipboard-target='#idb35cae2e3bdf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot certonly --help <span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...<br/> Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the certificate. The most common SUBCOMMANDS and flags are:<br/> obtain, install, and renew certificates: (default) run Obtain &amp; install a certificate in your current webserver certonly Obtain or renew a certificate, but do not install it renew Renew all previously obtained certificates that are near expiry enhance Add security enhancements to your existing configuration -d DOMAINS Comma-separated list of domains to obtain a certificate for<br/> (the certbot apache plugin is not installed) --standalone Run a standalone webserver for authentication (the certbot nginx plugin is not installed) --webroot Place files in a server&#39;s webroot folder for authentication --manual Obtain certificates interactively, or using shell script hooks<br/> -n Run non-interactively --test-cert Obtain a test certificate from a staging server --dry-run Test &quot;renew&quot; or &quot;certonly&quot; without saving any certificates to disk<br/> manage certificates: certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-name or --cert-path) delete Delete a certificate (supply --cert-name)<br/> manage your account: register Create an ACME account unregister Deactivate an ACME account update_account Update an ACME account --agree-tos Agree to the ACME server&#39;s Subscriber Agreement -m EMAIL Email address for important account notifications<br/> More detailed help:<br/> -h, --help [TOPIC] print this message, or detailed help on a topic; the available TOPICS are:<br/> all, automation, commands, paths, security, testing, or any of the subcommands or plugins (certonly, renew, install, register, nginx, apache, standalone, webroot, etc.) -h all print a detailed help page including all topics --version print the version number - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre> </div> <!-- endregion --> <p> Here is the help message for the <code>certbot certonly</code> subcommand: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idefccba0bb32d'><button class='copyBtn' data-clipboard-target='#idefccba0bb32d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot renew --help <span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -<br/> certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...<br/> Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the certificate. The most common SUBCOMMANDS and flags are:<br/> obtain, install, and renew certificates: (default) run Obtain &amp; install a certificate in your current webserver certonly Obtain or renew a certificate, but do not install it renew Renew all previously obtained certificates that are near expiry enhance Add security enhancements to your existing configuration -d DOMAINS Comma-separated list of domains to obtain a certificate for<br/> (the certbot apache plugin is not installed) --standalone Run a standalone webserver for authentication (the certbot nginx plugin is not installed) --webroot Place files in a server&#39;s webroot folder for authentication --manual Obtain certificates interactively, or using shell script hooks<br/> -n Run non-interactively --test-cert Obtain a test certificate from a staging server --dry-run Test &quot;renew&quot; or &quot;certonly&quot; without saving any certificates to disk<br/> manage certificates: certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-name or --cert-path) delete Delete a certificate (supply --cert-name)<br/> manage your account: register Create an ACME account unregister Deactivate an ACME account update_account Update an ACME account show_account Display account details --agree-tos Agree to the ACME server&#39;s Subscriber Agreement -m EMAIL Email address for important account notifications<br/> More detailed help:<br/> -h, --help [TOPIC] print this message, or detailed help on a topic; the available TOPICS are:<br/> all, automation, commands, paths, security, testing, or any of the subcommands or plugins (certonly, renew, install, register, nginx, apache, standalone, webroot, etc.) -h all print a detailed help page including all topics --version print the version number - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre> </div> <!-- endregion --> <p> Complete documentation for <code>certbot</code> is <a href='https://eff-certbot.readthedocs.io/en/stable/intro.html' target='_blank' rel="nofollow">here</a>. </p> <!-- endregion --> <!-- #region dns --> <h2 id="genericDns">Generic DNS Authentication</h2> <p> I used <code>certbot</code> to create a special file within the website as follows, without using <code>sudo</code>. I want a <a href='https://en.wikipedia.org/wiki/Wildcard_certificate' target='_blank' rel="nofollow">wildcard SSL certificate</a>, which <a href='https://certbot.eff.org/faq#does-let-s-encrypt-issue-wildcard-certificates' target='_blank' rel="nofollow">requires certbot to use DNS authentication</a>. </p> <p> BTW, the following options include two <code>-d</code> options, one for the domain apex (<code>mslinn.com</code>) and one for the subdomains (<code>*.mslinn.com</code>) </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idff5648611620'><button class='copyBtn' data-clipboard-target='#idff5648611620' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot certonly \ --manual \ --agree-tos \ --preferred-challenges dns-01 \ --rsa-key-size 4096 \ -d mslinn.com -d *.mslinn.com \ --config-dir ~/.certbot/mslinn.com/config \ --logs-dir ~/.certbot/mslinn.com/logs \ --work-dir ~/.certbot/mslinn.com/work <span class='unselectable'>Saving debug log to /home/mslinn/.certbot/mslinn.com/logs/letsencrypt.log Enter email address (used for urgent renewal and security notices) (Enter &#39;c&#39; to cancel): </span> mslinn@mslinn.com <br/><span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let&#39;s Encrypt project and the non-profit organization that develops Certbot? We&#39;d like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: </span>n <span class='unselectable'>Account registered. Requesting a certificate for mslinn.com and *.mslinn.com<br/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please deploy a DNS TXT record under the name:<br/> _acme-challenge.mslinn.com.<br/> with the following value:<br/> n6o3qMw5N7qxAzV4uNQePKxJjaw0f-Bo32CybWr-lhE<br/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue (This must be set up in addition to the previous challenges; do not remove, replace, or undo the previous challenge tasks yet. Note that you might be asked to create multiple distinct TXT records with the same name. This is permitted by DNS standards.) Before continuing, verify the TXT record has been deployed. Depending on the DNS provider, this may take some time, from a few seconds to multiple minutes. You can check if it has finished deploying with aid of online tools, such as the Google Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.mslinn.com. Look for one or more bolded line(s) below the line ';ANSWER'. It should show the value(s) you&rsquo;ve just added. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue Successfully received certificate. Certificate is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/fullchain.pem Key is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/privkey.pem This certificate expires on 2022-09-29. These files will be updated when the certificate renews. NEXT STEPS: - This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate&rsquo;s expiry date. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: * Donating to ISRG / Let&rsquo;s Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre> </div> <!-- endregion --> <p> I made a bash script (<code>~/.local/bin/sslCert</code>) to create SSL certificates, which performs the same steps as the above, but it works for any site, and can renew certificates as well as create them: </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,sslCert' download='sslCert' title='Click on the file name to download the file'>~/.local/bin/sslCert</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id9678c6f29f85"><button class='copyBtn' data-clipboard-target='#id9678c6f29f85'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; echo "Creates or updates an wildcard SSL certificate Usage: $(basename $0) DOMAIN Example: $(basename $0) scalacourses.com The relevant files are stored in ~/.certbot/DOMAIN/ Creating a new certificate requires an interactive user session; updating a certificate can be done in a cron job. Currently, the renew verb is capable of either renewing all installed certificates that are due to be renewed or renewing a single certificate specified by its name. If you would like to renew specific certificates by their domains, use the certonly command instead. The renew verb may provide other options for selecting certificates to renew in the future. Ask for help or search for solutions at https://community.letsencrypt.org See the logfile /home/mslinn/.certbot/ancientwarmth.com/logs/letsencrypt.log or re-run Certbot with -v for more details. BTW, you can test the date range of the certificate with this incantation: curl https://domain.com -vI --stderr - | grep 'date:' " exit 1 &#125; if [ -z "$1" ]; then help; fi DOMAIN="$1" if [ -d "$HOME/.certbot/$DOMAIN" ]; then # Renew existing certificate CMD=certonly # renew OPTIONS=--force-renew else # Make new certificate (interactively) CMD=certonly unset OPTIONS fi certbot "$CMD" $OPTIONS \ --agree-tos \ --config-dir "$HOME/.certbot/$DOMAIN/config" \ -d "$DOMAIN" -d "*.$DOMAIN" \ --email mslinn@mslinn.com \ --logs-dir "$HOME/.certbot/$DOMAIN/logs" \ --manual \ --preferred-challenges dns-01 \ --rsa-key-size 4096 \ --work-dir "$HOME/.certbot/$DOMAIN/work" </pre> <p> Here is the help message for the script: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id809ddf92e9b5'><button class='copyBtn' data-clipboard-target='#id809ddf92e9b5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sslCert <span class='unselectable'>Creates or updates an wildcard SSL certificate Usage: sslCert DOMAIN Example: sslCert scalacourses.com The relevant files are stored in ~/.certbot/DOMAIN/ Creating a new certificate requires an interactive user session; updating a certificate can be done in a cron job. </span></pre> </div> <!-- endregion --> <p> The following <code>crontab</code> entry causes the script to run every 60 days. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>crontab</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id598332cf8b86'><button class='copyBtn' data-clipboard-target='#id598332cf8b86' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>0 0 1 */2 * .local/bin/sslCert mslinn.com</pre> </div> <p> The next time the script was run, output looked like: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide0192d199d06'><button class='copyBtn' data-clipboard-target='#ide0192d199d06' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sslCert mslinn.com <span class='unselectable'>Saving debug log to /home/mslinn/.certbot/mslinn.com/logs/letsencrypt.log Renewing an existing certificate for mslinn.com and *.mslinn.com<br/> Successfully received certificate. Certificate is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/fullchain.pem Key is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/privkey.pem This certificate expires on 2022-12-13. These files will be updated when the certificate renews.<br/> NEXT STEPS: - This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate&#39;s expiry date.<br/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: * Donating to ISRG / Let&#39;s Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre> </div> <!-- endregion --> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <!-- endregion --> <!-- #region checking with With A Web Browser --> <h2 id="chrome">Checking the Certificate With A Web Browser</h2> <p> Microsoft Windows caches SSL certificates, which can prevent your web browser from detecting newly updated SSL certificates. To clear the Windows SSL cache: </p> <ol> <li>Press the <kbd>Windows</kbd> key, type <code>Internet Options</code>, and press <kbd>Enter</kbd>.</li> <li>Select the <b>Content</b> tab.</li> <li>Click the <kbd>Clear SSL state</kbd> button.</li> <li>Click the <kbd>OK</kbd> button.</li> </ol> <!-- endregion --> Considering Cloudflare R2 for Static Websites 2022-06-09T00:00:00-04:00 https://mslinn.github.io/blog/2022/06/09/cloudflare-r2 <p> After deciding to <a href='/blog/2022/05/26/aws-hijacking.html'>close my AWS account</a>, I started looking for alternatives to host my static websites. Without getting into my selection criteria, my short list of possible hosting companies was <a href='https://azure.microsoft.com/' target='_blank' rel="nofollow">Microsoft Azure</a>, <a href='https://www.cloudflare.com' target='_blank' rel="nofollow">Cloudflare</a>, <a href='https://www.digitalocean.com/' target='_blank' rel="nofollow">Digital Ocean</a>, <a href='https://www.linode.com' target='_blank' rel="nofollow">Linode</a>, and <a href='https://www.netlify.com/' target='_blank' rel="nofollow">Netlify</a>. Many other options exist. </p> <p> Cloudflare has a free tier that has no time limit. It includes an S3 work-alike called R2, SSL and a built-in world-wide CDN that works automatically. 250 GB of storage and 1 TB/month of transfer are provided at no charge, forever. There are also no ingress or egress charges. <a href='https://www.fool.com/investing/2022/05/02/this-tumbling-cloud-stock-is-still-way-too-expensi/' target='_blank' rel="nofollow">Cloudflare&rsquo;s edge network</a> now spans 275 cities around the world, with nearly all Internet users within 50 milliseconds of a Cloudflare server. </p> <p> This article documents my experience with Cloudflare. If you don't care about details, and want to know my verdict on Cloudflare R2, <a href='#verdict'>skip to the end</a>. </p> <p class="alert rounded shadow"> For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc.: &ldquo;pay-as-you-go&rdquo; is shorthand for &ldquo;there is nothing you can do to limit your financial liability&rdquo;. </p> <h2 id="history">This is what I did</h2> <p> After creating an account, I followed the directions at <a href='https://developers.cloudflare.com/r2/get-started/' target='_blank' rel="nofollow">R2 get started guide</a>. </p> <h2 id="wrangler">Wrangler</h2> <p> The directions told me to set up <a href='https://github.com/cloudflare/wrangler2' target='_blank' rel="nofollow">Wrangler v2</a>, a command-line interface for transferring files to R2. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8b17decfa1b8'><button class='copyBtn' data-clipboard-target='#id8b17decfa1b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g wrangler <span class='unselectable'>npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead. npm WARN deprecated rollup-plugin-inject@3.0.2: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.<br/> added 56 packages, and audited 57 packages in 8s<br/> 10 high severity vulnerabilities<br/> To address issues that do not require attention, run: npm audit fix<br/> To address all issues, run: npm audit fix --force<br/> Run `npm audit` for details. </span></pre> </div> <p> The above messages do not inspire confidence. Wrangler is written using Node. I believe that Node has more security issues than any other computer language, particularly in <a href='https://turingpoint.de/en/blog/node-package-manager-security-everything-about-npm-package-security/' target='_blank' rel="nofollow">package management</a>. <a href='https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html' target='_blank' rel="nofollow">The OWasp recommendations</a> do not address the <a href='https://news.ycombinator.com/item?id=29245080' target='_blank' rel="nofollow">fundamental security vulnerabilities</a> in the Node package management infrastructure. </p> <h2 id="rclone">RClone</h2> <p> I looked for alternatives to Wrangler and found <a href='https://rclone.org/docs/' target='_blank' rel="nofollow">RClone</a>. RClone is a command-line program to manage files on cloud storage. It has <a href='https://rclone.org/docs/#subcommands' target='_blank' rel="nofollow">many subcommands</a>, including two types of sync. Although the RClone documentation does not mention Cloudflare, the <a href='https://developers.cloudflare.com/r2/examples/rclone/' target='_blank' rel="nofollow">Cloudflare docs described how to set up RClone</a>. </p> <h2 id="limits">Limits</h2> <p> <a href='https://developers.cloudflare.com/workers/platform/limits/' target='_blank' rel="nofollow">Platform limits</a> are important. Not only are the technical limits important for defining inputs and outputs, users should be particularly interested in spend limits, so they are not subject to unlimited financial liability. </p> <div class='quote'> <div class='quoteText clearfix'> Workers Paid plan is separate from any other Cloudflare plan (Free, Professional, Business) you may have. If you are an Enterprise customer, reach out to your account team to confirm pricing details. <br><br> Only requests that hit a Worker will count against your limits and your bill. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://developers.cloudflare.com/workers/platform/pricing/#fine-print' rel='nofollow' target='_blank'>Cloudflare Workers Pricing Fine Print</a></div> </div> <h2 id="setup">Setup</h2> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cloudflare/bucket0.svg" type="image/svg"> <!---<source srcset="/blog/images/cloudflare/bucket0.avif" type="image/avif">--> <source srcset="/blog/images/cloudflare/bucket0.webp" type="image/webp"> <source srcset="/blog/images/cloudflare/bucket0.apng" type="image/apng"> <source srcset="/blog/images/cloudflare/bucket0.png" type="image/png"> <source srcset="/blog/images/cloudflare/bucket0.jpg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/bucket0.jpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/bucket0.jfif" type="image/jpeg"> <source srcset="/blog/images/cloudflare/bucket0.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/bucket0.pjp" type="image/jpeg"> <source srcset="/blog/images/cloudflare/bucket0.gif" type="image/gif"> <source srcset="/blog/images/cloudflare/bucket0.tif" type="image/tiff"> <source srcset="/blog/images/cloudflare/bucket0.tiff" type="image/tiff"> <source srcset="/blog/images/cloudflare/bucket0.bmp" type="image/bmp"> <source srcset="/blog/images/cloudflare/bucket0.ico" type="image/x-icon"> <source srcset="/blog/images/cloudflare/bucket0.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/cloudflare/bucket0.png" style='width: 100%; ' /> </picture> </div> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cloudflare/analytics0.svg" type="image/svg"> <!---<source srcset="/blog/images/cloudflare/analytics0.avif" type="image/avif">--> <source srcset="/blog/images/cloudflare/analytics0.webp" type="image/webp"> <source srcset="/blog/images/cloudflare/analytics0.apng" type="image/apng"> <source srcset="/blog/images/cloudflare/analytics0.png" type="image/png"> <source srcset="/blog/images/cloudflare/analytics0.jpg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/analytics0.jpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/analytics0.jfif" type="image/jpeg"> <source srcset="/blog/images/cloudflare/analytics0.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/analytics0.pjp" type="image/jpeg"> <source srcset="/blog/images/cloudflare/analytics0.gif" type="image/gif"> <source srcset="/blog/images/cloudflare/analytics0.tif" type="image/tiff"> <source srcset="/blog/images/cloudflare/analytics0.tiff" type="image/tiff"> <source srcset="/blog/images/cloudflare/analytics0.bmp" type="image/bmp"> <source srcset="/blog/images/cloudflare/analytics0.ico" type="image/x-icon"> <source srcset="/blog/images/cloudflare/analytics0.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/cloudflare/analytics0.png" style='width: 100%; ' /> </picture> </div> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cloudflare/billing0.svg" type="image/svg"> <!---<source srcset="/blog/images/cloudflare/billing0.avif" type="image/avif">--> <source srcset="/blog/images/cloudflare/billing0.webp" type="image/webp"> <source srcset="/blog/images/cloudflare/billing0.apng" type="image/apng"> <source srcset="/blog/images/cloudflare/billing0.png" type="image/png"> <source srcset="/blog/images/cloudflare/billing0.jpg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/billing0.jpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/billing0.jfif" type="image/jpeg"> <source srcset="/blog/images/cloudflare/billing0.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/billing0.pjp" type="image/jpeg"> <source srcset="/blog/images/cloudflare/billing0.gif" type="image/gif"> <source srcset="/blog/images/cloudflare/billing0.tif" type="image/tiff"> <source srcset="/blog/images/cloudflare/billing0.tiff" type="image/tiff"> <source srcset="/blog/images/cloudflare/billing0.bmp" type="image/bmp"> <source srcset="/blog/images/cloudflare/billing0.ico" type="image/x-icon"> <source srcset="/blog/images/cloudflare/billing0.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/cloudflare/billing0.png" style='width: 100%; ' /> </picture> </div> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cloudflare/workers0.svg" type="image/svg"> <!---<source srcset="/blog/images/cloudflare/workers0.avif" type="image/avif">--> <source srcset="/blog/images/cloudflare/workers0.webp" type="image/webp"> <source srcset="/blog/images/cloudflare/workers0.apng" type="image/apng"> <source srcset="/blog/images/cloudflare/workers0.png" type="image/png"> <source srcset="/blog/images/cloudflare/workers0.jpg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers0.jpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers0.jfif" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers0.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers0.pjp" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers0.gif" type="image/gif"> <source srcset="/blog/images/cloudflare/workers0.tif" type="image/tiff"> <source srcset="/blog/images/cloudflare/workers0.tiff" type="image/tiff"> <source srcset="/blog/images/cloudflare/workers0.bmp" type="image/bmp"> <source srcset="/blog/images/cloudflare/workers0.ico" type="image/x-icon"> <source srcset="/blog/images/cloudflare/workers0.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/cloudflare/workers0.png" style='width: 100%; ' /> </picture> </div> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cloudflare/workers1.svg" type="image/svg"> <!---<source srcset="/blog/images/cloudflare/workers1.avif" type="image/avif">--> <source srcset="/blog/images/cloudflare/workers1.webp" type="image/webp"> <source srcset="/blog/images/cloudflare/workers1.apng" type="image/apng"> <source srcset="/blog/images/cloudflare/workers1.png" type="image/png"> <source srcset="/blog/images/cloudflare/workers1.jpg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers1.jpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers1.jfif" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers1.pjp" type="image/jpeg"> <source srcset="/blog/images/cloudflare/workers1.gif" type="image/gif"> <source srcset="/blog/images/cloudflare/workers1.tif" type="image/tiff"> <source srcset="/blog/images/cloudflare/workers1.tiff" type="image/tiff"> <source srcset="/blog/images/cloudflare/workers1.bmp" type="image/bmp"> <source srcset="/blog/images/cloudflare/workers1.ico" type="image/x-icon"> <source srcset="/blog/images/cloudflare/workers1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/cloudflare/workers1.png" style='width: 100%; ' /> </picture> </div> <h2 id="cache">Caching</h2> <p> <a href='https://dash.cloudflare.com/de87d0e0cbc74c70e8feb87e9671cdd0/mslinn.com/caching/configuration' target='_blank' rel="nofollow">Cache control</a>. </p> <h2 id="pages">Pages</h2> <div class='quote'> <div class='quoteText clearfix'>> Cloudflare Pages is a JAMstack platform for frontend developers to collaborate and deploy websites. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://pages.cloudflare.com' rel='nofollow' target='_blank'>pages.cloudflare.com</a></div> </div> <p> Cloudflare Pages supports <a href='https://developers.cloudflare.com/pages/framework-guides/deploy-a-jekyll-site/' target='_blank' rel="nofollow">Jekyll sites</a>. However, it looks like Cloudflare Pages builds the site in the cloud. While this might be a useful mechanism for many, my Jekyll builds need to access my local machine, and use my <a href="/jekyll/3000-jekyll-plugins.html">Jekyll plugins</a>. </p> <p> <a href='https://developers.cloudflare.com/workers/platform/sites/start-from-existing' target='_blank' rel="nofollow">Cloudflare Workers Sites</a> suits my use case. The <a href='https://developers.cloudflare.com/workers/platform/sites/start-from-existing' target='_blank' rel="nofollow">Start From Existing</a> documentation looks appropriate, except it is written for using Wrangler, which I view as a security threat. </p> <h2 id="verdict">The Verdict: No to Cloudflare</h2> <p class="alert rounded shadow"> CloudFlare does not offer a spend limit for accounts on paid plans. This is unacceptable. I tried to remove my credit card, but found I could not. I then deleted my user account, and saw: </p> <p class="warning shadow rounded"> It could take up to 12 months to delete your information completely. </p> <p> There is no way to frame this as an example of how Cloudflare is looking out for the best interests of their customers. </p> Upgrading PostgreSQL Ubuntu 2022-05-28T00:00:00-04:00 https://mslinn.github.io/blog/2022/05/28/psql-upgrade <!-- #region intro --> <p> This article describes my recommended steps to follow when upgrading a Postgres database forced upon you when upgrading Ubuntu, and the new OS includes a new release of Postgres. </p> <p> I use PostgreSQL as the database for <a href='https://ScalaCourses.com'><code>ScalaCourses.com</code></a>. The site is served from an Ubuntu instance, and there is a backup instance. </p> <p> When upgrading to Ubuntu 22.04, PostgreSQL upgraded from v13 to v14; I used <code>pg_upgrade</code>, which required temporarily swapping the ports that Postgres used. </p> <p> However, I used <code>pg_upgradecluster</code> when upgrading to Ubuntu 23.04; and PostgreSQL upgraded from v14 to v15. Unfortunately, the documented procedure to use <code>pg_upgradecluster</code> has no consideration for <code>systemd</code>, and this makes the upgrade slightly more awkward than it might otherwise be. </p> <p> While both approaches have issues, I found that it is easier and faster to upgrade a Postgres installation using <code>pg_upgradecluster</code> than using <code>pg_upgrade</code>. </p> <!-- endregion --> <!-- #region Upgrade Backup System First --> <h2 id="upgradeubuntu">Upgrade Backup System First</h2> <p> One of the benefits of a backup system is that you can work through upgrade problems on the backup, before doing the same with the live instance. </p> <p> <a href='https://gorails.com/guides/upgrading-postgresql-version-on-ubuntu-server' target='_blank' rel="nofollow">This article</a> suggests to upgrade the entire system to Ubuntu 23.04, then upgrade the active Postgres database cluster. The alternative would be to add a new PPA for Postgres, and just update that program before upgrading the entire system; this merely substitutes one set of potential issues for another. The article also demonstrates renaming the extra database cluster that the Postgres upgrade creates, but that just creates cruft since it is not required for anything. </p> <p> I decided to delete the new database cluster instead, since upgrading the old database cluster is all that is required. As you will read in the <a href='#ug'>Upgrade Procedure</a> section, this is also the advice given during the upgrade procedure. </p> <p> I decided to first <a href='https://help.ubuntu.com/community/LunarUpgrades' target='_blank' rel="nofollow">upgrade the Ubuntu OS</a>, then migrate the Postgres database cluster. </p> <!-- endregion --> <!-- #region Upgrade Removed NVidia Support --> <h2 id="nvidia">Upgrade Removed NVidia Support</h2> <p> After running <code>do-release upgrade</code> on the production system, the system worked fine, except the console had no graphics. I fixed it by automatically installing all video drivers: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id129c992aee50'><button class='copyBtn' data-clipboard-target='#id129c992aee50' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ubuntu-drivers autoinstall</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region pg_lsclusters --> <h2 id="pg_lsclusters">Examining the Postgres Databases</h2> <p> <code>pg_lsclusters</code> displays the available PostgreSQL database clusters. Prior to upgrading to Postgres 15, I had never deleted the old versions. They had quietly accumulated over the 11 years that I had been running ScalaCourses: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id69cb7d399c71'><button class='copyBtn' data-clipboard-target='#id69cb7d399c71' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pg_lsclusters <span class='unselectable'>Ver Cluster Port Status Owner Data directory Log file 9.5 main 5431 online postgres /var/lib/postgresql/9.5/main /var/log/postgresql/postgresql-9.5-main.log 9.6 main 5433 online postgres /var/lib/postgresql/9.6/main /var/log/postgresql/postgresql-9.6-main.log 10 main 5434 online postgres /var/lib/postgresql/10/main /var/log/postgresql/postgresql-10-main.log 12 main 5432 online postgres /var/lib/postgresql/12/main /var/log/postgresql/postgresql-12-main.log 13 main 5435 online postgres /var/lib/postgresql/13/main /var/log/postgresql/postgresql-13-main.log 14 main 5436 online postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log </span></pre> </div> <p> Listing the databases in the active cluster showed an unnecessary database: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0eb939f06578'><button class='copyBtn' data-clipboard-target='#id0eb939f06578' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres psql -l <span class='unselectable'>List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges --------------+----------+----------+-------------+-------------+----------------------- bix_dev | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | postgres | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | scalacourses | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | template0 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres (5 rows) </span></pre> </div> <p> I do not care about the <code>bix_dev</code> database, so I decided to delete it. The database that I care about is called <code>scalacourses</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc2dffb259a00'><button class='copyBtn' data-clipboard-target='#idc2dffb259a00' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres psql -c 'drop database bix_dev;' <span class='unselectable'>psql (14.7 (Ubuntu 14.7-0ubuntu0.22.10.1)) Type "help" for help. DROP DATABASE </span></pre> </div> <!-- endregion --> <!-- #region Upgrade Procedure --> <h2 id="ug">Upgrade Procedure</h2> <p> Ubuntu is normally upgraded by running <code>do-release-upgrade</code>. Near the end of that process, the following message appeared. </p> <p> At this point <code>postgresql-15</code> and <code>postgresql-client-15</code> had just been installed. The message did not reflect the true state of the system, which caused me some consternation for a moment. </p> <p class="quote shadow rounded"> The PostgreSQL version 14 is obsolete, but the server or client packages are still installed. Please install the latest packages (<code>postgresql-15</code> and <code>postgresql-client-15</code>) and upgrade the existing clusters with pg_upgradecluster (<a href='https://manpages.ubuntu.com/manpages/trusty/man8/pg_upgradecluster.8.html' target='_blank' rel="nofollow">see manpage</a>). <br><br> Please be aware that the installation of postgresql-15 will automatically create a default cluster 15/main. If you want to upgrade the 14/main cluster, you need to remove the already existing 15 cluster (<code>pg_dropcluster --stop 15 main</code>, see manpage for details). <br><br> The old server and client packages are no longer supported. After the existing clusters are upgraded, the <code>postgresql-14</code> and <code>postgresql-client-14</code> packages should be removed. <br><br> Please see /usr/share/doc/postgresql-common/README.Debian.gz for details. </p> <p> Following is <code>/usr/share/doc/postgresql-common/README.Debian.gz</code>, which was referenced by the above message. Most of that file is uninteresting, however two sections are important, and we will look at them. </p> <!-- #region README.Debian.gz --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/usr/share/doc/postgresql-common/README.Debian.gz</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id25725248ac40'><button class='copyBtn' data-clipboard-target='#id25725248ac40' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>PostgreSQL for Debian =====================<br/> PostgreSQL is a fully featured object-relational database management system. It supports a large part of the SQL standard and is designed to be extensible by users in many aspects. Its features include ACID transactions, foreign keys, views, sequences, subqueries, triggers, outer joins, multiversion concurrency control, and user-defined types and functions.<br/> Since the on-disk data format of all major PostgreSQL versions (like 9.6, 11, etc.) is incompatible to each other, Debian&#39;s PostgreSQL packaging architecture is designed to maintain clusters of different major versions in parallel.<br/> This postgresql-common package provides the common infrastructure and all frontend programs that users and administrators use. The version specific server and client programs are shipped in postgresql-*-&lt;version&gt; packages.<br/> For a detailed description of the architecture, please see<br/> /usr/share/doc/postgresql-common/README.md.gz<br/> First steps for the impatient ----------------------------- Eventually you will not get around reading at least some parts of the manual, but if you want to get straight into playing SQL, here are the steps to create a database user and a database for the Unix user &#39;joe&#39;:<br/> 1. Install a database server with the major version of your choice (&#39;postgresql-XY&#39;, e. g. &#39;postgresql-11&#39;). Preferably the latest version, which you can get by installing the metapackage &#39;postgresql&#39;. This will automatically create a default cluster &#39;main&#39; with the database superuser &#39;postgres&#39;.<br/> 2. Get a shell for the database superuser &#39;postgres&#39;. If your system has an active root user, use su:<br/> # su -s /bin/bash postgres<br/> If your system uses sudo to get administrative rights, use sudo instead:<br/> joe$ sudo -u postgres bash<br/> 3. In this postgres shell, create a database user with the same name as your Unix login:<br/> $ createuser -DRS joe<br/> For details about the options, see createuser(1).<br/> 4. Create a database &quot;joework&quot; which is owned by &quot;joe&quot;:<br/> $ createdb -O joe joework<br/> For details about the options, see createdb(1).<br/> 5. Exit the postgres shell.<br/> 6. As user joe, you should now be able to connect to your database with<br/> $ psql joework<br/> Cluster management ------------------ For managing clusters, the following commands are provided (each with its own manual page):<br/> pg_createcluster - Create a new cluster or integrate an existing one into the postgresql-common architecture. pg_dropcluster - Completely remove a cluster. pg_ctlcluster - Control the server process of a cluster (start, stop, restart). pg_lsclusters - Show a list of all existing clusters and their status. pg_upgradecluster - Migrate a cluster from one major version to another one. pg_renamecluster - Rename a cluster.<br/> Please note that you can of course also use the upstream tools for creating clusters, such as initdb(1). However, please note that in this case you cannot expect *any* of above pg_* tools to work, since they use different configuration settings (SSL, data directories, etc.) and file locations (e. g. /etc/postgresql/11/main/postgresql.conf). If in doubt, then do *not* use initdb, but only pg_createcluster. Since merely installing postgresql-NN will already set up a default cluster which is ready to work, most people do not need to bother about initdb or pg_createcluster at all.<br/> Port assignment --------------- Please note that the pg_* tools automatically manage the server ports unless you specify them manually. The first cluster which is ever created (by any major version) will run on the default port 5432, and each new cluster will use the next higher free one.<br/> E. g. if you first install &quot;postgresql-11&quot; on a clean system, the default 11/main cluster will run on port 5432. If you then create another 11 cluster, or install the &quot;postgresql-12&quot; package, that new one will run on 5433.<br/> Please use &quot;pg_lsclusters&quot; for displaying the cluster &lt;-&gt; port mapping, and please have a look at the pg_createcluster manpage (the --port option) for details.<br/> Default clusters and upgrading ------------------------------ When installing a postgresql-NN package from scratch, a default cluster &#39;main&#39; will automatically be created. This operation is equivalent to doing &#39;pg_createcluster NN main --start&#39;.<br/> Due to this default cluster, an immediate attempt to upgrade an earlier &#39;main&#39; cluster to a new version will fail and you need to remove the newer default cluster first. E. g., if you have postgresql-9.6 installed and want to upgrade to 11, you first install postgresql-11:<br/> apt-get install postgresql-11<br/> Then drop the default 11 cluster that was just created:<br/> pg_dropcluster 11 main --stop<br/> And then upgrade the 9.6 cluster to the latest installed version (e. g. 11):<br/> pg_upgradecluster 9.6 main<br/> SSL --- The PostgreSQL server packages support SSL, which provides encrypted and authenticated network communication. SSL should be used if you have an untrusted network between a database server and a client and these exchange security sensitive data like passwords or confidential database contents.<br/> When a cluster is created with pg_createcluster, SSL support will automatically be enabled. postgresql-common makes use of the &#39;snakeoil&#39; SSL certificate that is generated by the ssl-cert package, so that SSL works out of the box (ssl_cert_file, ssl_key_file). In addition, if /etc/postgresql-common/root.crt exists, it will be used as CA certificate file (ssl_ca_file).<br/> /etc/postgresql-common/root.crt is a dummy file by default, so that client-side authentication is not performed. To enable it, you should add some root certificates to it. A reasonable choice is to just symlink the file to /etc/ssl/certs/ssl-cert-snakeoil.pem; in this case, client certificates need to be signed by the snakeoil certificate, which might be desirable in many cases. See<br/> /usr/share/doc/postgresql-doc-11/html/ssl-tcp.html<br/> for details (in package postgresql-doc).<br/> Further documentation --------------------- All commands shipped by postgresql-common have detailed manpages. See postgresql-common(7) for the documentation of the database client program wrapping, and user_clusters(5) and postgresqlrc(5) for the cluster configuration.<br/> The documentation of the database server and client functions, SQL commands, modules, etc. documented is shipped in the per-version packages postgresql-doc-&lt;version&gt;.</pre> </div> <!-- endregion --> <p> The <b>Cluster management</b> section in the above file provides some background information for our purposes, so I repeat it here: </p> <!-- #region Cluster management --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Cluster management</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb504419e0160'><button class='copyBtn' data-clipboard-target='#idb504419e0160' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>For managing clusters, the following commands are provided (each with its own manual page): pg_createcluster - Create a new cluster or integrate an existing one into the postgresql-common architecture. pg_dropcluster - Completely remove a cluster. pg_ctlcluster - Control the server process of a cluster (start, stop, restart). pg_lsclusters - Show a list of all existing clusters and their status. pg_upgradecluster - Migrate a cluster from one major version to another one. pg_renamecluster - Rename a cluster. Please note that you can of course also use the upstream tools for creating clusters, such as initdb(1). However, please note that in this case you cannot expect *any* of above pg_* tools to work, since they use different configuration settings (SSL, data directories, etc.) and file locations (e. g. /etc/postgresql/11/main/postgresql.conf). If in doubt, then do *not* use initdb, but only pg_createcluster. Since merely installing postgresql-NN will already set up a default cluster which is ready to work, most people do not need to bother about initdb or pg_createcluster at all.</pre> </div> <!-- endregion --> <p> The <b>Default clusters and upgrading</b> section is more important, however it contained errors. I fixed the errors in the following instructions. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Default clusters and upgrading</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddd37a0c46f43'><button class='copyBtn' data-clipboard-target='#iddd37a0c46f43' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>When installing a postgresql-NN package from scratch, a default cluster 'main' will automatically be created. This operation is equivalent to doing 'pg_createcluster NN main --start'. Due to this default cluster, an immediate attempt to upgrade an earlier 'main' cluster to a new version will fail and you need to remove the newer default cluster first. E. g., if you have postgresql-9.6 installed and want to upgrade to 11, you first install postgresql-11: yes | sudo apt install postgresql-11 Then drop the default 11 cluster that was just created: sudo -iu postgres pg_dropcluster 11 main --stop And then upgrade the 9.6 cluster to the latest installed version (e. g. 11): sudo -iu postgres pg_upgradecluster 9.6 main</pre> </div> <!-- endregion --> <p> <a href='https://freedesktop.org/wiki/Software/systemd/' target='_blank' rel="nofollow">Systemd</a> can significantly affect the upgrade process, and unfortunately it is completely ignored by the above instructions. I muddled through, as you will see. </p> <!-- endregion --> <!-- #region transcript --> <h2 id="trans">Transcript</h2> <p> The following is a transcript of what I actually did: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide92520aaae37'><button class='copyBtn' data-clipboard-target='#ide92520aaae37' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres pg_dropcluster --stop 15 main <span class='unselectable'>Warning: stopping the cluster using pg_ctlcluster will mark the systemd unit as failed. Consider using systemctl: sudo systemctl stop postgresql@15-main Warning: systemd was not informed about the removed cluster yet. Operations like "service postgresql start" might fail. To fix, run: sudo systemctl daemon-reload </span> <span class='unselectable'>$ </span>sudo -iu postgres pg_upgradecluster 14 main <span class='unselectable'>Stopping old cluster... Warning: stopping the cluster using pg_ctlcluster will mark the systemd unit as failed. Consider using systemctl: sudo systemctl stop postgresql@14-main Restarting old cluster with restricted connections... Notice: extra pg_ctl/postgres options given, bypassing systemctl for start operation Creating new PostgreSQL cluster 15/main ... /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/15/main --auth-local peer --auth-host scram-sha-256 --no-instructions --encoding UTF8 --lc-collate en_CA.UTF-8 --lc-ctype en_CA.UTF-8 The files belonging to this database system will be owned by user "postgres". This user must also own the server process. The database cluster will be initialized with locale "en_CA.UTF-8". The default text search configuration will be set to "english". Data page checksums are disabled. fixing permissions on existing directory /var/lib/postgresql/15/main ... ok creating subdirectories ... ok selecting dynamic shared memory implementation ... posix selecting default max_connections ... 100 selecting default shared_buffers ... 128MB selecting default time zone ... America/Toronto creating configuration files ... ok running bootstrap script ... ok performing post-bootstrap initialization ... ok syncing data to disk ... ok Warning: systemd does not know about the new cluster yet. Operations like "service postgresql start" will not handle it. To fix, run: sudo systemctl daemon-reload Copying old configuration files... Copying old start.conf... Copying old pg_ctl.conf... Starting new cluster... Notice: extra pg_ctl/postgres options given, bypassing systemctl for start operation Roles, databases, schemas, ACLs... set_config ------------ (1 row) set_config ------------ (1 row) set_config ------------ (1 row) set_config ------------ (1 row) Fixing hardcoded library paths for stored procedures... Upgrading database template1... Analyzing database template1... Fixing hardcoded library paths for stored procedures... Upgrading database postgres... Analyzing database postgres... Fixing hardcoded library paths for stored procedures... Upgrading database scalacourses... Analyzing database scalacourses... Stopping target cluster... Stopping old cluster... Disabling automatic startup of old cluster... Starting upgraded cluster on port 5432... Warning: the cluster will not be running as a systemd service. Consider using systemctl: sudo systemctl start postgresql@15-main Success. Please check that the upgraded cluster works. If it does, you can remove the old cluster with pg_dropcluster 14 main Ver Cluster Port Status Owner Data directory Log file 14 main 5433 down postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log Ver Cluster Port Status Owner Data directory Log file 15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log </span></pre> </div> <!-- endregion --> <p> When I attempted to following the above instructions for systemd, errors appeared: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9d0591a51ddb'><button class='copyBtn' data-clipboard-target='#id9d0591a51ddb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl daemon-reload <span class='unselectable'>$ </span>sudo systemctl start postgresql@15-main <span class='unselectable'>Job for postgresql@15-main.service failed because the service did not take the steps required by its unit configuration. See "systemctl status postgresql@15-main.service" and "journalctl -xeu postgresql@15-main.service" for details. </span></pre> </div> <!-- endregion --> <p> Great, an obscure failure message. <a href='https://askubuntu.com/questions/1311254/postgres-12-on-ubuntu-20-04-replication-checkpoint-has-wrong-magic-539122744-in' target='_blank' rel="nofollow">Others have fussed with this problem.</a> I expected that rebooting would be much faster than trying to fix it, and that did prove to be the case: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id654b5d5b97f1'><button class='copyBtn' data-clipboard-target='#id654b5d5b97f1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo reboot</pre> </div> <!-- endregion --> <p> After rebooting, the upgraded Postgres cluster worked fine: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6c318af9fcf3'><button class='copyBtn' data-clipboard-target='#id6c318af9fcf3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pg_lsclusters <span class='unselectable'>Ver Cluster Port Status Owner Data directory Log file 14 main 5433 down postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log 15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log </span> <span class='unselectable'>$ </span>sudo -iu postgres psql -l <span class='unselectable'>List of databases Name | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges --------------+--------------+----------+-------------+-------------+------------+-----------------+----------------------- postgres | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | scalacourses | scalacourses | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | template0 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | =c/postgres + | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | =c/postgres + | | | | | | | postgres=CTc/postgres (4 rows) </span></pre> </div> <!-- endregion --> <p> Here is how I deleted the older database clusters: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc59c92ca38b3'><button class='copyBtn' data-clipboard-target='#idc59c92ca38b3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>for X in 9.5 9.6 10 12 13; do sudo -iu postgres pg_dropcluster --stop $X main end</pre> </div> <!-- endregion --> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> All done! </p> <!-- endregion --> Montréal International vs. Bill 96 2022-05-27T00:00:00-04:00 https://mslinn.github.io/blog/2022/05/27/mi-bill96 <p> Last night I attended the 25th Annual General Meeting of <a href='https://www.montrealinternational.com/en/' target='_blank' rel="nofollow">Montréal International</a>, an energetic organization dedicated to attracting foreign business to establish a presence in the greater Montréal region. My key takeaways were: </p> <ol> <li> I spoke with about 20 Montréal International employees; all of them were bilingual or trilingual, intelligent, motivated and well-informed. Their talent and commitment is palpable. Clearly, the hiring process is successful. </li> <li> The event was marred by long-winded, self-congratulatory speeches by senior management, and former senior managers who were invited to a panel discussion. This lack of discipline caused the event to significantly run overtime. </li> <li> The proceedings were conducted entirely in French. For an organization that calls itself &ldquo;Montréal International&rdquo;, this is unforgivable. </li> <li> I asked every employee I spoke with if <a href='https://qcgn.ca/bill-96/' target='_blank' rel="nofollow">Quebec&rsquo;s new Bill 96</a> impacted their goals. All of them expressed grave concern, and said that their mandate to attract foreign business had become extremely difficult as a result. However, not one mention was made by anyone on stage about Bill 96. </li> </ol> <p> Montréal International&rsquo;s mandate is severely impacted by Quebec's Bill 96. If any organization has an intrinsic mandate to demonstrate public leadership towards repealing or modifying Bill 96 so it respects basic human rights, it would be Montréal International. Instead, Montréal International&rsquo;s leadership, which consists entirely of old white French-speaking alpha males, is complicit in their silence, and by their actions. </p> Limit Your Financial Vulnerability From AWS Account Hijacking 2022-05-26T00:00:00-04:00 https://mslinn.github.io/blog/2022/05/26/aws-hijacking <p class="alert shadow rounded"> I am a free agent, independent and critical. <a href='/softwareexpert/index.html'>You can buy my time as a software expert</a>, but not my opinion. Just because Amazon / AWS hired me in the past to help them against a patent troll does not mean they own me. Quite the contrary. </p> <p> In the time you take a quick shower, your AWS account can be highjacked and tens of thousands of dollars in fees can be incurred. EC2 is the service that provides the greatest financial risk. You can take steps to limit your liability, and this article shows you how. </p> <h2 id="backlash">Out-of-Control Cloud Costs</h2> <p> Out-of-control costs are causing cloud customers to reduce or eliminate their cloud use. <a href='https://techcrunch.com/2023/03/20/the-cloud-backlash-has-begun-why-big-data-is-pulling-compute-back-on-premises/' target='_blank' rel="nofollow">The cloud backlash has begun: Why big data is pulling compute back on premises</a> &ndash; Thomas Robinson, published by TechCrunch 2023-03-20. </p> <p> <a href='https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0' target='_blank' rel="nofollow">Why we&rsquo;re leaving the cloud</a>, by David Heinemeier Hansson, creator of Ruby on Rails and CTO of 37signals. Mr. Hansson also wrote <a href='https://world.hey.com/dhh/we-stand-to-save-7m-over-five-years-from-our-cloud-exit-53996caa' target='_blank' rel="nofollow">We stand to save $7m over five years from our cloud exit</a>. </p> <h2 id="nolimits">AWS IAM Users and Roles Have No Budget Limitations</h2> <div class="pullQuote"> AWS customers are exposed to unlimited financial liability </div> <p> I encountered two main issues when attempting to secure against AWS account hijacking: </p> <ol> <li> <b>AWS does not provide a mechanism to guard against launching expensive services.</b> For example, if an IAM user has a role that allows EC2 instances to be launched, then that user can launch an unlimited number of EC2 instances of any size. <br><br> The most expensive Amazon EC2 is currently the <a href='https://aws.amazon.com/ec2/instance-types/p4/' target='_blank' rel="nofollow"><code>p4de.24xlarge</code></a>, which costs $40.96 USD per hour on-demand in the US East (N. Virginia) region. The instance comes with 96 vCPUs, 1152 GiB of RAM and eight 1TB NVMe SSDs, and has eight NVIDIA A100 Tensor Core GPUs. <br><br> Using stolen credentials that allow EC2 instances to be launched, an attacker using scripts could spin up an armada of <code>p4de.24xlarge</code> instances and incur eye-popping costs in minutes. </li> <li> AWS Budgets provides a convenient way to shut down pre-designated services when a total budgetary amount is exceeded. <b>However, the AWS AMI security model is not integrated into real-time cost monitoring.</b> Thus, there is no way to automatically shut down newly launched service instances that exceed a pre-authorized budgetary amount. </li> </ol> <div class="alert rounded shadow"> <h2 id="vercel"><a href='https://vercel.com/pricing' target='_blank' rel="nofollow">Vercel&rsquo;s Pricing Policy Incorporates Limits</a></h2> <p> <i>We provide customers with tools to observe, control, and alert on their infrastructure spend with Spend Management. You can define a spend amount (e.g. $40) and receive email, web, and SMS notifications as you reach that amount. When reaching 100%, you can optionally automatically pause all projects with a hard limit. Over the past year, we've added features to the platform based on your feedback to help automatically prevent runaway spend, including recursion protection, improved function defaults, hard spend limits, Attack Challenge Mode, and more.</i> </p> <p> AWS, Azure, Google App Engine, IBM Cloud and Linode all need a pricing policy like Vercel&rsquo;s. </p> </div> <h2 id="orElse">Shared Security Responsibility</h2> <p> The <a href='https://aws.amazon.com/agreement/' target='_blank' rel="nofollow">AWS Customer Agreement</a> and <a href='https://aws.amazon.com/compliance/shared-responsibility-model/' target='_blank' rel="nofollow">Shared Responsibility Model</a> describe how AWS shares responsibility for security with users. If you follow the AWS security recommendations, described next, you will not be held accountable for extra charges if your AWS account is hijacked. </p> <p> The AWS Security Blog published a nice overview, entitled <a href='https://aws.amazon.com/blogs/security/getting-started-follow-security-best-practices-as-you-configure-your-aws-resources/' target='_blank' rel="nofollow">Getting Started: Follow Security Best Practices as You Configure Your AWS Resources</a>. Some of the same information is also provided in <a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html' target='_blank' rel="nofollow">Security best practices in IAM</a>. </p> <p class="alert rounded shadow"> Even though the AWS instructions do not prevent bad guys taking over your account, follow them anyway so you won't be held liable for the costs incurred if your AWS account is hijacked. </p> <h2 id="recommendations">AWS Security Recommendations</h2> <p> Security needs to have depth. Any single measure can fail, and given enough scale, all measures will eventually fail. Layer your security measures so that one failure, no matter how grave, will not be fatal. </p> <p> AWS recommends the following. I have highlighted what AWS personnel have described to me as being the quickest and easiest path; this article walks through those steps to the extent that I have been able. </p> <ol> <li> Set up at least two of the following services to monitor cost and usage: <ol> <li> <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html' class='bg_yellow>Managing' target='_blank' rel="nofollow"><span your costs with AWS Budgets </span></a> </li> <li> <a href='https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/gs_monitor_estimated_charges_with_cloudwatch.html#gs_creating_billing_alarm' class='bg_yellow>Create' target='_blank' rel="nofollow"><span a billing alarm Using CloudWatch</span></a>. </li> <li> <a href='https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html' target='_blank' rel="nofollow">CloudTrail User Guide</a>. </li> <li> <a href='https://docs.aws.amazon.com/waf/latest/developerguide/what-is-aws-waf.html' target='_blank' rel="nofollow">Web Application Firewall (WAF)</a>. </li> <li> <a href='https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html' target='_blank' rel="nofollow">Trusted Advisor</a>. <span class="bg_yellow">I found that Trusted Advisor was somewhat easier to use than CloudWatch.</span> </li> </ol> For more information about managing your AWS cost and usage, see the <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-costmanagement.html' target='_blank' rel="nofollow">AWS Cost Management User Guide</a>. </li> <li>Set up at least one of the following security best practices: <ol> <li> <a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable.html' class='bg_yellow>Using' target='_blank' rel="nofollow"><span multi-factor authentication (MFA) in AWS</span></a>. </li> <li> <a href='https://docs.aws.amazon.com/securityhub/index.html' target='_blank' rel="nofollow">AWS Security Hub</a>. </li> <li> <a href='https://docs.aws.amazon.com/guardduty/index.html' target='_blank' rel="nofollow">Amazon GuardDuty</a>. </li> </ol> </ol> <h2 id="easiestFirst">Do The Easiest Things First</h2> <p> I usually prefer to do the easiest things first. <a href='https://www.lifehack.org/articles/productivity/easy-tasks-difficult-tasks-first-which-one-more-productive.html' target='_blank' rel="nofollow">Not everyone agrees.</a> Doing even one highlighted item above provides a significant security improvement, so why wait? </p> <div class='quote'> <div class='quoteText clearfix'> The best is the enemy of the good. </div><div class='quoteAttribution'> &nbsp;&ndash; Voltaire </div> </div> <div class='quote'> <div class='quoteText clearfix'> Better a diamond with a flaw than a pebble without. </div><div class='quoteAttribution'> &nbsp;&ndash; Confucius </div> </div> <div class='quote'> <div class='quoteText clearfix'> Striving to better, oft we mar what&rsquo;s well. </div><div class='quoteAttribution'> &nbsp;&ndash; Shakespeare </div> </div> <h2 id="root">Root Credentials</h2> <p> If a bad guy has your root credentials, they can change budget limits. For greatest security, <a href='https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/security_credentials' target='_blank' rel="nofollow">delete your root credentials</a> by selecting <b>Access Keys (access key ID and secret access key)</b> as shown in the image below. However, if you do so, <a href='https://docs.aws.amazon.com/general/latest/gr/root-vs-iam.html#aws_tasks-that-require-root' target='_blank' rel="nofollow">many things become impossible</a>. </p> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/rootKeys.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/rootKeys.avif" type="image/avif">--> <source srcset="/blog/images/aws/rootKeys.webp" type="image/webp"> <source srcset="/blog/images/aws/rootKeys.apng" type="image/apng"> <source srcset="/blog/images/aws/rootKeys.png" type="image/png"> <source srcset="/blog/images/aws/rootKeys.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/rootKeys.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/rootKeys.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/rootKeys.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/rootKeys.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/rootKeys.gif" type="image/gif"> <source srcset="/blog/images/aws/rootKeys.tif" type="image/tiff"> <source srcset="/blog/images/aws/rootKeys.tiff" type="image/tiff"> <source srcset="/blog/images/aws/rootKeys.bmp" type="image/bmp"> <source srcset="/blog/images/aws/rootKeys.ico" type="image/x-icon"> <source srcset="/blog/images/aws/rootKeys.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/rootKeys.png" style='width: 100%; ' /> </picture> </div> <h3 id="mfa">Enabling MFA</h3> <div class='imgWrapper imgBlock right' style='width: 10em; '> <figure> <a href='https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/aws/yubikey.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/yubikey.avif" type="image/avif">--> <source srcset="/blog/images/aws/yubikey.webp" type="image/webp"> <source srcset="/blog/images/aws/yubikey.apng" type="image/apng"> <source srcset="/blog/images/aws/yubikey.png" type="image/png"> <source srcset="/blog/images/aws/yubikey.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/yubikey.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/yubikey.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/yubikey.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/yubikey.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/yubikey.gif" type="image/gif"> <source srcset="/blog/images/aws/yubikey.tif" type="image/tiff"> <source srcset="/blog/images/aws/yubikey.tiff" type="image/tiff"> <source srcset="/blog/images/aws/yubikey.bmp" type="image/bmp"> <source srcset="/blog/images/aws/yubikey.ico" type="image/x-icon"> <source srcset="/blog/images/aws/yubikey.cur" type="image/x-icon"> <img alt='Yubikey NFC' class="imgImg rounded shadow" src="/blog/images/aws/yubikey.png" style='width: 100%; ' title='Yubikey NFC' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL" target='_blank' > Yubikey NFC </a> </figcaption> </figure> </div> <p> The easiest highlighted item above is to enable multifactor authentication (MFA). </p> <p> I use a virtual MFA device, <a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html#enable-virt-mfa-for-root' target='_blank' rel="nofollow">enabled for my root account</a> Google Authenticator for <a href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2' target='_blank' rel="nofollow">Android</a> and <a href='https://apps.apple.com/us/app/google-authenticator/id388497605' target='_blank' rel="nofollow">iOS</a>. Google Authenticator features a <a href='https://tools.ietf.org/html/rfc6238' target='_blank' rel="nofollow">RFC 6238</a> standards-based TOTP (time-based one-time password) algorithm. </p> <p> I use a <a href='https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL' target='_blank' rel="nofollow">YubiKey</a> to provide MFA for the AMI user ID that I use to log into the AWS console for everyday work. </p> <p class="pullQuote"> Only use root credentials when absolutely necessary. </p> <h2 id="trustedAdvisor">Trusted Advisor</h2> <p> After adding MFA to root credentials, I found that using <a href='https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html' target='_blank' rel="nofollow">AWS Trusted Advisor</a> was the next easiest thing to try to make my AWS account secure. </p> <p> Once you visit the <a href='https://console.aws.amazon.com/trustedadvisor/home' target='_blank' rel="nofollow">Trusted Advisor Console</a>, and agree to enable Trusted Advisor, <a href='https://docs.aws.amazon.com/awssupport/latest/user/security-trusted-advisor.html' target='_blank' rel="nofollow">IAM permissions are added to the AWS IAM user</a> that you are logged in as. </p> <p> Usage seems straightforward; however, I found the information for securing S3 buckets puzzling at first: </p> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/trustedAdvisor1.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/trustedAdvisor1.avif" type="image/avif">--> <source srcset="/blog/images/aws/trustedAdvisor1.webp" type="image/webp"> <source srcset="/blog/images/aws/trustedAdvisor1.apng" type="image/apng"> <source srcset="/blog/images/aws/trustedAdvisor1.png" type="image/png"> <source srcset="/blog/images/aws/trustedAdvisor1.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor1.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor1.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor1.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor1.gif" type="image/gif"> <source srcset="/blog/images/aws/trustedAdvisor1.tif" type="image/tiff"> <source srcset="/blog/images/aws/trustedAdvisor1.tiff" type="image/tiff"> <source srcset="/blog/images/aws/trustedAdvisor1.bmp" type="image/bmp"> <source srcset="/blog/images/aws/trustedAdvisor1.ico" type="image/x-icon"> <source srcset="/blog/images/aws/trustedAdvisor1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/trustedAdvisor1.png" style='width: 100%; ' /> </picture> </div> <p> The column labeled <b>ACL Allows List</b> means that buckets flagged with <b>Yes</b> have an ACL that allows objects to be listed. AWS recommends that S3 buckets not allow objects to be listed by the public. To correct this: </p> <ol> <li>Click on the bucket name.</li> <li>Click on the <b>Permissions</b> tab in the page that opens next.</li> <li>Scroll down to <b>Access Control List</b>.</li> <li>Click on the <kbd>Edit</kbd> button.</li> <li>Disable the items marked in red, as shown below.</li> </ol> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/trustedAdvisor2.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/trustedAdvisor2.avif" type="image/avif">--> <source srcset="/blog/images/aws/trustedAdvisor2.webp" type="image/webp"> <source srcset="/blog/images/aws/trustedAdvisor2.apng" type="image/apng"> <source srcset="/blog/images/aws/trustedAdvisor2.png" type="image/png"> <source srcset="/blog/images/aws/trustedAdvisor2.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor2.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor2.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor2.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor2.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/trustedAdvisor2.gif" type="image/gif"> <source srcset="/blog/images/aws/trustedAdvisor2.tif" type="image/tiff"> <source srcset="/blog/images/aws/trustedAdvisor2.tiff" type="image/tiff"> <source srcset="/blog/images/aws/trustedAdvisor2.bmp" type="image/bmp"> <source srcset="/blog/images/aws/trustedAdvisor2.ico" type="image/x-icon"> <source srcset="/blog/images/aws/trustedAdvisor2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/trustedAdvisor2.png" style='width: 100%; ' /> </picture> </div> <p> Referring back to the previous screenshot, the <b>Policy Allows Access</b> column flags all S3 buckets used to serve webpages as insecure. To serve HTML pages and assets stored in an S3 bucket, the permissions must be set to allow objects to be read by everyone. Trusted Advisor flags these buckets as insecure, which does not make sense to me. </p> <p> Perhaps there is a better security policy for when webpages are served via CloudFront, but as is often the case with AWS documentation, it is difficult to piece together how to optimally configure S3 permissions with CloudFront options. </p> <h3 id="evolution">Best Practices Evolve With Technology</h3> <p> The AWS documentation is written as if the products described have always existed in their present state. In the course of researching this article, I discovered that AWS provides revision histories for their services. For example, the revision history of S3 is documented in the <a href='https://docs.aws.amazon.com/AmazonS3/latest/userguide/WhatsNew.html' target='_blank' rel="nofollow">AWS S3 User Guide</a>, and the CloudFront revision history is documented in the <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/WhatsNew.html' target='_blank' rel="nofollow">AWS CloudFront Developer Guide</a>. Revision histories are useful for experienced technologists to review periodically, so they can keep up with best practices as they evolve. </p> <p> I started using AWS S3 to serve websites in 2013, and at that time, CloudFront did not exist yet. As the two services matured, best practices, which were at first unknown, evolved and interactions between them became more complex. In particular, CloudFront <code>http</code>/<code>https</code> promotion, how origins are specified, and S3 permissions, all interact in ways that were not well documented for several years. This was a source of security problems and unwanted downtime for me. </p> <div class='imgWrapper imgFlex right' style='width: 12em; margin-right: 1em;'> <picture class='imgPicture'> <source srcset="/blog/images/aws/roadToHell.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/roadToHell.avif" type="image/avif">--> <source srcset="/blog/images/aws/roadToHell.webp" type="image/webp"> <source srcset="/blog/images/aws/roadToHell.apng" type="image/apng"> <source srcset="/blog/images/aws/roadToHell.png" type="image/png"> <source srcset="/blog/images/aws/roadToHell.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/roadToHell.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/roadToHell.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/roadToHell.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/roadToHell.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/roadToHell.gif" type="image/gif"> <source srcset="/blog/images/aws/roadToHell.tif" type="image/tiff"> <source srcset="/blog/images/aws/roadToHell.tiff" type="image/tiff"> <source srcset="/blog/images/aws/roadToHell.bmp" type="image/bmp"> <source srcset="/blog/images/aws/roadToHell.ico" type="image/x-icon"> <source srcset="/blog/images/aws/roadToHell.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/roadToHell.png" style='width: 100%; ' /> </picture> </div> <p class="alert rounded shadow"> I have 15 buckets that are used to serve web pages. On my <a href='https://us-east-1.console.aws.amazon.com/support/home?region=us-east-1&skipRegion=true#/' target='_blank' rel="nofollow">AWS Support Center console</a>, Trusted Advisor shows that &ldquo;You have a yellow check affecting 15 of 24 resources&rdquo;; however, the Trusted Advisor console displays those items as red triangles with exclamation marks. <br><br> This is confusing, and became frustrating when an AWS security support technician suggested I disregard anything that seemed annoying. For an IT/security person, that advice is the quickest way to hell that I know. <br><br><br> </p> <h3 id="rough">Rough Spots That Need Love</h3> <p> While researching this article I found: </p> <div class='quote'> <div class='quoteText clearfix'> Under <b>Origin Settings</b>, for <b>Origin Domain Name</b>, choose the Amazon S3 bucket that you created previously. <span class="bg_yellow">For <b>S3 bucket access</b>, select <b>Yes, use OAI (bucket can restrict access to only CloudFront)</b></span>. For the <b>Origin access identity</b>, you can choose from the list, or choose <b>Create new OAI</b> (both will work). For <b>Bucket policy</b>, select <b>Yes, update the bucket policy</b>. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started-cloudfront-overview.html#getting-started-cloudfront-distribution' rel='nofollow' target='_blank'>Use an Amazon CloudFront distribution to serve a static website</a></div> </div> <p> However, I found conflicting information near the top of this article: </p> <div class='quote'> <div class='quoteText clearfix'> <h2>Important</h2> <p> If you use an Amazon S3 bucket configured as a website endpoint, you must set it up with CloudFront as a custom origin. <span class="bg_yellow">You can&rsquo;t use the origin access identity feature described in this topic.</span> However, you can restrict access to content on a custom origin by setting up custom headers and configuring your origin to require them. For more information, see <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html#forward-custom-headers-restrict-access' target='_blank' rel="nofollow">Restricting access to files on custom origins</a>. </p> </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html' rel='nofollow' target='_blank'>Restricting access to Amazon S3 content by using an origin access identity (OAI)</a></div> </div> <p> As if this is not confusing enough, <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html#concept_S3Origin_website' target='_blank' rel="nofollow">Using various origins with CloudFront distributions</a> states that in order for Amazon S3 redirects to work, a non-standard format for the origin must be used:<br><br> <code>http://<span style="color: red;">bucket-name</span>.s3-website-<span style="color: red;">region</span>.amazonaws.com</code> <br><br> As I discussed in a <a href='/jekyll/10500-redirects.html'>previous article</a>, AWS S3 buckets support two types of redirects, but there is no indication of which type might or might not work with the non-standard origin format. <br><br> Also, there is no mention of whether <code>https</code> would work or not, or if these disparate instructions even work together. As usual, there is lots of uncertainty about how things actually work on AWS, and for how long before things change. </p> <p> As per standard operating procedure with AWS, one would have to muck about extensively to determine which of the conflicting statements above is generally prevalent, and under what circumstances. I expect that one article is newer than the other, and it likely supercedes the older article somewhat to some degree under as-yet unknown circumstances. If I get lucky, the Trusted Advisor warnings will go away. Trusted Advisor should reference whichever article is currently correct. </p> <h3 id="2022-05-31">Update 2022-05-31</h3> <p> I received the following email from Trusted Advisor. Unfortunately, the contents of the email were different from what I found on the Trusted Advisor console. Perhaps this is because a higher-level, and more expensive, service contract would be required to see those messages. If so, then this is not the way to gain happy customers. The cost of the higher service level would be 4 times more than my total AWS costs at present, clearly not something that I would want to do. </p> <div class="quote"> Dear AWS customer:<br><br> AWS Trusted Advisor currently shows alerts for 3 checks (1 red and 2 yellow) and $0 of potential monthly savings based on your usage.<br><br> Here is a summary of status changes for this week:<br><br> The alert severity of these checks has improved:<br><br> From red to yellow:<br> &nbsp;&nbsp;&nbsp;Amazon S3 Bucket Permissions<br><br> From red to green:<br> &nbsp;&nbsp;&nbsp;VPC<br> &nbsp;&nbsp;&nbsp;Security Groups - Specific Ports Unrestricted<br><br> From yellow to green:<br> &nbsp;&nbsp;&nbsp;VPC Internet Gateways<br><br> The alert status of 5 checks has not changed. For details and recommendations, visit AWS Trusted Advisor.<br><br> Sign in to the Trusted Advisor console to view your check results.<br><br> Best,<br><br> AWS Support<br> Was this report helpful? <a href='https://www.amazon.com/gp/f.html' target='_blank' rel="nofollow">Send us your feedback.</a> </div> <p> To add insult to injury, the final "Send us your feedback" just links to a generic form for AWS sales, where one can <a href='https://writingexplained.org/idiom-dictionary/go-pound-sand' target='_blank' rel="nofollow">pound sand</a>. </p> <p class="alert rounded shadow"> There is abundant evidence to demonstrate that whereas <code>amazon.com</code> is consumer-centric, AWS marches to a very different drummer. Perhaps small businesses are not presently viewed by AWS as a significant growth opportunity. My personal experience is that the current risk/value proposition provided by AWS is no longer attractive for that demographic. </p> <h3 id="2022-05-31">Update 2022-06-03</h3> <p> The credit card I used to pay for my AWS usage had a very high limit. In retrospect that was unwise. </p> <p> AWS took weeks to agree to refund the charge on my credit card, and then they said it would take another week for the reversed charges to appear. This meant that if I only paid for the other charges on that card, the month-to-month credit card interest on the fraudulent AWS charges would have been problematic. </p> <p> Just before the credit card billing period ended, I called Visa and asked what my options were. Apparently I am not the first person who had this problem with AWS. The answer was immediate: report the charges as fraud. OK, that made sense. This meant my card was immediately cancelled, the charges were forgiven by Visa, and a new card with a new number would arrive in about a week. </p> <h2 id="budgets">Managing Costs With AWS Budgets</h2> <p> Short-lived hijacking is the most serious financial threat, and AWS Budgets was seemingly not designed to guard against that. Ticking off this item will enable AWS to absolve you of financial responsibility for being hacked, but you should not expect that it will do much to slow down an attacker. </p> <p> AWS Budgets are easy to set up in a passive manner, but they are clumsy to work with and not effective for preventing new services from being launched that would exceed the budget. The cost limiting functionality is not automatic, it must be set up manually. AWS documentation does not provide detailed how-to instructions for common scenarios. Tens of thousands of words of documentation must be digested before the full capabilities can be harnessed. </p> <p> Another significant issue is that only three types of actions are supported: </p> <ol> <li> <b>IAM Policy</b> &ndash; I am unclear on how to set this up so the bad guys cannot launch new services, and I am not certain this is possible. </li> <li> <a href='https://us-east-1.console.aws.amazon.com/organizations/v2/home/policies/service-control-policy' target='_blank' rel="nofollow"><b>Service Control Policy</b></a> &ndash; Hopefully this could prevent new services from being launched that would exceed the budget; however, my head exploded when I researched this. <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/serviceControlPolicies1.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/serviceControlPolicies1.avif" type="image/avif">--> <source srcset="/blog/images/aws/serviceControlPolicies1.webp" type="image/webp"> <source srcset="/blog/images/aws/serviceControlPolicies1.apng" type="image/apng"> <source srcset="/blog/images/aws/serviceControlPolicies1.png" type="image/png"> <source srcset="/blog/images/aws/serviceControlPolicies1.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies1.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies1.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies1.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies1.gif" type="image/gif"> <source srcset="/blog/images/aws/serviceControlPolicies1.tif" type="image/tiff"> <source srcset="/blog/images/aws/serviceControlPolicies1.tiff" type="image/tiff"> <source srcset="/blog/images/aws/serviceControlPolicies1.bmp" type="image/bmp"> <source srcset="/blog/images/aws/serviceControlPolicies1.ico" type="image/x-icon"> <source srcset="/blog/images/aws/serviceControlPolicies1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/serviceControlPolicies1.png" style='width: 100%; ' /> </picture> </div> Attempting to use this option to prevent new, expensive services from being launched would seem to introduce lots of complexity. Perhaps AWS did not consider this use case, or deemed it unimportant. </li> <li> <b>Automate Instances to Stop for EC2 or RDS</b> &ndash; If you run a <a href='https://aws.amazon.com/ec2/instance-types/' target='_blank' rel="nofollow"><code>t3.nano</code></a> instance, for example, bad guys can only incur a certain amount of financial damage. Instead of shutting down the EC2 or RDS instances that I want to run, my main concern is to prevent bad guys from launching expensive new services. This option is therefore unsuitable for my use case. </li> </ol> <p> If AWS Budgets provides an easy way of preventing new services to be launched that would exceed the budget, I could not find it after several hours of reading. </p> <h3 id="passive">Passively Monitoring Budget Spend</h3> <p> Simply follow <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-create.html' target='_blank' rel="nofollow">the directions</a> and accept the defaults. Daily budgets do not support enabling forecasted alerts, or daily budget planning, so select <b>Monthly Budgets</b>. There is no need to set up SNS alerts or AWS Chatbot alerts, email notification works just as well for most users. </p> <h3 id="passive">Attaching Actions</h3> <p> Following are my notes from my attempt to enable cost limiting. I was unable to attach an action that prevented expensive new services from being started, and it is unclear if resolving the problem will, in fact, provide the desired benefit. Perhaps someone reading these notes will be able to tell me about an approach that might work for a reasonable amount of effort. </p> <p class="alert rounded shadow"> Because AWS will absolve you of financial liability if you create a Budget without any actions, don't stress about the pointless exercise involved in making a Budget that cannot protect you. Just do it and give AWS more time to figure out a better way of preventing account hijacking. <br><br> You can stop reading this article now, unless the details interest you. </p> <p class="alert rounded shadow"> For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc: &ldquo;pay-as-you-go&rdquo; is shorthand for &ldquo;there is nothing you can do to limit your financial liability&rdquo;. </p> <ol> <li>When you get to the step labeled <b>Attach actions - Optional</b>, choose <b>Add Action</b>.</li> <li> You must choose between using an existing IAM role for AWS Budgets to use, or you can create a new IAM role expressly for this purpose. IAM roles are free. I like the idea of a dedicated IAM role, so I clicked on <a href='https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/roles' target='_blank' rel="nofollow">manually create an IAM role</a>.<br> <div class='imgWrapper imgFlex inline' style='width: 75%; '> <picture class='imgPicture'> <source srcset="/blog/images/aws/budgetIamRole1.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/budgetIamRole1.avif" type="image/avif">--> <source srcset="/blog/images/aws/budgetIamRole1.webp" type="image/webp"> <source srcset="/blog/images/aws/budgetIamRole1.apng" type="image/apng"> <source srcset="/blog/images/aws/budgetIamRole1.png" type="image/png"> <source srcset="/blog/images/aws/budgetIamRole1.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole1.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole1.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole1.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole1.gif" type="image/gif"> <source srcset="/blog/images/aws/budgetIamRole1.tif" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole1.tiff" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole1.bmp" type="image/bmp"> <source srcset="/blog/images/aws/budgetIamRole1.ico" type="image/x-icon"> <source srcset="/blog/images/aws/budgetIamRole1.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/budgetIamRole1.png" style='width: 100%; margin-left: 1.2em;' /> </picture> </div> </li> <li> A new browser tab opened, and I clicked on the blue <kbd>Create role</kbd> button. </li> <li> Now a confusing page appeared: <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/budgetIamRole2.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/budgetIamRole2.avif" type="image/avif">--> <source srcset="/blog/images/aws/budgetIamRole2.webp" type="image/webp"> <source srcset="/blog/images/aws/budgetIamRole2.apng" type="image/apng"> <source srcset="/blog/images/aws/budgetIamRole2.png" type="image/png"> <source srcset="/blog/images/aws/budgetIamRole2.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole2.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole2.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole2.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole2.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole2.gif" type="image/gif"> <source srcset="/blog/images/aws/budgetIamRole2.tif" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole2.tiff" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole2.bmp" type="image/bmp"> <source srcset="/blog/images/aws/budgetIamRole2.ico" type="image/x-icon"> <source srcset="/blog/images/aws/budgetIamRole2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/budgetIamRole2.png" style='width: 100%; ' /> </picture> </div> </li> <li> I selected the default <b>Trusted entity type</b>, AWS Service, then from the pulldown menu labeled <b>Use cases for other AWS services:</b> I selected <b>Budgets</b>. </li> <li> A new radio button appeared underneath the pull-down, also labeled <b>Budgets</b>, and I clicked on that. This does not win any usability awards. </li> <li> I clicked on the button labeled <kbd>Next</kbd>. </li> <li> <div class='quote'> <div class='quoteText clearfix'> Managed policy name: <code><a href='https://docs.aws.amazon.com/cost-management/latest/userguide/billing-permissions-ref.html#budget-managedIAM-SSM' target='_blank' rel="nofollow"><code>AWSBudgetsActionsRolePolicyForResourceAdministrationWithSSM</code></a></code><br><br> This managed policy is focused on specific actions that AWS Budgets takes on your behalf when completing a specific action. This policy gives AWS Budgets broad permission to control AWS resources. For example, starts and stops Amazon EC2 or Amazon RDS instances by running AWS Systems Manager (SSM) scripts. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/billing-permissions-ref.html#budget-managedIAM-SSM' rel='nofollow' target='_blank'>Using identity-based policies (IAM policies) for AWS Cost Management</a></div> </div> On the page, I applied <code>AWSBudgetsActionsRolePolicyForResourceAdministrationWithSSM</code>, then I clicked on <kbd>Next</kbd>. <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/budgetIamRole3.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/budgetIamRole3.avif" type="image/avif">--> <source srcset="/blog/images/aws/budgetIamRole3.webp" type="image/webp"> <source srcset="/blog/images/aws/budgetIamRole3.apng" type="image/apng"> <source srcset="/blog/images/aws/budgetIamRole3.png" type="image/png"> <source srcset="/blog/images/aws/budgetIamRole3.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole3.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole3.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole3.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole3.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole3.gif" type="image/gif"> <source srcset="/blog/images/aws/budgetIamRole3.tif" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole3.tiff" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole3.bmp" type="image/bmp"> <source srcset="/blog/images/aws/budgetIamRole3.ico" type="image/x-icon"> <source srcset="/blog/images/aws/budgetIamRole3.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/budgetIamRole3.png" style='width: 100%; ' /> </picture> </div> </li> <li> On the final page, I named the IAM role <b>Budget</b> and clicked on <kbd>Create role</kbd>. <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/budgetIamRole4.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/budgetIamRole4.avif" type="image/avif">--> <source srcset="/blog/images/aws/budgetIamRole4.webp" type="image/webp"> <source srcset="/blog/images/aws/budgetIamRole4.apng" type="image/apng"> <source srcset="/blog/images/aws/budgetIamRole4.png" type="image/png"> <source srcset="/blog/images/aws/budgetIamRole4.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole4.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole4.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole4.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole4.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole4.gif" type="image/gif"> <source srcset="/blog/images/aws/budgetIamRole4.tif" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole4.tiff" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole4.bmp" type="image/bmp"> <source srcset="/blog/images/aws/budgetIamRole4.ico" type="image/x-icon"> <source srcset="/blog/images/aws/budgetIamRole4.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/budgetIamRole4.png" style='width: 100%; ' /> </picture> </div> </li> <li> Back in the <b>Billing Management Console</b>, I refreshed the list of available IAM roles, then selected the new role called <b>Budget</b>. <div class='imgWrapper imgFlex inline' style='width: 75%; '> <picture class='imgPicture'> <source srcset="/blog/images/aws/budgetIamRole5.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/budgetIamRole5.avif" type="image/avif">--> <source srcset="/blog/images/aws/budgetIamRole5.webp" type="image/webp"> <source srcset="/blog/images/aws/budgetIamRole5.apng" type="image/apng"> <source srcset="/blog/images/aws/budgetIamRole5.png" type="image/png"> <source srcset="/blog/images/aws/budgetIamRole5.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole5.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole5.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole5.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole5.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/budgetIamRole5.gif" type="image/gif"> <source srcset="/blog/images/aws/budgetIamRole5.tif" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole5.tiff" type="image/tiff"> <source srcset="/blog/images/aws/budgetIamRole5.bmp" type="image/bmp"> <source srcset="/blog/images/aws/budgetIamRole5.ico" type="image/x-icon"> <source srcset="/blog/images/aws/budgetIamRole5.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/budgetIamRole5.png" style='width: 100%; ' /> </picture> </div> </li> <li> I selected <b>Service Control Policy</b> from the pull-down that appeared next, labeled <b>Which action type should be applied when the budget threshold has been exceeded?</b>. I have no idea how to proceed, or even if going in this direction might provide the desired results. <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/serviceControlPolicies2.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/serviceControlPolicies2.avif" type="image/avif">--> <source srcset="/blog/images/aws/serviceControlPolicies2.webp" type="image/webp"> <source srcset="/blog/images/aws/serviceControlPolicies2.apng" type="image/apng"> <source srcset="/blog/images/aws/serviceControlPolicies2.png" type="image/png"> <source srcset="/blog/images/aws/serviceControlPolicies2.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies2.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies2.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies2.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies2.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/serviceControlPolicies2.gif" type="image/gif"> <source srcset="/blog/images/aws/serviceControlPolicies2.tif" type="image/tiff"> <source srcset="/blog/images/aws/serviceControlPolicies2.tiff" type="image/tiff"> <source srcset="/blog/images/aws/serviceControlPolicies2.bmp" type="image/bmp"> <source srcset="/blog/images/aws/serviceControlPolicies2.ico" type="image/x-icon"> <source srcset="/blog/images/aws/serviceControlPolicies2.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/serviceControlPolicies2.png" style='width: 100%; ' /> </picture> </div> </li> </ol> Microsoft Clarity Lets Me Watch You Click and Scroll 2022-03-31T00:00:00-04:00 https://mslinn.github.io/blog/2022/03/31/clarity <!-- #region intro --> <p> I spent a fair bit of time designing the plugins for this Jekyll-powered website for <a href='https://moz.com/beginners-guide-to-seo' target='_blank' rel="nofollow">SEO</a>. These plugins are published as open source; click on the word Jekyll in the sentence above this paragraph to learn more. You are welcome! </p> <p> I do not use Google Analytics because it slows down page load time dramatically. I do use <a href='https://search.google.com' target='_blank' rel="nofollow">Google Search Console</a>, however, and recently started trying out <a href='https://www.bing.com/webmasters/' target='_blank' rel="nofollow">Microsoft Bing Webmaster Tools</a>. </p> <!-- endregion --> <!-- #region clarity --> <h2 id="clarity">Microsoft Clarity and Hotjar</h2> <p> The day before April Fool's Day in 2022, I stumbled across <a href='https://clarity.microsoft.com/' target='_blank' rel="nofollow">Microsoft Clarity</a>, a free product that I knew nothing about. I was curious to know what benefit it might provide. </p> <p> I learned that one of the things it can do is provide videos of actual user sessions as they interact with the website. Those videos are fantastic. </p> <p> As a writer of a lot of online content, I have spent hours watching you, dear readers. Individually yet anonymously. Watching people read teaches you a lot about what is important to them. As a writer, knowing your audience allows you to be more relevant. BTW, I do not know who or where my readers are, just the country they are in. </p> <!-- endregion --> <!-- #region update 1 --> <h2 id="upd">Update 2023-07-10</h2> <p class="pullQuoteFull"> I no longer need to watch the videos.<br> Microsoft Clarity's AI generates summaries.<br> Often it yields incredibly actionable information, but the <b>Key Takeaways</b> section is generally way off the mark. YMMV. </p> <p> Today I noticed the <b>insights</b> tab, visible when viewing an entry for a &lsquo;hit&rsquo;. Seems like Microsoft has employed AI to generate the following impressive summary of the user's behavior. I added punctuation, a light edit and a whimsical pad of notepaper. </p> <div class="notepaper"> <p style="text-align: left; "> Visited URL matches regex: <code>^https://www\.mslinn\.com/softwareexpert/index\.html(\?.*)?$</code><br> <b>Last 7 days<br><br> Session insights</b><br> Some key takeaways from this session are:<br><br> The user was interested in the software expert witness and computer expert services offered by Mike Slinn, as they visited the homepage from Google and spent about four minutes there. <br><br> The user was also curious about the technology expert article series, especially the ones related to Git and version control systems. They visited the article index page multiple times and clicked on several articles, spending about 15 minutes in total on this topic. <br><br> The user was most engaged with the article on libgit2, a library that provides low-level access to Git operations. They spent about one and a half minutes on this article and clicked on a link to git-fame, a tool that shows the contribution statistics of a Git repository. <br><br> The user frequently resized their browser window, which may indicate that they were using a mobile device or adjusting their screen for better readability. The user also switched between tabs or applications often, as indicated by the page hidden and page visible events. This may suggest that they were multitasking or comparing information from different sources. <br><br> 01:47 / 47:31 </p> </div> <!-- endregion --> <!-- #region update 2 --> <h2 id="update2">Update 2023-07-18</h2> <p> Today I stumbled across the <a href='https://chrome.google.com/webstore/detail/microsoft-clarity-live/cjfdbemmaeeohgibnhdhlakiahifjjcf/related' target='_blank' rel="nofollow">Clarity Live</a> plugin for the Google Chrome web browser. There is no such plugin for Firefox, sadly. </p> <div class='quote'> <div class='quoteText clearfix'> View instant heatmaps right on your live site and watch recent session recordings for any page you are on with our extension. <br><br> <ul> <li>GDPR & CCPA ready</li> <li>No sampling</li> <li>Built on open source</li> </ul> </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://clarity.microsoft.com/live-extension' rel='nofollow' target='_blank'>Microsoft Clarity</a></div> </div> <!-- endregion --> <!-- #region update 3 --> <h2 id="update3">Update 2023-10-04</h2> <p> I just stumbled across the <a href='https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api#customize-your-identifiers' target='_blank' rel="nofollow">Clarity Client API</a>. </p> <div class='quote'> <p> You can quickly get started with Clarity without coding but by interacting with the Clarity client API. This API can help you access advanced features as described in this reference. Add the following calls to Clarity APIs to the HTML or JavaScript of your webpage to access these features. </p> <h2>Note</h2> <p> Your Clarity ID serves as your API key. No other client API key is necessary, and there is no cost for using Clarity client APIs. </p> </div> <!-- endregion --> <!-- #region video --> <h2 id="video">Check out this video!</h2> <p> This is one of the first user sessions that Microsoft Clarity recorded for this website. </p> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/N-Tip0Y4GMU" frameborder="0" allowfullscreen></iframe></div> <p> As you can see, Microsoft Clarity lets me watch movies of users clicking and scrolling through my website; spooky yet very informative. </p> <p> Clarity is open source. <a href='https://github.com/microsoft/clarity' target='_blank' rel="nofollow">Here</a> is the GitHub project. </p> <p> I have since learned that <a href='https://www.hotjar.com/' target='_blank' rel="nofollow">Hotjar</a> is similar to Microsoft Clarity. </p> <!-- endregion --> <!-- #region behavior --> <h2 id="behavior">Online Behavior Matches Real-World Behavior</h2> <p> The user in the above video read a bit about my experience as a software expert witness, then straightaway downloaded my resume. They were on the website for just over one minute. Although they did call me, their online behavior showed a lack of urgency, and their &lsquo;real-world&rsquo; behavior mirrored what I saw in the movie. </p> <p> A week later another party visited my site, and spent 80 minutes carefully reading 3 pages, among others. Their &lsquo;real-world&rsquo; behavior also matched their online behavior, in that they exhibited a sense of urgency towards engaging a software expert. </p> <!-- endregion --> <!-- #region downloads --> <h2 id="downloads">Tracking Downloads And Other Behavior</h2> <p> Microsoft Clarity does not consider a download as a click. It does not even notice downloads. For me, downloads are what I care about most. If you never download my resume, you probably are not a candidate for hiring me as a <a href='/softwareexpert/index.html'>software expert</a>. I want to track downloads, not clicks. Clicks are nice, but there is a direct relationship between resume downloads and signing contracts with legal firms who represent new clients. </p> <p> AWS <a href='https://aws.amazon.com/premiumsupport/knowledge-center/view-iam-history/' target='_blank' rel="nofollow">CloudTrail</a> and <a href='https://aws.amazon.com/aws-cost-management/aws-cost-optimization/monitor-track-and-analyze/' target='_blank' rel="nofollow">CloudWatch</a> can provide download details and much more. For example, user IP addresses and geographic location can be captured when a monitored event occurs. </p> <!-- endregion --> <!-- #region surveil --> <h2 id="others">Many Websites Perform Surveillance</h2> <p> Wired Magazine published an article on a similar type of surveillance (keyloggers) on May 11, 2022. I paraphrased two sentences from that article: </p> <div class='quote'> <ul> <li>1.8% of websites studied gathered an EU user's email address without their consent, and a staggering 2.95% logged a US user's email.</li> <li>For US users, 8.4% of sites may have been leaking data to Meta, Facebook&rsquo;s parent company, and 7.4% of sites may be impacted for EU users.</li> </ul> <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://www.wired.com/story/leaky-forms-keyloggers-meta-tiktok-pixel-study/' rel='nofollow' target='_blank'>Thousands of Popular Websites See What You Type—Before You Hit Submit</a></span> </div> <p> Yours truly does nothing of the sort. </p> <!-- endregion --> <!-- #region Popular Pages --> <h2 id="pp">Popular Pages</h2> <p> The data that Clarity manages can be downloaded with the <a href='https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-data-export-api' target='_blank' rel="nofollow">Clarity Data Export API</a>. API calls are limited to 10 calls per project, per day; that is not a problem. The endpoint is <code>www.clarity.ms/export-data/api/v1/project-live-insights</code>. </p> <p> An access token is required to use the API. To get one, open the Clarity project and select <b>Settings</b> -> <b>Data Export</b> -> <b>Generate new API token</b>. I called my token <code>clarity_data_export</code> and stored it in an environment variable called <code>CLARITY_DATA</code> </p> <p> Here is how I retrieved the information about the most popular pages in the last 30 days using the bash command line: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c99758e3f49'><button class='copyBtn' data-clipboard-target='#id3c99758e3f49' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>DAYS=30<br> <span class='unselectable'>$ </span>DIM1="PopularPages"<br> <span class='unselectable'>$ </span>PARAMS="numOfDays=$DAYS&dimension1=$DIM1"<br> <span class='unselectable'>$ </span>URL="https://www.clarity.ms/export-data/api/v1/project-live-insights"<br> <span class='unselectable'>$ </span>curl \ --location "$URL?$PARAMS" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $CLARITY_DATA" <span class='unselectable'>$ </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region next --> <h2 id="next">Looking Ahead</h2> <p> I would very much like to receive a continuous data feed of the above AI-generated per-visit summary, in real time as it becomes available. An email stream would be a quick way to start. A streaming api for data would be a logical next step. </p> <p> Perhaps the <a href='https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api' target='_blank' rel="nofollow">Clarity API</a> might be a place to start. </p> <!-- endregion --> Make a Visual Studio Code Extension 2022-03-01T00:00:00-05:00 https://mslinn.github.io/blog/2022/03/01/make-vscode-extension <p> I want to be able to move pages in my Jekyll-powered website without breaking links from the outside. The <a href='https://github.com/jekyll/jekyll-redirect-from' target='_blank' rel="nofollow"><code>jekyll-redirect-from</code></a> Jekyll plugin generates little HTML files that contain <code>http-meta-refresh</code> client-side redirects as desired. I wanted an easy way to inject the names of those redirect pages into the Jekyll front matter. Since I usually author my website using Visual Studio Code, a custom Visual Studio Code extension seems like a good way to create the redirects. </p> <h2 id="requirements">Extension Requirements</h2> <p> All the extension has to do is discover the URL path component of the page currently being edited, and write an entry into the front matter. For example, I have set up Jekyll such that given a page at <code>collections/_posts/2022/2022-02-21-jekyll-debugging.html</code>, it would deploy to <code>/blog/2022/02/21/jekyll-debugging.html</code>. The required front matter would be: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0546abc1518f'><button class='copyBtn' data-clipboard-target='#id0546abc1518f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>redirect_from: - /blog/2022/02/21/jekyll-debugging.html</pre> </div> <p> With that in mind, if I want to move a published article to another location without breaking it, the extension should: </p> <ol> <li>Present a new menu option when a right-click on a file in the sidebar, or a right-click on a file name tab in the editor.</li> <li> When the menu item is selected, obtain the relative path to the file within the project directory (for example: <code>collections/_posts/2022/2022-02-21-jekyll-debugging.html</code>) </li> <li> Convert the relative path to the deployed relative path (for example, <code>/blog/2022/02/21/jekyll-debugging.html</code>) </li> <li> Write the entry into the front matter of the file. For example: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbaf96727a461'><button class='copyBtn' data-clipboard-target='#idbaf96727a461' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>redirect_from:<br> - /blog/2022/02/21/jekyll-debugging.html</pre> </div> </li> </ol> <h2 id="background">Background</h2> <p> The <a href='https://code.visualstudio.com/api/get-started/your-first-extension' target='_blank' rel="nofollow">Microsoft documentation</a> describes how to write a Visual Studio Code extension. </p> <h2 id="setup">Setup</h2> <p> Visual Studio Code extensions are written in JavaScript (actually, node.js) or TypeScript. Ensure that a version of node.js has been installed: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcdaa04499c29'><button class='copyBtn' data-clipboard-target='#idcdaa04499c29' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>which node <span class='unselectable'>/home/mslinn/.nvm/versions/node/v17.3.1/bin/node </span></pre> </div> <p> Ensure you have <a href='properly'>set up your global package manager</a> for node.js, then type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id431bf8897370'><button class='copyBtn' data-clipboard-target='#id431bf8897370' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g <a href='https://github.com/yeoman/yo/' target='_blank' rel="nofollow">yo</a> <a href='https://github.com/microsoft/vscode-generator-code' target='_blank' rel="nofollow">generator-code</a> <span class='unselectable'>&nbsp; npm WARN deprecated har-validator@5.1.5: this library is no longer supported npm WARN deprecated uuid@3.4.0: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142<br> added 872 packages, and audited 873 packages in 57s<br> 15 vulnerabilities (13 moderate, 2 high)<br> To address issues that do not require attention, run: npm audit fix<br> To address all issues (including breaking changes), run: npm audit fix --force<br> Run `npm audit` for details. </span></pre> </div> <p> The <code>yo</code> module is the culprit; lets address its vulnerabilities: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaa6d354297b2'><button class='copyBtn' data-clipboard-target='#idaa6d354297b2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g npm-check-updates <span class='unselectable'>added 270 packages, and audited 271 packages in 9s<br> found 0 vulnerabilities </span></pre> </div> <h2 id="generate">Generating the Skeleton</h2> <p> I decided to use <a href='https://www.typescriptlang.org/' target='_blank' rel="nofollow">TypeScript</a> instead of JavaScript for the extension. TypeScript code converts to JavaScript, however it uses type inference and integrates better with some editors. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6c8b0f2ca96d'><button class='copyBtn' data-clipboard-target='#id6c8b0f2ca96d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir redirect_generator<br> <span class='unselectable'>$ </span>cd redirect_generator/<br> <span class='unselectable'>$ </span>$ yo code <span class='unselectable'>? ========================================================================== We&rsquo;re constantly looking for ways to make yo better! May we anonymously report usage statistics to improve the tool over time? More info: https://github.com/yeoman/insight &amp; http://yeoman.io ========================================================================== </span> No <span class='unselectable'><br> _-----_ ╭──────────────────────────╮ | | │ Welcome to the Visual │ |--(o)--| │ Studio Code Extension │ `---------´ │ generator! │ ( _´U`_ ) ╰──────────────────────────╯ /___A___\ / | ~ | __'.___.'__ ´ ` |° ´ Y ` ? What type of extension do you want to create? </span> New Extension (TypeScript) <span class='unselectable'>? What&apos;s the name of your extension? </span> redirect_generator <span class='unselectable'>? What&apos;s the identifier of your extension? </span> redirect-generator ? What's the description of your extension? Injects the URL of a redirect page into Jekyll front matter <span class='unselectable'>? Initialize a git repository? </span>Yes <span class='unselectable'>? Bundle the source code with webpack? </span>No <span class='unselectable'>? Which package manager to use? </span>npm <span class='unselectable'>Writing in /mnt/f/work/jekyll/redirect_generator/redirect-generator... create redirect-generator/.vscode/extensions.json create redirect-generator/.vscode/launch.json create redirect-generator/.vscode/settings.json create redirect-generator/.vscode/tasks.json create redirect-generator/package.json create redirect-generator/tsconfig.json create redirect-generator/.vscodeignore create redirect-generator/vsc-extension-quickstart.md create redirect-generator/README.md create redirect-generator/CHANGELOG.md create redirect-generator/src/extension.ts create redirect-generator/src/test/runTest.ts create redirect-generator/src/test/suite/extension.test.ts create redirect-generator/src/test/suite/index.ts create redirect-generator/.eslintrc.json<br> Changes to package.json were detected.<br> Running npm install for you to install the required dependencies.<br> added 203 packages, and audited 204 packages in 29s<br> found 0 vulnerabilities<br> Your extension redirect-generator has been created!<br> To start editing with Visual Studio Code, use the following commands:<br> code redirect-generator<br> Open vsc-extension-quickstart.md inside the new extension for further instructions on how to modify, test and publish your extension.<br> For more information, also visit http://code.visualstudio.com and follow us @code.<br><br> ? Do you want to open the new folder with Visual Studio Code? Open with `code`<br> _-----_ ╭───────────────────────╮ | | │ Bye from us! │ |--(o)--| │ Chat soon. │ `---------´ │ Yeoman team │ ( _´U`_ ) │ http://yeoman.io │ /___A___\ /╰───────────────────────╯ | ~ | __'.___.'__ ´ ` |° ´ Y ` </span></pre> </div> <p> The instructions in the web page assume the <a href='https://dictionary.cambridge.org/dictionary/english/happy-path' target='_blank' rel="nofollow">happy path</a> is the only possibility. Instead: </p> <ol> <li>Do not work on the extension in a workspace that has any other projects in it.</li> <li>The online instructions assume that TypeScript was used, not JavaScript, so the file types are assumed to be <code>.ts</code>. </ol> <p> The instructions in <code>vsc-extension-quickstart.md</code> do not make assumptions about TypeScript. </p> <h2 id="debug">Debugging the Extension</h2> <p> I found the procedure awkward at first: </p> <ol> <li>Press <kbd>F5</kbd> to activate the first defined launcher</li> <li> Once the new debug VSCode instance settles down, click in it and type <kbd>CTRL</kbd>-<kbd>Shift</kbd>-<kbd>P</kbd>, then type <code>redirect</code>. </li> <li>Any breakpoints in the original VSCode instance will trigger.</li> <li> Once the breakpoint is cleared, a message saying <b>Add redirecct from redirect_generator!</b> will appear in the debugged instance of VSCode. </li> </ol> <h2 id="anatomy">Extension Anatomy</h2> <p> I moved on to the next part of the tutorial, <a href='https://code.visualstudio.com/api/get-started/extension-anatomy' target='_blank' rel="nofollow">Extension Anatomy</a>. </p> Node.js, NVM, NPM and Yarn 2022-03-01T00:00:00-05:00 https://mslinn.github.io/blog/2022/03/01/node-package-managers <!-- #region intro --> <p> <a href='https://nodejs.dev/learn/a-brief-history-of-nodejs' target='_blank' rel="nofollow"><code>Node.js</code></a>, a JavaScript runtime, is not my favorite programming environment. Originally released 15 years ago by <a href='https://en.wikipedia.org/wiki/Ryan_Dahl' target='_blank' rel="nofollow">Ryan Dahl</a>, a software engineer at Google, it&rsquo;s <a href='https://www.shiftleft.io/blog/node.js-vulnerability-cheatsheet/' target='_blank' rel="nofollow">historical disregard</a> for <a href='https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html' target='_blank' rel="nofollow">security</a> and stability is problematic. </p> <p> However, <code>node.js</code> has significant traction, especially amongst Millennial software technologists, and is used by many Ruby and Scala projects for HTML <a href='http://graphics.cs.cmu.edu/courses/15-466-f19/notes/asset-pipelines.html' target='_blank' rel="nofollow">asset pipelines</a>. </p> <p> I put together these notes for installing and maintaining <code>node.js</code> on Ubuntu/WSL using a virtualized version manager. </p> <!-- endregion --> <!-- #region Why Virtualize Node.js --> <h2 id="nvm">Why Virtualize <span class="code">Node.js</span>?</h2> <div class='imgWrapper imgFlex right' style='width: 40%; '> <picture class='imgPicture'> <source srcset="/blog/images/node/pushback.svg" type="image/svg"> <!---<source srcset="/blog/images/node/pushback.avif" type="image/avif">--> <source srcset="/blog/images/node/pushback.webp" type="image/webp"> <source srcset="/blog/images/node/pushback.apng" type="image/apng"> <source srcset="/blog/images/node/pushback.png" type="image/png"> <source srcset="/blog/images/node/pushback.jpg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.jpeg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.jfif" type="image/jpeg"> <source srcset="/blog/images/node/pushback.pjpeg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.pjp" type="image/jpeg"> <source srcset="/blog/images/node/pushback.gif" type="image/gif"> <source srcset="/blog/images/node/pushback.tif" type="image/tiff"> <source srcset="/blog/images/node/pushback.tiff" type="image/tiff"> <source srcset="/blog/images/node/pushback.bmp" type="image/bmp"> <source srcset="/blog/images/node/pushback.ico" type="image/x-icon"> <source srcset="/blog/images/node/pushback.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/node/pushback.png" style='width: 100%; ' /> </picture> </div> <p> It is better to use virtualized user- and project-specific <code>node.js</code> instances, instead of working with a system-wide installation of <code>node.js</code>. This allows you to install and upgrade <code>node.js</code> packages without using supervisor privileges. Also, virtualized instances allows you to work on many different independent <code>node.js</code> projects at the same time, without package version collisions. </p> <p> <code>Docker</code> is over-sold. It adds unnecessary complexity to software projects. Instead of virtualizing the entire software environment, as <code>docker</code> attempts to do, virtualizing the programming environment with <code>node.js nvm</code>, <a href='/blog/2021/04/09/python-venvs.html'>Python <code>venv</code></a>, or <a href='/ruby/1000-ruby-setup.html'>Ruby <code>rbenv</code></a> are much easier and more productive approaches. </p> <p> I think <code>docker</code> has been pushed hard in the media because it is a gateway technology to <a href='https://www.infoworld.com/article/3223434/what-is-paas-platform-as-a-service-a-simpler-way-to-build-software-applications.html' target='_blank' rel="nofollow">PaSS</a>. This is a trend that PaSS vendors like AWS and Azure want to encourage, but <a href='https://www.datacenterdynamics.com/en/news/37signals-spent-more-than-3-million-on-the-cloud-in-2022-for-basecamp-and-hey' target='_blank' rel="nofollow">customers are pushing back</a>. </p> <!-- endregion --> <!-- #region Nvm --> <h2 id="nvm" class="clear"><span class="code">Nvm</span></h2> <p> <code>Nvm</code>, the Node Version Manager, makes it easy to install multiple virtualized <code>node.js</code> instances, and to easily switch between them. <code>Nvm</code> retains a unique set of installed packages for each <code>node.js</code> instance. The <code>node.js</code> version in each instance is distinct. </p> <p> <code>Nvm</code> is installed and updated as follows: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb816146457de'><button class='copyBtn' data-clipboard-target='#idb816146457de' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash <span class='unselectable'>&nbsp; % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 14926 100 14926 0 0 84806 0 --:--:-- --:--:-- --:--:-- 85291 => nvm is already installed in /home/mslinn/.nvm, trying to update using git => => Compressing and cleaning up git repository => nvm source string already in /home/mslinn/.bashrc => bash_completion source string already in /home/mslinn/.bashrc => Close and reopen your terminal to start using nvm or run the following to use it now: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion </span></pre> </div> <p> View the currently installed versions of <code>node.js</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideba881065935'><button class='copyBtn' data-clipboard-target='#ideba881065935' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm list <span class='unselectable'>-> system iojs -> N/A (default) node -> stable (-> N/A) (default) unstable -> N/A (default) </span></pre> </div> <!-- #region --> <p> View the very long list of available versions of <code>node.js</code> like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7d07bde51237'><button class='copyBtn' data-clipboard-target='#id7d07bde51237' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm list-remote <span class='unselectable'>&nbsp; v17.7.0 v17.7.1 v17.7.2 v17.8.0 v17.9.0 v17.9.1 v18.0.0 v18.1.0 v18.2.0 v18.3.0 v18.4.0 v18.5.0 v18.6.0 v18.7.0 v18.8.0 v18.9.0 v18.9.1 v18.10.0 v18.11.0 v18.12.0 (LTS: Hydrogen) v18.12.1 (LTS: Hydrogen) v18.13.0 (Latest LTS: Hydrogen) v19.0.0 v19.0.1 v19.1.0 v19.2.0 v19.3.0 v19.4.0 v19.5.0 </span></pre> </div> <!-- endregion --> <h2 id="npm_install_node">Using <span class="code">Nvm</span> to Install <span class="code">Node.js</span></h2> <p> Install the latest release of <code>node.js</code> using <code>nvm</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf2dbe5787edf'><button class='copyBtn' data-clipboard-target='#idf2dbe5787edf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm install <span class="bg_yellow">node</span> <span class='unselectable'>Downloading and installing node v19.5.0... Downloading https://nodejs.org/dist/v19.5.0/node-v19.5.0-linux-x64.tar.xz... ##################################################################### 100.0% Computing checksum with sha256sum Checksums matched! Now using node v19.5.0 (npm v9.3.1) Creating default alias: default -> node (-> v19.5.0) </span></pre> </div> <p> In the above example, <code class="bg_yellow">node</code> is an alias for &ldquo;the latest version of <code>node.js</code>&rdquo;. To install a specific version of <code>node.js</code>, for example 18.13.0: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2fc6397c7f5b'><button class='copyBtn' data-clipboard-target='#id2fc6397c7f5b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm install 18.13.0</pre> </div> <!-- endregion --> <!-- #region Yarn --> <h2 id="yarn"><span class="code">Yarn</span></h2> <!-- #region implicit --> <div class='imgWrapper imgBlock right halfsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/node/needle_and_thread.svg" type="image/svg"> <!---<source srcset="/blog/images/node/needle_and_thread.avif" type="image/avif">--> <source srcset="/blog/images/node/needle_and_thread.webp" type="image/webp"> <source srcset="/blog/images/node/needle_and_thread.apng" type="image/apng"> <source srcset="/blog/images/node/needle_and_thread.png" type="image/png"> <source srcset="/blog/images/node/needle_and_thread.jpg" type="image/jpeg"> <source srcset="/blog/images/node/needle_and_thread.jpeg" type="image/jpeg"> <source srcset="/blog/images/node/needle_and_thread.jfif" type="image/jpeg"> <source srcset="/blog/images/node/needle_and_thread.pjpeg" type="image/jpeg"> <source srcset="/blog/images/node/needle_and_thread.pjp" type="image/jpeg"> <source srcset="/blog/images/node/needle_and_thread.gif" type="image/gif"> <source srcset="/blog/images/node/needle_and_thread.tif" type="image/tiff"> <source srcset="/blog/images/node/needle_and_thread.tiff" type="image/tiff"> <source srcset="/blog/images/node/needle_and_thread.bmp" type="image/bmp"> <source srcset="/blog/images/node/needle_and_thread.ico" type="image/x-icon"> <source srcset="/blog/images/node/needle_and_thread.cur" type="image/x-icon"> <img alt='Needle and thread under an electron microscope' class="imgImg rounded shadow" src="/blog/images/node/needle_and_thread.png" style='width: 100%; ' title='Needle and thread under an electron microscope' /> </picture> <figcaption class='imgFigCaption halfsize'> Needle and thread under an electron microscope </figcaption> </figure> </div> <p> Whether you use <code>nvm</code> as your <code>node.js</code> virtualization mechanism, you also need a <code>node.js</code> package manager to install and maintain project dependencies. </p> <p> <a href='https://yarnpkg.com/' target='_blank' rel="nofollow"><code>Yarn</code></a> supposedly stands for Yet Another Resource Negotiator, and it is a package manager like <code>npm</code>, described <a href="#npm">next</a>. It was developed by Facebook and is now open-source. <code>Yarn</code> was developed to address <code>npm</code>&rsquo;s performance and security issues. </p> <p> The name actually suggests the major advantage <code>yarn</code> has over its predecessor, <code>npm</code>: multi-threading instead of single-threading. </p> <p> Yarn generates a <code>yarn.lock</code> file, which helps easy merges. The merges are predictable. <code>Yarn</code> caches every package it has downloaded, so it never needs to download the same package again. It also does almost everything concurrently to maximize resource utilization. This means faster installs. <code>Yarn</code> guarantees that any installation that works on one system will work exactly the same on another system, unlike <code>npm</code>. </p> <p> The notes for the <a href='https://www.npmjs.com/package/yarn' target='_blank' rel="nofollow"><code>yarn</code> package</a> state that <code>Yarn</code> was inspired by <a href='https://bundler.io/' target='_blank' rel="nofollow">Bundler</a> and <a href='https://doc.rust-lang.org/cargo/' target='_blank' rel="nofollow">Cargo</a>, and is nearly command-line compatible with <code>npm</code>. </p> <p> <code>Yarn</code> introduces the zero-install concept, which means that a project should be able to be used as soon as it is cloned. It uses Plug&rsquo;n&rsquo;Play to resolve dependencies via the <code>yarn</code> cache folder and not from <code>node_modules</code>. The cache folder is by default stored within your project folder, in <code>.yarn/cache</code>. </p> <!-- endregion --> <!-- #region Installing Yarn --> <h3 id="install_yarn">Installing <span class="code">Yarn</span></h3> <p> To install <code>yarn</code> on Ubuntu: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id27abb72617b0'><button class='copyBtn' data-clipboard-target='#id27abb72617b0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | \ sudo apt-key add - <span class='unselectable'>$ </span>echo "deb https://dl.yarnpkg.com/debian/ stable main" | \ sudo tee /etc/apt/sources.list.d/yarn.list <span class='unselectable'>$ </span>sudo apt update && sudo apt install yarn</pre> </div> <!-- endregion --> <!-- #region Configuring a Project With Yarn --> <h3 id="yarn_config">Configuring a Project With Yarn</h3> <p> The <a href='https://classic.yarnpkg.com/lang/en/docs/cli/init/' target='_blank' rel="nofollow"><code>yarn init</code></a> command initiates an interactive session that creates a <code>package.json</code> file. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id12be10ccf41c'><button class='copyBtn' data-clipboard-target='#id12be10ccf41c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/node/project<br> <span class='unselectable'>$ </span>yarn init <span class='unselectable'>yarn init v1.22.19 question name (node): blah question version (1.0.0): 0.1.0 question description: Just a little song I wrote question entry point (index.js): question repository url: https://github.com/mslinn/blah question author: Mike Slinn question license (MIT): question private: success Saved package.json Done in 48.29s. </span></pre> </div> <!-- endregion --> <p> The above dialog creates <code>package.json</code> in the current directory: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>package.json</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id59525231ca32'><button class='copyBtn' data-clipboard-target='#id59525231ca32' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "name": "blah", "version": "0.1.0", "description": "Just a little song I wrote", "main": "index.js", "repository": "https://github.com/mslinn/blah", "author": "Mike Slinn", "license": "MIT" }</pre> </div> <!-- endregion --> <!-- #region Adding a Dependency to a Project --> <h3 id="yarn_config">Adding a Dependency to a Project</h3> <p> Yarn stores dependencies locally. If the proper version is present locally, it is fetched from the disk during a <code>yarn add</code> command, otherwise it is downloaded. This is the general format of the command to add a dependency to a <code>node.js</code> project using <code>yarn</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide4d5916d29be'><button class='copyBtn' data-clipboard-target='#ide4d5916d29be' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn add package_name</pre> </div> <p> To add a specific version of a package: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddc4160b3ccac'><button class='copyBtn' data-clipboard-target='#iddc4160b3ccac' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn add package_name<span class="bg_yellow">@version_number</span></pre> </div> <p> To install a global package, the general formats are: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7beec1f38edb'><button class='copyBtn' data-clipboard-target='#id7beec1f38edb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn <span class="bg_yellow">global</span> add package_name <span class='unselectable'>$ </span>yarn <span class="bg_yellow">global</span> add package_name@version_number</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Yarn and Git --> <h3 id="git_yarn"><span class="code">Yarn</span> and <span class="code">Git</span></h3> <p> The <a href='https://yarnpkg.com/getting-started/install' target='_blank' rel="nofollow">official Yarn docs</a> say to add the following to <code>.gitignore</code> for git projects that use <code>yarn</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>.gitignore</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb12ed5d9248e'><button class='copyBtn' data-clipboard-target='#idb12ed5d9248e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>.yarn/* !.yarn/cache !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Installing Dependencies --> <h3 id="yarn_install">Installing Dependencies</h3> <p> To install all the dependencies in a <code>node.js</code> project, type <a href='https://yarnpkg.com/getting-started/usage#installing-all-the-dependencies' target='_blank' rel="nofollow"><code>yarn</code> or <code>yarn install</code></a>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id417fb247d957'><button class='copyBtn' data-clipboard-target='#id417fb247d957' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/project <span class='unselectable'>$ </span>yarn</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Updating Dependencies --> <h3 id="yarn_update">Updating Dependencies</h3> <p> To upgrade all the dependencies in a <code>node.js</code> project, use <a href='https://classic.yarnpkg.com/lang/en/docs/cli/upgrade/' target='_blank' rel="nofollow"><code>yarn upgrade</code></a>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idae6c0648c21e'><button class='copyBtn' data-clipboard-target='#idae6c0648c21e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/project <span class='unselectable'>$ </span>yarn upgrade</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region Npm --> <h2 id="npm"><span class="code">Npm</span></h2> <!-- #region implicit --> <p> <code>Npm</code> is the original package manager for the family of JavaScript programming languages, and it is still the default package manager for <code>node.js</code>. This will likely change as <code>yarn</code> matures. <code>Npm</code> helps install libraries, plugins, frameworks and applications. It consists of a command-line client, also called <code>npm</code>, and an online database of public and paid-for private packages called the <code>npm</code> registry. </p> <div class="pullQuote"> If you are using <span class="code">yarn</span> then you do not need to use <span class="code">npm</span>. </div> <p> <code>Npm</code> fetches dependencies from the <code>npm</code> registry for every <code>npm install</code> command. </p> <p> <code>Npm</code> generates a <code>package-lock.json</code> file. The layout of this file is a trade-off between determinism and simplicity. The same <code>node_modules/</code> folder will be generated by every version of <code>npm</code>. Every dependency will have a version number associated with it in the <code>package-lock</code> file. </p> <!-- endregion --> <!-- #region Npm vs. Yarn --> <h3 id="npm_yarn"><span class="code">Npm</span> vs. <span class="code">Yarn</span></h3> <p> When using <code>npm install</code>, dependencies are installed sequentially, one after another. The output logs in the terminal are informative but a bit hard to read. To install the packages with Yarn, run the <code>yarn</code> command. Yarn installs packages in parallel, which is one of the reasons it is quicker than npm. </p> <p> For more information, see <a href='https://www.geeksforgeeks.org/difference-between-npm-and-yarn/' target='_blank' rel="nofollow">Difference between npm and yarn</a>. </p> <!-- endregion --> <!-- #region Install Npm With a Node Version Manager --> <h3 id="npm_install">Install <span class="code">Npm</span> With a Node Version Manager</h3> <p> Follow <a href='https://docs.npmjs.com/downloading-and-installing-node-js-and-npm#using-a-node-version-manager-to-install-nodejs-and-npm' target='_blank' rel="nofollow">these instructions</a>. In summary: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2ddc85b265c9'><button class='copyBtn' data-clipboard-target='#id2ddc85b265c9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir ~/.npm-global<br> <span class='unselectable'>$ </span>npm config set prefix ~/.npm-global<br> <span class='unselectable'>$ </span>cat >> ~/.bashrc &lt;&lt;EOF export PATH="$HOME/.npm-global/bin:$PATH"<br> export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" 1>&2 # Loads nvm EOF<br> <span class='unselectable'>$ </span>source ~/.bashrc<br> <span class='unselectable'>$ </span>curl -fsSL https://deb.nodesource.com/setup_21.x | \ sudo -E bash - && \ sudo apt-get install -y nodejs</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Install A Global Package --> <h3 id="global_install">Install A Global Package</h3> <p> To install a global package, the command template for <code>npm</code> is: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1db868061b13'><button class='copyBtn' data-clipboard-target='#id1db868061b13' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g package_name[@version_number]</pre> </div> <p> For example: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf209f3adb8d4'><button class='copyBtn' data-clipboard-target='#idf209f3adb8d4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g broken-link-checker <span class='unselectable'>npm WARN deprecated calmcard@0.1.1: no longer maintained npm WARN deprecated nopter@0.3.0: try optionator npm WARN deprecated uuid@2.0.3: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. npm WARN deprecated urlobj@0.0.11: use universal-url, minurl, relateurl, url-relation added 104 packages, and audited 105 packages in 5s </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> Fun With Python Enums 2022-02-10T00:00:00-05:00 https://mslinn.github.io/blog/2022/02/10/python-3.4-enums <!-- #region intro --> <p> This article demonstrates how to define additional properties for <a href='https://docs.python.org/3/library/enum.html' target='_blank' rel="nofollow">Python 3 enums</a>. Defining an additional property in a Python enum can provide a simple way to provide string values. The concept is then expanded to demonstrate composition, an important concept for functional programming. This article concludes with a demonstration of dynamic dispatch in Python, by further extending an enum. </p> <!-- endregion --> <!-- #region Adding Properties to Python Enums --> <h2 id="enum">Adding Properties to Python Enums</h2> <p> Searching for <a href='https://www.google.com/search?q=python+enum+string+value' target='_blank' rel="nofollow"><code>python enum string value</code></a> yields some complex and arcane ways to approach the problem. </p> <p> Below is a short example of a Python enum that demonstrates a simple way to provide lower-case string values for enum constants: a new property, <code>to_s</code>, is defined. This property provides the string representation that is required. You could define other properties and methods to suit the needs of other projects. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id62544e068fe0'><button class='copyBtn' data-clipboard-target='#id62544e068fe0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"""Defines enums"""<br/> from enum import Enum, auto<br/> class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()<br/> <span class="bg_yellow"> @property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()</span></pre> </div> <!-- endregion --> <p> Adding the following to the bottom of the program allows us to demonstrate it: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py (part 2)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb5b93ec040f3'><button class='copyBtn' data-clipboard-target='#idb5b93ec040f3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>if __name__ == "__main__": # Just for demonstration print("Specifying individual values:") print(f" {EntityType.SITE.value}: {EntityType.SITE.to_s}") print(f" {EntityType.GROUP.value}: {EntityType.GROUP.to_s}") print(f" {EntityType.COURSE.value}: {EntityType.COURSE.to_s}") print(f" {EntityType.SECTION.value}: {EntityType.SECTION.to_s}") print(f" {EntityType.LECTURE.value}: {EntityType.LECTURE.to_s}")<br> print("\nIterating through all values:") for entity_type in EntityType: print(f" {entity_type.value}: {entity_type.to_s}")</pre> </div> <!-- endregion --> <p> Running the program produces this output: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc1ff5dbf4f60'><button class='copyBtn' data-clipboard-target='#idc1ff5dbf4f60' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cad_enums.py <span class='unselectable'>Specifying individual values: 1: site 2: group 3: course 4: section 5: lecture<br> Iterating through all values: 1: site 2: group 3: course 4: section 5: lecture </span></pre> </div> <!-- endregion --> <div class="right" style="font-size: 3em;">&#128513;</div> <p> Easy! </p> <!-- endregion --> <!-- #region Constructing Enums --> <h2 id="constructor">Constructing Enums</h2> <p> Enum constructors work the same as other Python class constructors. There are several ways to make a new instance of a Python enum. Let's try two ways by using the <a href='https://docs.python.org/3/tutorial/interpreter.html' target='_blank' rel="nofollow">Python interpreter</a>. Throughout this article I've inserted a blank line between Python interpreter prompts for readability. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc20baa6306e1'><button class='copyBtn' data-clipboard-target='#idc20baa6306e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python <span class='unselectable'>Python 3.9.7 (default, Sep 10 2021, 14:59:43) [GCC 11.2.0] on linux Type "help", "copyright", "credits" or "license" for more information.<br> >>> </span>from cad_enums import EntityType<br> <span class='unselectable'>>>> </span># Specify the desired enum constant value symbolically <span class='unselectable'>>>> </span><span class="bg_yellow">gtype = EntityType.GROUP</span> <span class='unselectable'>>>> </span>print(gtype) <span class='unselectable'>EntityType.GROUP </span><br> <span class='unselectable'>>>> </span># Specify the desired enum constant value numerically <span class='unselectable'>>>> </span><span class="bg_yellow">stype = EntityType(1)</span> <span class='unselectable'>>>> </span>print(stype) <span class='unselectable'>EntityType.SITE </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Enum Ordering --> <h2 id="ordering">Enum Ordering</h2> <p> A program I am working on needs to obtain the parent <code>EntityType</code>. By 'parent' I mean the <code>EntityType</code> with the next lowest numeric value. For example, the parent of <code>EntityType.GROUP</code> is <code>EntityType.SITE</code>. We can obtain a parent enum by computing its numeric value by adding the following method to the <code>EntityType</code> class definition. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id61cf1626009b'><button class='copyBtn' data-clipboard-target='#id61cf1626009b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@property def parent(self) -> <span class="bg_yellow">'EntityType'</span>: """:return: entity type of parent; site has no parent""" return EntityType(max(self.value - 1, 1))</pre> </div> <!-- endregion --> <p> The <span class="bg_yellow">return type</span> above is enclosed in quotes (<code>'EntityType'</code>) to keep Python's type checker happy, because this is a <a href='https://www.python.org/dev/peps/pep-0484/#forward-references' target='_blank' rel="nofollow">forward reference</a>. This is a forward reference because the type is referenced before it is fully compiled. </p> <p> The complete enum class definition is now: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4fb2f96b45dc'><button class='copyBtn' data-clipboard-target='#id4fb2f96b45dc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"""Defines enums"""<br/> from enum import Enum, auto<br/> class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()<br/> @property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()<br> @property def parent(self) -> 'EntityType': """:return: entity type of parent; site has no parent""" return EntityType(max(self.value - 1, 1))</pre> </div> <!-- endregion --> <p> Lets try out the new <code>parent</code> property in the Python interpreter. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb6c3a5aa2da'><button class='copyBtn' data-clipboard-target='#idcb6c3a5aa2da' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.parent <span class='unselectable'>&lt;EntityType.SECTION: 4> </span><br> <span class='unselectable'>>>> </span>EntityType.SECTION.parent <span class='unselectable'>&lt;EntityType.COURSE: 3> </span><br> <span class='unselectable'>>>> </span>EntityType.COURSE.parent <span class='unselectable'>&lt;EntityType.GROUP: 2> </span><br> <span class='unselectable'>>>> </span>EntityType.GROUP.parent <span class='unselectable'>&lt;EntityType.SITE: 1> </span><br> <span class='unselectable'>>>> </span>EntityType.SITE.parent <span class='unselectable'>&lt;EntityType.SITE: 1> </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Enum Composition --> <h2 id="compose">Enum Composition</h2> <p> Like methods and properties in all other Python classes, enum methods and properties compose if they return an instance of the class. Composition is also known as <a href='https://en.wikipedia.org/wiki/Method_chaining' target='_blank' rel="nofollow">method chaining</a>, and also can apply to class properties. Composition is an essential practice of a functional programming style. </p> <p> The <code>parent</code> property returns an instance of the <code>EntityType</code> enum class, so it can be composed with any other property or method of that class, for example the <code>to_s</code> property shown earlier. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5d8b6a3d0686'><button class='copyBtn' data-clipboard-target='#id5d8b6a3d0686' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.parent.to_s <span class='unselectable'>'section' </span><br> <span class='unselectable'>>>> </span>EntityType.SECTION.parent.to_s <span class='unselectable'>'course' </span><br> <span class='unselectable'>>>> </span>EntityType.COURSE.parent.to_s <span class='unselectable'>'group' </span><br> <span class='unselectable'>>>> </span>EntityType.GROUP.parent.to_s <span class='unselectable'>'site' </span><br> <span class='unselectable'>>>> </span>EntityType.SITE.parent.to_s <span class='unselectable'>'site' </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Dynamic Dispatch --> <h2 id="dispatch">Dynamic Dispatch</h2> <p> The <a href='https://docs.python.org/3/library/typing.html#callable' target='_blank' rel="nofollow">Python documentation</a> might lead someone to assume that writing <a href='https://en.wikipedia.org/wiki/Dynamic_dispatch' target='_blank' rel="nofollow">dynamic dispatch</a> code is more complex than it actually is. </p> <p> To summarize the documentation, all Python classes, methods and instances are callable. <a href='https://www.tutorialsteacher.com/python/callable-method' target='_blank' rel="nofollow"><code>Callable</code> functions</a> have type <code>Callable[[InputArg1Type, InputArg2Type], ReturnType]</code>. If you do not want any type checking, write <code>Callable[..., Any]</code>. However, this is not very helpful information for dynamic dispatch. Fortunately, working with <code>Callable</code> is very simple. </p> <p class="notepaper"> You can pass around any Python class, constructor, function or method, and later provide it with the usual arguments. Invocation just works. </p> <p> Let me show you how easy it is to write dynamic dispatch code in Python, let's construct one of five classes, depending on the value of an enum. First, we need a class definition for each enum value: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8170e9800ec4'><button class='copyBtn' data-clipboard-target='#id8170e9800ec4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># pylint: disable=too-few-public-methods<br> class BaseClass(): """Demo only"""<br> class TestLecture(BaseClass): """This constructor has type Callable[[int, str], TestLecture]""" def __init__(self, id_: int, action: str): print(f"CadLecture constructor called with id {id_} and action {action}")<br> class TestSection(BaseClass): """This constructor has type Callable[[int, str], TestSection]""" def __init__(self, id_: int, action: str): print(f"CadSection constructor called with id {id_} and action {action}")<br> class TestCourse(BaseClass): """This constructor has type Callable[[int, str], TestCourse]""" def __init__(self, id_: int, action: str): print(f"CadCourse constructor called with id {id_} and action {action}")<br> class TestGroup(BaseClass): """This constructor has type Callable[[int, str], TestGroup]""" def __init__(self, id_: int, action: str): print(f"CadGroup constructor called with id {id_} and action {action}")<br> class TestSite(BaseClass): """This constructor has type Callable[[int, str], TestSite]""" def __init__(self, id_: int, action: str): print(f"CadSite constructor called with id {id_} and action {action}")</pre> </div> <!-- endregion --> <p> Now lets add another method, called <code>construct</code>, to <code>EntityType</code> that invokes the appropriate constructor according to the value of an <code>EntityType</code> instance: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc59f9bef79ba'><button class='copyBtn' data-clipboard-target='#idc59f9bef79ba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@property def construct(self) -> Callable: """:return: the appropriate Callable for each enum value""" if self == EntityType.LECTURE: return TestLecture if self == EntityType.SECTION: return TestSection if self == EntityType.COURSE: return TestCourse if self == EntityType.GROUP: return TestGroup return TestSite</pre> </div> <!-- endregion --> <p class="alert right rounded shadow" style="width: 50%;"> Using named arguments makes your code resistant to problems that might sneak in due to parameters changing over time. </p> <p> I favor using named arguments at all times; it avoids many problems. As code evolves, arguments might be added or removed, or even reordered. </p> <p class="clear"> Let's test out dynamic dispatch in the Python interpreter. A class specific to each <code>EntityType</code> value is constructed by invoking the appropriate <code>Callable</code> and passing it named arguments <a href='https://stackoverflow.com/a/28091085/553865' target='_blank' rel="nofollow"><code>id_</code></a> and <code>action</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Python</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id32b8d97b4ee2'><button class='copyBtn' data-clipboard-target='#id32b8d97b4ee2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.construct(id_=55, action="gimme_lecture") <span class='unselectable'>TestLecture constructor called with id 55 and action gimme_lecture &lt;entity_types.TestLecture object at 0x7f9aac690070> </span><br> <span class='unselectable'>>>> </span>EntityType.SECTION.construct(id_=13, action="gimme_section") <span class='unselectable'>TestSection constructor called with id 13 and action gimme_section &lt;entity_types.TestSection object at 0x7f9aac5c1730> </span><br> <span class='unselectable'>>>> </span>EntityType.COURSE.construct(id_=40, action="gimme_course") <span class='unselectable'>TestCourse constructor called with id 40 and action gimme_course &lt;entity_types.TestCourse object at 0x7f9aac6900a0> </span><br> <span class='unselectable'>>>> </span>EntityType.GROUP.construct(id_=103, action="gimme_group") <span class='unselectable'>TestGroup constructor called with id 103 and action gimme_group &lt;entity_types.TestGroup object at 0x7f9aac4c6b20> </span><br> <span class='unselectable'>>>> </span>EntityType.SITE.construct(id_=1, action="gimme_site") <span class='unselectable'>TestSite constructor called with id 1 and action gimme_site &lt;entity_types.TestSite object at 0x7f9aac5c1730> </span></pre> </div> <!-- endregion --> <p> Because these factory methods return the newly created instance of the desired type, the string representation is printed on the console after the method finishes outputting its processing results, for example: <code>&lt;entity_types.TestLecture object at 0x7f9aac690070></code>. </p> <p> Using enums to construct class instances and/or invoke methods (aka dynamic dispatch) is super powerful. It rather resembles generics, actually, even though <a href='https://docs.python.org/3/library/typing.html#building-generic-types' target='_blank' rel="nofollow">Python&rsquo;s support for generics</a> is still in its infancy. </p> <p> The complete Python program discussed in this article is <a href='/blog/python/entity_types.py'>here</a>. </p> <!-- endregion --> Linking Directories on NTFS and Ext4 Volumes 2022-02-07T00:00:00-05:00 https://mslinn.github.io/blog/2022/02/07/wsl-volumes <!-- #region intro --> <p> Sometimes I need to insert some code into a program that depends on the type of format that a drive volume has. For example, today I need to either make a Windows junction to connect two directories on NTFS volumes. On the other hand, if one or both directories were on other types of volumes, I would have to connect the directories using a Linux symlink. </p> <p> All of the bash scripts shown in this article are meant to run in a Bash shell running on WSL or WSL2. </p> <!-- endregion --> <!-- #region Use Windows Junctions When Possible --> <h2 id="possible">Use Windows Junctions When Possible</h2> <p> When working on WSL, Windows junctions are more desirable than Linux hard links and symlinks because junctions are visible in Windows and also in WSL. Unlike Linux hard links, which only work within a single volume, Windows junctions can span two volumes. Linux symlinks are only visible from WSL; a symlinked directory only appears as a useless file when viewed from Windows. </p> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.svg" type="image/svg"> <!---<source srcset="/blog/images/wsl-volumes/windowsFileMgr.avif" type="image/avif">--> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.webp" type="image/webp"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.apng" type="image/apng"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.png" type="image/png"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.jpg" type="image/jpeg"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.jpeg" type="image/jpeg"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.jfif" type="image/jpeg"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.pjpeg" type="image/jpeg"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.pjp" type="image/jpeg"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.gif" type="image/gif"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.tif" type="image/tiff"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.tiff" type="image/tiff"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.bmp" type="image/bmp"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.ico" type="image/x-icon"> <source srcset="/blog/images/wsl-volumes/windowsFileMgr.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/wsl-volumes/windowsFileMgr.png" style='width: 100%; ' /> </picture> </div> <p> Both directories need to be on NTFS volumes to make a Windows junction between them. Junctions are permitted within a single NTFS volume, or between two NTFS volumes. Linux symlinks can be used on all volume types, but only work properly when viewed from Linux. </p> <p> Windows junctions are shown with a small arrow icon in Windows File Manager. In the image above, the <code>curriculum</code> directory is a junction. </p> <!-- endregion --> <!-- #region Determining a Volume Type --> <h2 id="volumeType">Determining a Volume Type</h2> <p> I wrote the <code>volumeType</code> bash function to obtain the type of the volume that contains a file or directory. Linux <code>ext4</code> volumes have partition type <code>ext4</code>. NTFS volumes have partition type <code>9p</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida950d4a7c8d2'><button class='copyBtn' data-clipboard-target='#ida950d4a7c8d2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function volumeType { # Usually returns volume types ext4 or 9p (for NTFS) df -Th "$1" | tail -n 1 | awk '{print $2}' }</pre> </div> <!-- endregion --> <p> Here are examples of using <code>volumeType</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id678e93a41af2'><button class='copyBtn' data-clipboard-target='#id678e93a41af2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>volumeType /mnt/c # Under WSL/WSL2 this is usually NTFS <span class='unselectable'>9p </span><br> <span class='unselectable'>$ </span>volumeType / # For Ubuntu this defaults to ext4 <span class='unselectable'>ext4 </span></pre> </div> <!-- endregion --> <p> All of the remaining scripts on this page either return a value (indicating <code>true</code>), or they do not return anything (indicating <code>false</code>). </p> <p> Two more bash functions test if a file or directory is part of an NTFS or ext4 volume: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1c79a5f186be'><button class='copyBtn' data-clipboard-target='#id1c79a5f186be' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function isNTFS { if [ "$( volumeType "$1" )" == 9p ]; then echo yes; fi }<br> function isExt4 { if [ "$( volumeType "$1" )" == ext4 ]; then echo yes; fi }</pre> </div> <!-- endregion --> <p> Here are examples of using <code>isNTFS</code> and <code>isExt4</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf741f892f332'><button class='copyBtn' data-clipboard-target='#idf741f892f332' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>isNTFS /mnt/c <span class='unselectable'>yes </span><br> <span class='unselectable'>$ </span>isExt4 /mnt/c<br> <span class='unselectable'>$ </span>isNTFS /<br> <span class='unselectable'>$ </span>isExt4 / <span class='unselectable'>yes </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Windows Junctions --> <h2 id="junctions">Windows Junctions</h2> <p> The <code>bothOnNTFS</code> bash function indicates if both of the paths passed to it are on NTFS volumes. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8ae5fc3f5d0b'><button class='copyBtn' data-clipboard-target='#id8ae5fc3f5d0b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function bothOnNTFS { if [ "$( isNTFS "$1" )" ] && [ "$( isNTFS "$2" )" ]; then echo yes; fi }</pre> </div> <p> Let's try out <code>bothOnNTFS</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id74b3855f4923'><button class='copyBtn' data-clipboard-target='#id74b3855f4923' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>bothOnNTFS /mnt/c /mnt/f <span class='unselectable'>yes </span><br> <span class='unselectable'>$ </span>bothOnNTFS /mnt/c /</pre> </div> <!-- endregion --> <p> <code>bothOnNTFS</code> lets us decide how to connect two directories. If they are both on NTFS volumes, we can connect them using a Windows junction; otherwise we'll need to symlink them. </p> <!-- endregion --> <!-- #region Connecting Via a Windows Junction or Linux Symlink --> <h2 id="connect">Connecting Via a Windows Junction or Linux Symlink</h2> <p> We could either make a Windows junction using the <a href='https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/mklink' target='_blank' rel="nofollow"><code>mklink</code></a> command, or we could make a Linux symlink using the <a href='https://man7.org/linux/man-pages/man1/ln.1.html' target='_blank' rel="nofollow"><code>ln -s</code></a> command. Notice how the order of parameters between <code>mklink</code> is the reverse of the order of the Linux <code>ln</code> command. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc4dc438c5e04'><button class='copyBtn' data-clipboard-target='#idc4dc438c5e04' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>if [ $( bothOnNTFS "$cadenzaCurriculum" . ) ]; then WINDOWS_PATH="$( wslpath -w "$cadenzaCurriculum/site_$TITLE" )" echo "Making Windows junction from $cadenzaCurriculum/site_$TITLE to curriculum/" cmd.exe /C mklink /j curriculum "$WINDOWS_PATH" else echo "Symlinking $cadenzaCurriculum/site_$TITLE to curriculum/" ln -s "$cadenzaCurriculum/site_$TITLE" curriculum fi</pre> </div> When the above code ran it produced: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idee9b1e828b13'><button class='copyBtn' data-clipboard-target='#idee9b1e828b13' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Making Windows junction from /mnt/f/work/cadenzaHome/cadenzaCurriculum/site_ScalaCourses.com to curriculum/ Junction created for curriculum <<===>> F:\work\cadenzaHome\cadenzaCurriculum\site_ScalaCourses.com</pre> </div> <!-- endregion --> <!-- #region Windows Junction for Windows Home in Ubuntu --> <h2 id="">Windows Junction for Windows Home in Ubuntu</h2> <p> Neither Linux nor Java appreciate spaces in directories. Spaces in home directories are by nature problematic. If you have a space in your home directory on Windows, you can use the PowerShell <code>New-Item</code> commandlet to create an equivalent junction to a new directory node without spaces in the name. </p> <p> The following uses the PowerShell <a href='https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/new-item?view=powershell-7.3' target='_blank' rel="nofollow"><code>New-Item</code></a> commandlet to create a new directory called <code>C:\<wbr>Users\<wbr>mslinn</code>, accessible from WSL as <code>/mnt/<wbr>c/<wbr>mslinn</code>, which points to the same directory as <code>C:\Users\<wbr>Mike Slinn</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>PowerShell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idda2121518c74'><button class='copyBtn' data-clipboard-target='#idda2121518c74' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>PS C:\Users\Mike Slinn> </span>ni "C:\Users\mslinn" -i SymbolicLink -ta "C:\Users\Mike Slinn"</pre> </div> <!-- endregion --> Handcrafted Dynamic DNS for AWS Route53 and Namecheap 2022-01-30T00:00:00-05:00 https://mslinn.github.io/blog/2022/01/30/ddns-route53 <p> Now that I have fiber-optic internet service in my apartment, with 500 GB/s upload and download, I thought I would save money by hosting my <a href='https://scalacourses.com'><code>scalacourses.com</code></a> website on an Ubuntu server that runs here, instead of AWS. My home IP address is quite stable, and only changes when the fiber modem boots up. The modem is branded as a <a href='https://support.bell.ca/internet/products/home-hub-4000-modem' target='_blank' rel="nofollow">Bell Home Hub 4000</a>, but I believe it is actually made by Arris (formerly known as Motorola). </p> <p> It makes little sense to pay the commercial cost of dedicated dynamic DNS services (typically $55 USD / year) when it is so easy to automate, and the operational cost is less than one cent per year. </p> <p> I wrote two little scripts that automatically check my public IP address, and modifies the DNS record for my home IP address whenever the IP address changes. One script is for sites that use DNS provided by AWS Route53, and the other is for DNS provided by Namecheap. </p> <p> The approach shown here could be used for all DNS servers that have a command-line interface. This was originally written for AWS Route53, but when I <a href='/blog/2022/05/26/aws-hijacking.html'>moved off AWS</a> I made a version for <a href='https://www.namecheap.com/support/knowledgebase/article.aspx/29/11/how-to-dynamically-update-the-hosts-ip-with-an-http-request/' target='_blank' rel="nofollow">Namecheap</a>. Alternative DNS providers include <a href='https://docs.microsoft.com/en-us/cli/azure/network/dns?view=azure-cli-latest' target='_blank' rel="nofollow">Azure DNS</a>, <a href='https://developers.cloudflare.com/cloudflare-one/tutorials/cli' target='_blank' rel="nofollow">Cloudflare DNS</a>, <a href='https://support.dnsmadeeasy.com/support/solutions/articles/47001119947-the-ddns-shell-script' target='_blank' rel="nofollow">DNSMadeEasy</a>, <a href='https://developer.dnsimple.com/libraries/' target='_blank' rel="nofollow">DNSimple</a>, <a href='https://cloud.google.com/sdk/gcloud/reference/dns' target='_blank' rel="nofollow">Google Cloud DNS</a>, and <a href='https://github.com/ultradns/dns_sprockets' target='_blank' rel="nofollow">UltraDNS</a>. </p> <h2 id="fwd">Forwarding HTTP Requests</h2> <p> I added some entries to the modem so incoming HTTP traffic on ports 80 and 443 would be forwarded to ports 9000 and 9443 on my home server, <code>gojira</code>. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/ddns/homeHubPortForward.svg" type="image/svg"> <!---<source srcset="/blog/images/ddns/homeHubPortForward.avif" type="image/avif">--> <source srcset="/blog/images/ddns/homeHubPortForward.webp" type="image/webp"> <source srcset="/blog/images/ddns/homeHubPortForward.apng" type="image/apng"> <source srcset="/blog/images/ddns/homeHubPortForward.png" type="image/png"> <source srcset="/blog/images/ddns/homeHubPortForward.jpg" type="image/jpeg"> <source srcset="/blog/images/ddns/homeHubPortForward.jpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/homeHubPortForward.jfif" type="image/jpeg"> <source srcset="/blog/images/ddns/homeHubPortForward.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ddns/homeHubPortForward.pjp" type="image/jpeg"> <source srcset="/blog/images/ddns/homeHubPortForward.gif" type="image/gif"> <source srcset="/blog/images/ddns/homeHubPortForward.tif" type="image/tiff"> <source srcset="/blog/images/ddns/homeHubPortForward.tiff" type="image/tiff"> <source srcset="/blog/images/ddns/homeHubPortForward.bmp" type="image/bmp"> <source srcset="/blog/images/ddns/homeHubPortForward.ico" type="image/x-icon"> <source srcset="/blog/images/ddns/homeHubPortForward.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/ddns/homeHubPortForward.png" style='width: 100%; ' /> </picture> </div> <h2 id="usage">Using the Scripts</h2> <p> The version for AWS Route53 is called <code>dynamicDnsAws</code>, and the version for Namecheap is called <code>dynamicDnsNamecheap</code>. </p> <p> The scripts save the IP address to a file, and periodically compare the saved value to the current value. Then the scripts modify the DNS record for a specified subdomain whenever the value of the public IP address changes. </p> <h3 id="aws">Using the AWS Script</h3> <p> Here is the help information for the script: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id569696a34873'><button class='copyBtn' data-clipboard-target='#id569696a34873' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsAws <span class='unselectable'>dynamicDnsAws - Maintains a dynamic DNS record in AWS Route53<br> Saves data in '/home/mslinn/.dynamicDnsAws'<br> Syntax: dynamicDnsAws [OPTIONS] SUB_DOMAIN DOMAIN<br> OPTIONS: -v Verbose mode<br> Example usage: dynamicDnsAws www scalacourses.com dynamicDnsAws -v www scalacourses.com </span></pre> </div> <p> Here is a sample usage: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8785b6d38b14'><button class='copyBtn' data-clipboard-target='#id8785b6d38b14' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsAws www scalacourses.com <span class='unselectable'>{ "ChangeInfo": { "Id": "/change/C075751811HI18SH4L8L0", "Status": "PENDING", "SubmittedAt": "2022-01-30T21:10:09.261Z", "Comment": "UPSERT a record for www.scalacourses.com" } } </span></pre> </div> <h3 id="aws">Using the Namecheap Script</h3> <p> Here is the help information for the script: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcd4150f91274'><button class='copyBtn' data-clipboard-target='#idcd4150f91274' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsNamecheap <span class='unselectable'>dynamicDnsNamecheap - Maintains two Namecheap dynamic DNS records<br> Saves data in '/home/mslinn/.dynamicDns'<br> Syntax: dynamicDnsNamecheap [OPTIONS] DOMAIN PASSWORD<br> OPTIONS: -v Verbose mode<br> Example usage: dynamicDnsNamecheap mydomain.com asdfasdfasdfasdfasdf dynamicDnsNamecheap -v mydomain.com asdfasdfasdfasdf </span></pre> </div> <p> Here is sample usage: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbcc15520fbea'><button class='copyBtn' data-clipboard-target='#idbcc15520fbea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsNamecheap scalacourses.com asdfasdfasdfasdfasdf <span class='unselectable'>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-16&quot;?&gt; &lt;interface-response&gt; &lt;Command&gt;SETDNSHOST&lt;/Command&gt; &lt;Language&gt;eng&lt;/Language&gt; &lt;IP&gt;142.126.4.220&lt;/IP&gt; &lt;ErrCount&gt;0&lt;/ErrCount&gt; &lt;errors /&gt; &lt;ResponseCount&gt;0&lt;/ResponseCount&gt; &lt;responses /&gt; &lt;Done&gt;true&lt;/Done&gt; &lt;debug&gt;&lt;![CDATA[]]&gt;&lt;/debug&gt; &lt;/interface-response&gt;&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-16&quot;?&gt; &lt;interface-response&gt; &lt;Command&gt;SETDNSHOST&lt;/Command&gt; &lt;Language&gt;eng&lt;/Language&gt; &lt;IP&gt;142.126.4.220&lt;/IP&gt; &lt;ErrCount&gt;0&lt;/ErrCount&gt; &lt;errors /&gt; &lt;ResponseCount&gt;0&lt;/ResponseCount&gt; &lt;responses /&gt; &lt;Done&gt;true&lt;/Done&gt; &lt;debug&gt;&lt;![CDATA[]]&gt;&lt;/debug&gt; &lt;/interface-response&gt; </span></pre> </div> <h2 id="crontab">Invoking the Scripts from Crontab</h2> <p> A personal <code>crontab</code> can be modified by typing: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id78a24f47083a'><button class='copyBtn' data-clipboard-target='#id78a24f47083a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>crontab -e</pre> </div> <p> I pasted in the following into <code>crontab</code> on my Ubuntu server, running at home. These lines invoke the <code>dynamicDnsNamecheap</code> script via <code>crontab</code> every 5 minutes. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb64b667533a'><button class='copyBtn' data-clipboard-target='#idcb64b667533a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>*/5 * * * * /path/to/dynamicDnsNamecheap my_domain.com asdfasdfasdfasdfasdf</pre> </div> <p> One the above is saved, <code>crontab</code> will run the script every 5 minutes. </p> <h2 id="source">Script Source Codes</h2> <p> Here are the bash scripts: </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,dynamicDnsAws' download='dynamicDnsAws' title='Click on the file name to download the file'>dynamicDnsAws</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="iddbeff676b14c"><button class='copyBtn' data-clipboard-target='#iddbeff676b14c'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # Author: Mike Slinn mslinn@mslinn.com # Written 2022-01-30 export SAVE_FILE_NAME="$HOME/.dynamicDns" function help &#123; echo "$( basename $0 ) - Maintains a dynamic DNS record in AWS Route53 Saves data in '$SAVE_FILE_NAME' Syntax: $( basename $0) [OPTIONS] SUB_DOMAIN DOMAIN OPTIONS: -v Verbose mode Example usage: $( basename $0) my_subdomain mydomain.com $( basename $0) -v my_subdomain mydomain.com " exit 1 &#125; function upsert &#123; export HOSTED_ZONES="$( aws route53 list-hosted-zones )" export HOSTED_ZONE_RECORD="$( jq -r ".HostedZones[] | select(.Name == \"$DOMAIN.\")" &lt;&lt;&lt; "$HOSTED_ZONES" )" export HOSTED_ZONE_RECORD_ID="$( jq -r .Id &lt;&lt;&lt; "$HOSTED_ZONE_RECORD" )" aws route53 change-resource-record-sets \ --hosted-zone-id "$HOSTED_ZONE_RECORD_ID" \ --change-batch "&#123; \"Comment\": \"UPSERT a record for $SUBDOMAIN.$DOMAIN\", \"Changes\": [&#123; \"Action\": \"UPSERT\", \"ResourceRecordSet\": &#123; \"Name\": \"$SUBDOMAIN.$DOMAIN\", \"Type\": \"A\", \"TTL\": 300, \"ResourceRecords\": [&#123; \"Value\": \"$IP\"&#125;] &#125; &#125;] &#125;" echo "$IP" > "$SAVE_FILE_NAME" &#125; if [ "$1" == -v ]; then export VERBOSE=true shift fi if [ -z "$2" ]; then help; fi set -e export SUBDOMAIN="$1" export DOMAIN="$2" export IP="$( dig +short myip.opendns.com @resolver1.opendns.com )" if [ ! -f "$SAVE_FILE_NAME" ]; then if [ "$VERBOSE" ]; then echo "Creating $SAVE_FILE_NAME"; fi upsert; elif [ $( cat "$SAVE_FILE_NAME" ) != "$IP" ]; then if [ "$VERBOSE" ]; then echo "Updating $SAVE_FILE_NAME" echo "'$IP' was not equal to '$( cat "$SAVE_FILE_NAME" )'" fi upsert; else if [ "$VERBOSE" ]; then echo "No change necessary for $SAVE_FILE_NAME"; fi fi </pre> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,dynamicDnsNamecheap' download='dynamicDnsNamecheap' title='Click on the file name to download the file'>dynamicDnsNamecheap</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="ide11614701356"><button class='copyBtn' data-clipboard-target='#ide11614701356'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # Author: Mike Slinn mslinn@mslinn.com # Modified from AWS version (dynameicDnsAws) 2022-06-30 # See https://www.namecheap.com/support/knowledgebase/article.aspx/36/11/how-do-i-start-using-dynamic-dns/ export SAVE_FILE_NAME="$HOME/.dynamicDns" function help &#123; echo "$( basename $0 ) - Maintains two Namecheap dynamic DNS records Saves data in '$SAVE_FILE_NAME' Syntax: $( basename $0) [OPTIONS] DOMAIN PASSWORD OPTIONS: -v Verbose mode Example usage: $( basename $0) mydomain.com asdfasdfasdfasdfasdf $( basename $0) -v mydomain.com asdfasdfasdfasdf " exit 1 &#125; function upsert &#123; curl "https://dynamicdns.park-your-domain.com/update?host=@&amp;domain=$DOMAIN&amp;password=$PASSWORD&amp;ip=$IP" curl "https://dynamicdns.park-your-domain.com/update?host=www&amp;domain=$DOMAIN&amp;password=$PASSWORD&amp;ip=$IP" echo "$IP" > "$SAVE_FILE_NAME" echo "" &#125; if [ "$1" == -v ]; then export VERBOSE=true shift fi if [ -z "$2" ]; then help; fi set -e export DOMAIN="$1" export PASSWORD="$2" export IP="$( dig +short myip.opendns.com @resolver1.opendns.com )" if [ ! -f "$SAVE_FILE_NAME" ]; then if [ "$VERBOSE" ]; then echo "Creating $SAVE_FILE_NAME"; fi upsert; elif [ "$( cat "$SAVE_FILE_NAME" )" != "$IP" ]; then if [ "$VERBOSE" ]; then echo "Updating $SAVE_FILE_NAME" echo "'$IP' was not equal to '$( cat "$SAVE_FILE_NAME" )'" fi upsert; else if [ "$VERBOSE" ]; then echo "No change necessary for $SAVE_FILE_NAME"; fi fi </pre> Windows Diskpart Cooperates With Diskmgmt 2022-01-14T00:00:00-05:00 https://mslinn.github.io/blog/2022/01/14/diskpart <!-- #region intro --> <p> Recently, I wanted to use some old hard drives as backup media. That meant scrubbing all the partitions off the drives, and installing new partitions, which of course would be empty. </p> <div class="right rounded shadow warning" style="width: 45%"> <p> Warning &ndash; Working with a command line program for system-level operations, without having backed up the system, is like walking on a tightrope without a net. </p> <p style="margin-bottom: 0"> A mistake could inadvertently wipe out a different hard drive on the computer than you intended. Without the ability to restore the system disk from backup, your computer could become inoperable. </p> </div> <p> However, I was unable to repartition one of those old drives using the GUI-based Windows <code>diskmgmt.msc</code> disk manager. The drive had soft errors. For some reason, those errors made it impossible for <code>diskmgmt.msc</code> to scan the drive. </p> <p> The more powerful Windows <a href='https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/diskpart' target='_blank' rel="nofollow"><code>diskpart</code></a>, a command line program, was able to get the job done. </p> <p> This article <a href ="#gui">ends</a> by demonstrating how you can get benefit from the <code>diskmgmt</code> GUI even when you are using the <code>diskpart</code> command line interface. This is possible because every command you type into <code>diskpart</code> causes a Windows system event to be published, and because <code>diskmgmt</code> subscribes to those events, it is able to display the results as they happen. </p> <!-- endregion --> <!-- #region Starting Diskpart --> <h2 id="starting">Starting <span class="code">Diskpart</span></h2> <p> Run <code>diskpart</code> as administrator as follows: </p> <ol> <li> Press the <kbd>Windows</kbd> key. Do not hold it down, just depress it once, as you would do for any other key, and let it go. </li> <li>Type <code>diskpart</code>.</li> <li>Use the mouse or arrow keys to select <b>Run as administrator</b>.</li> </ol> <div class='imgWrapper imgFlex center' style='width: 75%; '> <picture class='imgPicture'> <source srcset="/blog/images/diskpart/diskpartLaunch.svg" type="image/svg"> <!---<source srcset="/blog/images/diskpart/diskpartLaunch.avif" type="image/avif">--> <source srcset="/blog/images/diskpart/diskpartLaunch.webp" type="image/webp"> <source srcset="/blog/images/diskpart/diskpartLaunch.apng" type="image/apng"> <source srcset="/blog/images/diskpart/diskpartLaunch.png" type="image/png"> <source srcset="/blog/images/diskpart/diskpartLaunch.jpg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskpartLaunch.jpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskpartLaunch.jfif" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskpartLaunch.pjpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskpartLaunch.pjp" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskpartLaunch.gif" type="image/gif"> <source srcset="/blog/images/diskpart/diskpartLaunch.tif" type="image/tiff"> <source srcset="/blog/images/diskpart/diskpartLaunch.tiff" type="image/tiff"> <source srcset="/blog/images/diskpart/diskpartLaunch.bmp" type="image/bmp"> <source srcset="/blog/images/diskpart/diskpartLaunch.ico" type="image/x-icon"> <source srcset="/blog/images/diskpart/diskpartLaunch.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/diskpart/diskpartLaunch.png" style='width: 100%; ' /> </picture> </div> <p> A window should open up labeled <b>Microsoft DiskPart</b>. Let's start by listing all the <code>diskpart</code> commands. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide2b1bba66559'><button class='copyBtn' data-clipboard-target='#ide2b1bba66559' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>Microsoft DiskPart version 10.0.19041.964 Copyright (C) Microsoft Corporation. On computer: BEAR<br> DISKPART> </span>help<br> <span class='unselectable'>Microsoft DiskPart version 10.0.19041.964<br> ACTIVE - Mark the selected partition as active. ADD - Add a mirror to a simple volume. ASSIGN - Assign a drive letter or mount point to the selected volume. ATTRIBUTES - Manipulate volume or disk attributes. ATTACH - Attaches a virtual disk file. AUTOMOUNT - Enable and disable automatic mounting of basic volumes. BREAK - Break a mirror set. CLEAN - Clear the configuration information, or all information, off the disk. COMPACT - Attempts to reduce the physical size of the file. CONVERT - Convert between different disk formats. CREATE - Create a volume, partition or virtual disk. DELETE - Delete an object. DETAIL - Provide details about an object. DETACH - Detaches a virtual disk file. EXIT - Exit DiskPart. EXTEND - Extend a volume. EXPAND - Expands the maximum size available on a virtual disk. FILESYSTEMS - Display current and supported file systems on the volume. FORMAT - Format the volume or partition. GPT - Assign attributes to the selected GPT partition. HELP - Display a list of commands. IMPORT - Import a disk group. INACTIVE - Mark the selected partition as inactive. LIST - Display a list of objects. MERGE - Merges a child disk with its parents. ONLINE - Online an object that is currently marked as offline. OFFLINE - Offline an object that is currently marked as online. RECOVER - Refreshes the state of all disks in the selected pack. Attempts recovery on disks in the invalid pack, and resynchronizes mirrored volumes and RAID5 volumes that have stale plex or parity data. REM - Does nothing. This is used to comment scripts. REMOVE - Remove a drive letter or mount point assignment. REPAIR - Repair a RAID-5 volume with a failed member. RESCAN - Rescan the computer looking for disks and volumes. RETAIN - Place a retained partition under a simple volume. SAN - Display or set the SAN policy for the currently booted OS. SELECT - Shift the focus to an object. SETID - Change the partition type. SHRINK - Reduce the size of the selected volume. UNIQUEID - Displays or sets the GUID partition table (GPT) identifier or master boot record (MBR) signature of a disk. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Listing Drives, Partitions and Volumes --> <h2 id="listing">Listing Drives, Partitions and Volumes</h2> <p> Listing the drives is generally a good first step. Let's discover the command for that: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id480e963f2829'><button class='copyBtn' data-clipboard-target='#id480e963f2829' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list<br> <span class='unselectable'>Microsoft DiskPart version 10.0.19041.964<br> DISK - Display a list of disks. For example, LIST DISK. PARTITION - Display a list of partitions on the selected disk. For example, LIST PARTITION. VOLUME - Display a list of volumes. For example, LIST VOLUME. VDISK - Displays a list of virtual disks. </span></pre> </div> <!-- endregion --> <p> OK, we can list disks, partitions, volumes and virtual disks. Let's list the disk drives. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id38964e1c2494'><button class='copyBtn' data-clipboard-target='#id38964e1c2494' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list disk<br> <span class='unselectable'>Disk ### Status Size Free Dyn Gpt -------- ------------- ------- ------- --- --- Disk 0 Online 1863 GB 1024 KB * Disk 1 Online 465 GB 1024 KB Disk 2 Online 1863 GB 1024 KB * Disk 3 Online 1863 GB 0 B * Disk 5 Online 931 GB 931 GB * </span></pre> </div> <!-- endregion --> <div class='imgWrapper imgBlock right quartersize' style=' '> <figure> <a href='https://www.amazon.com/NewerTech-Enclosure-Interface-NWTU3S3HD-hot-swapping/dp/B007TTQQIA' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.svg" type="image/svg"> <!---<source srcset="/blog/images/diskpart/newerTechVoyagerS3.avif" type="image/avif">--> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.webp" type="image/webp"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.apng" type="image/apng"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.png" type="image/png"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.jpg" type="image/jpeg"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.jpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.jfif" type="image/jpeg"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.pjpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.pjp" type="image/jpeg"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.gif" type="image/gif"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.tif" type="image/tiff"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.tiff" type="image/tiff"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.bmp" type="image/bmp"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.ico" type="image/x-icon"> <source srcset="/blog/images/diskpart/newerTechVoyagerS3.cur" type="image/x-icon"> <img alt='NewerTech Voyager S3 caddy' class="imgImg rounded shadow" src="/blog/images/diskpart/newerTechVoyagerS3.png" style='width: 100%; ' title='NewerTech Voyager S3 caddy' /> </picture> </a> <figcaption class='imgFigCaption quartersize'> <a href="https://www.amazon.com/NewerTech-Enclosure-Interface-NWTU3S3HD-hot-swapping/dp/B007TTQQIA" target='_blank' > NewerTech Voyager S3 caddy </a> </figcaption> </figure> </div> <p> At this point, I inserted the old 5.25" SATA drive that I wanted to repurpose into a NewerTech Voyager S3 caddy, connected to my computer via a USB 3 cable, and Windows automatically mounted it. The caddy also accepts 2.5" SATA drives, such as those commonly found in laptops. </p> <p> Now I told <code>diskpart</code> to rescan the drives, and then I listed the volumes on all disks. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer clear' id='id54ddee38a7ba'><button class='copyBtn' data-clipboard-target='#id54ddee38a7ba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>rescan<br> <span class='unselectable'>Please wait while DiskPart scans your configuration...<br> DiskPart has finished scanning your configuration.<br> DISKPART> </span>list volume<br> <span class='unselectable'>Volume ### Ltr Label Fs Type Size Status Info ---------- --- ----------- ----- ---------- ------- --------- -------- Volume 0 D DVD-ROM 0 B No Media Volume 1 C BEAR_C NTFS Partition 1861 GB Healthy Boot Volume 2 FAT32 Partition 100 MB Healthy System Volume 3 NTFS Partition 539 MB Healthy Hidden Volume 4 NTFS Partition 450 MB Healthy Hidden Volume 5 F Work NTFS Partition 1863 GB Healthy Volume 6 E BEAR_E NTFS Partition 1863 GB Healthy <span class="bg_yellow"> Volume 8 FAT32 Partition 512 MB Healthy Hidden</span> </span></pre> </div> <!-- endregion --> <p> <code>Diskpart</code> displayed the hidden volume (#8) in the drive in the caddy. This drive has <code>readonly</code> status set, which prevents its contents from being modified or deleted. That would be good if I wanted to use this drive as an archive, but instead I want to scrub it and write new information on it. The currently existing partitions on this drive cannot be erased until <code>readonly</code> is cleared. Lets remove <code>readonly</code> status from the drive now: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida68ef58d9c7d'><button class='copyBtn' data-clipboard-target='#ida68ef58d9c7d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>select volume 8 <span class='unselectable'>Volume 8 is the selected volume.<br> DISKPART> </span>attributes disk clear readonly <span class='unselectable'>Disk attributes cleared successfully.<br> DISKPART> </span>rescan <span class='unselectable'>Please wait while DiskPart scans your configuration... DiskPart has finished scanning your configuration.<br> DISKPART> </span>list volume <span class='unselectable'>Volume ### Ltr Label Fs Type Size Status Info ---------- --- ----------- ----- ---------- ------- --------- -------- Volume 0 D DVD-ROM 0 B No Media Volume 1 C BEAR_C NTFS Partition 1861 GB Healthy Boot Volume 2 FAT32 Partition 100 MB Healthy System Volume 3 NTFS Partition 539 MB Healthy Hidden Volume 4 NTFS Partition 450 MB Healthy Hidden Volume 5 F Work NTFS Partition 1863 GB Healthy Volume 6 E BEAR_E NTFS Partition 1863 GB Healthy Volume 8 FAT32 Partition 512 MB Healthy Hidden </span></pre> </div> <!-- endregion --> <!-- #region Wiping the Drive --> <h2 id="wipe">Wiping the Drive</h2> <p> Now it is time to wipe the drive clean, which removes all partitions. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide58a5943731c'><button class='copyBtn' data-clipboard-target='#ide58a5943731c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>select disk 5 <span class='unselectable'>Disk 5 is now the selected disk.<br> DISKPART> </span>list disk <span class='unselectable'>Disk ### Status Size Free Dyn Gpt -------- ------------- ------- ------- --- --- Disk 0 Online 1863 GB 1024 KB * Disk 1 Online 465 GB 1024 KB Disk 2 Online 1863 GB 1024 KB * Disk 3 Online 1863 GB 0 B * Disk 5 Online 931 GB 931 GB *<br> DISKPART> </span>clean <span class='unselectable'>DiskPart succeeded in cleaning the disk. </span></pre> </div> <!-- endregion --> <!-- #region Archiving a Drive --> <h2 id="setRO">Archiving a Drive</h2> <p> If instead of wiping the drive, I wanted to archive the drive, I would want to set the read-only status. To do that, first select the drive as before, then type: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id71a62466815c'><button class='copyBtn' data-clipboard-target='#id71a62466815c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list disk <span class='unselectable'># Output as shown above </span><br> <span class='unselectable'>DISKPART> </span>select disk N <span class='unselectable'>Disk N is now the selected disk. </span><br> <span class='unselectable'>DISKPART> </span>attributes disk set readonly</pre> </div> <!-- endregion --> <p> Now the drive's contents could not accidently be erased or modified. </p> <!-- endregion --> <!-- #region Create A New Partition --> <h2 id="partition">Create A New Partition</h2> <p> We need to create a new partition that spans the entire disk on the now-empty selected drive. The <code>create</code> command can do that. Let's look at the help before using the command: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8075fe1873b9'><button class='copyBtn' data-clipboard-target='#id8075fe1873b9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>create <span class='unselectable'>Microsoft DiskPart version 10.0.19041.964<br> PARTITION - Create a partition. VOLUME - Create a volume. VDISK - Creates a virtual disk file.<br> DISKPART> </span>create partition<br> <span class='unselectable'>Microsoft DiskPart version 10.0.19041.964<br> EFI - Create an EFI system partition. EXTENDED - Create an extended partition. LOGICAL - Create a logical drive. MSR - Create a Microsoft Reserved partition. PRIMARY - Create a primary partition. </span></pre> </div> <!-- endregion --> <p> To create a new partition that spans the entire disk on the now-empty selected drive and make it active: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfc6f0179cc1e'><button class='copyBtn' data-clipboard-target='#idfc6f0179cc1e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>create partition primary <span class='unselectable'>DiskPart succeeded in creating the specified partition.<br> DISKPART> </span>select partition 1 <span class='unselectable'>Partition 1 is now the selected partition.<br> DISKPART> </span>active <span class='unselectable'>DiskPart marked the current partition as active. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Format A Volume --> <h2 id="format">Format A Volume</h2> <p> Let's format the entire selected drive as one volume. Like the <code>clean</code> command, the <code>format</code> command operates on the currently selected disk. First, let's look at the help: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaae0e455d924'><button class='copyBtn' data-clipboard-target='#idaae0e455d924' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>help format<br> <span class='unselectable'>Formats the specified volume for use with Windows.<br> Syntax: FORMAT [[FS=&lt;FS>] [REVISION=&lt;X.XX>] | RECOMMENDED] [LABEL=&lt;"label">] [UNIT=&lt;N>] [QUICK] [COMPRESS] [OVERRIDE] [DUPLICATE] [NOWAIT] [NOERR]<br> FS=<FS> Specifies the type of file system. If no file system is given, the default file system displayed by the FILESYSTEMS command is used.<br> REVISION=&lt;X.XX><br> Specifies the file system revision (if applicable).<br> RECOMMENDED If specified, use the recommended file system and revision instead of the default if a recommendation exists. The recommended file system (if one exists) is displayed by the FILESYSTEMS command.<br> LABEL=&lt;"label"><br> Specifies the volume label.<br> UNIT=&lt;N> Overrides the default allocation unit size. Default settings are strongly recommended for general use. The default allocation unit size for a particular file system is displayed by the FILESYSTEMS command.<br> NTFS compression is not supported for allocation unit sizes above 4096.<br> QUICK Performs a quick format.<br> COMPRESS NTFS only: Files created on the new volume will be compressed by default.<br> OVERRIDE Forces the file system to dismount first if necessary. All opened handles to the volume would no longer be valid.<br> DUPLICATE UDF Only: This flag applies to UDF format, version 2.5 or higher. This flag instructs the format operation to duplicate the file system meta-data to a second set of sectors on the disk. The duplicate meta-data is used by applications, for example repair or recovery applications. If the primary meta-data sectors are found to be corrupted, the file system meta-data will be read from the duplicate sectors.<br> NOWAIT Forces the command to return immediately while the format process is still in progress. If NOWAIT is not specified, DiskPart will display format progress in percentage.<br> NOERR For scripting only. When an error is encountered, DiskPart continues to process commands as if the error did not occur. Without the NOERR parameter, an error causes DiskPart to exit with an error code.<br> A volume must be selected for this operation to succeed.<br> Examples:<br> FORMAT FS=NTFS LABEL="New Volume" QUICK COMPRESS FORMAT RECOMMENDED OVERRIDE </span></pre> </div> <!-- endregion --> <p> <code>Diskpart</code> automatically chooses the optimal file system for the selected drive if you do not specify it. Choices include FAT, FAT32 and NTFS. To quick format the selected drive using the optimal file system: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide6e38aeb469d'><button class='copyBtn' data-clipboard-target='#ide6e38aeb469d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format quick</pre> </div> <p> The default is to fully format the selected drive, which is what you want if the selected drive is suspect: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3bb82ff69071'><button class='copyBtn' data-clipboard-target='#id3bb82ff69071' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format</pre> </div> <p> You could specify multiple parameters, for example: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id958b1190b4ec'><button class='copyBtn' data-clipboard-target='#id958b1190b4ec' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format fs=ntfs label="My Drive" quick</pre> </div> <!-- endregion --> <!-- #region Assigning a Drive Letter --> <h2 id="assign">Assigning a Drive Letter</h2> <p> Once the drive was formatted, I assigned the selected drive the letter <code>G</code>. You do not normally need to perform this step, unless you want to <a href='https://www.groovypost.com/howto/assign-permanent-letter-removable-usb-drive-windows/' target='_blank' rel="nofollow">define a default letter for this drive</a>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf8fdb28e306a'><button class='copyBtn' data-clipboard-target='#idf8fdb28e306a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>assign letter=G</pre> </div> <!-- endregion --> <!-- #region Diskmgmt GUI Shows Progress --> <h2 id="gui"><span class="code">Diskmgmt</span> GUI Shows Progress</h2> <p> Having a GUI continuously report the current state of the drive maintenance you are performing using a command-line interface is a good practice. </p> <p> The GUI-based Windows disk manager, <code>diskmgmt.msc</code>, can show the instantaneous progress of all <code>diskpart</code> commands, working on every drive, including creating and deleting partitions, volumes, formatting and much more. For example, formatting progress can be seen here: </p> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/diskpart/diskUI.svg" type="image/svg"> <!---<source srcset="/blog/images/diskpart/diskUI.avif" type="image/avif">--> <source srcset="/blog/images/diskpart/diskUI.webp" type="image/webp"> <source srcset="/blog/images/diskpart/diskUI.apng" type="image/apng"> <source srcset="/blog/images/diskpart/diskUI.png" type="image/png"> <source srcset="/blog/images/diskpart/diskUI.jpg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskUI.jpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskUI.jfif" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskUI.pjpeg" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskUI.pjp" type="image/jpeg"> <source srcset="/blog/images/diskpart/diskUI.gif" type="image/gif"> <source srcset="/blog/images/diskpart/diskUI.tif" type="image/tiff"> <source srcset="/blog/images/diskpart/diskUI.tiff" type="image/tiff"> <source srcset="/blog/images/diskpart/diskUI.bmp" type="image/bmp"> <source srcset="/blog/images/diskpart/diskUI.ico" type="image/x-icon"> <source srcset="/blog/images/diskpart/diskUI.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/diskpart/diskUI.png" style='width: 100%; ' /> </picture> </div> <p> Run <code>diskmgmt.msc</code> by: </p> <ol> <li>Press the <kbd>Windows</kbd> key once and let go.</li> <li>Type <code>diskmgmt</code></li> <li>Press <kbd>Enter</kbd></li> </ol> <!-- endregion --> <!-- #region Exit --> <h2 id="exit"><span class="code">Exit</span></h2> <p> To exit <code>diskpart</code>, either type <code>Exit</code> or press <kbd>CTRL</kbd>-<kbd>C</kbd>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Microsoft&nbsp;DiskPart</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcfee5a2545b1'><button class='copyBtn' data-clipboard-target='#idcfee5a2545b1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>exit</pre> </div> <p> The drive is ready for its next assignment! </p> <!-- endregion --> WSL / WSL 2 Backup and Restore 2022-01-10T00:00:00-05:00 https://mslinn.github.io/blog/2022/01/10/wsl-backup <!-- #region intro --> <p> I needed to back up my WSL2 installation before <a href='https://www.microsoft.com/en-us/software-download/windows10' target='_blank' rel="nofollow">reinstalling Windows 10</a>, as one must do every 6 months if you work your machine like a developer. It had been years since I had last refreshed this machine, and it was now bluescreening several times a day. Reboots took forever. </p> <div class="pullQuoteFull liImg"> This article presents the best WSL backup approaches I found </div> <p> I wanted to retain my WSL2 instance. When refreshing Windows you must reinstall all your programs, and you lose all the WSL/WSL2 instances. The refresh decommissions them, then moves them into a hidden directory tree, along with the rest of the stuff stored in your old Windows 10 profile directory tree. </p> <p> I also wanted to be able to replicate my WSL2 instance reliably and easily. It was very important to me to be able to administer WSL instances separately from my Windows 10 profile. </p> <!-- endregion --> <!-- #region Location, Location, --> <h2 id="location">Location, Location, Location</h2> <p> Only recently has it become possible to work with WSL/WSL2 images on non-system drives. Most online documentation is now out of date in this regard. Running Windows off a different drive than the drive that a WSL/WSL2 client OS runs from can provide a dramatic performance boost for some applications. </p> <div class="pullQuoteFull liImg"> Reinstalling Windows is less traumatic if the WSL/WSL2 image elsewhere in the filesystem </div> <!-- endregion --> <!-- #region Dell's Ignorance Causes Pain --> <h2 id="dell">Dell&rsquo;s Ignorance Causes Pain</h2> <p> I use Dell laptops, because I love their onsite warranty price and product features. However, sometimes it feels like Dell&rsquo;s solution to every problem is to replace the motherboard. </p> <p> When Windows 10 encounters a new motherboard, it takes anti-piracy measures, and refuses to do much of anything useful. </p> <p> Refreshing Windows solves that problem, but doing that blows away the standard WSL/WSL2 image. Again. And again. And again! </p> <p> Dell replaced the motherboard 4 times in 2 years for one of my laptops. They simply do not understand how replacing the motherboard creates long-term issues. </p> <!-- endregion --> <!-- #region Test Your Backup --> <h2 id="ready">Test Your Backup</h2> <p> Backing up WSL/WSL2 can be problematic. Restoring it is even more delicate. Knowing this, I decided to backup and test the restoration process before refreshing Windows and potentially losing my working system. </p> <p> I have tried several times before to make this work. As I said, it had been years since I allowed the OS in my main workstation to be refreshed. The reason I resisted doing proper maintenance was because I wanted to preserve my WSL2 Ubuntu image. Until today, I met with frustrating failures every time I attempted to use the standard tools. Today I succeeded, via new software, hence the publication of my notes. Who wants to read stories about all the things that do not work? </p> <p class="alert rounded shadow"> Following is my experience. I began by following the directions in <a href='https://www.windowscentral.com/how-backup-windows-subsystem-linux-wsl-distribution' target='_blank' rel="nofollow">How to back up a Windows Subsystem for Linux (WSL) distribution</a>, published 18 Feb 2021 by Windows Central. It is a nice story, but I've tried this stuff on a variety of computers, and this is not Microsoft&rsquo;s most robust code base. Read on and I will tell you of reality as I found it. </p> <!-- endregion --> <!-- #region WSL Command-Line Options --> <h2 id="wslOptions">WSL Command-Line Options</h2> <p> First lets note the software versions I am using: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id39750502c3b6'><button class='copyBtn' data-clipboard-target='#id39750502c3b6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>Microsoft Windows [Version 10.0.19045.4412] (c) Microsoft Corporation. All rights reserved. </span><br> <span class='unselectable'>C:\Users\Mike Slinn></span>wsl -v WSL version: 2.1.3.0 Kernel version: 5.15.146.1-2 WSLg version: 1.0.60 MSRDC version: 1.2.5105 Direct3D version: 1.611.1-81528511 DXCore version: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp Windows version: 10.0.19045.4412</pre> </div> <!-- endregion --> <p> Now lets look at the command-line options for the Native Windows <code>wsl</code> command (which is actually <code>wsl.exe</code>). </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2ffec3024a99'><button class='copyBtn' data-clipboard-target='#id2ffec3024a99' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --help <span class='unselectable'>Copyright (c) Microsoft Corporation. All rights reserved. For privacy information about this product please visit https://aka.ms/privacy.<br> Usage: wsl.exe [Argument] [Options...] [CommandLine]<br> Arguments for running Linux binaries:<br> If no command line is provided, wsl.exe launches the default shell.<br> --exec, -e <CommandLine> Execute the specified command without using the default Linux shell.<br> --shell-type <standard|login|none> Execute the specified command with the provided shell type.<br> -- Pass the remaining command line as-is.<br> Options: --cd <Directory> Sets the specified directory as the current working directory. If ~ is used the Linux user&apos;s home path will be used. If the path begins with a / character, it will be interpreted as an absolute Linux path. Otherwise, the value must be an absolute Windows path.<br> --distribution, -d <Distro> Run the specified distribution.<br> --user, -u <UserName> Run as the specified user.<br> --system Launches a shell for the system distribution.<br> Arguments for managing Windows Subsystem for Linux: --help Display usage information. --debug-shell Open a WSL2 debug shell for diagnostics purposes. --install [Distro] [Options...] Install a Windows Subsystem for Linux distribution. For a list of valid distributions, use 'wsl.exe --list --online'. Options: --no-launch, -n Do not launch the distribution after install. --web-download Download the distribution from the internet instead of the Microsoft Store. --no-distribution Only install the required optional components, does not install a distribution. --enable-wsl1 Enable WSL1 support. --manage <Distro> <Options...> Changes distro specific options. Options: --set-sparse, -s <true|false> Set the vhdx of distro to be sparse, allowing disk space to be automatically reclaimed. --mount <Disk> Attaches and mounts a physical or virtual disk in all WSL 2 distributions. Options: --vhd Specifies that <Disk> refers to a virtual hard disk. --bare Attach the disk to WSL2, but don&apos;t mount it. --name <Name> Mount the disk using a custom name for the mountpoint. --type <Type> Filesystem to use when mounting a disk, if not specified defaults to ext4. --options <Options> Additional mount options. --partition <Index> Index of the partition to mount, if not specified defaults to the whole disk. --set-default-version <Version> Changes the default install version for new distributions. --shutdown Immediately terminates all running distributions and the WSL 2 lightweight utility virtual machine. --status Show the status of Windows Subsystem for Linux. --unmount [Disk] Unmounts and detaches a disk from all WSL2 distributions. Unmounts and detaches all disks if called without argument. --update Update the Windows Subsystem for Linux package. Options: --pre-release Download a pre-release version if available. --version, -v Display version information. Arguments for managing distributions in Windows Subsystem for Linux: --export <Distro> <FileName> [Options] Exports the distribution to a tar file. The filename can be - for stdout. Options: --vhd Specifies that the distribution should be exported as a .vhdx file. --import <Distro> <InstallLocation> <FileName> [Options] Imports the specified tar file as a new distribution. The filename can be - for stdin. Options: --version <Version> Specifies the version to use for the new distribution. --vhd Specifies that the provided file is a .vhdx file, not a tar file. This operation makes a copy of the .vhdx file at the specified install location. --import-in-place <Distro> <FileName> Imports the specified .vhdx file as a new distribution. This virtual hard disk must be formatted with the ext4 filesystem type. --list, -l [Options] Lists distributions. Options: --all List all distributions, including distributions that are currently being installed or uninstalled. --running List only distributions that are currently running. --quiet, -q Only show distribution names. --verbose, -v Show detailed information about all distributions. --online, -o Displays a list of available distributions for install with &apos;wsl.exe --install&apos;. --set-default, -s <Distro> Sets the distribution as the default. --set-version <Distro> <Version> Changes the version of the specified distribution. --terminate, -t <Distro> Terminates the specified distribution. --unregister <Distro> Unregisters the distribution and deletes the root filesystem. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Backing Up With WSL --> <h2 id="wslBackup">Backing Up With WSL</h2> <p> Now let&apos;s use the <code>wsl</code> command to back up the Ubuntu VM in WSL2. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd41d0ed6a7b7'><button class='copyBtn' data-clipboard-target='#idd41d0ed6a7b7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --export Ubuntu ubuntuBear_2021-01-10.tar</pre> </div> <p> Well, well, it backed up without any problem in about an hour! Color me <s>surprised</s>happy. File size was about 50 GB. </p> <p> Alright, let&apos;s try importing the image now. We&apos;ll locate the new image at <code>f:\ubuntuBear</code>. It used to be that WSL did not support moving or installing a distro to non-system drives. No longer! </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd9a14a55beb1'><button class='copyBtn' data-clipboard-target='#idd9a14a55beb1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --import Ubuntu f:\ubuntuBear ubuntuBear_2021-01-10.tar <span class='unselectable'>A distribution with the supplied name already exists. </span></pre> </div> <p> The error message <code>A distribution with the supplied name already exists</code> makes sense because I backed up a WSL2 instance called <code>Ubuntu</code> and instance names must be unique. Let&apos;s import under the name <code>UbuntuBear</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id59bb06e929f2'><button class='copyBtn' data-clipboard-target='#id59bb06e929f2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --import UbuntuBear f:\ubuntuBear ubuntuBear_2021-01-10.tar <span class='unselectable'>Unspecified error </span></pre> </div> <p> Ahh, the dreaded <code>Unspecified error</code> that <code>wsl import</code> is infamous for. I found a few potential solutions, detailed below; I show the most desirable solution first. </p> <!-- endregion --> <!-- #region Solution 1: Importing the VDHX File --> <h2 id="vhdx">Solution 1: Importing the <span class="code">VDHX File</span></h2> <p> Importing the original <code>vhdx</code> file directly instead of creating and importing the <code>tar</code> file has been reported to work by <a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-1370093052' target='_blank' rel="nofollow">several people</a>. This is a more desirable solution because it requires less work and is faster. Note that the <kbd>^</kbd> character is a DOS command-line continuation character, much like the Bash <kbd>\</kbd> character. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfcc5e36b6d25'><button class='copyBtn' data-clipboard-target='#idfcc5e36b6d25' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --import Ubuntu-20.04-d^ D:\WSL\Ubuntu-22.04\^ %HOMEDRIVE%%HOMEPATH%\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu20.04onWindows_79rhkp1fndgsc\LocalState\ext4.vhdx^ --vhd</pre> </div> <!-- endregion --> <!-- #region Solution 2: Updating Linux Kernel --> <h2 id="vhdx">Solution 2: Updating Linux Kernel</h2> <p> <a href='https://github.com/microsoft/WSL/issues/4735' target='_blank' rel="nofollow">The following</a> was written by <a href='https://github.com/ken-cdit' target='_blank' rel="nofollow"><code>ken-cdit</code></a>: </p> <p> Had the same issue. I exported two WSL 2 distros and then did a clean install of my Windows OS. "Unspecified error" came up when I tried to import them. </p> <p> I fixed it by downloading the Linux kernel update package referred to at <a href='https://https://docs.microsoft.com/en-us/windows/wsl/install-win10' target='_blank' rel="nofollow"><code>https:/<wbr>/<wbr>docs.microsoft.com/<wbr>en-us/<wbr>windows/<wbr>wsl/<wbr>install-win10</code></a> and then running the command <code>wsl --set-default-version 2</code>. </p> <p> After this, my two distros imported without error. </p> <p> I had the whole thing still in the console so here is a screencap... </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/wslBackup/solution.svg" type="image/svg"> <!---<source srcset="/blog/images/wslBackup/solution.avif" type="image/avif">--> <source srcset="/blog/images/wslBackup/solution.webp" type="image/webp"> <source srcset="/blog/images/wslBackup/solution.apng" type="image/apng"> <source srcset="/blog/images/wslBackup/solution.png" type="image/png"> <source srcset="/blog/images/wslBackup/solution.jpg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/solution.jpeg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/solution.jfif" type="image/jpeg"> <source srcset="/blog/images/wslBackup/solution.pjpeg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/solution.pjp" type="image/jpeg"> <source srcset="/blog/images/wslBackup/solution.gif" type="image/gif"> <source srcset="/blog/images/wslBackup/solution.tif" type="image/tiff"> <source srcset="/blog/images/wslBackup/solution.tiff" type="image/tiff"> <source srcset="/blog/images/wslBackup/solution.bmp" type="image/bmp"> <source srcset="/blog/images/wslBackup/solution.ico" type="image/x-icon"> <source srcset="/blog/images/wslBackup/solution.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/wslBackup/solution.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region Solution 3: LxRunOffline --> <h2 id="LxRunOfflineInstall">Solution 3: <span class="code">LxRunOffline</span></h2> <h3 id="LxRunOfflineInstall">Installing <span class="code">LxRunOffline</span></h3> <p> <a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-800103777' target='_blank' rel="nofollow">Google brought me</a> to a potential solution, <a href='https://github.com/DDoSolitary/LxRunOffline' target='_blank' rel="nofollow"><code>LxRunOffline</code></a>, which provided me limited success. </p><p> I downloaded the binaries in <a href='https://github.com/DDoSolitary/LxRunOffline/releases/download/v3.5.0/LxRunOffline-v3.5.0-msvc.zip' target='_blank' rel="nofollow"><code>LxRunOffline-v3.5.0-msvc.zip</code></a> directly from <a href='https://github.com/DDoSolitary/LxRunOffline/releases' target='_blank' rel="nofollow">GitHub</a> into <code>C:\Program Files\LxRunOffline</code>. </p> <p> Now add <code>C:\Program Files\LxRunOffline</code> to the Windows <code>PATH</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0a80b4b511eb'><button class='copyBtn' data-clipboard-target='#id0a80b4b511eb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>setx PATH "%PATH%;C:\Program Files\LxRunOffline" SUCCESS: Specified value was saved. %}</pre> </div> <p> I then ran the following in an administrative shell to register the DLL: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idedbfe477d41c'><button class='copyBtn' data-clipboard-target='#idedbfe477d41c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>regsvr32 "C:\Program Files\LxRunOffline\LxRunOfflineShellExt.dll"</pre> </div> <!-- endregion --> <!-- #region LxRunOffline Help Info --> <h3 id="LxRunOffline" class="code">LxRunOffline Help Info</h3> <p> Let&apos;s progressively discover how this command-line program can be used. First I&apos;ll just type the command name, which causes the program to list its top-level actions. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf17776f2ed6c'><button class='copyBtn' data-clipboard-target='#idf17776f2ed6c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>LxRunOffline <span class='unselectable'>[ERROR] No action is specified.<br> Supported actions are: l, list List all installed distributions. gd, get-default Get the default distribution, which is used by bash.exe. sd, set-default Set the default distribution, which is used by bash.exe. i, install Install a new distribution. ui, uninstall Uninstall a distribution. rg, register Register an existing installation directory. ur, unregister Unregister a distribution but not delete the installation directory. m, move Move a distribution to a new directory. d, duplicate Duplicate an existing distribution in a new directory. e, export Export a distribution&quot;s filesystem to a .tar.gz file, which can be imported by the "install" command. r, run Run a command in a distribution. di, get-dir Get the installation directory of a distribution. gv, get-version Get the filesystem version of a distribution. ge, get-env Get the default environment variables of a distribution. se, set-env Set the default environment variables of a distribution. ae, add-env Add to the default environment variables of a distribution. re, remove-env Remove from the default environment variables of a distribution. gu, get-uid Get the UID of the default user of a distribution. su, set-uid Set the UID of the default user of a distribution. gk, get-kernelcmd Get the default kernel command line of a distribution. sk, set-kernelcmd Set the default kernel command line of a distribution. gf, get-flags Get some flags of a distribution. See https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/wslapi/ne-wslapi-wsl_distribution_flags for details. sf, set-flags Set some flags of a distribution. See https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/wslapi/ne-wslapi-wsl_distribution_flags for details. s, shortcut Create a shortcut to launch a distribution. ec, export-config Export configuration of a distribution to an XML file. ic, import-config Import configuration of a distribution from an XML file. sm, summary Get general information of a distribution. version Get version information about this LxRunOffline.exe. </span></pre> </div> <!-- endregion --> <p> Alright, let's get information about the <code>i</code> (<code>install</code>) option. </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc3c377ca6d39'><button class='copyBtn' data-clipboard-target='#idc3c377ca6d39' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>LxRunOffline i <span class='unselectable'>[ERROR] the option '-d' is required but missing<br> Options: -n arg Name of the distribution -d arg The directory to install the distribution into. -f arg The tar file containing the root filesystem of the distribution to be installed. If a file of the same name with a .xml extension exists and "-c" isn&quot;t specified, that file will be imported as a config file. -r arg The directory in the tar file to extract. This argument is optional. -c arg The config file to use. This argument is optional. -v arg (=2) The version of filesystem to use, latest available one if not specified. -s Create a shortcut for this distribution on Desktop. </span></pre> </div> <!-- endregion --> <p> I&apos;ll use the <code>-d</code> option to specify the directory to install into, as well as the <code>-f</code> and <code>-n</code> options. </p> <!-- endregion --> <!-- #region LxRunOffline Import --> <h3 id="LxRunOfflineImport"><span class="code">LxRunOffline</span> Import</h3> <p> I feel brave, let&apos;s try importing for real now: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbe008a5b5094'><button class='copyBtn' data-clipboard-target='#idbe008a5b5094' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>LxRunOffline i -d f:/ubuntuBear -n UbuntuBear -f ubuntuBear_2021-01-10.tar <span class='unselectable'>[ERROR] The distro "UbuntuBear" already exists. </span></pre> </div> <p> Some debris remains from the failed import (remember the <code>Unspecified error</code> a moment ago?). I will delete the b0rked <code>UbuntuBear</code> instance in a moment. Let&apos;s call this newly cloned linux instance <code>UbuntuBear2</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddd3ea0c006d8'><button class='copyBtn' data-clipboard-target='#iddd3ea0c006d8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>LxRunOffline i -d f:/ubuntuBear -n UbuntuBear2 -f ubuntuBear_2021-01-10.tar <span class='unselectable'>[WARNING] Ignoring an unsupported file "var/lib/docker/volumes/backingFsBlockDev" of type 0060000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-17041-8893592-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10716-846285-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1899-75308257-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10151-8749190-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-18789-129803847-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-811-127664322-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12447-115564106-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-24480-65839207-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-23230-107496852-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-23230-107496852-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-24480-65839207-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-14211-5836039-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19778-110293600-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1969-34761241-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-17041-8893592-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1899-75308257-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-7642-17996-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10151-8749190-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19082-8772885-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12857-5829248-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-26692-70009588-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12447-115564106-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-22582-1181861-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-14211-5836039-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1969-34761241-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-7642-17996-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10716-846285-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-11424-57667328-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-28497-49814437-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-18789-129803847-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-2234-80045-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19082-8772885-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12857-5829248-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-22582-1181861-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-26692-70009588-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10990-5814132-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-811-127664322-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10990-5814132-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-11424-57667328-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-2234-80045-out" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19778-110293600-in" of type 0010000. [WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-28497-49814437-in" of type 0010000. [WARNING] Ignoring an unsupported file "dev/random" of type 0020000. [WARNING] Ignoring an unsupported file "dev/tty" of type 0020000. [WARNING] Ignoring an unsupported file "dev/full" of type 0020000. [WARNING] Ignoring an unsupported file "dev/urandom" of type 0020000. [WARNING] Ignoring an unsupported file "dev/ptmx" of type 0020000. [WARNING] Ignoring an unsupported file "dev/zero" of type 0020000. [WARNING] Ignoring an unsupported file "dev/console" of type 0020000. [WARNING] Ignoring an unsupported file "dev/null" of type 0020000. [WARNING] Ignoring an unsupported file "dev/mapper/control" of type 0020000. [ERROR] Couldn&quot;t create the file "\\?\f:\ubuntuBear\rootfs\home\mslinn\.atom\packages\markdown-preview-plus\spec\fixtures\subdir\�cc�nt�d.md". Reason: The file exists. </span></pre> </div> <!-- endregion --> <p> Most of the warnings do not seem to be important. I do not care about Docker anyway. Also, now I know that temporary files and <code>dev</code> nodes should all be deleted before running this program. Even better, the program that created the tar should be modified to not attempt to replicate any temporary files. </p> <p> I don&apos;t understand the problem with the atom package, but I&apos;ll try to delete all of the atom settings from the tar, along with the other problematic directories, and then retry. </p> <!-- endregion --> <!-- #region Cleaning Up WSL --> <h3 id="cleanup">Cleaning Up WSL</h3> <p> First let&apos;s see the VMs registered with WSL: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id19d3f02acf5f'><button class='copyBtn' data-clipboard-target='#id19d3f02acf5f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl -l <span class='unselectable'>Windows Subsystem for Linux Distributions: Ubuntu (Default) UbuntuBear UbuntuBear2 </span></pre> </div> <!-- endregion --> <p> Let&apos;s delete the debris remaining from the failed <code>UbuntuBear</code> and <code>UbuntuBear2</code> imports: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd61542e56231'><button class='copyBtn' data-clipboard-target='#idd61542e56231' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>wsl --unregister UbuntuBear <span class='unselectable'>Unregistering... </span> <span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --unregister UbuntuBear2 <span class='unselectable'>Unregistering... </span></pre> </div> <!-- endregion --> <p> Attempting to delete the directory created by LxRunOffline hit a corrupted directory (scroll the output below to see the error). </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe (continued)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc5ed83e18b01'><button class='copyBtn' data-clipboard-target='#idc5ed83e18b01' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn></span>del /s /q f:\ubuntuBear <span class='unselectable'>Deleted file - f:\ubuntuBear\rootfs\init Deleted file - f:\ubuntuBear\rootfs\lib Deleted file - f:\ubuntuBear\rootfs\lib32 Deleted file - f:\ubuntuBear\rootfs\lib64 Deleted file - f:\ubuntuBear\rootfs\libx32 Deleted file - f:\ubuntuBear\rootfs\sbin Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.wget-hsts Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.Xauthority Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump-Bear-5.8 Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump-localhost-5.8 Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshenv Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshrc Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshrc.pre-oh-my-zsh Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zsh_history Deleted file - f:\ubuntuBear\rootfs\home\mslinn\ancient_warmth_workspace.code-workspace Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bear2.zip Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bear3.zip Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bearDirs.tar Deleted file - f:\ubuntuBear\rootfs\home\mslinn\dead.letter Deleted file - f:\ubuntuBear\rootfs\home\mslinn\django_bash_completion Deleted file - f:\ubuntuBear\rootfs\home\mslinn\jekyll_workspace.code-workspace Deleted file - f:\ubuntuBear\rootfs\home\mslinn\msp.txt Deleted file - f:\ubuntuBear\rootfs\home\mslinn\nodesource_setup.sh Deleted file - f:\ubuntuBear\rootfs\home\mslinn\package-lock.json Deleted file - f:\ubuntuBear\rootfs\home\mslinn\package.json Deleted file - f:\ubuntuBear\rootfs\home\mslinn\worldPeaceMusicCollective.png Deleted file - f:\ubuntuBear\rootfs\home\mslinn\worldPeaceMusicCollectiveBordered.png Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\package.json Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\pom.xml Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\README.md <span class='bg_yellow'>The file or directory is corrupted and unreadable.</span> </span></pre> </div> <!-- endregion --> <p class="error code rounded shadow"> &nbsp;The file or directory is corrupted and unreadable. </p> <p> That needs to be dealt with right away! I fixed the directory errors in drive F like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id397ae2b09076'><button class='copyBtn' data-clipboard-target='#id397ae2b09076' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Program Files></span>chkdsk /F F: <span class='unselectable'>The type of the file system is NTFS. Chkdsk cannot run because the volume is in use by another process. Chkdsk may run if this volume is dismounted first. ALL OPENED HANDLES TO THIS VOLUME WOULD THEN BE INVALID. </span>y <span class='unselectable'>Chkdsk cannot dismount the volume because it is a system drive or there is an active paging file on it. Would you like to schedule this volume to be checked the next time the system restarts? (Y/N)</span> y <span class='unselectable'>This volume will be checked the next time the system restarts. </span></pre> </div> <p> I rebooted the system, and it fixed the errors on drive F:. </p> <!-- endregion --> <!-- #region Pruning the tar --> <h3 id="prune">Pruning the tar</h3> <p> First lets back up the tar that we want to prune. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id944a50cc0c53'><button class='copyBtn' data-clipboard-target='#id944a50cc0c53' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pushd '/mnt/c/Users/Mike Slinn/'<br> <span class='unselectable'>$ </span>ls -alF *.tar <span class='unselectable'>-rwxr--r-- 1 mslinn mslinn 524482560 Jan 10 18:45 'bear_ubuntu_2021-01-10.tar'* -rwxr--r-- 1 mslinn mslinn 51464028160 Jan 10 21:11 'ubuntuBear_2021-01-10.tar'* </span><br> <span class='unselectable'>$ </span>cp ubuntuBear_2021-01-10.tar ubuntuBearPruned_2021-01-10.tar</pre> </div> <p> Now lets try to prune out all the problematic, and unnecessary, files. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc4abc1946bf9'><button class='copyBtn' data-clipboard-target='#idc4abc1946bf9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tar -f ubuntuBearPruned_2021-01-10.tar \ --delete dev/* \ --delete tmp/* \ --delete home/mslinn/.atom/* \ --delete var/lib/docker/* \ --delete var/sitesUbuntu/* \ --delete var/work/* \ --delete var/tmp/* <span class='unselectable'>tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.user.fuseoverlayfs.opaque' tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.user.fuseoverlayfs.opaque' tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: dev/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: tmp/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: home/mslinn/.atom/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: var/lib/docker/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: var/sitesUbuntu/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: var/work/*: Not found in archive tar: Pattern matching characters used in file names tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning tar: var/tmp/*: Not found in archive tar: Exiting with failure status due to previous errors </span></pre> </div> <!-- endregion --> <p> I think the error messages just indicate that the tar was made by <a href='https://www.freebsd.org/cgi/man.cgi?tar(1)' target='_blank' rel="nofollow">BSD-TAR</a>, while Ubuntu uses <a href='https://www.gnu.org/software/tar/' target='_blank' rel="nofollow">GNU-TAR</a>. I installed <code>bsdtar</code> like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida15eb55cf43e'><button class='copyBtn' data-clipboard-target='#ida15eb55cf43e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt-get install <a href='https://packages.ubuntu.com/hirsute/libarchive-tools' target='_blank' rel="nofollow">libarchive-tools</a> <span class='unselectable'>Reading package lists... Done Building dependency tree... Done Reading state information... Done The following NEW packages will be installed: libarchive-tools 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. Need to get 57.1 kB of archives. After this operation, 207 kB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu hirsute/universe amd64 libarchive-tools amd64 3.4.3-2 [57.1 kB] Fetched 57.1 kB in 0s (167 kB/s) Selecting previously unselected package libarchive-tools. (Reading database ... 244498 files and directories currently installed.) Preparing to unpack .../libarchive-tools_3.4.3-2_amd64.deb ... Unpacking libarchive-tools (3.4.3-2) ... Setting up libarchive-tools (3.4.3-2) ... Processing triggers for man-db (2.9.4-2) ... </span></pre> </div> <!-- endregion --> <p> I <a href='https://stackoverflow.com/a/56031400/553865' target='_blank' rel="nofollow">tried again</a> using <code>bsdtar</code>, which does not support GNU tar's <code>--delete</code> option. Unfortunately, I only got a 1KB file out, no matter what I did. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb6188f95300'><button class='copyBtn' data-clipboard-target='#idcb6188f95300' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>bsdtar -cvf ubuntuBearPruned2_2021-01-10.tar \ --exclude 'dev/*' \ --exclude 'tmp/*' \ --exclude 'home/mslinn/.atom/*' \ --exclude 'var/lib/docker/*' \ --exclude 'var/sitesUbuntu/*' \ --exclude 'var/work/*' \ --exclude 'var/tmp/*' \ @ubuntuBear_2021-01-10.tar</pre> </div> <p> Maybe there is a bug. Maybe there is bad documentation. I do not think I made an error. Life is short. Bugs are rampant. <a href='https://en.wikipedia.org/wiki/Illegitimi_non_carborundum' target='_blank' rel="nofollow">Illegitimi non carborundum!</a> </p> <p> I give up on this direction. Let's try to win some other way! </p> <!-- endregion --> <!-- #region Success Duplicating the Ubuntu Instance --> <h3 id="duplicate">Success Duplicating the Ubuntu Instance</h3> <p> Let&rsquo;s try duplicating the Ubuntu instance with <code>LxRunOffline</code>. First the <code>Ubuntu</code> VM must be terminated. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3da7cdf099f1'><button class='copyBtn' data-clipboard-target='#id3da7cdf099f1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -t Ubuntu<br> <span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline duplicate -n Ubuntu -N UbuntuWsl2 -d f:\UbuntuWsl2</pre> </div> <p> The above ran for a couple of hours and concluded without error. <code>LxRunOffline duplicate</code> automatically registers the new instance. </p> <p> The <code>F:\UbuntuWsl2</code>directory was 239 GB. That's a fair-sized VM. The directory only contains one file, called <code>ext4.vhdx</code>. </p> <p> Let's see the Ubuntu instances that are currently set: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' style='margin-bottom: 1em;' id='id523e13f6421f'><button class='copyBtn' data-clipboard-target='#id523e13f6421f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list <span class='unselectable'>Windows Subsystem for Linux Distributions: Ubuntu (Default) UbuntuWsl2 </span></pre> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> That is what I wanted to see! </p> <p> I started the new <code>UbuntuWsl2</code> Ubuntu instance like this: </p> <div class="jekyll_pre clear" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc441f715e0b8'><button class='copyBtn' data-clipboard-target='#idc441f715e0b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -d UbuntuWsl2</pre> </div> <p> To set the default Ubuntu instance I typed: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb2d748233edc'><button class='copyBtn' data-clipboard-target='#idb2d748233edc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --setdefault UbuntuWsl2</pre> </div> <p> Let's verify that worked: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc34ca4bde247'><button class='copyBtn' data-clipboard-target='#idc34ca4bde247' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list <span class='unselectable'>Windows Subsystem for Linux Distributions: UbuntuWsl2 (Default) Ubuntu </span></pre> </div> <p> I then deleted the huge tar files that I had created in earlier steps. I never needed them because I used <code>LxRunOffline duplicate</code>. </p> <!-- endregion --> <!-- #region Ubuntu on 500GB USB3 SSD --> <h2 id="usb3">Ubuntu on 500GB USB3 SSD</h2> <div class='imgWrapper imgFlex right' style=' '> <a href='https://www.amazon.com/gp/product/B08GTXVG9P' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/wslBackup/sandiskExtreme.svg" type="image/svg"> <!---<source srcset="/blog/images/wslBackup/sandiskExtreme.avif" type="image/avif">--> <source srcset="/blog/images/wslBackup/sandiskExtreme.webp" type="image/webp"> <source srcset="/blog/images/wslBackup/sandiskExtreme.apng" type="image/apng"> <source srcset="/blog/images/wslBackup/sandiskExtreme.png" type="image/png"> <source srcset="/blog/images/wslBackup/sandiskExtreme.jpg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/sandiskExtreme.jpeg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/sandiskExtreme.jfif" type="image/jpeg"> <source srcset="/blog/images/wslBackup/sandiskExtreme.pjpeg" type="image/jpeg"> <source srcset="/blog/images/wslBackup/sandiskExtreme.pjp" type="image/jpeg"> <source srcset="/blog/images/wslBackup/sandiskExtreme.gif" type="image/gif"> <source srcset="/blog/images/wslBackup/sandiskExtreme.tif" type="image/tiff"> <source srcset="/blog/images/wslBackup/sandiskExtreme.tiff" type="image/tiff"> <source srcset="/blog/images/wslBackup/sandiskExtreme.bmp" type="image/bmp"> <source srcset="/blog/images/wslBackup/sandiskExtreme.ico" type="image/x-icon"> <source srcset="/blog/images/wslBackup/sandiskExtreme.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/wslBackup/sandiskExtreme.png" style='width: 100%; ' /> </picture> </a> </div> <p> I have a <a href='https://www.amazon.com/gp/product/B08GTXVG9P' target='_blank' rel="nofollow">Sandisk Extreme 500GB USB3 SSD drive</a>. This tiny, light, and very portable storage device should be perfect for holding my Ubuntu development system. How cool it would be to be able to drop it into a small pocket! </p> <p> Better yet, the <a href='https://www.quora.com/Can-a-USB-3-0-external-SSD-be-faster-than-an-internal-laptop%E2%80%99s-HDD' target='_blank' rel="nofollow">performance of portable USB3 SSD drives</a> blows away traditional hard drives. </p> <p> For example, assume that your SSD drive appears as drive <code>X</code>. Also assume that <code>LxRunOffline.exe</code> and <code>LxRunOffline.dll</code> have been copied to the root directory of the SSD drive. To register your portable Ubuntu you would simply type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8995355ef793'><button class='copyBtn' data-clipboard-target='#id8995355ef793' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>X:LxRunOffline register X:\MyUbuntu -n UbuntuMSlinn</pre> </div> <div class="pullQuote"> You can walk up to any recently updated Windows 10 machine, plug your tiny USB3 SSD drive into a USB3 or USB3.1 port, run the registration command, and boom! you are productive with great storage performance. </div> <div style='font-size: 3em;;text-align: center'>&#x1F601;</div> <!-- endregion --> <!-- #region Duplicating to SSD --> <h2 id="duplicate">Duplicating to SSD</h2> <p> Again, the <code>Ubuntu</code> VM must be terminated. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ided0863d3a4f3'><button class='copyBtn' data-clipboard-target='#ided0863d3a4f3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -t Ubuntu<br> <span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline duplicate -n Ubuntu -N UbuntuWsl2Extreme -d I:\UbuntuWsl2Extreme</pre> </div> <p> The above ran for a couple of hours and concluded without error. <code>LxRunOffline duplicate</code> automatically registers the new instance. </p> <p> Let's see the Ubuntu instances that are currently set: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc00788b6b0db'><button class='copyBtn' data-clipboard-target='#idc00788b6b0db' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list <span class='unselectable'>Windows Subsystem for Linux Distributions: Ubuntu (Default) UbuntuWsl2 UbuntuWsl2Extreme </span></pre> </div> <div style='font-size: 3em;;text-align: center'>&#x1F601;</div> <!-- endregion --> <!-- #region LxRunOffline Update 2023-04-25 --> <h2 id="LxRunOfflineUpdate"><span class="code">LxRunOffline Update 2023-04-25</span></h2> <p> <a href='https://github.com/armanexplorer' target='_blank' rel="nofollow">Arman Mazloumzadeh</a> posted the <a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-1522509689' target='_blank' rel="nofollow">following</a>: </p> <blockquote> <p> I finally resolved this amiss during the following steps (Windows 10.0.19045 Build 19045): </p> <!-- #region --> <div class="jekyll_pre" > <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf74061710547'><button class='copyBtn' data-clipboard-target='#idf74061710547' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Running wsl.exe --import Ubuntu A:\wsl\Ubuntu A:\wsl\Ubuntu.tar = > Unspecified error Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => [ERROR] Couldn't get the value "DistributionName" of the registry key "Software\Microsoft\Windows\CurrentVersion\Lxss\AppxInstallerCache" Removing the Software\Microsoft\Windows\CurrentVersion\Lxss\AppxInstallerCache registry key Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => [ERROR] Couldn't get the value "DistributionName" of the registry key "Software\Microsoft\Windows\CurrentVersion\Lxss\TryStoreWSL" Removing the Software\Microsoft\Windows\CurrentVersion\Lxss\TryStoreWSL registry key Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => ✔ Running ubuntu.exe config --default-user arman => ✔ done 🎉</pre> </div> <!-- endregion --> </blockquote> <!-- endregion --> <!-- #region Starting and Stopping Portable VMs --> <h2 id="startStop">Starting and Stopping Portable VMs</h2> <p> You can start the WSL VM called <code>UbuntuMSlinn</code> by using <code>wsl -d</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcaf0b10ebd83'><button class='copyBtn' data-clipboard-target='#idcaf0b10ebd83' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>wsl -d UbuntuMSlinn</pre> </div> <p> You can stop it by using <code>wsl -t</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5909d9a52e21'><button class='copyBtn' data-clipboard-target='#id5909d9a52e21' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>wsl -t UbuntuMSlinn</pre> </div> <!-- endregion --> <!-- #region Microsoft WSL Issue Reporting --> <h2 id="issues">Microsoft WSL Issue Reporting</h2> <p> In the course of writing this article I found the <a href='https://github.com/microsoft/WSL/blob/master/CONTRIBUTING.md#8-collect-wsl-logs' target='_blank' rel="nofollow">web page</a> for reporting a WSL issue, so that Microsoft can look at it. <!-- endregion --> AI / ML System Behavior Reflects the Society That Produced It 2021-12-26T00:00:00-05:00 https://mslinn.github.io/blog/2021/12/26/ai-society <p> Artificial intelligence systems, including machine learning systems, are primarily software-driven. Like all software, AI and ML systems reflect the societies that produced them. <a href='https://www.thoughtworks.com/insights/articles/demystifying-conways-law' target='_blank' rel="nofollow">Conway&rsquo;s Law</a> explains how the internal organization of software reflects the organization that created it. </p> <p> I postulate that the behavior of AI & ML systems reflect the society that the systems are embedded in. Do you find the AI systems are <a href='https://www.bruegel.org/blog-post/dark-side-artificial-intelligence-manipulation-human-behaviour' target='_blank' rel="nofollow">instrusive or exploitive</a>? Look in the collective mirror for the society it was created by. </p> <p class="alert rounded shadow"> If the populace is viewed as merely something for corporations to exploit, and this view is accepted at enough levels in society, that becomes the status quo. In my opinion, the USA in 2022 has definitely reached that point. </p> <div class="quoteCite shadow rounded"> <h2 style="margin-top: 0">Conway&rsquo;s Law</h2> Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure. <br><br> &nbsp;&ndash; Melvin E. Conway, 1967 </div> <div class="quoteCite shadow rounded"> <h2 style="margin-top: 0">Allan Kelly’s Corollary</h2> Organisational design is system design. <br><br> &nbsp;&ndash; Allan Kelly, 2005 </div> <p> The <a href='https://www.hbs.edu/ris/Publication%20Files/16-124_7ae90679-0ce6-4d72-9e9d-828872c7af49.pdf' target='_blank' rel="nofollow">The Mirroring Hypothesis: Theory, Evidence and Exceptions</a> by Lyra J. Colfer and Carliss Y. Baldwin was published by the Harvard Business School in 2016. </p> <div class="quoteCite shadow rounded"> A large-scale system is one that stretches across time and space. A large organization similarly stretches across time and space. Conway’s Law ties the two. How we organize defines how we think collectively, and thus what we make collectively...<br><br> ... To build software, first build a community...<br><br> Software is essentially about people, and the larger-scale we make it, the more that truth becomes visible. One program reflects how one person thinks. A large-scale application reflects how many people think together. <br><br> &nbsp;&ndash; Pieter Hintjes, <a href='http://hintjens.com/blog:73' target='_blank' rel="nofollow">Sex in Title, and Other Stories</a>, Dec 16, 2013 </div> <p> Do you even care? If so, please be the change you want to see in the world. </p> <div class='quote'> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/gandhi.svg" type="image/svg"> <!---<source srcset="/blog/images/gandhi.avif" type="image/avif">--> <source srcset="/blog/images/gandhi.webp" type="image/webp"> <source srcset="/blog/images/gandhi.apng" type="image/apng"> <source srcset="/blog/images/gandhi.png" type="image/png"> <source srcset="/blog/images/gandhi.jpg" type="image/jpeg"> <source srcset="/blog/images/gandhi.jpeg" type="image/jpeg"> <source srcset="/blog/images/gandhi.jfif" type="image/jpeg"> <source srcset="/blog/images/gandhi.pjpeg" type="image/jpeg"> <source srcset="/blog/images/gandhi.pjp" type="image/jpeg"> <source srcset="/blog/images/gandhi.gif" type="image/gif"> <source srcset="/blog/images/gandhi.tif" type="image/tiff"> <source srcset="/blog/images/gandhi.tiff" type="image/tiff"> <source srcset="/blog/images/gandhi.bmp" type="image/bmp"> <source srcset="/blog/images/gandhi.ico" type="image/x-icon"> <source srcset="/blog/images/gandhi.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/gandhi.png" style='width: 100%; ' /> </picture> </div> We but mirror the world. All the tendencies present in the outer world are to be found in the world of our body. If we could change ourselves, the tendencies in the world would also change. As a man changes his own nature, so does the attitude of the world change towards him. This is the divine mystery supreme. A wonderful thing it is and the source of our happiness. We need not wait to see what others do. <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://commonground.ca/be-the-change-you-want-to-see-in-the-world/' rel='nofollow' target='_blank'>Mahatma Gandhi</a></span> </div> Spring-Breezifier: Solving COVID-19 With HVAC 2021-12-22T00:00:00-05:00 https://mslinn.github.io/blog/2021/12/22/covid-hvac <!-- #region intro --> <p> When the COVID-19 pandemic began, the world was unprepared and no one knew how the virus was transmitted. As the medical profession lurched awkwardly into action, it gave advice that later turned out to be incorrect. As time went by, important details about how the virus is transmitted were revealed, and the previous medical advice had become politicized. The new advice has not yet been generally adopted as policy and is currently unknown to most people. </p> <div class='quote'> <div class='quoteText clearfix'> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/mencken.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/mencken.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/mencken.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/mencken.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/mencken.png" type="image/png"> <source srcset="/blog/images/covidHvac/mencken.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/mencken.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/mencken.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/mencken.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/mencken.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/mencken.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/mencken.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/mencken.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/mencken.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/mencken.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/mencken.cur" type="image/x-icon"> <img class="imgImg rounded" src="/blog/images/covidHvac/mencken.png" style='width: 100%; ' /> </picture> </div> For every complex problem there is an answer that is clear, simple, and wrong. <br><br><br><br><br><br> </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://www.britannica.com/biography/H-L-Mencken' rel='nofollow' target='_blank'>H. L. Mencken 1880-1956.</a></div> </div> <!-- endregion --> <!-- #region about --> <h2 id="about">About This Article</h2> <p> This article's intent is to show how the COVID-19 pandemic could be dealt with simply, cheaply, and with a minimum of aggravation by raising awareness of the potential role of <a href='https://www.cdc.gov/infectioncontrol/guidelines/environmental/background/air.html#c3' target='_blank' rel="nofollow">HVAC</a> (heating, ventilation and air conditioning) equipment. </p> <p> The author is an electrical engineer with no health-related or HVAC-related qualifications. However, he can read and write, ideas often come to mind when presented with new information. </p> <div class="formalNotice rounded shadow"> After initially publishing this article, an old friend showed me a YouTube video made by a consulting civil engineer who specializes in this topic. <a href="#youtube2">You can skip to it if you are impatient</a> and want to know in-depth technical specifics. </div> <p> All the new information referenced in this article is generally available, from qualified and reputable sources, and this article just disseminates it. </p> <p> This article concludes with actionable suggestions. </p> <!-- endregion --> <!-- #region update --> <h2 id="update">Update 2023-06-08</h2> <div class='quote'> <p> [The] federal agency has set a target - five air changes per hour - for how much rooms and buildings should be ventilated...<br><br> it&rsquo;s easy to see the guidance only in the context of Covid-19, it will help with many other airborne hazards like wildfire smoke, allergens and other infectious diseases, such as the flu...<br><br> &ldquo;If they had broadcast and implemented these changes at the beginning, there never would have been a pandemic&rdquo;, said Kimberly Prather, an atmospheric chemist at the University of California at San Diego and the Scripps Institution of Oceanography. </p> <span class='quoteAttribution'> &nbsp;&ndash; From <a href='https://www.cnn.com/2023/05/12/health/cdc-new-ventilation-target/index.html' rel='nofollow' target='_blank'>CDC sets first target for indoor air ventilation to prevent spread of Covid-19</a></span> </div> <!-- endregion --> <!-- #region aerosols --> <h2 id="trans">COVID-19 Is Primarily Transmitted Via Aerosols</h2> <p> The most significant bit of information about COVID-19, which was not generally accepted at the beginning of the pandemic, is that it is <a href='https://www.nature.com/articles/d41586-021-00251-4' target='_blank' rel="nofollow">mostly transmitted via aerosols</a>; that is, via small airborne droplets only a few microns in diameter. (One micron is a millionth of a meter, or one twenty-five thousandth of an inch). Aerosol particles can hang in the air for hours and travel hundreds of feet. </p> <p> The Centers for Disease Control and Prevention (CDC) officially recently officially recognized that SARS-CoV-2 (the virus that causes COVID-19) is airborne, meaning it is highly transmissible through the air. </p> <div class="quoteCite shadow rounded"> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/cdc.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/cdc.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/cdc.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/cdc.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/cdc.png" type="image/png"> <source srcset="/blog/images/covidHvac/cdc.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/cdc.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/cdc.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/cdc.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/cdc.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/cdc.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/cdc.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/cdc.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/cdc.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/cdc.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/cdc.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/covidHvac/cdc.png" style='width: 100%; maxwidth; 25%; width: 130px; height: auto;' /> </picture> </div> Exposure occurs in three principal ways: <ol> <li>inhalation of very fine respiratory droplets and aerosol particles</li> <li> deposition of respiratory droplets and particles on exposed mucous membranes in the mouth, nose, or eye by direct splashes and sprays </li> <li> touching mucous membranes with hands that have been soiled either directly by virus-containing respiratory fluids or indirectly by touching surfaces with virus on them. </li> </ol> <p> People release respiratory fluids during exhalation (e.g., quiet breathing, speaking, singing, exercise, coughing, sneezing) in the form of droplets across a spectrum of sizes. These droplets carry virus and transmit infection. To stay healthy, avoid these droplets. </p> <p> The largest droplets settle out of the air rapidly, within seconds to minutes. The smallest very fine droplets, and aerosol particles formed when these fine droplets rapidly dry, are small enough that they can remain suspended in the air for minutes to hours. </p> &nbsp;&ndash;From the CDC <a href='https://www.cdc.gov/coronavirus/2019-ncov/science/science-briefs/sars-cov-2-transmission.html' target='_blank' rel="nofollow">Scientific Brief: SARS-CoV-2 Transmission</a>, Updated May 7, 2021. </div> <p> The idea that physical distancing of 6 feet (2 meters) might help keep people healthy is an example of bad science from a study <a href='https://www.businessinsider.com/6-foot-distancing-rule-is-outdated-oxford-mit-new-system-2020-8' target='_blank' rel="nofollow">80 years ago</a> that was not debunked until recently. </p> <div class="quoteCite shadow rounded"> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/epaLogo.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/epaLogo.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/epaLogo.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/epaLogo.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/epaLogo.png" type="image/png"> <source srcset="/blog/images/covidHvac/epaLogo.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/epaLogo.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/epaLogo.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/epaLogo.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/epaLogo.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/epaLogo.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/covidHvac/epaLogo.png" style='width: 100%; ' /> </picture> </div> Transmission of COVID-19 from inhalation of virus in the air can occur at distances greater than six feet. Particles from an infected person can move throughout an entire room or indoor space. The particles can also linger in the air after a person has left the room – they can remain airborne for hours in some cases. <br><br> &nbsp;&ndash;From the US Environmental Protection Agency (EPA): <a href='https://www.epa.gov/coronavirus/indoor-air-and-coronavirus-covid-19' target='_blank' rel="nofollow">Indoor Air and Coronavirus (COVID-19)</a>, web page was updated December 15, 2021. </div> <p> Distance between people is not a significant transmissibility factor. Imagine two people, back to back, one (downwind) very sick with COVID-19, and the other (healthy) upwind, and the wind is blowing at 20 mph (32 km/h). The sick person will not infect the healthy person, unless they exchange positions. Just control the air that is breathed, and all airborne viruses such as COVID-19 will be controled. </p> <div class="quoteCite shadow rounded"> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/ucsdMedicine.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/ucsdMedicine.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/ucsdMedicine.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/ucsdMedicine.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/ucsdMedicine.png" type="image/png"> <source srcset="/blog/images/covidHvac/ucsdMedicine.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/ucsdMedicine.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/ucsdMedicine.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/ucsdMedicine.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/ucsdMedicine.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/ucsdMedicine.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/ucsdMedicine.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/ucsdMedicine.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/ucsdMedicine.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/ucsdMedicine.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/ucsdMedicine.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/covidHvac/ucsdMedicine.png" style='width: 100%; maxwidth; 25%; width: 130px; height: auto;' /> </picture> </div> What we learned during the pandemic is that aerosols were one of the main drivers in spreading the COVID-19 virus and that their importance in the transmission of many other respiratory pathogens has been systematically underappreciated. <br><br> &nbsp;&ndash; Dr. Robert Schooley, Professor in the Department of Medicine at the University of California San Diego (November 22, 2021), quoted from <a href='https://ucsdnews.ucsd.edu/pressrelease/covid-gets-airborne' target='_blank' rel="nofollow">COVID Gets Airborne &ndash; UC San Diego develops computer model to aid understanding of how viruses travel through the air</a> </div> <!-- endregion --> <!-- #region controlling virus infiltration --> <h2 id="virus">Controlling Virus Infiltration</h2> <p> We would do well to remember that the COVID-19 virus causes sickness. Without exposure to the virus people would not get sick. The risk that a person might catch the disease is directly related to the number of viruses inhaled per unit of time. For example, the more viruses someone inhales in an hour, the more likely they will get sick. </p> <p> <a href='https://www.statnews.com/2020/04/14/how-much-of-the-coronavirus-does-it-take-to-make-you-sick/' target='_blank' rel="nofollow">The amount of virus necessary to make a person sick is called the infectious dose.</a> All the countermeasures that have been used (hand washing, physical distancing, masks, curfews, etc.) are intended to reduce the number of viruses people are exposed to per unit time. Pandemic policy treats countermeasures as proxies for virus transmission vectors. By not updating pandemic policy as new information becomes available, we loose the ability to control the spread of the virus. </p> <!-- endregion --> <!-- #region hepa --> <h2 id="hepa">HEPA Filters</h2> <p> HEPA filters on HVAC equipment are designed to remove these aerosols. They come in a huge variety of sizes, shapes and air flow capacities. If you could just breathe the pure air emitted from a fan that had a HEPA filter, you would not get sick from any airborne virus. Any number of people could gather safely, if each of them received an adequate supply of pure air in their face at all times. </p> <div class="quoteCite shadow rounded"> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/epaLogo.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/epaLogo.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/epaLogo.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/epaLogo.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/epaLogo.png" type="image/png"> <source srcset="/blog/images/covidHvac/epaLogo.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/epaLogo.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/epaLogo.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/epaLogo.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/epaLogo.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/epaLogo.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/epaLogo.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/covidHvac/epaLogo.png" style='width: 100%; ' /> </picture> </div> HEPA is a type of pleated mechanical air filter. It is an acronym for “high efficiency particulate air filter” (as officially defined by the U.S. Dept. of Energy). This type of air filter can theoretically remove at least 99.97% of dust, pollen, mold, bacteria, and any airborne particles with a size of 0.3 microns (µm). <br><br> &nbsp;&ndash;<a href='https://www.epa.gov/indoor-air-quality-iaq/what-hepa-filter-1' target='_blank' rel="nofollow">US Environment Protection Agency</a> </div> <p> HEPA filters are inexpensive and are readily available. They are not new. In fact, <a href='https://www.apcfilters.com/the-history-of-hepa-filters/' target='_blank' rel="nofollow">HEPA filter technology was created in the 1940s</a> by the US Army Chemical Corps and National Defense Research Committee as part of the Manhattan Project. HEPA technology was declassified after World War II and became available for commercial and personal use. HEPA filters play a key role in the research and development of modern pharmaceuticals, aerospace engineering, and computer chip manufacturing. </p> <div class="quoteCite shadow rounded"> HEPA air cleaners, which remain little-used in Canadian hospitals, are a cheap and easy way to reduce risk from airborne pathogens. <br><br> &nbsp;&ndash;From Nature Magazine, October 6, 2021: <a href='https://www.nature.com/articles/d41586-021-02669-2' target='_blank' rel="nofollow">Real-world data show that filters clean COVID-causing virus from air &ndash; An inexpensive type of portable filter efficiently screened SARS-CoV-2 and other disease-causing organisms from hospital air.</a> </div> <p> It is easy to attach a HEPA filter to a fan, or to replace an existing HVAC filter with a HEPA filter. Here is a very <a href='https://www.jefftk.com/p/ceiling-air-purifier' target='_blank' rel="nofollow">scientific approach to a home-made solution</a>. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <a href='https://youtu.be/kH5APw_SLUU?t=106' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/covidHvac/hepaFan.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/hepaFan.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/hepaFan.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/hepaFan.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/hepaFan.png" type="image/png"> <source srcset="/blog/images/covidHvac/hepaFan.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/hepaFan.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/hepaFan.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/hepaFan.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/hepaFan.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/hepaFan.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/hepaFan.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/hepaFan.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/hepaFan.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/hepaFan.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/hepaFan.cur" type="image/x-icon"> <img alt='Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center, demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store.' class="imgImg rounded shadow" src="/blog/images/covidHvac/hepaFan.png" style='width: 100%; ' title='Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center, demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store.' /> </picture> </a> <figcaption class='imgFigCaption fullsize'> <a href="https://youtu.be/kH5APw_SLUU?t=106" target='_blank' > Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center, demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store. </a> </figcaption> </figure> </div> <p> One of the comments on the above video, from Rick Rude in 2014, was: </p> <div class="quoteCite shadow rounded"> Better to pull air thru the filter, if you push air through the filter dust that builds up will get blown off and go back into the room. Also, if the fan is pulling air through, the filter will stick to the fan and won't need as much tape to hold it on. Always handle used filters with care because handling them rough will release concentrated dust back into your air. If possible, always take air cleaners or vacuum cleaners outside to change the filters or bags. </div> <!-- endregion --> <!-- #region merv filters --> <h2 id="merv">MERV Filters</h2> <p> MERV is a graduated standard; MERV 7 is the minimum standard for furnaces, while MERV 13 is the highest. </p> <p> MERV 13 filters are more efficient at removing large particles from the air, while HEPA filters are more efficient at removing small particles from the air. MERV 13 filters can remove up to 99.97% of particles from the air, while HEPA filters can remove up to 99.99% of particles from the air. </p> <p> <a href='https://engineering.ucdavis.edu/news/science-action-how-build-corsi-rosenthal-box' target='_blank' rel="nofollow">How to Build a Corsi-Rosenthal Box</a>. </p> <!-- endregion --> <!-- #region youtube2 --> <h2 id="youtube2">How to Identify and Rectify Poorly Ventilated Indoor Spaces Using Engineering Controls</h2> <p> <a href='https://www.linkedin.com/in/elfstrom/' target='_blank' rel="nofollow">David Elfstrom, P. Eng.</a>, is a independent civil engineer based in Simcoe, Ontario who consults on the efficient use of energy in buildings. During the pandemic he has been drawing attention to the importance of ventilation, filtration, and overall indoor environmental quality. Earlier this year Mr. Elfstrom completed a ventilation and air pathways assessment of an apartment building in outbreak for a public health unit. This presentation was part of Passive Buildings Canada's 2021 Annual General Meeting. </p> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/9J96F32tv1s" frameborder="0" allowfullscreen></iframe></div> <p style="margin-top: 1em;"> Mr. Elfstrom also co-authored the <a href='https://docs.google.com/document/d/17tKk8Da8tnchtnp9ZRe7fPazGAmXtvoA-n4GZcY0_fQ/edit' target='_blank' rel="nofollow">Masks4Canada Room Ventilation/Filtration Guide and Tip Sheet</a>. </p> <!-- endregion --> <!-- #region N95 --> <h2 id="hvac">N95, KF94, FFP2, and 9152 Masks and Their Cousins</h2> <p> If you need to move about in a crowd, or enter a space that contained people a few hours ago, you need to wear a properly fitting face mask that filters out the tiny aerosol particles that contain the COVID-19 virus. <a href='https://www.travelawaits.com/2559161/n95-vs-kn95-vs-kf94-masks-for-travel/' target='_blank' rel="nofollow">N95 masks and their cousins</a> (KN95, KF94, etc.) do the job because they filter particles as small as 0.3 microns. This is similar to the particle size filtered by HEPA filters. Caution: KN95 is a self-reported test standard, and lacks strict government regulation by China, resulting in many underperforming and often flat-out fake masks. </p> <p> The US National Personal Protective Technology Laboratory (NPPTL), which is part of the US National Institute for Occupational Safety and Health (NIOSH), which itself is part of the CDC, evaluated various masks and published the results as <a href='https://www.cdc.gov/niosh/npptl/respirators/testing/NonNIOSHresults.html' target='_blank' rel="nofollow">NPPTL Respirator Assessments to Support the COVID-19 Response</a>. </p> <p> The following types of masks do <b>not</b> reliably filter tiny aerosols, so they do not adequately protect you from COVID-19 variants such as Omicron: </p> <ol> <li> <a href='https://www.theguardian.com/commentisfree/2021/dec/27/best-masks-covid-tests-cloth-surgical-respirators' target='_blank' rel="nofollow">Surgical masks</a> (what most people wear these days) </li> <li>Masks with activated charcoal</li> <li>Vented masks</li> <li>Cloth masks</li> <li>Gaiters</li> <li>Ill-fitting masks that do not cover and seal the mouth and nose</li> </ol> <iframe width="690" height="388" src="https://www.youtube.com/embed/WE5Uo3F2TdU" frameborder="0" allowfullscreen class="rounded shadow liImg"></iframe> <!-- endregion --> <!-- #region inoculations and pills --> <h2 id="other">Inoculations and Pills</h2> <div class='imgWrapper imgFlex right quartersize' style=' '> <a href='https://www.reuters.com/business/healthcare-pharmaceuticals/us-fda-set-authorize-pfizer-merck-covid-19-pills-this-week-bloomberg-news-2021-12-21/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/covidHvac/covidPills.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/covidPills.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/covidPills.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/covidPills.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/covidPills.png" type="image/png"> <source srcset="/blog/images/covidHvac/covidPills.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/covidPills.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/covidPills.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/covidPills.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/covidPills.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/covidPills.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/covidPills.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/covidPills.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/covidPills.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/covidPills.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/covidPills.cur" type="image/x-icon"> <img alt='COVID-19 pills' class="imgImg rounded shadow" src="/blog/images/covidHvac/covidPills.png" style='width: 100%; ' title='COVID-19 pills' /> </picture> </a> </div> <p> Inoculations, including boosters, have proven to be very helpful. Pills from Pfizer Inc. and Merck & Co. for people sick with COVID-19 will also be very helpful once they become available. However, pills will only help sick people get better, they will not prevent getting sick. </p> <p> Until everyone in the entire world has somehow acquired an effective level of antibodies, COVID-19 will continue to mutate. </p> <!-- endregion --> <!-- #region the only solution --> <h2 id="go">The Only Solution Available Today</h2> <p> Inoculations and masks are good, but they are not perfect preventative measures against COVID-19. Protection against infection is not 100%. At present, the only preventative measure against COVID-19 that would allow people to safely mingle in person would be to guarantee continuous streams of pure air directed individually at each person's face. </p> <p> This measure would also prevent the spread of <a href='https://bmcinfectdis.biomedcentral.com/articles/10.1186/s12879-019-3707-y' target='_blank' rel="nofollow">all other diseases and their variants that are primarily spread via aerosols</a>, including coronoviruses, colds, flus, tuberculosis, MERS-CoV, measles, ebola and chickenpox. Pollen, dust and other particulates would also be removed, so athsma sufffers would feel relief from extended periods breathing pure air. </p> <p> The solution to living with COVID-19 is simple, safe, inexpensive and is not disruptive: </p> <div class="formalNotice rounded shadow"> <h2 class="centered" style="margin: 0; padding: 0">Live your social life with a pure breeze in your face.</h2> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/covidHvac/breeze-dandelion.svg" type="image/svg"> <!---<source srcset="/blog/images/covidHvac/breeze-dandelion.avif" type="image/avif">--> <source srcset="/blog/images/covidHvac/breeze-dandelion.webp" type="image/webp"> <source srcset="/blog/images/covidHvac/breeze-dandelion.apng" type="image/apng"> <source srcset="/blog/images/covidHvac/breeze-dandelion.png" type="image/png"> <source srcset="/blog/images/covidHvac/breeze-dandelion.jpg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/breeze-dandelion.jpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/breeze-dandelion.jfif" type="image/jpeg"> <source srcset="/blog/images/covidHvac/breeze-dandelion.pjpeg" type="image/jpeg"> <source srcset="/blog/images/covidHvac/breeze-dandelion.pjp" type="image/jpeg"> <source srcset="/blog/images/covidHvac/breeze-dandelion.gif" type="image/gif"> <source srcset="/blog/images/covidHvac/breeze-dandelion.tif" type="image/tiff"> <source srcset="/blog/images/covidHvac/breeze-dandelion.tiff" type="image/tiff"> <source srcset="/blog/images/covidHvac/breeze-dandelion.bmp" type="image/bmp"> <source srcset="/blog/images/covidHvac/breeze-dandelion.ico" type="image/x-icon"> <source srcset="/blog/images/covidHvac/breeze-dandelion.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/covidHvac/breeze-dandelion.png" style='width: 100%; ' /> </picture> </div> <div style="clear:both; font-size: 0"></div> </div> <p> Life as we knew it before the pandemic started 2 years ago could resume, mostly. </p> <!-- endregion --> <!-- #region hvac upgrades --> <h2 id="hvac">HVAC Upgrades Are Key</h2> <p> The engineering group that encompasses HVAC is ASHRAE. ASHRAE was formed as the American Society of Heating, Refrigerating and Air-Conditioning Engineers by the merger in 1959 of American Society of Heating and Air-Conditioning Engineers (ASHAE), founded in 1894 and The American Society of Refrigerating Engineers (ASRE), founded in 1904. </p> <p> ASHRAE offers <a href='https://www.ashrae.org/file%20library/technical%20resources/covid-19/core-recommendations-for-reducing-airborne-infectious-aerosol-exposure.pdf' target='_blank' rel="nofollow">Core Recommendations for Reducing Airborne Infectious Aerosol Exposure</a>. <a href='https://www.iso-aire.com/blog/what-are-the-differences-between-a-merv-13-and-a-hepa-filter' target='_blank' rel="nofollow">MERV 13 or better</a> levels of performance are recommended, and HEPA filters surpass that specifation. In fact, all HEPA filters have a rating of a MERV 17 or higher. </p> <!-- endregion --> <!-- #region entrepreneurs --> <h2 id="entre">Entrepreneurs</h2> <p> This represents an opportunity for entrepreneurs. </p> <!-- #region restaurants --> <h3 id="restos">Restaurants</h2> <p> Imagine a restaurant that stays open with near-normal seating capacity because each table is equipped with a fan and ducting that blows a gentle breeze of pure air directly into the face of every patron. The air would recirculate within the restaurant, so there would be no need to upgrade the existing HVAC system because the HEPA filters located at each table would continuously clean the air within the restaurant. Those filters would need to be cleaned and disinfected daily. The wait staff would all wear N95 or similar masks, however each cashier could instead enjoy their own gentle breeeze of pure air. </p> <p> Cost to equip each seat in the restaurant could be less than $50. For do-it-yourself owners, the cost could approach $10 per seat. For example, a 100-seat restaurant might be able to retrofit 75 of those seats at a cost of less than $3750. They would never have to close again due to any pandemic caused by airborne aerosols... provided, of course, that local regulations recognized this approach. </p> <!-- endregion --> <!-- #region retail --> <h3 id="stores">Retail Stores and Office Buildings</h2> <p> Enclosing sales clerks in stores behind clear plastic walls is wrong because it decreases air circulation. Instead, each those staff members should have a dedicated ducted fan that blows a gentle breeze of pure air into their face at all times. </p> <p> Similarly, queues of people awaiting their turn at checking out should have fans blowing pure air at their heads. The air would recirculate within the store, there would be no need to upgrade the existing HVAC system because the HEPA filters would continuously clean the air within the building. </p> <!-- endregion --> <!-- #region spring-breezifier --> <h2 id="domain">Spring-Breezifier Is Offered Into the Public Domain</h2> <p> I offer this idea to the world; I am an idea machine so I cannot act on most of them. Just for fun, let's call this idea the <i>Spring-Breezifier</i>. </p> <p> Designing and building Spring-Breezifiers seems like it might be a good high school or youth group project. HVAC installers in particular should be able to make short work of this idea; go ahead and make lots of money providing pure airflows by building custom Spring-Breezifiers for your customers, we all thank you! </p> <p> If anyone would like to talk to me about their Spring-Breezifier project, I would be happy to speak with them. </p> <!-- endregion --> <!-- #region next steps --> <h2 id="next">Next Steps</h2> <ol> <li> Medical HVAC specialists could suggest the necessary cubic feet per minute of air required per person sufficient to guarantee a continuous stream of pure air, and provide guidelines for designing ductwork to direct that air effectively. </li> <li> Build and test prototypes. Most of the effort will be in designing and building the ducting and/or the scaffolding. Adding inner baffles would raise the cost and weight, lower the noise and make the airflow less turbulent. Perhaps Spring-Breezifiers could be built entirely from off-the-shelf parts for some or even most installations. Large-format 3D printers for special circumstances might be helpful. </li> <li>Propose the inclusion of pure air streams into public spaces as a matter of public health policy.</li> </ol> <!-- endregion --> <!-- endregion --> Disappointing Scala 3 Installation Experience 2021-05-19T00:00:00-04:00 https://mslinn.github.io/blog/2021/05/19/installing-scala-3.0 <p> I run <a href='https://scalacourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a>, an online Scala training web site. After years of hype, Scala 3 is now available. </p> <h2 id="prime">Scala 3: Not Yet Ready for Production</h2> <p> <a href='https://www.infoq.com/news/2021/03/scala3/' target='_blank' rel="nofollow">Scala 3</a> was eight years in the making. You would never know that from the horrible installation process and the disappointing installation instructions. </p> <p> It is going to take quite a while before <a href='https://scalatimes.com/d374aea433' target='_blank' rel="nofollow">Scala 3</a>, which was known as Dotty before it was released, can be trusted in production. According to the <a href='https://github.com/lampepfl/dotty' target='_blank' rel="nofollow">Dotty GitHub project</a>, the only published future milestone is <a href='https://github.com/lampepfl/dotty/milestones' target='_blank' rel="nofollow">v3.1.0, which has no due date</a>. Given that Scala 3 uses an entirely new build process, and an entirely new (and nonstandard) installation process, and that the internals of the Scala compiler were almost completely replaced, I doubt that version will be stable enough for use on production projects. </p> <h2 id="choices">Installation Choices</h2> <p> Installation cholices include: </p> <ul> <li>Command-line Scala has limited use cases, but is nice to have around for occassional experimentation.</li> <li> <a href='https://www.scala-lang.org/blog/2021/04/08/scala-3-in-sbt.html' target='_blank' rel="nofollow">SBT</a> (which features an enhanced Scala REPL) is very helpful for interactively developing code, as well as for building and testing. </li> <li> <a href='https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html' target='_blank' rel="nofollow">IntelliJ</a> provides the best Scala coding productivity and code quality. </li> <li> <a href='https://shunsvineyard.info/2020/11/20/setting-up-vs-code-for-scala-development-on-wsl/' target='_blank' rel="nofollow">VSCode</a> has been playing catch-up but is not full-featured yet. </li> </ul> <h2 id="install">Installation Transcript</h2> <p> The following is the transcript of how I installed command-line Scala on Ubuntu 20.10 running under WSL2. </p> <h3 class="numbered" id="remove_scala2">Remove Scala 2</h3> <p> This step is not required. Scala 2 and Scala 3 can easily co-exist on the same system because their names are different. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8134cde946c5'><button class='copyBtn' data-clipboard-target='#id8134cde946c5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt remove scala <span class='unselectable'>Reading package lists... Done Building dependency tree Reading state information... Done The following packages will be REMOVED: scala 0 upgraded, 0 newly installed, 1 to remove and 0 not upgraded. After this operation, 666 MB disk space will be freed. Do you want to continue? [Y/n] (Reading database ... 236024 files and directories currently installed.) Removing scala (2.13.4-400) ... Processing triggers for man-db (2.9.3-2) ... </span></pre> </div> <h3 class="numbered" id="cs">Install Coursier</h3> <p> Coursier is a multithreaded downloader for project dependencies, and it now also downloads Scala 3. SBT uses coursier internally. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddf72c3f773e4'><button class='copyBtn' data-clipboard-target='#iddf72c3f773e4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -fLo cs https://git.io/coursier-cli-"$(uname | tr LD ld)" <span class='unselectable'>&nbsp; % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 144 100 144 0 0 285 0 --:--:-- --:--:-- --:--:-- 6000 100 57.1M 100 57.1M 0 0 3656k 0 0:00:15 0:00:15 --:--:-- 4092k </span> <span class='unselectable'>$ </span>mv cs ~/.local/bin/ <span class='unselectable'>$ </span>chmod a+x ~/.local/bin/cs <span class='unselectable'>$ </span>cs install cs <span class='unselectable'>https://repo1.maven.org/maven2/io/get-coursier/apps/maven-metadata.xml 100.0% [##########] 1.8 KiB (8.5 KiB / s) https://repo1.maven.org/maven2/io/get-coursier/coursier-cli_2.12/maven-metadata.xml No new update since 2021-03-23 14:35:16 Wrote cs Warning: /home/mslinn/.local/share/coursier/bin is not in your PATH To fix that, add the following line to ~/.bashrc export PATH="$PATH:/home/mslinn/.local/share/coursier/bin" </span> <span class='unselectable'>$ </span> export PATH="$PATH:/home/mslinn/.local/share/coursier/bin"</pre> </div> <p> Now let's test Coursier: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id437198d9e99d'><button class='copyBtn' data-clipboard-target='#id437198d9e99d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs <span class='unselectable'>Coursier 2.0.16 Usage: cs [options] [command] [command-options] Available commands: bootstrap, channel, complete, fetch, get, install, java, java-home, launch, list, publish, resolve, setup, uninstall, update, search Type cs command --help for help on an individual command </span></pre> </div> <p> This is the Coursier help message for the <code>install</code> subcommand: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id32559297f8bb'><button class='copyBtn' data-clipboard-target='#id32559297f8bb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs install --help <span class='unselectable'>Command: install Usage: cs install --cache &lt;string?&gt; Cache directory (defaults to environment variable COURSIER_CACHE, or ~/.cache/coursier/v1 on Linux and ~/Library/Caches/Coursier/v1 on Mac) --mode | -m &lt;offline|update-changing|update|missing|force&gt; Download mode (default: missing, that is fetch things missing from cache) --ttl | -l &lt;duration&gt; TTL duration (e.g. &quot;24 hours&quot;) --parallel | -n &lt;int&gt; Maximum number of parallel downloads (default: 6) --checksum &lt;checksum1,checksum2,...&gt; Checksum types to check - end with none to allow for no checksum validation if no checksum is available, example: SHA-256,SHA-1,none --retry-count &lt;int&gt; Retry limit for Checksum error when fetching a file --cache-file-artifacts | --cfa &lt;bool&gt; Flag that specifies if a local artifact should be cached. --follow-http-to-https-redirect &lt;bool&gt; Whether to follow http to https redirections --credentials &lt;host(realm) user:pass|host user:pass&gt; Credentials to be used when fetching metadata or artifacts. Specify multiple times to pass multiple credentials. Alternatively, use the COURSIER_CREDENTIALS environment variable --credential-file &lt;string*&gt; Path to credential files to read credentials from --use-env-credentials &lt;bool&gt; Whether to read credentials from COURSIER_CREDENTIALS (env) or coursier.credentials (Java property), along those passed with --credentials and --credential-file --quiet | -q &lt;counter&gt; Quiet output --verbose | -v &lt;counter&gt; Increase verbosity (specify several times to increase more) --progress | -P &lt;bool&gt; Force display of progress bars --log-changing &lt;bool&gt; Log changing artifacts --log-channel-version | --log-index-version | --log-jvm-index-version &lt;bool&gt; Log app channel or JVM index version --graalvm-home &lt;string?&gt; --graalvm-option &lt;string*&gt; --graalvm-default-version &lt;string?&gt; --install-dir | --dir &lt;string?&gt; --install-platform &lt;string?&gt; Platform for prebuilt binaries (e.g. &quot;x86_64-pc-linux&quot;, &quot;x86_64-apple-darwin&quot;, &quot;x86_64-pc-win32&quot;) --install-prefer-prebuilt &lt;bool&gt; --only-prebuilt &lt;bool&gt; Require prebuilt artifacts for native applications, don&#39;t try to build native executable ourselves --repository | -r &lt;maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|ivy:$pattern&gt; Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots) --default-repositories &lt;bool&gt; --proguarded &lt;bool?&gt; --channel &lt;org:name&gt; Channel for apps --default-channels &lt;bool&gt; Add default channels --contrib &lt;bool&gt; Add contrib channel --file-channels &lt;bool&gt; Add channels read from the configuration directory --jvm &lt;string?&gt; --jvm-dir &lt;string?&gt; --system-jvm &lt;bool?&gt; --local-only &lt;bool&gt; --update &lt;bool&gt; --jvm-index &lt;string?&gt; --repository | -r &lt;maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|scala-integration|scala-nightlies|ivy:$pattern|jitpack|clojars|jcenter|apache:$repo&gt; Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots) --no-default &lt;bool&gt; Do not add default repositories (~/.ivy2/local, and Central) --sbt-plugin-hack &lt;bool&gt; Modify names in Maven repository paths for sbt plugins --drop-info-attr &lt;bool&gt; Drop module attributes starting with &#39;info.&#39; - these are sometimes used by projects built with sbt --channel &lt;org:name&gt; Channel for apps --default-channels &lt;bool&gt; Add default channels --contrib &lt;bool&gt; Add contrib channel --file-channels &lt;bool&gt; Add channels read from the configuration directory --repository | -r &lt;maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|scala-integration|scala-nightlies|ivy:$pattern|jitpack|clojars|jcenter|apache:$repo&gt; Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots) --no-default &lt;bool&gt; Do not add default repositories (~/.ivy2/local, and Central) --sbt-plugin-hack &lt;bool&gt; Modify names in Maven repository paths for sbt plugins --drop-info-attr &lt;bool&gt; Drop module attributes starting with &#39;info.&#39; - these are sometimes used by projects built with sbt --channel &lt;org:name&gt; Channel for apps --default-channels &lt;bool&gt; Add default channels --contrib &lt;bool&gt; Add contrib channel --file-channels &lt;bool&gt; Add channels read from the configuration directory --env &lt;bool&gt; --disable-env | --disable &lt;bool&gt; --setup &lt;bool&gt; --user-home &lt;string?&gt; --add-channel &lt;string*&gt; (deprecated) --force | -f &lt;bool&gt; </span></pre> </div> <h3 class="numbered" id="s3">Install Scala 3</h3> <p> The Scala 3 compiler and REPL are separate programs: <code>scala3-compiler</code> and <code>scala3-repl</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id73439bf7986b'><button class='copyBtn' data-clipboard-target='#id73439bf7986b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs install scala3-compiler <span class='unselectable'>https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/3.0.0/scala3-compiler_3-3.0.0.pom 100.0% [##########] 4.8 KiB (79.7 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/tasty-core_3/3.0.0/tasty-core_3-3.0.0.pom 100.0% [##########] 3.5 KiB (69.5 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/3.0.0/scala3-library_3-3.0.0.pom 100.0% [##########] 3.6 KiB (53.9 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-interfaces/3.0.0/scala3-interfaces-3.0.0.pom 100.0% [##########] 3.4 KiB (65.9 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-interfaces/3.0.0/scala3-interfaces-3.0.0.jar 100.0% [##########] 3.4 KiB (113.9 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/tasty-core_3/3.0.0/tasty-core_3-3.0.0.jar 100.0% [##########] 71.9 KiB (192.7 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/3.0.0/scala3-library_3-3.0.0.jar 100.0% [##########] 1.1 MiB (1.8 MiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/3.0.0/scala3-compiler_3-3.0.0.jar 100.0% [##########] 14.7 MiB (3.7 MiB / s) Wrote scala3-compiler </span> <span class='unselectable'>$ </span>cs install scala3-repl <span class='unselectable'>https://repo1.maven.org/maven2/io/get-coursier/apps/maven-metadata.xml No new update since 2021-05-14 04:42:19 Wrote scala3-repl </span></pre> </div> <h3 class="numbered" id="repl3">Run Scala 3 REPL</h3> <p> The part is easy! </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida844dd8c6e84'><button class='copyBtn' data-clipboard-target='#ida844dd8c6e84' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>scala3-repl --version <span class='unselectable'>Scala code runner version 3.0.0 -- Copyright 2002-2021, LAMP/EPFL </span> <span class='unselectable'>$ </span>scala3-repl <span class='unselectable'>scala&gt; </span></pre> </div> <h2 id="sbt">Easily Run Scala REPL With SBT</h2> <p> If you do not mind directories called <code>project/</code> and <code>target/</code> being created in your current directory, and you have already <a href='https://www.scala-sbt.org/download.html' target='_blank' rel="nofollow">installed sbt</a>, you can get a REPL powered by Scala 3 like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9171e1be3131'><button class='copyBtn' data-clipboard-target='#id9171e1be3131' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sbt "-Dsbt.version=1.5.2" ++3.0.0! console <span class='unselectable'>[info] welcome to sbt 1.5.2 (Ubuntu Java 11.0.11) [info] loading global plugins from /home/mslinn/.sbt/1.0/plugins [info] loading project definition from /var/work/ancientWarmth/ancientWarmth/project [info] set current project to ancientwarmth (in build file:/var/work/ancientWarmth/ancientWarmth/) [info] Forcing Scala version to 3.0.0 on all projects. [info] Reapplying settings... [info] set current project to ancientwarmth (in build file:/var/work/ancientWarmth/ancientWarmth/) [info] Updating [info] Resolved dependencies [info] Updating https://repo1.maven.org/maven2/org/scala-lang/scaladoc_3/3.0.0/scaladoc_3-3.0.0.pom 100.0% [##########] 6.1 KiB (82.6 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scala3-tasty-inspector_3/3.0.0/scala3-tasty-inspector_3-3.0.0.pom 100.0% [##########] 3.6 KiB (80.8 KiB / s) [info] Resolved dependencies [info] Fetching artifacts of [info] Fetched artifacts of [info] Fetching artifacts of https://repo1.maven.org/maven2/org/scala-lang/scala3-tasty-inspector_3/3.0.0/scala3-tasty-inspector_3-3.0.0.jar 100.0% [##########] 16.6 KiB (338.1 KiB / s) https://repo1.maven.org/maven2/org/scala-lang/scaladoc_3/3.0.0/scaladoc_3-3.0.0.jar 100.0% [##########] 1.5 MiB (3.1 MiB / s) [info] Fetched artifacts of scala&gt; </span></pre> </div> <p> Thanks to <a href='https://twitter.com/renghenKornel/status/1395684928440791040' target='_blank' rel="nofollow">@renghen</a> for this tip. </p> <h2 id="sc">ScalaCourses</h2> <p> If you want to learn how to work effectively with Scala for functional and object-oriented programming, <a href='https://scalacourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a> is your best option. The course material is suitable for Scala 2 and Scala 3. Visit ScalaCourses.com to learn how to become a proficient Scala programmer. </p> OCI / Docker / AWS Lambda / Django / Buildah / podman 2021-04-29T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/29/buildah-podman-python-lambda <style> body { counter-reset: pcounter; } p.count:before { counter-increment: pcounter; content: counter(pcounter) ")\A0"; } </style> <!-- #region intro --> <p> This article is a work in progress. Some of it may be incorrect, and some thoughts might lead nowhere. I am publicly posting it in this state so I can discuss it with others. This article will be improved as information becomes available. </p> <!-- endregion --> <!-- #region Goal --> <h2 id="goal">Goal</h2> <div class='imgWrapper imgFlex right' style=' '> <a href='https://podman.io' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/podman-logo-crop.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/podman-logo-crop.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/podman-logo-crop.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.png" type="image/png"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/podman-logo-crop.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/podman-logo-crop.png" style='width: 100%; padding: 1em; height: 191px; width: auto;' /> </picture> </a> </div> <div class='imgWrapper imgFlex right' style=' '> <a href='https://buildah.io/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/buildah-logo-crop.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.png" type="image/png"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/buildah-logo-crop.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/buildah-logo-crop.png" style='width: 100%; padding: 0.73em; height: 191px; width: auto;' /> </picture> </a> </div> <p> <a href='/blog/2021/04/28/buildah-podman.html'>As previously discussed</a>, Buildah is a drop-in replacement for using <code>docker build</code> and a <code>Dockerfile</code>. Buildah’s <code>build-using-dockerfile</code>, or <code>bud</code> argument makes it behave just like <code>docker build</code> does. </p> <p> The goal of this article is to use Buildah / <code>podman</code> to create an Open Container Initiative (OCI) container image with a Django app, including the Python 3.8 runtime installed. The Django app will start when the container is created. The code for the Django app will be stored on the local machine where its source code can be edited, and it will be mapped into the container from the host system. Changes made to the code from the host system will be immediately visible inside the container. </p> <!-- endregion --> <!-- #region TODO --> <h2 id="todo">TODO</h2> <p class="count"> Background: AWS publishes <a href='https://docs.aws.amazon.com/lambda/latest/dg/python-image.html' target='_blank' rel="nofollow">Deploying Python with an AWS base image</a>, but that does not discuss running or testing. <a href='https://docs.aws.amazon.com/lambda/latest/dg/getting-started-create-function.html' target='_blank' rel="nofollow">Create a Lambda function with the console</a> is a more complete article, but is focused on using the web browser console, using Docker, and Node.js. So many differences from the desired goal make the articles difficult to translate to AWS CLI, Buildah / <code>podman</code> and Python. </p> <p class="count"> Talk about the <a href='https://github.com/aws/aws-lambda-runtime-interface-emulator' target='_blank' rel="nofollow">AWS Lambda Runtime Interface Emulator</a>, compare and contrast with the <a href='https://pypi.org/project/awslambdaric/' target='_blank' rel="nofollow">AWS Lambda Python Runtime Interface Client</a>. </p> <p class="count"> Compare these <a href='https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html' target='_blank' rel="nofollow">AWS Lambda Runtimes</a> with other, equivalant runtimes. </p> <p class="count"> OCI images are swapped in when AWS Lambda is invoked. Do larger images cost more to use? If so, discuss. </p> <!-- endregion --> <!-- #region Deploy Python Lambda function with Container Image --> <h2 id="main">Deploy Python Lambda function with Container Image</h2> <p> Consider this <code>Dockerfile</code>, which launches a Python 3.8 command-line application in a manner compatible with AWS Lambda: </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,Dockerfile' download='Dockerfile' title='Click on the file name to download the file'>Dockerfile</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id4346677f79fe"><button class='copyBtn' data-clipboard-target='#id4346677f79fe'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>FROM public.ecr.aws/lambda/python:3.8 COPY app.py ./ CMD ["app.handler"] </pre> <p> Following is a small Python app called <code>app.py</code>, which will be launched by the <code>Dockerfile</code>. The Python app can be run as an AWS Lambda program because it implements the <code>handler</code> entry point. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,app.py' download='app.py' title='Click on the file name to download the file'>app.py</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idf77eab672122"><button class='copyBtn' data-clipboard-target='#idf77eab672122'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>import sys def handler(event, context): return f"Hello from AWS Lambda using Python &#123;sys.version&#125;!" </pre> <!-- endregion --> <!-- #region Build image --> <h2 id="build">Build Image</h2> <p> Buildah builds the image, just the same way that Docker would: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id544c114e48da'><button class='copyBtn' data-clipboard-target='#id544c114e48da' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello . <span class='unselectable'>STEP 1: FROM public.ecr.aws/lambda/python:3.8 Getting image source signatures Copying blob 03ac043af787 skipped: already exists Copying blob 420e64b38334 done Copying blob ff259f25b075 done Copying blob 3ff716981d54 done Copying blob 6b6e623a48a8 done Copying blob 9aa8f1e66d54 done Copying config 67dc3a2a54 done Writing manifest to image destination Storing signatures STEP 2: COPY app.py ./ STEP 3: CMD ["app.handler"] STEP 4: COMMIT hello Getting image source signatures Copying blob 683073d39306 skipped: already exists Copying blob 658871a69e1f skipped: already exists Copying blob 6fa16f35d11e skipped: already exists Copying blob d6fa53d6caa6 skipped: already exists Copying blob 61c062506436 skipped: already exists Copying blob 1c1d66a5fd95 skipped: already exists Copying blob 33af9dc6463a done Copying config 98862dfd20 done Writing manifest to image destination Storing signatures --&gt; 98862dfd208 98862dfd2087152ee821553d6cb1c033e735af06e5f11c814bcc9300fb65584e </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Test Lambda function Locally --> <h2 id="deploy_local">Test Lambda function Locally</h2> <p> Before calling the Lambda API from a local container, first run the container. Containers default to running in the foreground, but the <code>-d</code> option causes a container to be run as a background process. This container is given the name <code>hello</code>, the external HTTP endpoint at 9000 is mapped to internal port 8080, and the latest version of the <code>hello</code> lambda function is run in the container. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id41006da83e4f'><button class='copyBtn' data-clipboard-target='#id41006da83e4f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman run \ -d \ --name hello \ -p 9000:8080 \ hello:latest <span class='unselectable'>d4d296e4c91d01c98d312e3f79599dca53990d95218e94bbdfbbac6a43cde9e8 </span></pre> </div> <p> Call the local version of the Lambda API: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb503831acd8c'><button class='copyBtn' data-clipboard-target='#idb503831acd8c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl \ -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \ -d '{}' <span class='unselectable'>"Hello from AWS Lambda using Python 3.8.9 (default, Apr 20 2021, 13:58:54) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-12)]!" </span></pre> </div> <p> Stop the container called <code>hello</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6efb6e7d99b6'><button class='copyBtn' data-clipboard-target='#id6efb6e7d99b6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman stop hello <span class='unselectable'>96cc1b1ed92368a1165d6a6ad0b1e5544d4ac751b64e94df33bf2322e6d7b30c </span></pre> </div> <!-- endregion --> <!-- #region Create AWS ECR Repository --> <h2 id="podman_tag">Create AWS ECR Repository</h2> <p> AWS provides a registry for OCI-compatible image repositories called the <a href='https://aws.amazon.com/ecr/' target='_blank' rel="nofollow">AWS Elastic Container Registry (ECR)</a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8a8a5961c2b1'><button class='copyBtn' data-clipboard-target='#id8a8a5961c2b1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr create-repository help CREATE-REPOSITORY() CREATE-REPOSITORY()<br/> NAME create-repository -<br/> DESCRIPTION Creates a repository. For more information, see Amazon ECR Repositories in the Amazon Elastic Container Registry User Guide .<br/> See also: AWS API Documentation<br/> See &#39;aws help&#39; for descriptions of global parameters.<br/> SYNOPSIS create-repository --repository-name &lt;value&gt; [--tags &lt;value&gt;] [--image-tag-mutability &lt;value&gt;] [--image-scanning-configuration &lt;value&gt;] [--cli-input-json &lt;value&gt;] [--generate-cli-skeleton &lt;value&gt;]<br/> OPTIONS --repository-name (string) The name to use for the repository. The repository name may be spec- ified on its own (such as nginx-web-app ) or it can be prepended with a namespace to group the repository into a category (such as project-a/nginx-web-app ).<br/> --tags (list) The metadata that you apply to the repository to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters.<br/> (structure) The metadata that you apply to a resource to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters.<br/> Key -&gt; (string) One part of a key-value pair that make up a tag. A key is a general label that acts like a category for more specific tag values.<br/> Value -&gt; (string) The optional part of a key-value pair that make up a tag. A value acts as a descriptor within a tag category (key).<br/> Shorthand Syntax:<br/> Key=string,Value=string ...<br/> JSON Syntax:<br/> [ { &quot;Key&quot;: &quot;string&quot;, &quot;Value&quot;: &quot;string&quot; } ... ]<br/> --image-tag-mutability (string) The tag mutability setting for the repository. If this parameter is omitted, the default setting of MUTABLE will be used which will al- low image tags to be overwritten. If IMMUTABLE is specified, all im- age tags within the repository will be immutable which will prevent them from being overwritten.<br/> Possible values:<br/> o MUTABLE<br/> o IMMUTABLE<br/> --image-scanning-configuration (structure) The image scanning configuration for the repository. This setting determines whether images are scanned for known vulnerabilities af- ter being pushed to the repository.<br/> scanOnPush -&gt; (boolean) The setting that determines whether images are scanned after be- ing pushed to a repository. If set to true , images will be scanned after being pushed. If this parameter is not specified, it will default to false and images will not be scanned unless a scan is manually started with the StartImageScan API.<br/> Shorthand Syntax:<br/> scanOnPush=boolean<br/> JSON Syntax:<br/> { &quot;scanOnPush&quot;: true|false }<br/> --cli-input-json (string) Performs service operation based on the JSON string provided. The JSON string follows the format provided by --gen- erate-cli-skeleton. If other arguments are provided on the command line, the CLI values will override the JSON-provided values. It is not possible to pass arbitrary binary values using a JSON-provided value as the string will be taken literally.<br/> --generate-cli-skeleton (string) Prints a JSON skeleton to standard output without sending an API request. If provided with no value or the value input, prints a sample input JSON that can be used as an argument for --cli-input-json. If provided with the value output, it validates the command inputs and returns a sample output JSON for that command.<br/> See &#39;aws help&#39; for descriptions of global parameters.<br/> EXAMPLES Example 1: To create a repository<br/> The following create-repository example creates a repository inside the specified namespace in the default registry for an account.<br/> aws ecr create-repository \ --repository-name project-a/nginx-web-app<br/> Output:<br/> { &quot;repository&quot;: { &quot;registryId&quot;: &quot;123456789012&quot;, &quot;repositoryName&quot;: &quot;sample-repo&quot;, &quot;repositoryArn&quot;: &quot;arn:aws:ecr:us-west-2:123456789012:repository/project-a/nginx-web-app&quot; } }<br/> For more information, see Creating a Repository in the Amazon ECR User Guide.<br/> Example 2: To create a repository configured with image tag immutabil- ity<br/> The following create-repository example creates a repository configured for tag immutability in the default registry for an account.<br/> aws ecr create-repository \ --repository-name sample-repo \ --image-tag-mutability IMMUTABLE<br/> Output:<br/> { &quot;repository&quot;: { &quot;registryId&quot;: &quot;123456789012&quot;, &quot;repositoryName&quot;: &quot;sample-repo&quot;, &quot;repositoryArn&quot;: &quot;arn:aws:ecr:us-west-2:123456789012:repository/sample-repo&quot;, &quot;imageTagMutability&quot;: &quot;IMMUTABLE&quot; } }<br/> For more information, see Image Tag Mutability in the Amazon ECR User Guide.<br/> Example 3: To create a repository configured with a scanning configura- tion<br/> The following create-repository example creates a repository configured to perform a vulnerability scan on image push in the default registry for an account.<br/> aws ecr create-repository \ --repository-name sample-repo \ --image-scanning-configuration scanOnPush=true<br/> Output:<br/> { &quot;repository&quot;: { &quot;registryId&quot;: &quot;123456789012&quot;, &quot;repositoryName&quot;: &quot;sample-repo&quot;, &quot;repositoryArn&quot;: &quot;arn:aws:ecr:us-west-2:123456789012:repository/sample-repo&quot;, &quot;imageScanningConfiguration&quot;: { &quot;scanOnPush&quot;: true } } }<br/> For more information, see Image Scanning in the Amazon ECR User Guide.<br/> OUTPUT repository -&gt; (structure) The repository that was created.<br/> repositoryArn -&gt; (string) The Amazon Resource Name (ARN) that identifies the repository. The ARN contains the arn:aws:ecr namespace, followed by the re- gion of the repository, AWS account ID of the repository owner, repository namespace, and repository name. For example, arn:aws:ecr:region:012345678910:repository/test .<br/> registryId -&gt; (string) The AWS account ID associated with the registry that contains the repository.<br/> repositoryName -&gt; (string) The name of the repository.<br/> repositoryUri -&gt; (string) The URI for the repository. You can use this URI for Docker push or pull operations.<br/> createdAt -&gt; (timestamp) The date and time, in JavaScript date format, when the reposi- tory was created.<br/> imageTagMutability -&gt; (string) The tag mutability setting for the repository.<br/> imageScanningConfiguration -&gt; (structure) The image scanning configuration for a repository.<br/> scanOnPush -&gt; (boolean) The setting that determines whether images are scanned after being pushed to a repository. If set to true , images will be scanned after being pushed. If this parameter is not speci- fied, it will default to false and images will not be scanned unless a scan is manually started with the StartImageScan API.<br/> <br/> CREATE-REPOSITORY()</pre> </div> <!-- endregion --> <p> The following creates an AWS ECR image repository in called <code>hello</code> within the <code>test</code> namespace. <a href='https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html' target='_blank' rel="nofollow">Images are scanned</a> for known vulnerabilities after they are pushed to the repository. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6ec2c02a6883'><button class='copyBtn' data-clipboard-target='#id6ec2c02a6883' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr create-repository \ --repository-name test/hello \ --image-scanning-configuration scanOnPush=true <span class='unselectable'>{ "repository": { "repositoryArn": "arn:aws:ecr:us-east-1:031372724784:repository/test/hello", "registryId": "031372724784", "repositoryName": "test/hello", "repositoryUri": "031372724784.dkr.ecr.us-east-1.amazonaws.com/test/hello", "createdAt": 1620232146.0, "imageTagMutability": "MUTABLE", "imageScanningConfiguration": { "scanOnPush": true } } } </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Tag Image --> <h2 id="podman_tag">Tag Image</h2> <p class="quote"> <b><code>podman tag</code></b> &ndash; Assigns a new image name to an existing image. A full name refers to the entire image name, including the optional tag after the <code>:</code>. If there is no tag provided, then podman will default to latest for both the image and the target-name. &nbsp; &ndash; From <code>man podman-tag</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id49b9a57f55cd'><button class='copyBtn' data-clipboard-target='#id49b9a57f55cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>IMAGE_NAME=hello <span class='unselectable'>$ </span>IMAGE_VERSION=0.1 <span class='unselectable'>$ </span>podman tag $IMAGE_NAME:$IMAGE_VERSION \ $REGISTRY/$IMAGE_NAME:$IMAGE_VERSION <span class='unselectable'>$ </span>podman images <span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE localhost/hello 0.1 98862dfd2087 39 minutes ago 622 MB 752246127823.dkr.ecr.us-east-1.amazonaws.com/hello latest 98862dfd2087 39 minutes ago 622 MB public.ecr.aws/lambda/python 3.8 67dc3a2a54fb 25 hours ago 622 MB 752246127823.dkr.ecr.us-east-1.amazonaws.com/ancientwarmth latest 5d18ea34fc30 28 hours ago 2.03 GB localhost/ancientwarmth latest 5d18ea34fc30 28 hours ago 2.03 GB &lt;none&gt; &lt;none&gt; 40ef32b39cf4 5 days ago 622 MB docker.io/library/amazonlinux latest 53ef897d731f 5 days ago 170 MB docker.io/amazon/aws-lambda-python 3.8 e12ea62c5582 9 days ago 622 MB docker.io/library/alpine latest 6dbb9cc54074 2 weeks ago 5.88 MB docker.io/lambci/lambda build-python3.8 714c659c9f6f 3 months ago 2.03 GB </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Push Image to ECR --> <h2 id="push">Push Image to ECR</h2> <p> <code>Podman</code> will use the IAM credentials for the <code>dev</code> <a href='https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html' target='_blank' rel="nofollow">profile</a> in <code>~/.aws/credentials</code> to log into that AWS account: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.aws/credentials</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2d7c2acdf460'><button class='copyBtn' data-clipboard-target='#id2d7c2acdf460' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[default] aws_access_key_id = ******************** aws_secret_access_key = **************************************** region = us-east-1<br> [dev] aws_access_key_id = ******************** aws_secret_access_key = **************************************** region = us-east-1</pre> </div> <!-- endregion --> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8d1861848c8c'><button class='copyBtn' data-clipboard-target='#id8d1861848c8c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export AWS_PROFILE=dev<br> <span class='unselectable'>$ </span>AWS_ACCOUNT="$( aws sts get-caller-identity \ --query Account \ --output text )"<br> <span class='unselectable'>$ </span>AWS_REGION="$( aws configure get region )"<br> <span class='unselectable'>$ </span>REGISTRY="$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com"<br> <span class='unselectable'>$ </span>aws ecr get-login-password \ --region "$AWS_REGION" | \ podman login \ --password-stdin \ --username AWS \ "$REGISTRY" <span class='unselectable'>Login Succeeded! </span></pre> </div> <!-- endregion --> <p> Now that <code>podman</code> is logged into AWS, use <code>podman</code> push the image to AWS ECR: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id81b9c41b194d'><button class='copyBtn' data-clipboard-target='#id81b9c41b194d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman push test/$IMAGE_NAME \ $REGISTRY/$IMAGE_NAME:$IMAGE_VERSION <span class='unselectable'>Getting image source signatures Copying blob 692590faf2d1 [--------------------------------------] 8.0b / 8.2MiB Copying blob 397718cff58d [--------------------------------------] 8.0b / 206.2MiB Copying blob 9ca787b1c91c [--------------------------------------] 8.0b / 93.1MiB Copying blob ef26f5221b79 [--------------------------------------] 8.0b / 196.7MiB Copying blob 0a3f69c27a89 [--------------------------------------] 8.0b / 316.4MiB Copying blob 5b3cbb76df75 [--------------------------------------] 8.0b / 1.1GiB Copying blob e9cad39831b0 [--------------------------------------] 8.0b / 3.5KiB Error: Error copying image to the remote destination: Error writing blob: Error initiating layer upload to /v2/ancientwarmth/blobs/uploads/ in 752246127823.dkr.ecr.us-east-1.amazonaws.com: name unknown: The repository with name 'hello' does not exist in the registry with id '752246127823' </span></pre> </div> <!-- endregion --> <p> The results of an <a href='https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecr/describe-image-scan-findings.html' target='_blank' rel="nofollow">image scan</a> for the new repository can be retrieved as follows: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1fd1491f9a2b'><button class='copyBtn' data-clipboard-target='#id1fd1491f9a2b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr describe-image-scan-findings \ --repository-name test/hello \ --image-id imageTag=tag_name</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Deploy Python Lambda function with Container Image --> <h2 id="buildah_python">Deploy Python Lambda function with Container Image</h2> <p> <code>Podman</code> can invoke the app using an OCI container with Amazon Linux 2 and Python 3.8: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idad103bf3095b'><button class='copyBtn' data-clipboard-target='#idad103bf3095b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container run -ti \ public.ecr.aws/lambda/python:3.8 \ blog/docker/podman/app.py <span class='unselectable'>Trying to pull public.ecr.aws/lambda/python:3.8... Getting image source signatures Copying blob 1de4740de1c2 done Copying blob 03ac043af787 done Copying blob 2e2bb77ae2dc done Copying blob 842c9dce67e8 done Copying blob df513d38f4d9 done Copying blob 031c6369fb2b done Copying config e12ea62c55 done Writing manifest to image destination Storing signatures time="2021-05-02T23:38:30.971" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)" </span></pre> </div> <!-- endregion --> <!-- endregion --> Docker, OCI Images, Buildah and podman 2021-04-28T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/28/buildah-podman <!-- #region intro --> <p> There are many ways to create and run Docker-compatible images. Docker is probably the worst option, mostly because it runs as a daemon, and all *nix daemons run with <code>root</code> privileges. Also, the <code>docker-ce</code> package lists <code>iptables</code> as a dependency, which needs <code>systemd</code> to be running normally, and WSL2 only partially supports <code>systemd</code>. </p> <p> <a href='https://www.capitalone.com/tech/cloud/container-runtime/' target='_blank' rel="nofollow">A Comprehensive Container Runtime Comparison</a> provides helpful background information and an interesting historical viewpoint. </p> <!-- endregion --> <!-- #region Open Container Initiative (OCI) --> <h2 id="oci">Open Container Initiative (OCI)</h2> <div class='imgWrapper imgFlex inline fullsize' style=' '> <a href='https://opencontainers.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/oci_logo.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/oci_logo.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/oci_logo.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/oci_logo.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/oci_logo.png" type="image/png"> <source srcset="/blog/images/buildahPodman/oci_logo.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/oci_logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/oci_logo.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/oci_logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/oci_logo.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/oci_logo.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/oci_logo.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/oci_logo.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/oci_logo.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/oci_logo.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/oci_logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/oci_logo.png" style='width: 100%; ' /> </picture> </a> </div> <p> The latest evolution of Docker-compatible images, <a href='https://github.com/opencontainers/image-spec' target='_blank' rel="nofollow">OCI image format</a> (not to be confused with <a href='https://www.oracle.com/ca-en/cloud/' target='_blank' rel="nofollow">Oracle Cloud Infrastructure</a>), is compatible with: </p> <ul style="column-count: 2;"> <li><a href='https://aws.amazon.com/lambda/' target='_blank' rel="nofollow">AWS Lambda</a></li> <li><a href='https://azure.microsoft.com/en-us/services/functions/' target='_blank' rel="nofollow">Azure Functions</a></li> <li><a href='https://azure.microsoft.com/en-us/services/kubernetes-service/' target='_blank' rel="nofollow">Azure Kubernetes Service</a></li> <li><a href='https://buildah.io/' target='_blank' rel="nofollow">Buildah</a></li> <li><a href='https://buildpacks.io/' target='_blank' rel="nofollow">Cloud Native Buildpacks</a></li> <li><a href='https://circleci.com/' target='_blank' rel="nofollow">CircleCI</a></li> <li><a href='https://www.docker.com/' target='_blank' rel="nofollow">Docker</a></li> <li><a href='https://dokku.com/' target='_blank' rel="nofollow">Dokku</a></li> <li><a href='https://gitlab.com' target='_blank' rel="nofollow">GitLab</a></li> <li><a href='https://cloud.google.com/container-registry/docs/image-formats' target='_blank' rel="nofollow">Google Cloud</a></li> <li><a href='https://heroku.com' target='_blank' rel="nofollow">Heroku</a></li> <li><a href='https://containerjournal.com/topics/container-management/what-is-knative-and-what-can-it-do-for-you/' target='_blank' rel="nofollow">Knative</a></li> <li><a href='https://kubernetes.io/' target='_blank' rel="nofollow">Kubernetes</a></li> <li><a href='https://podman.io/' target='_blank' rel="nofollow"><code>podman</code></a></li> <li><a href='https://github.com/containers/skopeo' target='_blank' rel="nofollow"><code>skopeo</code></a></li> <li><a href='https://spring.io/guides/topicals/spring-boot-docker/' target='_blank' rel="nofollow">Spring Boot</a></li> <li><a href='https://cloud.google.com/tekton' target='_blank' rel="nofollow">Tekton</a></li> </ul> <p> Supported OCI formats include: </p> <ul style="column-count: 2;"> <li>Docker containers schema 1</li> <li>Docker containers schema 2</li> <li>Pods (groups of containers)</li> <li>Images</li> <li>Volumes</li> </ul> <!-- endregion --> <!-- #region Buildah, podman and skopeo --> <h2 id="three">Buildah, podman and skopeo</h2> <!-- #region implicit --> <p> This article discusses 3 related open source projects from RedHat / IBM that provide an alternative to Docker: Buildah, <code>podman</code> and <code>skopeo</code>. These 3 projects share a common source code base, and are daemonless tools for managing Open Container Initiative (OCI) images. </p> <p> Paraphrasing the reasons expressed in <a href='https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/' target='_blank' rel="nofollow">Podman and Buildah for Docker Users</a> for using <code>podman</code> instead of Docker, wherever <code>podman</code> is mentioned, read &ldquo;<code>podman</code>, Buildah and <code>skopeo</code>&rdquo;: </p> <p class="quoteCite" cite="From &ldquo;Podman and Buildah for Docker Users&rdquo;"> The Podman approach is simply to directly interact with the image registry, with the container and image storage, and with the Linux kernel through the <code>runC</code> container runtime process (not a daemon).<br><br> Running Podman as a normal user means that Podman will, by default, store images and containers in the user’s home directory. Podman uses a repository in the user’s home directory: <code>~/.local/share/containers</code> (instead of <code>/var/lib/docker</code>).<br><br> Despite the new locations for the local repositories, the images created by Docker and Podman are compatible with the OCI standard. Podman can push to and pull from popular container registries like Quay.io and Docker hub, as well as private registries. </p> <!-- endregion --> <!-- #region Buildah vs. podman --> <h2 id="buildah_vs_podman">Buildah vs. podman</h2> <p> <code>Podman</code> can build OCI containers interactively or in batch mode. You can either build using a <code>Dockerfile</code> using <code>podman build</code> (batch mode), or you can interactively run a container, make changes to the running image, and then <code>podman commit</code> those changes to a new image tag. </p> <p> Buildah was written before <code>podman</code>. Some of Buildah's source code for creating and managing container images was ported to <code>podman</code>. The <code>podman build</code> command is a subset of Buildah&rsquo;s functionality. </p> <p> <p> However, apparently the differences between the two programs are important: </p> <p class="quote"> Buildah builds OCI images. Confusingly, <code>podman build</code> can also be used to build Docker images also, but it’s incredibly slow and used up a lot of disk space by using the <code>vfs</code> storage driver by default. <code>buildah bud</code> (‘build using Dockerfile’) was much faster for me, and uses the overlay storage driver. <br><br> &nbsp; &ndash; From <a href='https://zwischenzugs.com/page/3/' target='_blank' rel="nofollow">Goodbye Docker: Purging is Such Sweet Sorrow</a> by Ian Miell. </p> <!-- endregion --> <!-- endregion --> <!-- #region podman --> <h2 id="podman">podman</h2> <!-- #region implicit --> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://podman.io' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/podman-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/podman-logo.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/podman-logo.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/podman-logo.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/podman-logo.png" type="image/png"> <source srcset="/blog/images/buildahPodman/podman-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/podman-logo.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/podman-logo.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/podman-logo.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/podman-logo.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/podman-logo.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/podman-logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/podman-logo.png" style='width: 100%; padding: 1em' /> </picture> </a> </div> <p> <code>Podman</code> supports developing, managing, and running OCI Containers on Linux systems, including WSL, without requiring <code>root</code> privilege. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>shell Installation on Ubuntu</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id337f2d5040b1'><button class='copyBtn' data-clipboard-target='#id337f2d5040b1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install buildah podman skopeo</pre> </div> <div class="pullQuote"> Podman commands are very nearly the same as Docker’s. </div> <p> Because <code>podman</code> is a drop-in replacement for <code>docker</code>, the following alias enables the <code>docker</code> command to invoke <code>podman</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.bash_aliases</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id224807a47d50'><button class='copyBtn' data-clipboard-target='#id224807a47d50' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>alias docker=podman</pre> </div> <p> As described in <a href='https://www.vultr.com/docs/how-to-install-and-use-podman-on-ubuntu-20-04' target='_blank' rel="nofollow">How to Install and Use Podman on Ubuntu 20.04</a>, I added <code>'registry.access.redhat.com'</code> to the list of <code>registries</code> in <code>/etc/containers/registries.conf</code>. I also added <a href='https://gallery.ecr.aws/' target='_blank' rel="nofollow"><code>gallery.ecr.aws</code></a> and <a href='https://cloud.google.com/container-registry/docs/pushing-and-pulling#add-registry' target='_blank' rel="nofollow"><code>gcr.io</code></a>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/containers/registries.conf</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3f9071d6f21a'><button class='copyBtn' data-clipboard-target='#id3f9071d6f21a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[registries.search] registries = ['docker.io', 'gallery.ecr.aws', 'gcr.io', 'quay.io', 'registry.access.redhat.com']</pre> </div> <!-- endregion --> <!-- #region podman Help--> <h3 id="podmanHelp"><span class="code">podman</span> Help</h3> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5d3cd7823cd2'><button class='copyBtn' data-clipboard-target='#id5d3cd7823cd2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman --help <span class='unselectable'>Manage pods, containers and images<br/> Usage: podman [flags] podman [command]<br/> Available Commands: attach Attach to a running container auto-update Auto update containers according to their auto-update policy build Build an image using instructions from Containerfiles commit Create new image based on the changed container container Manage containers cp Copy files/folders between a container and the local filesystem create Create but do not start a container diff Display the changes to the object&#39;s file system events Show podman events exec Run a process in a running container export Export container&#39;s filesystem contents as a tar archive generate Generate structured data based on containers and pods. healthcheck Manage health checks on containers help Help about any command history Show history of a specified image image Manage images images List images in local storage import Import a tarball to create a filesystem image info Display podman system information init Initialize one or more containers inspect Display the configuration of object denoted by ID kill Kill one or more running containers with a specific signal load Load an image from container archive login Login to a container registry logout Logout of a container registry logs Fetch the logs of one or more containers manifest Manipulate manifest lists and image indexes mount Mount a working container&#39;s root filesystem network Manage networks pause Pause all the processes in one or more containers play Play a pod and its containers from a structured file. pod Manage pods port List port mappings or a specific mapping for the container ps List containers pull Pull an image from a registry push Push an image to a specified destination restart Restart one or more containers rm Remove one or more containers rmi Removes one or more images from local storage run Run a command in a new container save Save image to an archive search Search registry for image start Start one or more containers stats Display a live stream of container resource usage statistics stop Stop one or more containers system Manage podman tag Add an additional name to a local image top Display the running processes of a container unmount Unmounts working container&#39;s root filesystem unpause Unpause the processes in one or more containers unshare Run a command in a modified user namespace untag Remove a name from a local image version Display the Podman Version Information volume Manage volumes wait Block on one or more containers<br/> Flags: --cgroup-manager string Cgroup manager to use (&quot;cgroupfs&quot;|&quot;systemd&quot;) (default &quot;cgroupfs&quot;) --cni-config-dir string Path of the configuration directory for CNI networks --conmon string Path of the conmon binary -c, --connection string Connection to use for remote Podman service --events-backend string Events backend to use (&quot;file&quot;|&quot;journald&quot;|&quot;none&quot;) (default &quot;file&quot;) --help Help for podman --hooks-dir strings Set the OCI hooks directory path (may be set multiple times) (default [/usr/share/containers/oci/hooks.d]) --identity string path to SSH identity file, (CONTAINER_SSHKEY) --log-level string Log messages above specified level (debug, info, warn, error, fatal, panic) (default &quot;error&quot;) --namespace string Set the libpod namespace, used to create separate views of the containers and pods on the system --network-cmd-path string Path to the command for configuring the network -r, --remote Access remote Podman service (default false) --root string Path to the root directory in which data, including images, is stored --runroot string Path to the &#39;run directory&#39; where all state information is stored --runtime string Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc --storage-driver string Select which storage driver is used to manage storage of images and containers (default is overlay) --storage-opt stringArray Used to pass an option to the storage driver --syslog Output logging information to syslog as well as the console (default false) --tmpdir string Path to the tmp directory for libpod state content.<br> Note: use the environment variable &#39;TMPDIR&#39; to change the temporary storage location for container images, &#39;/var/tmp&#39;.<br> --url string URL to access Podman service (CONTAINER_HOST) (default &quot;unix:/home/mslinn/.docker/run/podman/podman.sock&quot;) -v, --version Version of Podman<br/> Use &quot;podman [command] --help&quot; for more information about a command. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region podman info --> <h3 id="padman_info"><span class="code">podman info</span></h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id63fdba7e8f1d'><button class='copyBtn' data-clipboard-target='#id63fdba7e8f1d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman info <span class='unselectable'>host: arch: amd64 buildahVersion: 1.15.2 cgroupVersion: v1 conmon: package: &#39;conmon: /usr/libexec/podman/conmon&#39; path: /usr/libexec/podman/conmon version: &#39;conmon version 2.0.20, commit: unknown&#39; cpus: 8 distribution: distribution: ubuntu version: &quot;20.10&quot; eventLogger: file hostname: Bear idMappings: gidmap: - container_id: 0 host_id: 1000 size: 1 - container_id: 1 host_id: 100000 size: 65536 uidmap: - container_id: 0 host_id: 1000 size: 1 - container_id: 1 host_id: 100000 size: 65536 kernel: 5.4.72-microsoft-standard-WSL2 linkmode: dynamic memFree: 897724416 memTotal: 6231638016 ociRuntime: name: runc package: &#39;containerd.io: /usr/bin/runc&#39; path: /usr/bin/runc version: |- runc version 1.0.0-rc93 commit: 12644e614e25b05da6fd08a38ffa0cfe1903fdec spec: 1.0.2-dev go: go1.13.15 libseccomp: 2.5.1 os: linux remoteSocket: path: /home/mslinn/.docker/run/podman/podman.sock rootless: true slirp4netns: executable: /bin/slirp4netns package: Unknown version: |- slirp4netns version 1.0.1 commit: 6a7b16babc95b6a3056b33fb45b74a6f62262dd4 libslirp: 4.3.1 swapFree: 0 swapTotal: 0 uptime: 306h 23m 6.37s (Approximately 12.75 days) registries: search: - quay.io - docker.io - gallery.ecr.aws - registry.access.redhat.com store: configFile: /home/mslinn/.config/containers/storage.conf containerStore: number: 8 paused: 0 running: 0 stopped: 8 graphDriverName: overlay graphOptions: overlay.mount_program: Executable: /bin/fuse-overlayfs Package: Unknown Version: |- fusermount3 version: 3.9.3 fuse-overlayfs: version 1.0.0 FUSE library version 3.9.3 using FUSE kernel interface version 7.31 graphRoot: /home/mslinn/.local/share/containers/storage graphStatus: Backing Filesystem: extfs Native Overlay Diff: &quot;false&quot; Supports d_type: &quot;true&quot; Using metacopy: &quot;false&quot; imageStore: number: 4 runRoot: /home/mslinn/.docker/run/containers volumePath: /home/mslinn/.local/share/containers/storage/volumes version: APIVersion: 1 Built: 0 BuiltTime: Wed Dec 31 19:00:00 1969 GitCommit: &quot;&quot; GoVersion: go1.14.7 OsArch: linux/amd64 Version: 2.0.6 </span></pre> </div> <!-- endregion --> <!-- #region podman container help --> <h3 id="padman_container_help"><span class="code">podman container</span> Help</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5c5ac54b900a'><button class='copyBtn' data-clipboard-target='#id5c5ac54b900a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man podman-container <span class='unselectable'>podman-container(1) General Commands Manual podman-container(1)<br/> NAME podman-container - Manage containers<br/> SYNOPSIS podman container subcommand<br/> DESCRIPTION The container command allows you to manage containers<br/> COMMANDS &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9474;Command &#9474; Man Page &#9474; Description &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;attach &#9474; podman-attach(1) &#9474; Attach to a running container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;checkpoint &#9474; podman-container-checkpoint(1) &#9474; Checkpoints one or more running &#9474; &#9474; &#9474; &#9474; containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;cleanup &#9474; podman-container-cleanup(1) &#9474; Cleanup the container&#39;s network &#9474; &#9474; &#9474; &#9474; and mountpoints. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;commit &#9474; podman-commit(1) &#9474; Create new image based on the &#9474; &#9474; &#9474; &#9474; changed container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;cp &#9474; podman-cp(1) &#9474; Copy files/folders between a &#9474; &#9474; &#9474; &#9474; container and the local &#9474; &#9474; &#9474; &#9474; filesystem. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;create &#9474; podman-create(1) &#9474; Create a new container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;diff &#9474; podman-diff(1) &#9474; Inspect changes on a container or &#9474; &#9474; &#9474; &#9474; image&#39;s filesystem. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;exec &#9474; podman-exec(1) &#9474; Execute a command in a running &#9474; &#9474; &#9474; &#9474; container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;exists &#9474; podman-container-exists(1) &#9474; Check if a container exists in &#9474; &#9474; &#9474; &#9474; local storage &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;export &#9474; podman-export(1) &#9474; Export a container&#39;s filesystem &#9474; &#9474; &#9474; &#9474; contents as a tar archive. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;init &#9474; podman-init(1) &#9474; Initialize a container &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;inspect &#9474; podman-inspect(1) &#9474; Display a container or image&#39;s &#9474; &#9474; &#9474; &#9474; configuration. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;kill &#9474; podman-kill(1) &#9474; Kill the main process in one or &#9474; &#9474; &#9474; &#9474; more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;list &#9474; podman-ps(1) &#9474; List the containers on the &#9474; &#9474; &#9474; &#9474; system.(alias ls) &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;logs &#9474; podman-logs(1) &#9474; Display the logs of a container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;mount &#9474; podman-mount(1) &#9474; Mount a working container&#39;s root &#9474; &#9474; &#9474; &#9474; filesystem. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;pause &#9474; podman-pause(1) &#9474; Pause one or more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;port &#9474; podman-port(1) &#9474; List port mappings for the &#9474; &#9474; &#9474; &#9474; container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;prune &#9474; podman-container-prune(1) &#9474; Remove all stopped containers &#9474; &#9474; &#9474; &#9474; from local storage. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;restart &#9474; podman-restart(1) &#9474; Restart one or more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;restore &#9474; podman-container-restore(1) &#9474; Restores one or more containers &#9474; &#9474; &#9474; &#9474; from a checkpoint. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;rm &#9474; podman-rm(1) &#9474; Remove one or more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;run &#9474; podman-run(1) &#9474; Run a command in a container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;runlabel &#9474; podman-container-runlabel(1) &#9474; Executes a command as described &#9474; &#9474; &#9474; &#9474; by a container image label. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;start &#9474; podman-start(1) &#9474; Starts one or more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;stats &#9474; podman-stats(1) &#9474; Display a live stream of one or &#9474; &#9474; &#9474; &#9474; more container&#39;s resource usage &#9474; &#9474; &#9474; &#9474; statistics. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;stop &#9474; podman-stop(1) &#9474; Stop one or more running &#9474; &#9474; &#9474; &#9474; containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;top &#9474; podman-top(1) &#9474; Display the running processes of &#9474; &#9474; &#9474; &#9474; a container. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;unmount &#9474; podman-unmount(1) &#9474; Unmount a working container&#39;s &#9474; &#9474; &#9474; &#9474; root filesystem.(Alias unmount) &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;unpause &#9474; podman-unpause(1) &#9474; Unpause one or more containers. &#9474; &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; &#9474;wait &#9474; podman-wait(1) &#9474; Wait on one or more containers to &#9474; &#9474; &#9474; &#9474; stop and print their exit codes. &#9474; &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;<br/> SEE ALSO podman, podman-exec, podman-run<br/> podman-container(1) </span></pre> </div> <!-- endregion --> <!-- #region Podman Run Help --> <h3 id="podman_help">Podman Run Help</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id80e455e13e75'><button class='copyBtn' data-clipboard-target='#id80e455e13e75' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container run --help <span class='unselectable'>Run a command in a new container<br/> Description: Runs a command in a new container from the given image<br/> Usage: podman container run [flags] IMAGE [COMMAND [ARG...]]<br/> Examples: podman container run imageID ls -alF /etc podman container run --network=host imageID dnf -y install java podman container run --volume /var/hostdir:/var/ctrdir -i -t fedora /bin/bash<br/> Flags: --add-host strings Add a custom host-to-IP mapping (host:ip) (default []) --annotation strings Add annotations to container (key:value) -a, --attach strings Attach to STDIN, STDOUT or STDERR --authfile string Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override --blkio-weight string Block IO weight (relative weight) accepts a weight value between 10 and 1000. --blkio-weight-device DEVICE_NAME:WEIGHT Block IO weight (relative device weight, format: DEVICE_NAME:WEIGHT) --cap-add strings Add capabilities to the container --cap-drop strings Drop capabilities from the container --cgroup-parent string Optional parent cgroup for the container --cgroupns string cgroup namespace to use --cgroups string control container cgroup configuration (&quot;enabled&quot;|&quot;disabled&quot;|&quot;no-conmon&quot;) (default &quot;enabled&quot;) --cidfile string Write the container ID to the file --conmon-pidfile string Path to the file that will receive the PID of conmon --cpu-period uint Limit the CPU CFS (Completely Fair Scheduler) period --cpu-quota int Limit the CPU CFS (Completely Fair Scheduler) quota --cpu-rt-period uint Limit the CPU real-time period in microseconds --cpu-rt-runtime int Limit the CPU real-time runtime in microseconds --cpu-shares uint CPU shares (relative weight) --cpus float Number of CPUs. The default is 0.000 which means no limit --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1) --cpuset-mems string Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. -d, --detach Run container in background and print container ID --detach-keys [a-Z] Override the key sequence for detaching a container. Format is a single character [a-Z] or a comma separated sequence of `ctrl-&lt;value&gt;`, where `&lt;value&gt;` is one of: `a-cf`, `@`, `^`, `[`, `\`, `]`, `^` or `_` (default &quot;ctrl-p,ctrl-q&quot;) --device strings Add a host device to the container --device-cgroup-rule strings Add a rule to the cgroup allowed devices list --device-read-bps strings Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb) --device-read-iops strings Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000) --device-write-bps strings Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) --device-write-iops strings Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000) --disable-content-trust This is a Docker specific option and is a NOOP --dns strings Set custom DNS servers --dns-opt strings Set custom DNS options --dns-search strings Set custom DNS search domains --entrypoint string Overwrite the default ENTRYPOINT of the image -e, --env stringArray Set environment variables in container (default [PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,TERM=xterm]) --env-file strings Read in a file of environment variables --env-host Use all current host environment variables in container --expose strings Expose a port or a range of ports --gidmap strings GID map to use for the user namespace --group-add strings Add additional groups to join --health-cmd string set a healthcheck command for the container (&#39;none&#39; disables the existing healthcheck) --health-interval string set an interval for the healthchecks (a value of disable results in no automatic timer setup) (default &quot;30s&quot;) --health-retries uint the number of retries allowed before a healthcheck is considered to be unhealthy (default 3) --health-start-period string the initialization time needed for a container to bootstrap (default &quot;0s&quot;) --health-timeout string the maximum time allowed to complete the healthcheck before an interval is considered failed (default &quot;30s&quot;) -h, --hostname string Set container hostname --http-proxy Set proxy environment variables in the container based on the host proxy vars (default true) --image-volume string Tells podman how to handle the builtin image volumes (&quot;bind&quot;|&quot;tmpfs&quot;|&quot;ignore&quot;) (default &quot;bind&quot;) --init Run an init binary inside the container that forwards signals and reaps processes --init-path string Path to the container-init binary -i, --interactive Keep STDIN open even if not attached --ip string Specify a static IPv4 address for the container --ipc string IPC namespace to use --kernel-memory &lt;number&gt;[&lt;unit&gt;] Kernel memory limit (format: &lt;number&gt;[&lt;unit&gt;], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes)) -l, --label stringArray Set metadata on container --label-file strings Read in a line delimited file of labels --log-driver string Logging driver for the container --log-opt strings Logging driver options --mac-address string Container MAC address (e.g. 92:d0:c6:0a:29:33) -m, --memory &lt;number&gt;[&lt;unit&gt;] Memory limit (format: &lt;number&gt;[&lt;unit&gt;], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes)) --memory-reservation &lt;number&gt;[&lt;unit&gt;] Memory soft limit (format: &lt;number&gt;[&lt;unit&gt;], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes)) --memory-swap string Swap limit equal to memory plus swap: &#39;-1&#39; to enable unlimited swap --memory-swappiness int Tune container memory swappiness (0 to 100, or -1 for system default) (default -1) --mount stringArray Attach a filesystem mount to the container --name string Assign a name to the container --network string Connect a container to a network (default &quot;slirp4netns&quot;) --no-healthcheck Disable healthchecks on container --no-hosts Do not create /etc/hosts within the container, instead use the version from the image --oom-kill-disable Disable OOM Killer --oom-score-adj int Tune the host&#39;s OOM preferences (-1000 to 1000) --pid string PID namespace to use --pids-limit int Tune container pids limit (set 0 for unlimited, -1 for server defaults) --pod string Run container in an existing pod --pod-id-file string Read the pod ID from the file --privileged Give extended privileges to container -p, --publish strings Publish a container&#39;s port, or a range of ports, to the host (default []) -P, --publish-all Publish all exposed ports to random ports on the host interface --pull string Pull image before creating (&quot;always&quot;|&quot;missing&quot;|&quot;never&quot;) (default &quot;missing&quot;) -q, --quiet Suppress output information when pulling images --read-only Make containers root filesystem read-only --read-only-tmpfs When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp (default true) --replace If a container with the same name exists, replace it --restart string Restart policy to apply when a container exits (&quot;always&quot;|&quot;no&quot;|&quot;on-failure&quot;) --rm Remove container (and pod if created) after exit --rmi Remove container image unless used by other containers --rootfs The first argument is not an image but the rootfs to the exploded container --seccomp-policy string Policy for selecting a seccomp profile (experimental) (default &quot;default&quot;) --security-opt stringArray Security Options --shm-size &lt;number&gt;[&lt;unit&gt;] Size of /dev/shm (format: &lt;number&gt;[&lt;unit&gt;], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes)) (default &quot;65536k&quot;) --sig-proxy Proxy received signals to the process (default true) --stop-signal string Signal to stop a container. Default is SIGTERM --stop-timeout uint Timeout (in seconds) to stop a container. Default is 10 (default 10) --subgidname string Name of range listed in /etc/subgid for use in user namespace --subuidname string Name of range listed in /etc/subuid for use in user namespace --sysctl strings Sysctl options --systemd string Run container in systemd mode (&quot;true&quot;|&quot;false&quot;|&quot;always&quot;) (default &quot;true&quot;) --tmpfs tmpfs Mount a temporary filesystem (tmpfs) into a container -t, --tty Allocate a pseudo-TTY for container --uidmap strings UID map to use for the user namespace --ulimit strings Ulimit options -u, --user string Username or UID (format: &lt;name|uid&gt;[:&lt;group|gid&gt;]) --userns string User namespace to use --uts string UTS namespace to use -v, --volume stringArray Bind mount a volume into the container --volumes-from strings Mount volumes from the specified container(s) -w, --workdir string Working directory inside the container </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region podman run --> <h2 id="podman_run">podman run</h2> <p> From <a href='https://chariotsolutions.com/blog/article/building-and-deploying-lambdas-from-a-docker-container/' target='_blank' rel="nofollow">Building and Deploying Lambdas from a Docker Container</a> by Keith Gregory: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9b116ffc876c'><button class='copyBtn' data-clipboard-target='#id9b116ffc876c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman run \ -it \ --entrypoint /bin/bash \ --rm \ -v /tmp:/mnt \ amazon/aws-lambda-python:3.8 <span class='unselectable'>Trying to pull quay.io/amazon/aws-lambda-python:3.8... Requesting bear token: invalid status code from registry 405 (Method Not Allowed) Trying to pull docker.io/amazon/aws-lambda-python:3.8... Getting image source signatures Copying blob df513d38f4d9 skipped: already exists Copying blob 2e2bb77ae2dc skipped: already exists Copying blob 031c6369fb2b skipped: already exists Copying blob 03ac043af787 skipped: already exists Copying blob 842c9dce67e8 skipped: already exists Copying blob 1de4740de1c2 [--------------------------------------] 0.0b / 0.0b Copying config e12ea62c55 done Writing manifest to image destination Storing signatures bash-4.2# </span>pwd <span class='unselectable'>/var/task </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Cleaning Up a Container --> <h2 id="cleanup">Cleaning Up a Container</h2> <p> <code>podman container cleanup</code> is a good command to know about. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5b5c7a569598'><button class='copyBtn' data-clipboard-target='#id5b5c7a569598' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container cleanup --help <span class='unselectable'>Cleanup network and mountpoints of one or more containers Description: podman container cleanup Cleans up mount points and network stacks on one or more containers from the host. The container name or ID can be used. This command is used internally when running containers, but can also be used if container cleanup has failed when a container exits. Usage: podman container cleanup [options] CONTAINER [CONTAINER...] Examples: podman container cleanup --latest podman container cleanup ctrID1 ctrID2 ctrID3 podman container cleanup --all Options: -a, --all Cleans up all containers --exec string Clean up the given exec session instead of the container -l, --latest Act on the latest container podman is aware of Not supported with the "--remote" flag --rm After cleanup, remove the container entirely --rmi After cleanup, remove the image entirely </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Buildah --> <h2 id="builah">Buildah</h2> <!-- #region implicit --> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://buildah.io/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/buildah-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/buildah-logo.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/buildah-logo.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/buildah-logo.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/buildah-logo.png" type="image/png"> <source srcset="/blog/images/buildahPodman/buildah-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/buildah-logo.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/buildah-logo.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/buildah-logo.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/buildah-logo.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/buildah-logo.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/buildah-logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/buildah-logo.png" style='width: 100%; padding: 1em' /> </picture> </a> </div> <p> <a href='https://buildah.io/' target='_blank' rel="nofollow">Buildah</a> is a drop-in replacement for using <code>docker build</code> and a <code>Dockerfile</code>. </p> <div class="quote"> Where Buildah really shines is in its native commands, which you can use to interact with container builds. Rather than using <code>build-using-dockerfile/bud</code> for each build, Buildah has commands to actually interact with the temporary container created during the build process. (Docker uses temporary, or intermediate containers, too, but you don’t really interact with them while the image is being built.) <br><br> Unlike <code>docker build</code>, Buildah doesn’t commit changes to a layer automatically for every instruction in the <code>Dockerfile</code> &ndash; it builds everything from top to bottom, every time. On the positive side, this means non-cached builds (for example, those you would do with automation or build pipelines) end up being somewhat faster than their Docker build counterparts, especially if there are many instructions. <br><br> &nbsp; &ndash; From <a href='https://opensource.com/article/18/6/getting-started-buildah' target='_blank' rel="nofollow">Getting started with Buildah.</a>, published by <code>opensource.com</code> </div> <p> Some key Buildah subcommands: </p> <dl> <dt class="code">buildah bud</dt> <dd>Buildah’s <code>build-using-dockerfile</code>, or <code>bud</code> argument makes it behave just like <code>docker build</code> does.</dd> <dt class="code">buildah from</dt> <dd>Build up a container root filesystem from an image or from scratch.</dd> <dt class="code">buildah config</dt> <dd>Adjust defaults in the image's configuration blob.</dd> <dt class="code">buildah run</dt> <dd> <code>buildah run</code> is for running commands that build a container image. This is similar to <code>RUN</code> in a <code>Dockerfile</code>, and unlike <code>docker run</code>. </dd> <dt class="code">buildah commit</dt> <dd>Commit changes to the container to a new image.</dd> <dt class="code">buildah push</dt> <dd>Push images to registries (such a Quay) or a local <code>dockerd</code> instance.</dd> <dt class="code"></dt> <dd></dd> <dt class="code"></dt> <dd></dd> <dt class="code"></dt> <dd></dd> </dl> <!-- endregion --> <!-- #region Buildah Help --> <h3 id="buildahHelp">Buildah Help</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idac092766cc9d'><button class='copyBtn' data-clipboard-target='#idac092766cc9d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah -h <span class='unselectable'>A tool that facilitates building OCI images<br/> Usage: buildah [flags] buildah [command]<br/> Available Commands: add Add content to the container build-using-dockerfile Build an image using instructions in a Dockerfile commit Create an image from a working container config Update image configuration settings containers List working containers and their base images copy Copy content into the container from Create a working container based on an image help Help about any command images List images in local storage info Display Buildah system information inspect Inspect the configuration of a container or image login Login to a container registry logout Logout of a container registry manifest Manipulate manifest lists and image indexes mount Mount a working container&#39;s root filesystem pull Pull an image from the specified location push Push an image to a specified destination rename Rename a container rm Remove one or more working containers rmi Remove one or more images from local storage run Run a command inside of the container tag Add an additional name to a local image umount Unmount the root file system of the specified working containers unshare Run a command in a modified user namespace version Display the Buildah version information<br/> Flags: -h, --help help for buildah --log-level string The log level to be used. Either &quot;debug&quot;, &quot;info&quot;, &quot;warn&quot; or &quot;error&quot;. (default &quot;error&quot;) --registries-conf string path to registries.conf file (not usually used) --registries-conf-dir string path to registries.conf.d directory (not usually used) --root string storage root dir (default &quot;/var/lib/containers/storage&quot;) --runroot string storage state dir (default &quot;/var/run/containers/storage&quot;) --storage-driver string storage-driver --storage-opt strings storage driver option --userns-gid-map ctrID:hostID:length default ctrID:hostID:length GID mapping to use --userns-uid-map ctrID:hostID:length default ctrID:hostID:length UID mapping to use -v, --version version for buildah<br/> Use &quot;buildah [command] --help&quot; for more information about a command. </span></pre> </div> <!-- endregion --> <!-- #region Buildah / Dockerfile Compatibility --> <h3 id="buildahUse">Buildah / <span class="code">Dockerfile</span> Compatibility</h3> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/whales.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/whales.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/whales.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/whales.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/whales.png" type="image/png"> <source srcset="/blog/images/buildahPodman/whales.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/whales.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/whales.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/whales.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/whales.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/whales.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/whales.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/whales.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/whales.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/whales.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/whales.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/whales.png" style='width: 100%; ' /> </picture> </div> <p> <a href='https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/building_running_and_managing_containers/building-container-images-with-buildah_porting-containers-to-systemd-using-podman' target='_blank' rel="nofollow">Buildah</a> can create an image from a Dockerfile by typing: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0257adb2bb12'><button class='copyBtn' data-clipboard-target='#id0257adb2bb12' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello .</pre> </div> <p> &hellip;instead of: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id078033501f61'><button class='copyBtn' data-clipboard-target='#id078033501f61' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker build -t hello .</pre> </div> <p> Buildah can create an image called <code>hello</code> from the <code>Dockerfile</code> and the Python app by typing: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida6cf5ad29b48'><button class='copyBtn' data-clipboard-target='#ida6cf5ad29b48' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello . <span class='unselectable'>STEP 1: FROM public.ecr.aws/lambda/python:3.8 Getting image source signatures Copying blob 1de4740de1c2 done Copying blob 2e2bb77ae2dc done Copying blob df513d38f4d9 done Copying blob 03ac043af787 done Copying blob 031c6369fb2b done Copying blob 842c9dce67e8 done Copying config e12ea62c55 done Writing manifest to image destination Storing signatures STEP 2: COPY app.py ./ STEP 3: CMD [&quot;app.handler&quot;] STEP 4: COMMIT hello Getting image source signatures Copying blob 109f575f8e6a skipped: already exists Copying blob ff64b4f854ad skipped: already exists Copying blob dd66ad8702f4 skipped: already exists Copying blob d6fa53d6caa6 skipped: already exists Copying blob 80166c3283e5 skipped: already exists Copying blob 61f74564c3aa skipped: already exists Copying blob d95ebdc79761 done Copying config 40ef32b39c done Writing manifest to image destination Storing signatures --&gt; 40ef32b39cf 40ef32b39cf4ffd3d2e4e3426bec4a5ea168524f7f3fcfe863a378abd9794270 </span></pre> </div> <!-- endregion --> <p> Once the build is complete, the new image can be displayed with the <code>buildah images</code> command: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2875f50f03cc'><button class='copyBtn' data-clipboard-target='#id2875f50f03cc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah images <span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE localhost/hello latest 40ef32b39cf4 56 seconds ago 622 MB </span></pre> </div> <p> The new image, tagged <code>hello:latest</code>, can be pushed to a remote image registry. This is easily accomplished with the <code>buildah push</code> command. </p> <!-- endregion --> <!-- #region buildah push Help --> <h3 id="buildah_push"><span class="code">buildah push</span> Help</h3> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbe4e258daff3'><button class='copyBtn' data-clipboard-target='#idbe4e258daff3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man buildah-push <span class='unselectable'>buildah-push(1) General Commands Manual buildah-push(1)<br/> NAME buildah-push - Push an image from local storage to elsewhere.<br/> SYNOPSIS buildah push [options] image [destination]<br/> DESCRIPTION Pushes an image from local storage to a specified destination, decompressing and recompessing layers as needed.<br/> imageID Image stored in local container/storage<br/> DESTINATION The DESTINATION is a location to store container images. If omitted, the source image parameter will be reused as destination.<br/> The Image &quot;DESTINATION&quot; uses a &quot;transport&quot;:&quot;details&quot; format. Multiple transports are supported:<br/> dir:path An existing local directory path storing the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.<br/> docker://docker-reference An image in a registry implementing the &quot;Docker Registry HTTP API V2&quot;. By default, uses the authorization state in $XDG\_RUNTIME\_DIR/containers/auth.json, which is set using (buildah login). If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using (docker login). If docker-reference does not include a registry name, the image will be pushed to a registry running on local&#8208; host.<br/> docker-archive:path[:docker-reference] An image is stored in the docker save formatted file. docker-reference is only used when creating such a file, and it must not contain a digest.<br/> docker-daemon:docker-reference An image _dockerreference stored in the docker daemon internal storage. If _dockerreference does not begin with a valid registry name (a domain name containing &quot;.&quot; or the reserved name &quot;localhost&quot;) then the default registry name &quot;docker.io&quot; will be prepended. _dockerreference must contain either a tag or a digest. Alternatively, when reading images, the format can also be docker-daemon:algo:digest (an image ID).<br/> oci:path:tag An image tag in a directory compliant with &quot;Open Container Image Layout Specification&quot; at path.<br/> oci-archive:path:tag An image tag in a tar archive compliant with &quot;Open Container Image Layout Specification&quot; at path.<br/> If the transport part of DESTINATION is omitted, &quot;docker://&quot; is assumed.<br/> OPTIONS --authfile path<br/> Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json, which is set using buildah lo&#8208; gin. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using docker login.<br/> --cert-dir path<br/> Use certificates at path (*.crt, *.cert, *.key) to connect to the registry. Default certificates directory is /etc/containers/certs.d.<br/> --creds creds<br/> The [username[:password]] to use to authenticate with the registry if required. If one or both values are not sup&#8208; plied, a command line prompt will appear and the value can be entered. The password is entered without echo.<br/> --digestfile Digestfile<br/> After copying the image, write the digest of the resulting image to the file.<br/> --disable-compression, -D<br/> Don&#39;t compress copies of filesystem layers which will be pushed.<br/> --encryption-key key<br/> The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@exam&#8208; ple.com or pkcs7:/path/to/x509-file.<br/> --format, -f<br/> Manifest Type (oci, v2s1, or v2s2) to use when saving image to directory using the &#39;dir:&#39; transport (default is manifest type of source)<br/> --quiet, -q<br/> When writing the output image, suppress progress output.<br/> --remove-signatures<br/> Don&#39;t copy signatures when pushing images.<br/> --sign-by fingerprint<br/> Sign the pushed image using the GPG key that matches the specified fingerprint.<br/> --tls-verify bool-value<br/> Require HTTPS and verify certificates when talking to container registries (defaults to true)<br/> EXAMPLE This example pushes the image specified by the imageID to a local directory in docker format.<br/> # buildah push imageID dir:/path/to/image<br/> This example pushes the image specified by the imageID to a local directory in oci format.<br/> # buildah push imageID oci:/path/to/layout:image:tag<br/> This example pushes the image specified by the imageID to a tar archive in oci format.<br/> # buildah push imageID oci-archive:/path/to/archive:image:tag<br/> This example pushes the image specified by the imageID to a container registry named registry.example.com.<br/> # buildah push imageID docker://registry.example.com/repository:tag<br/> This example pushes the image specified by the imageID to a container registry named registry.example.com and saves the digest in the specified digestfile.<br/> # buildah push --digestfile=/tmp/mydigest imageID docker://registry.example.com/repository:tag<br/> This example works like docker push, assuming registry.example.com/my_image is a local image.<br/> # buildah push registry.example.com/my_image<br/> This example pushes the image specified by the imageID to a private container registry named registry.example.com with authentication from /tmp/auths/myauths.json.<br/> # buildah push --authfile /tmp/auths/myauths.json imageID docker://registry.example.com/repository:tag<br/> This example pushes the image specified by the imageID and puts into the local docker container store.<br/> # buildah push imageID docker-daemon:image:tag<br/> This example pushes the image specified by the imageID and puts it into the registry on the localhost while turning off tls verification. # buildah push --tls-verify=false imageID docker://localhost:5000/my-imageID<br/> This example pushes the image specified by the imageID and puts it into the registry on the localhost using creden&#8208; tials and certificates for authentication. # buildah push --cert-dir /auth --tls-verify=true --creds=username:password imageID docker://local&#8208; host:5000/my-imageID<br/> ENVIRONMENT BUILD_REGISTRY_SOURCES<br/> BUILD_REGISTRY_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys insecureRegistries, blockedRegistries, and allowedRegistries.<br/> When pushing an image to a registry, if the portion of the destination image name that corresponds to a registry is compared to the items in the blockedRegistries list, and if it matches any of them, the push attempt is denied. If there are registries in the allowedRegistries list, and the portion of the name that corresponds to the registry is not in the list, the push attempt is denied.<br/> TMPDIR The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to &#39;/var/tmp&#39;.<br/> FILES registries.conf (/etc/containers/registries.conf)<br/> registries.conf is the configuration file which specifies which container registries should be consulted when com&#8208; pleting image names which do not include a registry or domain portion.<br/> policy.json (/etc/containers/policy.json)<br/> Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images.<br/> SEE ALSO buildah(1), buildah-login(1), containers-policy.json(5), docker-login(1), containers-registries.conf(5)<br/> buildah June 2017 buildah-push(1) </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id391978efbaa6'><button class='copyBtn' data-clipboard-target='#id391978efbaa6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah run \ --entrypoint /var/lang/bin/pip \ --rm \ --user "$(id -u):$(id -g)" \ -v "$(pwd):/mnt" \ amazon/aws-lambda-python:3.8 \ install --target /mnt/build --upgrade psycopg2-binary</pre> </div> <!-- endregion --> <div class='imgWrapper imgFlex right' style=' '> <a href='https://www.scholastic.ca/books/view/how-to-speak-dolphin' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/howToSpeakDolphin.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.png" type="image/png"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/howToSpeakDolphin.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/howToSpeakDolphin.png" style='width: 100%; width: 25%; height: auto;' /> </picture> </a> </div> <h2 id="howto">How To</h2> <p> The following was inspired by <a href='https://github.com/groda/big_data/blob/master/docker_for_beginners.md#recap-images-and-containers' target='_blank' rel="nofollow">Recap: images and containers</a> from <b>Docker for beginners</b>. The equivalent commands for Docker alternatives are shown. </p> <!-- #region --> <!-- #region Check software version --> <h3 class="clear" id="ver">Check software version</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf4f40998116d'><button class='copyBtn' data-clipboard-target='#idf4f40998116d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>docker -v<br><span class='unselectable'>Docker version 20.10.2, build 20.10.2-0ubuntu1~20.10.1 </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c7b41f89285'><button class='copyBtn' data-clipboard-target='#id3c7b41f89285' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah -v<br><span class='unselectable'>buildah version 1.15.2 (image-spec 1.0.1, runtime-spec 1.0.2-dev) </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideac2c38072d5'><button class='copyBtn' data-clipboard-target='#ideac2c38072d5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman -v<br><span class='unselectable'>podman version 2.0.6 </span></pre> </div> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/aws_linux.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/aws_linux.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/aws_linux.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/aws_linux.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/aws_linux.png" type="image/png"> <source srcset="/blog/images/buildahPodman/aws_linux.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/aws_linux.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/aws_linux.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/aws_linux.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/aws_linux.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/aws_linux.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/aws_linux.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/aws_linux.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/aws_linux.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/aws_linux.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/aws_linux.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/buildahPodman/aws_linux.png" style='width: 100%; width: 25%; height: auto; margin-bottom: 1em;' /> </picture> </div> <!-- endregion --> <!-- #region Download the Amazon Linux 2 image --> <h3 id="dlimg">Download the Amazon Linux 2 image</h3> <p> AWS Lambda functions run under Amazon Linux. </p> <p> Each of these 3 commands does a very similar task, downloading a specific image. Docker uses different subdirectories for images than Buildah and <code>podman</code> do. </p> <!-- #region --> <div class="clear"> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida9a2cc3de8fb'><button class='copyBtn' data-clipboard-target='#ida9a2cc3de8fb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker pull amazonlinux <span class='unselectable'>Using default tag: latest latest: Pulling from library/amazonlinux 3c2c91c7c431: Pull complete Digest: sha256:06b9e2433e4e563e1d75bc8c71d32b76dc49a2841e9253746eefc8ca40b80b5e Status: Downloaded newer image for amazonlinux:latest docker.io/library/amazonlinux:latest </span></pre> </div> <!-- endregion --> </div> <p> Buildah works without complaint. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id969bc653cfc0'><button class='copyBtn' data-clipboard-target='#id969bc653cfc0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah pull amazonlinux <span class='unselectable'>53ef897d731f9a5673c083d0e86d7911f85d6e63bb2be2346b17bdbacdc58637 </span></pre> </div> <p> <code>podman</code> seems to hiccup and then complete successfully. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id431e63b8cef2'><button class='copyBtn' data-clipboard-target='#id431e63b8cef2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman pull amazonlinux <span class='unselectable'>Trying to pull quay.io/amazonlinux... error parsing HTTP 404 response body: invalid character &#39;&lt;&#39; looking for beginning of value: &quot;&lt;!DOCTYPE HTML PUBLIC \&quot;-//W3C//DTD HTML 3.2 Final//EN\&quot;&gt;\n&lt;title&gt;404 Not Found&lt;/title&gt;\n&lt;h1&gt;Not Found&lt;/h1&gt;\n&lt;p&gt;The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.&lt;/p&gt;\n&quot; Trying to pull docker.io/library/amazonlinux... Getting image source signatures Copying blob 3c2c91c7c431 [--------------------------------------] 0.0b / 0.0b Copying config 53ef897d73 done Writing manifest to image destination Storing signatures 53ef897d731f9a5673c083d0e86d7911f85d6e63bb2be2346b17bdbacdc58637 </span></pre> </div> <!-- endregion --> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/bash-logo.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/bash-logo.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/bash-logo.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/bash-logo.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/bash-logo.png" type="image/png"> <source srcset="/blog/images/buildahPodman/bash-logo.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/bash-logo.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/bash-logo.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/bash-logo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/bash-logo.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/bash-logo.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/bash-logo.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/bash-logo.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/bash-logo.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/bash-logo.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/bash-logo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/bash-logo.png" style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;' /> </picture> </div> <h3 id="echo">Run a Bash Command in an OCI Container</h3> <p> Again, <code>Docker</code> must be run as root for this operation, this represents an unnecessary security risk. </p> <div class="clear"> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id88b0d18eff4a'><button class='copyBtn' data-clipboard-target='#id88b0d18eff4a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container run amazonlinux echo 'Hello World!' <span class='unselectable'>Hello World! </span></pre> </div> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7063d17a181a'><button class='copyBtn' data-clipboard-target='#id7063d17a181a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container run amazonlinux <a href='https://opensource.com/article/18/6/linux-version#how-to-find-the-linux-kernel-version' target='_blank' rel="nofollow">cat /etc/os-release</a> <span class='unselectable'>VERSION="2" ID="amzn" ID_LIKE="centos rhel fedora" VERSION_ID="2" PRETTY_NAME="Amazon Linux 2" ANSI_COLOR="0;33" CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2" HOME_URL="https://amazonlinux.com/" </span></pre> </div> <!-- endregion --> </div> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/kodak-carousel-projector.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.png" type="image/png"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/kodak-carousel-projector.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/kodak-carousel-projector.png" style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;' /> </picture> </div> <!-- endregion --> <!-- #region Show All Locally Available Images --> <h3 id="localImages">Show All Locally Available Images</h3> <p> Again, <code>Docker</code> must be run as root for this operation, this represents an unnecessary security risk. </p> <div class="clear"><div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4b7c368b3622'><button class='copyBtn' data-clipboard-target='#id4b7c368b3622' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker images <span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE amazonlinux latest 53ef897d731f 21 hours ago 163MB </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id71e0a356d1a8'><button class='copyBtn' data-clipboard-target='#id71e0a356d1a8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman images <span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE localhost/hello latest 40ef32b39cf4 5 hours ago 622 MB docker.io/library/amazonlinux latest 53ef897d731f 21 hours ago 170 MB </span></pre> </div> </div> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/containerShip.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/containerShip.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/containerShip.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/containerShip.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/containerShip.png" type="image/png"> <source srcset="/blog/images/buildahPodman/containerShip.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShip.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShip.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShip.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShip.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShip.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/containerShip.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerShip.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerShip.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/containerShip.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/containerShip.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/containerShip.png" style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;' /> </picture> </div> <h3 id="listCont">List OCI Containers</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc002414b5034'><button class='copyBtn' data-clipboard-target='#idc002414b5034' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ls<br><span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id535d083de7c3'><button class='copyBtn' data-clipboard-target='#id535d083de7c3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container ls<br><span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES </span></pre> </div> <!-- endregion --> <!-- #region View All OCI Containers (Running or Not) --> <h3 id="listcont4">View All OCI Containers (Running or Not)</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb8f97ea0bf09'><button class='copyBtn' data-clipboard-target='#idb8f97ea0bf09' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ls -a <span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 250f56d9aced amazonlinux "echo 'Hello World!'" 14 minutes ago Exited (0) 14 minutes ago competent_einstein </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3fe346aafd09'><button class='copyBtn' data-clipboard-target='#id3fe346aafd09' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container ls -a <span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0f8203e9d3b8 docker.io/library/amazonlinux:latest echo Hello world! 36 minutes ago Exited (0) 36 minutes ago beautiful_mestorf 14282ace8978 docker.io/library/amazonlinux:latest echo Hello world! 36 minutes ago Exited (0) 36 minutes ago beautiful_goldwasser 1b9a8db52fb9 docker.io/library/alpine:latest echo Hello World! About an hour ago Exited (0) About an hour ago zealous_easley 6444ee144488 docker.io/library/amazonlinux:latest echo Hello World! 12 minutes ago Exited (0) 12 minutes ago frosty_ritchie 7444122cbc59 docker.io/library/alpine:latest cat /etc/motd About an hour ago Exited (0) About an hour ago elated_sammet aef84973d6ad docker.io/library/amazonlinux:latest echo Hello world! About an hour ago Exited (0) About an hour ago lucid_sinoussi e210f74bc209 docker.io/library/amazonlinux:latest cat /etc/motd About an hour ago Exited (0) About an hour ago jovial_borg </span></pre> </div> <!-- endregion --> <!-- #region List Running OCI containers --> <h3 id="listCont3">List Running OCI containers</h3> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7ea25bd79234'><button class='copyBtn' data-clipboard-target='#id7ea25bd79234' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ps -a <span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 250f56d9aced amazonlinux "echo 'Hello World!'" 5 minutes ago Exited (0) 5 minutes ago competent_einstein </span></pre> </div> <p> <code>podman</code> has a problem with the <code>container ps</code> sub-subcommand. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id37009ce240a3'><button class='copyBtn' data-clipboard-target='#id37009ce240a3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container ps -a <span class='unselectable'>Error: unrecognized command `podman container ps` Try 'podman container --help' for more information. </span></pre> </div> <!-- endregion --> <!-- #region buildah push to Docker Daemon --> <h3 id="buildah_push_use"><span class="code">buildah push</span> to Docker Daemon</h3> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/containerShipTugboat.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/containerShipTugboat.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/containerShipTugboat.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.png" type="image/png"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/containerShipTugboat.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/containerShipTugboat.png" style='width: 100%; ' /> </picture> </div> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99f8ec79868b'><button class='copyBtn' data-clipboard-target='#id99f8ec79868b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah push hello:latest docker-daemon:hello:latest <span class='unselectable'>Getting image source signatures Copying blob sha256:72fcdba8cff9f105a61370d930d7f184702eeea634ac986da0105d8422a17028 247.02 MiB / 247.02 MiB [==================================================] 2s Copying blob sha256:e567905cf805891b514af250400cc75db3cb47d61219750e0db047c5308bd916 144.75 MiB / 144.75 MiB [==================================================] 1s Copying config sha256:6d54bef73e638f2e2dd8b7bf1c4dfa26e7ed1188f1113ee787893e23151ff3ff 1.59 KiB / 1.59 KiB [======================================================] 0s Writing manifest to image destination Storing signatures </span><br> <span class='unselectable'>$ </span>buildah images | head -n2 <span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/hello latest 6d54bef73e63 2 minutes ago 398 MB </span><br> <span class='unselectable'>$ </span>buildah run -t hello:latest <span class='unselectable'>Hello, world! </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Delete an OCI Image --> <h3 id="buildah_rmi">Delete an OCI Image</h3> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildahPodman/containerSky.svg" type="image/svg"> <!---<source srcset="/blog/images/buildahPodman/containerSky.avif" type="image/avif">--> <source srcset="/blog/images/buildahPodman/containerSky.webp" type="image/webp"> <source srcset="/blog/images/buildahPodman/containerSky.apng" type="image/apng"> <source srcset="/blog/images/buildahPodman/containerSky.png" type="image/png"> <source srcset="/blog/images/buildahPodman/containerSky.jpg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerSky.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerSky.jfif" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerSky.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerSky.pjp" type="image/jpeg"> <source srcset="/blog/images/buildahPodman/containerSky.gif" type="image/gif"> <source srcset="/blog/images/buildahPodman/containerSky.tif" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerSky.tiff" type="image/tiff"> <source srcset="/blog/images/buildahPodman/containerSky.bmp" type="image/bmp"> <source srcset="/blog/images/buildahPodman/containerSky.ico" type="image/x-icon"> <source srcset="/blog/images/buildahPodman/containerSky.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/buildahPodman/containerSky.png" style='width: 100%; ' /> </picture> </div> <p> Delete an OCI image in Buildah's <code>~/.local/share/container</code> directory with the <code>rmi</code> subcommand: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcc231df8b722'><button class='copyBtn' data-clipboard-target='#idcc231df8b722' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah rmi e12ea62c5582 <span class='unselectable'>e12ea62c5582f91a2228e3e284ea957f2df4f1cdb150fd2c189ef8f11d7633ce </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- endregion --> Stack Overflow Culture: Zero-Sum, Authoritarian and Hormonally Imbalanced 2021-04-18T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/18/so-culture <p> This article describes some problems that significantly impact <a href='https://www.crunchbase.com/organization/stack-overflow' target='_blank' rel="nofollow">Stack Overflow</a> users, and offers suggestions for improvement. If you are unfamiliar with Stack Overflow, or would like to read a summary of what this article is based on, Kevin Workman wrote a terrific summary in March 2019 entitled &ldquo;<a href='https://happycoding.io/blog/stack-overflow-culture-wars' target='_blank' rel="nofollow">The Stack Overflow Culture Wars</a>&rdquo;. Very little has changed since then. </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <a href='https://www.stackoverflow.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/stackoverflowLogo.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.png" type="image/png"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/stackoverflowLogo.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/stackOverflow/stackoverflowLogo.png" style='width: 100%; padding: 20px;' /> </picture> </a> </div> <fold-article intro> <h2 id="intro">Introduction</h2> <p> <a href='https://www.stackoverflow.com' target='_blank' rel="nofollow">Stack Overflow</a> is the premier website world-wide for programmers to help each other by asking and answering questions. It has a defined protocol for this type of interaction, however new user on-boarding is often ineffective, so newcomers are not properly informed, and old-timers often do not exhibit appropriate people skills. The <a href='https://games.greggman.com/game/done-with-stackoverflow/' target='_blank' rel="nofollow">protocol is somewhat misguided</a> and appropriate tools for better interaction are not provided. </p> <p> As a result, this website has developed a well-documented reputation for being “<a href='https://codeblog.jonskeet.uk/2018/03/17/stack-overflow-culture/' target='_blank' rel="nofollow">a valuable resource, but a scary place to contribute due to potential hostility.</a>” </p> <div class="pullQuote"> Sometimes, loving something means caring enough to admit that it has a problem.<br><br> &nbsp; &ndash; <a href='https://stackoverflow.blog/2018/04/26/stack-overflow-isnt-very-welcoming-its-time-for-that-to-change/' target='_blank' rel="nofollow">Jay Hanlon</a>, writing about Stack Overflow when he was EVP of Culture and Experience. </div> </fold-article> <fold-article problem> <div class="clear quote"> Stack Overflow suffers from militant moderators who close and delete reasonable submissions and answers due to Draconian rules.<br><br> &nbsp; &ndash; <code>sleavey</code> commenting on a Hacker News thread entitled <a href='https://news.ycombinator.com/item?id=16610353' target='_blank' rel="nofollow">Stack Overflow Culture</a>. </div> </fold-article> <fold-article top> <h2 id="topdown">Change Starts At the Top</h2> <h3 id="Chandrasekar">Prashanth Chandrasekar, CEO</h3> <p> Prashanth Chandrasekar became CEO of Stack Overflow in September, 2019. <a href='https://www.intercom.com/blog/podcasts/prashanth-chandrasekar-on-writing-the-script-of-the-future/' target='_blank' rel="nofollow">Inside Intercom interviewed Mr. Chandrasekar</a> in August 2020. This puff piece made no mention of any cultural problems. The focus was on the brilliance of Stack Overflow&rsquo;s technology. If Mr. Chandrasekar has a vision for how to guide social change, he did not mention it. Instead, in the article and his publications since then he only speaks publicly about <a href='https://stackoverflow.blog/author/pchandrasekar/' target='_blank' rel="nofollow">transitioning Stack Overflow to a product-led SaaS company</a>. </p> <p> Shortly after becoming CEO, Mr. Chandrasekar published &ldquo;<a href='https://stackoverflow.blog/2020/01/21/scripting-the-future-of-stack-2020-plans-vision/' target='_blank' rel="nofollow">Scripting the Future of Stack Overflow</a>, in which he wrote: </p> <p class="quote"> We learned that we needed much better channels to listen to our moderators and community members. We have not evolved the existing channels of engagement for power users in our community, like Meta, or articulated how we intended to make improvements going forward. This has caused friction as our user base and business have rapidly grown. We acknowledge these issues, apologize for our mistakes, and have plans for improving in the future. </p> <p> Later in the article, he mentioned improvements to the code of conduct, a survey and establishing a moderator council. More than 2 years later, none of this has made the slightest difference in Stack Overflow's culture. </p> <h3 id="pathak">Mihir Pathak — EVP, Strategy & Transformation</h3> <p> Prior to his employment at Stack Overflow, Mr. Pathak was a derivatives strategist McKinsey &amp; Company, a <a href='https://www.mckinsey.com/business-functions/organization/our-insights/the-four-building-blocks--of-change#' target='_blank' rel="nofollow">world-renouned change management company</a>. That is to say, although Mr. Pathak worked at McKinsey, he was not there to assist other companies make structural changes; instead, he was responsible for pricing methodologies and hedging techniques underlying financial derivative products and options trading strategies &ndash; a heads-down money manager. </p> <p> <a href='https://stackoverflow.com/company/leadership/mihir-pathak' target='_blank' rel="nofollow">Mr. Pathak&rsquo;s page at StackOverflow</a> is no longer available. <a href='https://webcache.googleusercontent.com/search?q=cache:1eKtOpLW2VoJ:https://stackoverflow.com/company/leadership/mihir-pathak' target='_blank' rel="nofollow">Google cached it</a>. No announcement has been made about his departure or if there will be a replacement. </p> <p> Mr. Pathak's job description is still visible at <a href='https://www.themuse.com/profiles/stackoverflow' target='_blank' rel="nofollow"><code>themuse.com</code></a>: </p> <p class="quote"> Mihir is responsible for the long-term business strategy of Stack Overflow, which includes forming partnerships with like-minded organizations and understanding how to best serve the needs of future developers. </p> <p> Mr. Pathak was employed from September 2016 at Stack Overflow as a business development executive, not a change management champion. </p> <h3 id="dietrich">Teresa Dietrich, Chief Product and Technology Officer</h3> <p> In January 2020 <a href='https://www.crunchbase.com/person/teresa-dietrich' target='_blank' rel="nofollow">Teresa Dietrich</a> was made Chief Product and Technology Officer. She also came from McKinsey &amp; Company, where she was Global Head of Product &amp; Engineering. Two months after she took the job she wrote &ldquo;<a href='https://stackoverflow.blog/2020/02/25/sharing-our-first-quarter-2020-community-roadmap/' target='_blank' rel="nofollow">Sharing our first quarter 2020 community roadmap</a>&rdquo;. That rather bland article did not seem to indicate any recognition of serious problems within the Stack Overflow culture, and I could not find any publicly available results of the studies that were mentioned. </p> <p> 16 months after Ms. Dietrich started her job, I do not see any cultural change. Has Ms. Dietrich been tasked with leading such a change? If so: </p> <ul> <li>Does she have board-level support?</li> <li>Does she have what she needs to make significant cultural changes?</li> <li> Why has she not made any public acknowledgement of a cultural problem? One possible answer is that her boss, Mr. Chandrasekar, has not done so. </li> <li> Is Ms. Dietrich the right person for the job? This is not a purely technical problem, it is a social problem. I believe that women in general possess more highly developed social skills, but the skills necessary to climb to the top are not the skills required to make this type of cultural shift. </li> </ul> <div class="pullQuote"> Are Mr. Chandrasekar and Ms. Dietrich part of the cultural problem, or part of the solution? </div> </fold-article> <fold-article women> </fold-article> <fold-article me> <h2 id="me">Where Am I Coming From?</h2> <p> I have no agenda, no investment in the status quo, nothing to prove, no contacts at the company, I am not an undercover journalist, and I am not a competitor or investor. I am just Joe User... and I am not shy when I believe I have something that I think needs to be heard. </p> <p> If I mispeak, please tell me. If I missed something, again please tell me. If there is a bigger picture I would like to learn about it. </p> <p> I have used Stack Overflow and its <a href='https://stackexchange.com/sites' target='_blank' rel="nofollow">sibling websites</a> for more than 10 years. Until a few weeks ago, I contributed a few answers here and there, and otherwise did not spend much time on the sites. All contributors are volunteers, so the only reasons to contribute are for prestige, social bonding and altruism. </p> <p> For almost 30 days, in my spare time, I helped people who asked questions on Stack Overflow. I am writing this article because although I enjoy helping others, the enjoyment I experienced while doing so within the Stack Overflow environment was overshadowed by the regressive behavior tolerated and even enforced by other contributors. I should also say that I did encounter certain other contributors with whom interaction was very pleasant. However, interactions with alpha contributors with the highest scores seemed more often than not to be quite unpleasant. Since I first published this article, I have mostly not interacted on the site. I remain open to contributing to improvements in the culture. </p> <p> The next image is provided so readers know that I am able to effectively participate in the current Stack Overflow website. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/stackOverflow.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/stackOverflow.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/stackOverflow.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/stackOverflow.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/stackOverflow.png" type="image/png"> <source srcset="/blog/images/stackOverflow/stackOverflow.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflow.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflow.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflow.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflow.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflow.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/stackOverflow.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackOverflow.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackOverflow.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/stackOverflow.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/stackOverflow.cur" type="image/x-icon"> <img alt='The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days.' class="imgImg rounded shadow" src="/blog/images/stackOverflow/stackOverflow.png" style='width: 100%; ' title='The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days.' /> </picture> <figcaption class='imgFigCaption halfsize'> The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days. </figcaption> </figure> </div> <p> Over 100 of the answers I offered in a 30-day period were accepted as the preferred answer. In fact, most of my answers were selected as the preferred answer. That means many more alternative answers were not accepted. </p> <p> After losing, sometimes a contributor will delete their post. I have done it myself, when the winning post was significantly better by all measures. </p> <p> Some of the other contributors who had provided alternative answers that were not selected clearly felt they had lost a competition. This structure, and others that I describe below, define a system designed to generate hostility. Stack Overflow, as currently implemented, <a href='https://en.wikipedia.org/wiki/Dominance_hierarchy' target='_blank' rel="nofollow">promotes dominance behavior</a>, which for most primates (other than <a href='https://www.scientificamerican.com/article/bonobo-sex-and-society-2006-06/' target='_blank' rel="nofollow">bonobos</a>) is patriarchal. </p> </fold-article> <fold-article gamify> <div class='imgWrapper imgFlex right' style=' '> <a href='http://localhost:4001/blog/2021/04/18/so-culture.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/codinghorror-app-icon.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.png" type="image/png"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/codinghorror-app-icon.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/stackOverflow/codinghorror-app-icon.png" style='width: 100%; ' /> </picture> </a> </div> <h2 id="gamification">Gamification</h2> <p> Stack Overflow's success has be in part due to its successful <a href='https://en.wikipedia.org/wiki/Gamification' target='_blank' rel="nofollow">gamification</a> of the interaction between questioners and answerers. Gamification is powerful and addictive. Unfortunately, the model chosen resembles FPS (<a href='https://en.wikipedia.org/wiki/First-person_shooter' target='_blank' rel="nofollow">first-person shooter</a>) games, instead of co-operative games. </p> <p> Jeff Atwood, one of the two original authors of Stack Overflow, wrote an article in 2011 entitled <a href='https://blog.codinghorror.com/the-gamification/' target='_blank' rel="nofollow">The Gamification</a>, in which he writes: </p> <div class="quote"> Gaming elements are not tacked on to the Stack Exchange Q&A engine, but a natural and essential element of the design. </div> <p> Just below that sentence, Mr. Atwood shows a screenshot from an FPS video game: </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <a href='https://blog.codinghorror.com/the-gamification/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/fps.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/fps.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/fps.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/fps.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/fps.png" type="image/png"> <source srcset="/blog/images/stackOverflow/fps.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/fps.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/fps.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/fps.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/fps.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/fps.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/fps.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/fps.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/fps.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/fps.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/fps.cur" type="image/x-icon"> <img alt='FPS Game screenshot from 'The Gamification'.' class="imgImg rounded shadow" src="/blog/images/stackOverflow/fps.png" style='width: 100%; ' title='FPS Game screenshot from 'The Gamification'.' /> </picture> </a> <figcaption class='imgFigCaption halfsize'> <a href="https://blog.codinghorror.com/the-gamification/" target='_blank' > FPS Game screenshot from 'The Gamification'. </a> </figcaption> </figure> </div> <div class="quote"> I haven't ever quite come out and said it this way, but … I played a lot of Counter-Strike from 1998 to 2001, and Stack Overflow is in many ways my personal Counter-Strike.<br><br> &nbsp; &ndash; Jeff Atwood, from &ldquo;The Gamification&rdquo; </div> <p> FPS games are structured so the player only wins by killing others. This is an entirely different motivational structure than a scenario where a person only wins if others succeed. </p> </fold-article> <fold-article terms> <h2 id="terms">Terminology</h2> <p> I use a few non-standard terms in this article: </p> <dl> <dt>Questioner</dt> <dd>One who provides a question</dd> <dt>Answerer</dt> <dd>One who provides an answer</dd> <dt>Downvoter</dt> <dd>One who downvotes another person's contribution</dd> <dt>Downvotee</dt> <dd>One who has their contribution downvoted</dd> </dl> </fold-article> <fold-article downvote> <h2 id="dialog">Dialog Improves Most Questions</h2> <p> A small percentage of questions asked on Stack Overflow are unambiguous, contain all the necessary information, and are phrased well enough to be understood. For these questions, answers can be posted without any interaction between questioner and potential answerers. </p> <p> However, most questions involve a dialog between potential answerers and the questioner. In the dialog, the question is refined, and the questioner's code and any other relevant data is elicited and provided. The tools provided for such a dialog are unfortunately problematic. </p> <p> The only two mechanisms for interaction between questioner and potential answerers are comments and answers. Comments have severe limitations that greatly reduce their effectiveness for eliciting information from a questioner: </p> <ul> <li>Comments must be very short</li> <li>Comments cannot be formatted properly</li> <li>Comments cannot be edited for more than a few minutes</li> </ul> <p> This means that answerers who are trying to explain something to the questioner to elicit more information, or guide the questioner towards understanding their problem better, must resort to posting an incomplete answer. Posting an incomplete answer is risky; other potential answerers can attack the answer by downvoting it. </p> <div class="pullQuote"> Downvotes typically last forever </div> </fold-article> <fold-article incentives> <h2 class="clear" id="broken">Some Incentives Promote Hostility</h2> <p> Many long-time users have completely objectified other users, and act as if Stack Overflow is a video game. Points are accumulated, and at any given time there are a finite number of questions to answer. A person's reputation on Stack Overflow is represented by a single number, which is the number of points they have accumulated. This single number is the structural source of many problems. A more nuanced reputation score would be a giant step forward. </p> <p> Many of these long-term answerers have come to view their participation on Stack Overflow as a zero-sum competition; they can only win (that is, have their answer accepted) if everyone else loses (that is, no-one else provides an answer that is accepted). </p> <p> Some answerers employ intimidation order to suppress competition. Downvotes are often used in the same way against other answerers as a <a href='https://en.wikipedia.org/wiki/Brushback_pitch' target='_blank' rel="nofollow">brush back pitch</a> in baseball. </p> <div class="pullQuote"> Downvoting has no negative consequences for the downvoter </div> <p> Standard operating procedure for competitively-minded answerers is to downvote answers from others at every opportunity. There is no risk in downvoting: </p> <ul> <li>Downvotes are untraceable; there is no public record of who downvoted or when a downvote was cast.</li> <li>Downvotes are free to downvoters; this encourages liberal downvoting.</li> </ul> <p> This scenario incentivizes competitively-minded answerers to strafe the competition with downvotes at every opportunity. </p> <div class="pullQuote"> If downvotes cost the downvoter the same number of points as they penalize the downvotee, then downvotes would become much rarer. </div> <p> Downvotes typically last forever. Yes, a downvoter could theoretically reverse a downvote, but it is awkward for them to find their old downvotes, and there is no incentive to do so. </p> <p> Questions from newcomers are also frequently downvoted, without discussion, or along with hostile remarks. That leaves a permanent impression, and tends to select for new members who are comfortable with dominance-based hostility. This is a self-perpetuating, and highly toxic, social order. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <a href='https://www.focusforhealth.org/how-toxic-masculinity-harms-men-and-society-as-a-whole/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/toxicMasulinity.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/toxicMasulinity.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/toxicMasulinity.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.png" type="image/png"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/toxicMasulinity.cur" type="image/x-icon"> <img alt='Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health' class="imgImg rounded shadow" src="/blog/images/stackOverflow/toxicMasulinity.png" style='width: 100%; ' title='Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health' /> </picture> </a> <figcaption class='imgFigCaption halfsize'> <a href="https://www.focusforhealth.org/how-toxic-masculinity-harms-men-and-society-as-a-whole/" target='_blank' > Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health </a> </figcaption> </figure> </div> </fold-article> <fold-article suggest_onboard> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/stackOverflowHelp.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.png" type="image/png"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/stackOverflowHelp.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/stackOverflow/stackOverflowHelp.png" style='width: 100%; ' /> </picture> </div> <h2 id="onboard">Suggestion: Gamified Onboarding</h2> <p> The only information for new users that is directly accessible from the Stack Overflow front page, is the Help Center, under the question mark icon. It is obvious from the often very polite and tentative inquiries made by new users when they ask their first question that they never noticed that information, or if they did it did not seem relevant. ... and then those new users are mercilessly slammed. </p> <p> New users should be presented with a <a href='https://www.appcues.com/blog/the-5-best-user-onboarding-experiences' target='_blank' rel="nofollow">short instructional question and answer-style introduction</a>, where information is provided on how to be a good Stack Overflow user. This should happen before they get the opportunity to post their first question. Different levels of users should be explained. Although Stack Overflow is all-English, the onboarding should be multilingual. </p> </fold-article> <fold-article suggest_multilingual> <h2 id="multling">Suggestion: Multilingual Support</h2> <p> A high percentage of users do not speak English very well. They really struggle, and tolerance is low on Stack Overflow for bad English. Other sites, for example Facebook and LinkedIn, have a translation facility built right in. I think Facebook did a particularly good job. Why not do something similar on StackOverflow.com? English would be the official language, but everyone world-wide would be able to interact much more effectively. </p> <div class="pullQuote"> This site is multilingual.<br/> It is not that hard to do! </div> <p> Machine translation is really quite good. I have it on this site. Want to view this site in one of over 100 languages? Just <a href='#body'>go to the top of this page</a> and click on this pull-down menu labeled <b>Select Language</b>: </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/selectLanguage.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/selectLanguage.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/selectLanguage.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/selectLanguage.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/selectLanguage.png" type="image/png"> <source srcset="/blog/images/stackOverflow/selectLanguage.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguage.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguage.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguage.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguage.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguage.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/selectLanguage.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/selectLanguage.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/selectLanguage.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/selectLanguage.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/selectLanguage.cur" type="image/x-icon"> <img class="imgImg " src="/blog/images/stackOverflow/selectLanguage.png" style='width: 100%; ' /> </picture> </div> <p> You will then see the list of languages that you can view this website in: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/selectLanguages.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/selectLanguages.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/selectLanguages.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/selectLanguages.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/selectLanguages.png" type="image/png"> <source srcset="/blog/images/stackOverflow/selectLanguages.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguages.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguages.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguages.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguages.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/selectLanguages.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/selectLanguages.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/selectLanguages.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/selectLanguages.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/selectLanguages.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/selectLanguages.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/stackOverflow/selectLanguages.png" style='width: 100%; ' /> </picture> </div> <p> Morally speaking, not to provide a multilingual site discriminates against non-English-speaking people. </p> </fold-article> <fold-article suggest_rep> <h2 id="elicitation">Suggestion: More Nuanced Reputation Score</h2> <p> Instead of a single metric, answerers should be rated along several dimensions. Economists and psychologists both use multidimensional diagrams. The following diagram represents data in 4 dimensions. More dimensions can easily be shown in this type of diagram. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.svg" type="image/svg"> <!---<source srcset="/blog/images/stackOverflow/multiDimentionalPlot.avif" type="image/avif">--> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.webp" type="image/webp"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.apng" type="image/apng"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.png" type="image/png"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.jpg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.jpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.jfif" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.pjpeg" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.pjp" type="image/jpeg"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.gif" type="image/gif"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.tif" type="image/tiff"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.tiff" type="image/tiff"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.bmp" type="image/bmp"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.ico" type="image/x-icon"> <source srcset="/blog/images/stackOverflow/multiDimentionalPlot.cur" type="image/x-icon"> <img alt='Multi-dimensional data can easily be visualized by outlined shapes' class="imgImg rounded shadow" src="/blog/images/stackOverflow/multiDimentionalPlot.png" style='width: 100%; ' title='Multi-dimensional data can easily be visualized by outlined shapes' /> </picture> <figcaption class='imgFigCaption fullsize'> Multi-dimensional data can easily be visualized by outlined shapes </figcaption> </figure> </div> <p> Instead of "bigger is better" (a single number indicating status, with the high score indicating alpha status), more information would allow for more detail, so a fuzzy diagram would show little interaction, while a highly detailed and intricate design would indicate a lot of participation. Some answerers might be stronger regarding some metrics, while being weaker on other metrics. </p> <p> Instead of displaying a person's score, the shape of their participation would be shown. Some people prefer to be seen as well-rounded, others prefer to be the best on selected aspects and ignore the others. One size does not fit all. </p> <p> People would start to give pet names for various shapes. Jokes would be made about the shapes. </p> <p> HR personnel would start to hire teams based on how well these shapes meshed together. Money will be made by timely entrepreneurs because these shapes will quickly be adopted industry-wide. Some will aspire to change their shape. </p> <p> An entire industry will spring up servicing those who wish to modify their shape. </p> </fold-article> <fold-article suggest_active> <h2 id="restrict_down">Suggestion: Encourage and Highlight Elicitation</h2> <p> <a href='https://en.wikipedia.org/wiki/Elicitation_technique' target='_blank' rel="nofollow">Elicitation</a> is a difficult skill to master. The current high-scorers have no incentives to employ elicitation, and they act as if on a campaign to eradicate it. </p> <div class="pullQuote"> Introduce an Elicitation Phase </div> <p class="clear"> A button should be introduced that lets everyone who visits the question page that a potential answerer would like to elicit information. While at least one such button is enabled, no downvotes are possible relating to the question, and the question cannot be closed or moved to another forum by anyone, regardless of their privilege level. </p> <p> The elicitation mode would time out, but not suddenly or unexpectedly. Instead, it would back off, rather like the Ethernet back-off algorithm for collision resolution used in random access MAC protocols. </p> <p> Both the potential answerer and the questioner would be given cues that they have a question or a response waiting, and after a period of inactivity the special status ends. I leave the details of the timing undefined for others to discuss. </p> </fold-article> <fold-article suggest_crowd> <h2 class="restrict_down" id="crowd">Suggestion: Restrict Downvoting</h2> <p> Downvoting needs to incorporate: </p> <ul> <li>Accountability (no more anonymous downvoting)</li> <li>Cost (no more drive-by shootings without consequences)</li> <li>Time window (vote after the dust settles, not during the elicitation phase)</li> <li>Public displays of user profiles should prominently display that user's downvotes and upvotes, with links</li> <li>Aggregate statistics on user profiles of their percentage downvotes and upvotes, trends (absolute and relative), etc.</li> </ul> </fold-article> <fold-article suggest_crowd> </fold-article> Serverless E-Commerce 2021-04-14T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/14/serverless-ecommerce <style> h2.numbered:before { color: darkgreen; content: "Option " counter(h2counter) ":\A0\A0\A0"; } </style> <!-- #region --> <p> As readers of this blog know, I have been <a href='/django/index.html'>chronicling my adventure into Python-powered e-commerce</a> for several months. I have been focusing on Django in general, and <a href='https://django-oscar.readthedocs.io' target='_blank' rel="nofollow">Django-Oscar</a> in particular. Webapps made with this technology are almost exclusively run on dedicated real or virtual machines. <a href='https://www.cloudflare.com/learning/serverless/what-is-serverless/' target='_blank' rel="nofollow">Serverless</a> computing is a method of providing backend services on an as-used basis. AWS Lambda is the best-known example of serverless computing, and it combines nicely with a CDN like AWS CloudFront. </p> <p> This article discusses 3 goals for an e-commerce system. Two goals are provided by the technology behind <a href='https://martinfowler.com/articles/serverless.html' target='_blank' rel="nofollow">serverless webapps</a>: </p> <ol> <li>Enormous and instantaneous scalability.</li> <li>Pay-as-you-go without an up-front cost commitment.</li> </ol> <p> I have one more goal: very low latency for online shoppers. </p> <!-- endregion --> <!-- #region --> <h2 id="big">The Big Picture</h2> <p class="quote"> AWS Lambda consists of two main parts: the Lambda service which manages the execution requests, and the Amazon Linux micro virtual machines provisioned using AWS Firecracker, which actually runs the code. <br><br> A Firecracker VM is started the first time a given Lambda function receives an execution request (the so-called “Cold Start”), and as soon as the VM starts, it begins to poll the Lambda service for messages. When the VM receives a message, it runs your function code handler, passing the received message JSON to the function as the event object. <br><br> Thus every time the Lambda service receives a Lambda execution request, it checks if there is a Firecracker microVM available to manage the execution request. If so, it delivers the message to the VM to be executed. <br><br> In contrast, if no available Firecracker VM is found, it starts a new VM to manage the message. Each VM executes one message at a time, so if a lot of concurrent requests are sent to the Lambda service, for example due to a traffic spike received by an API gateway, several new Firecracker VMs will be started to manage the requests and the average latency of the requests will be higher since each VM takes roughly a second to start. <br><br> &nbsp; &ndash; From <a href='https://www.proud2becloud.com/how-to-run-any-programming-language-on-aws-lambda-custom-runtimes/' target='_blank' rel="nofollow">How to run any programming language on AWS Lambda: Custom Runtimes</a> by Matteo Moroni. </p> <!-- endregion --> <!-- #region --> <h2 id="lambdalimits">AWS Lambda Limits</h2> <p> AWS Lambda programs have access to considerable resources, enough for most e-commerce stores. The AWS Lambda runtime environment has the following limitations, some of which can be improved upon with some work: </p> <ul> <li>The disk space (ephemeral) is limited to 512 MB.</li> <li>The default deployment package size is 50 MB.</li> <li>The memory range is from 128 to 3008 MB.</li> <li>The maximum execution timeout for a function is 15 minutes.</li> <li>Request and response (synchronous calls) body payload size can be up to 6 MB.</li> <li>Event request (asynchronous calls) body can be up to 128 KB.</li> </ul> <!-- endregion --> <!-- #region --> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/django/clouds.svg" type="image/svg"> <!---<source srcset="/blog/images/django/clouds.avif" type="image/avif">--> <source srcset="/blog/images/django/clouds.webp" type="image/webp"> <source srcset="/blog/images/django/clouds.apng" type="image/apng"> <source srcset="/blog/images/django/clouds.png" type="image/png"> <source srcset="/blog/images/django/clouds.jpg" type="image/jpeg"> <source srcset="/blog/images/django/clouds.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/clouds.jfif" type="image/jpeg"> <source srcset="/blog/images/django/clouds.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/clouds.pjp" type="image/jpeg"> <source srcset="/blog/images/django/clouds.gif" type="image/gif"> <source srcset="/blog/images/django/clouds.tif" type="image/tiff"> <source srcset="/blog/images/django/clouds.tiff" type="image/tiff"> <source srcset="/blog/images/django/clouds.bmp" type="image/bmp"> <source srcset="/blog/images/django/clouds.ico" type="image/x-icon"> <source srcset="/blog/images/django/clouds.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/django/clouds.png" style='width: 100%; ' /> </picture> </div> <h2 id="cf">CloudFront</h2> <p> <a href='https://forum.djangoproject.com/u/briancaffey/summary' target='_blank' rel="nofollow">Brian Caffey</a> wrote <a href='https://forum.djangoproject.com/t/building-a-django-application-on-aws-with-cloud-development-kit-cdk/2830' target='_blank' rel="nofollow">Building a Django application on AWS with Cloud Development Kit (CDK)</a>. The website Mr. Caffey's article discusses does not use Lambda, instead his website is always running. So, this option is quite informative and well-thought-out, but it is AWS-specific and does not discuss serverless architecture. </p> <p> For me, the most interesting part about Mr. Caffey's article is it mentions using 3 origins with AWS CloudFront: (1) an origin for ALB (for hosting the Django API), (2) an origin for the S3 website (static Vue.js site), and (3) an S3 origin for Django assets. Mr. Caffey does not say why he used 3 origins, but feeding one CloudFront distribution from multiple origins would mean that all of their content would appear on the same Internet subdomain. </p> <p> This means that the <a href='https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests' target='_blank' rel="nofollow">extra HTTP handshaking required for certain CORS</a> (cross-origin HTTP requests) requests between subdomains would be avoided; specifically, there would be no need for pre-flight requests. This would make the website seem noticeably faster if users did lots of content editing and/or transactions with the website. My own pet project has users creating and modifying content, and purchasing product, so taking the requirement for the CORS handshakes away would be a win, plus the end user's web browser could reuse the origin HTTP connection, speeding up even non-cacheable requests. </p> <p> Tamás Sallai wrote <a href='https://advancedweb.hu/how-to-route-to-multiple-origins-with-cloudfront/' target='_blank' rel="nofollow">How to route to multiple origins with CloudFront &ndash; Set up path-based routing with Terraform</a>. Mr. Sallai <a href='https://advancedweb.hu/' target='_blank' rel="nofollow">is a prolific writer</a>! </p> <!-- endregion --> <!-- #region --> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/django/lifeOnTheEdge.svg" type="image/svg"> <!---<source srcset="/blog/images/django/lifeOnTheEdge.avif" type="image/avif">--> <source srcset="/blog/images/django/lifeOnTheEdge.webp" type="image/webp"> <source srcset="/blog/images/django/lifeOnTheEdge.apng" type="image/apng"> <source srcset="/blog/images/django/lifeOnTheEdge.png" type="image/png"> <source srcset="/blog/images/django/lifeOnTheEdge.jpg" type="image/jpeg"> <source srcset="/blog/images/django/lifeOnTheEdge.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/lifeOnTheEdge.jfif" type="image/jpeg"> <source srcset="/blog/images/django/lifeOnTheEdge.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/lifeOnTheEdge.pjp" type="image/jpeg"> <source srcset="/blog/images/django/lifeOnTheEdge.gif" type="image/gif"> <source srcset="/blog/images/django/lifeOnTheEdge.tif" type="image/tiff"> <source srcset="/blog/images/django/lifeOnTheEdge.tiff" type="image/tiff"> <source srcset="/blog/images/django/lifeOnTheEdge.bmp" type="image/bmp"> <source srcset="/blog/images/django/lifeOnTheEdge.ico" type="image/x-icon"> <source srcset="/blog/images/django/lifeOnTheEdge.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/django/lifeOnTheEdge.png" style='width: 100%; ' /> </picture> </div> <h2 id="edge">Edge Computing</h2> <p> Performing computations and serving assets from a <a href='https://aws.amazon.com/cloudfront/features/' target='_blank' rel="nofollow">nearby point of presence</a> minimizes latency for end users. E-commerce customers much prefer online stores that respond quickly. Edge computing can deliver that experience world-wide, and developers can deploy their work from wherever they are. </p> <p> <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html' target='_blank' rel="nofollow">AWS Lambda@Edge</a> (<a href='https://aws.amazon.com/lambda/edge/' target='_blank' rel="nofollow">console</a>) runs the Lambda computation in one of 13 regional AWS points of presence, one hop removed from the CloudFront edge locations, or at least in the same availability zone at the CloudFront point of presence. Distributed database issues would need to be addressed before significant benefits would accrue from this implementing this decentralized architecture. Unfortunately, Lambda@Edge has some significant restrictions that prevent it from running nontrivial Django apps. </p> <h3>Lambda@Edge Restrictions</h3> <p>From <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html' target='_blank' rel="nofollow">requirements and restrictions on using Lambda functions with CloudFront</a>, it is apparent that it is not possible to run non-trivial Django apps securely at the edge with good performance.</p> <ul> <li>You can add triggers only for functions in the US East (N. Virginia) Region.</li> <li>You can’t configure your Lambda function to access resources inside your VPC.</li> <li>AWS Lambda environment variables are not supported.</li> <li>Lambda functions with AWS Lambda layers are not supported.</li> <li>Using AWS X-Ray is not supported.</li> <li>AWS Lambda reserved concurrency and provisioned concurrency are not supported.</li> <li>Lambda functions defined as container images are not supported.</li> </ul> <p> Until such time as Lambda@Edge removes the above restrictions, Django webapps will continue to be deployed as centralized webapps, which means that ultra-low latency is not possible world-wide. </p> <h3 id="cf_fns">CloudFront Functions</h3> <p> <a href='https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/' target='_blank' rel="nofollow">CloudFront Functions</a> are closer to the user, but have even more restrictions than Lambda@Edge. Alas, CloudFront Functions do not seem likely to be able to support significant computation any time soon. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.svg" type="image/svg"> <!---<source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.avif" type="image/avif">--> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.webp" type="image/webp"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.apng" type="image/apng"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.png" type="image/png"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.jpg" type="image/jpeg"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.jpeg" type="image/jpeg"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.jfif" type="image/jpeg"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.pjpeg" type="image/jpeg"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.pjp" type="image/jpeg"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.gif" type="image/gif"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.tif" type="image/tiff"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.tiff" type="image/tiff"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.bmp" type="image/bmp"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.ico" type="image/x-icon"> <source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.cur" type="image/x-icon"> <img alt='From &ldquo;Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale&rdquo;' class="imgImg rounded shadow" src="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.png" style='width: 100%; ' title='From &ldquo;Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale&rdquo;' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/" target='_blank' > From &ldquo;Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale&rdquo; </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region --> <h2 id="vendor">Infrastructure as Code (IaC)</h2> <div class="quote"> For anything bigger than a toy cloud application, Infrastructure as Code (IaC) is table stakes. You’d be hard-pressed to find someone managing anything of scale who thinks letting folks point and click in the console is the optimal route. <br><br>&nbsp; &ndash; From <a href='https://acloudguru.com/blog/engineering/cloudformation-terraform-or-cdk-guide-to-iac-on-aws' target='_blank' rel="nofollow">CloudFormation, Terraform, or CDK? A guide to IaC on AWS</a> by Jared Short, published by <code>acloudguru.com</code>. </div> <div class="quote"> <a href='https://www.hashicorp.com/products/terraform' target='_blank' rel="nofollow">Terraform</a>, AWS CloudFormation, Packer, Pulumi, and GeoEngineer are the most popular tools in the category "Infrastructure Build Tools". <br> &nbsp; &ndash; <a href='https://stackshare.io/infrastructure-build-tools' target='_blank' rel="nofollow">from Stackshare.io</a> </div> <!-- endregion --> <!-- #region --> <h2 id="infographic">Infographic: Lambda Framework Comparison</h2> <p> Yan Cui at Lumigo.io made <a href='https://lumigo.io/aws-lambda-deployment/' target='_blank' rel="nofollow">this terrific infographic</a>, which compares 9 serverless application frameworks and infrastructure management tools according to opinionatedness and customizability. This article discusses some of those technologies. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <a href='https://lumigo.io/aws-lambda-deployment/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/django/lumigoComparison.svg" type="image/svg"> <!---<source srcset="/blog/images/django/lumigoComparison.avif" type="image/avif">--> <source srcset="/blog/images/django/lumigoComparison.webp" type="image/webp"> <source srcset="/blog/images/django/lumigoComparison.apng" type="image/apng"> <source srcset="/blog/images/django/lumigoComparison.png" type="image/png"> <source srcset="/blog/images/django/lumigoComparison.jpg" type="image/jpeg"> <source srcset="/blog/images/django/lumigoComparison.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/lumigoComparison.jfif" type="image/jpeg"> <source srcset="/blog/images/django/lumigoComparison.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/lumigoComparison.pjp" type="image/jpeg"> <source srcset="/blog/images/django/lumigoComparison.gif" type="image/gif"> <source srcset="/blog/images/django/lumigoComparison.tif" type="image/tiff"> <source srcset="/blog/images/django/lumigoComparison.tiff" type="image/tiff"> <source srcset="/blog/images/django/lumigoComparison.bmp" type="image/bmp"> <source srcset="/blog/images/django/lumigoComparison.ico" type="image/x-icon"> <source srcset="/blog/images/django/lumigoComparison.cur" type="image/x-icon"> <img alt='From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io' class="imgImg rounded shadow" src="/blog/images/django/lumigoComparison.png" style='width: 100%; ' title='From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io' /> </picture> </a> <figcaption class='imgFigCaption fullsize'> <a href="https://lumigo.io/aws-lambda-deployment/" target='_blank' > From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io </a> </figcaption> </figure> </div> <p> The trade-off between customizability and opinionatedness is that highly customizable frameworks require more code to do things that opinionated frameworks do more succinctly. On the other hand, very opinionated frameworks are more limited in their abilities. A classic example of an opinionated framework is Ruby on Rails, which is specifically designed for master/detail applications. Other types of applications should use a different framework, or no framework at all. </p> <p> Two of the technologies on the above infographic are Zappa and Terraform, both of which I discuss in this article. Zappa is rather opinionated, while Terraform is very customizable. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="cdk">AWS Cloud Development Kit (CDK)</h2> <p> AWS CDK provides a programmatic interface for modeling and provisioning cloud resources. Languages supported include Java, JavaScript, .NET, Node.js, Python and Typescript. </p> <p> Even if AWS is not directly the service provider, awareness of the <a href='https://aws.amazon.com/cdk/' target='_blank' rel="nofollow">AWS CDK</a> is important because some other options, for example the <a href='https://aws.amazon.com/blogs/developer/introducing-the-cloud-development-kit-for-terraform-preview/' target='_blank' rel="nofollow">Cloud Development Kit for Terraform</a> (cdktf), are based on <a href='https://github.com/aws/aws-cdk' target='_blank' rel="nofollow">AWS CDK</a>. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="chalice">Chalice &ndash; Serverless Django on AWS</h2> <p> <a href='https://aws.github.io/chalice/' target='_blank' rel="nofollow">Chalice</a> is an AWS open-source project that has good traction. This Python serverless microframework for AWS allows applications that use Amazon API Gateway and AWS Lambda to be quickly created and deployed. </p> <p> The name and logo of this project are suggestive of the Holy Grail. I found the thinly veiled references to Christianity to be off-putting. Religious references have no place in a professional environment. Programmers who work with this project have religious icons, words and phrases continuously presented to them, and they must write words that are strongly identified with Christian doctrine for them to write software. This is forced <a href='https://www.vocabulary.com/dictionary/indoctrination' target='_blank' rel="nofollow">indoctrination</a>. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="zappa">Django w/ Zappa &amp; AWS Lambda</h2> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/django/zappa_400x400.svg" type="image/svg"> <!---<source srcset="/blog/images/django/zappa_400x400.avif" type="image/avif">--> <source srcset="/blog/images/django/zappa_400x400.webp" type="image/webp"> <source srcset="/blog/images/django/zappa_400x400.apng" type="image/apng"> <source srcset="/blog/images/django/zappa_400x400.png" type="image/png"> <source srcset="/blog/images/django/zappa_400x400.jpg" type="image/jpeg"> <source srcset="/blog/images/django/zappa_400x400.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/zappa_400x400.jfif" type="image/jpeg"> <source srcset="/blog/images/django/zappa_400x400.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/zappa_400x400.pjp" type="image/jpeg"> <source srcset="/blog/images/django/zappa_400x400.gif" type="image/gif"> <source srcset="/blog/images/django/zappa_400x400.tif" type="image/tiff"> <source srcset="/blog/images/django/zappa_400x400.tiff" type="image/tiff"> <source srcset="/blog/images/django/zappa_400x400.bmp" type="image/bmp"> <source srcset="/blog/images/django/zappa_400x400.ico" type="image/x-icon"> <source srcset="/blog/images/django/zappa_400x400.cur" type="image/x-icon"> <img alt='Don't eat the yellow snow' class="imgImg rounded shadow" src="/blog/images/django/zappa_400x400.png" style='width: 100%; ' title='Don't eat the yellow snow' /> </picture> </div> <p> <a href='https://github.com/zappa/Zappa' target='_blank' rel="nofollow">Zappa</a> is a popular library for serverless web hosting of Python webapps. Zappa allows Python WSGI webapps like Django to run on AWS Lambda instead of from within a container like AWS EC2. I am particularly interested in using Zappa to package and run <code>django-oscar</code> for AWS Lambda and CloudFront. </p> <p> Zappa can perform two primary functions: </p> <ol> <li> <b>Packaging</b> &ndash; Zappa can build a Django webapp into an AWS Lambda package. The package can be delivered via other mechanisms, for example mechanisms that are not even Python aware. </li> <li> <b>Deploying</b> &ndash; Zappa can deploy and Django webapp to AWS Lambda, and configure several AWS services to feed events to the Django webapp. </li> </ol> <div class="quote"> Zappa does not provide a means to define additional resources as part of the overall infrastructure. It is also somewhat rigid in how it defines certain resources which can lead to friction when incorporating Zappa within organizations with more rigid requirements on cloud resource management. With Zappa, you are better off allowing it to manage all the pieces needed for your web application on its own and manage other resources with a separate tool such as stacker or Terraform. <br><br> &hellip; or use Zappa's <code>package</code> command to create an archive that is ready for upload to lambda and utilize the other helpful functions the project provides for use after code is deployed. <br><br> &nbsp; &ndash; from <a href='https://www.jbssolutions.com/resources/blog/evolution-maintainable-lambda-development-pt-2/' target='_blank' rel="nofollow">The Evolution of Maintainable Lambda Development Pt 2</a> by JBS Custom Software Solutions. </div> <p> The Zappa documentation is excellent. The project has some rough edges, but the new regime coming on board seem competent and fired up. They have some work ahead to set things straight, but the technical path seems clear. </p> <p> I think this project deserves special attention. Lots of moldy issues and PRs need to be processed, which a small team could get done fairly quickly. The project might also benefit from someone to hone the messaging. I opened an <a href='https://github.com/zappa/Zappa/issues/968' target='_blank' rel="nofollow">issue on the Zappa GitHub microsite</a> to discuss this. </p> <p> This seminal project has been around several years, and other well-known projects that have been developed since Zappa was first released have acknowledged that Zappa provided inspiration. Time to brush it up and set it straight again; its best days lie ahead! </p> <p> Edgar Roman wrote this helpful document: <a href='https://romandc.com/zappa-django-guide/' target='_blank' rel="nofollow">Guide to using Django with Zappa</a>. </p> <p> I've messing around with Zappa, will report back. </p> <h3 id="videos">Videos</h3> <p> <a href='https://www.google.com/search?client=firefox-b-d&q=aws+zappa+django+video' target='_blank' rel="nofollow">Videos of Zappa exist</a>. </p> <ul> <li> This video has got all the right technologies mixed together for me: <a href='https://www.youtube.com/watch?v=Gf0vpJQZeBI' target='_blank' rel="nofollow">Serverless Deployment of a Django Project with AWS Lambda, Zappa, S3 and PostgreSQL</a>. </li> </ul> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="djambda">Djambda / AWS Lambda / Terraform</h2> <p> Terraform does not impose a runtime dependency unless the realtime orchestration features are used. </p> <p> <a href='https://github.com/netsome/djambda' target='_blank' rel="nofollow">Djambda</a> is an example project setting up Django application in AWS Lambda managed by Terraform. I intend to play with it and write up my experience right here Real Soon Now. </p> <p> This project uses GitHub Actions to create environments for the master branch and pull requests. I wonder if this project can be used without GitHub actions? </p> <div class="quote"> [Terraform] does not provide an abstraction layer for the AWS, Azure, or Google Cloud. It does that deliberately, as you should embrace all aspects when using cloud - not extract a common denominator from the services delivered by the cloud provider. <br><br> &nbsp; &ndash; From <a href='https://awsmaniac.com/aws-cdk-why-not-terraform/' target='_blank' rel="nofollow">AWS CDK? Why not Terraform?</a> by Wojciech Gawroński. </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="serverless">Serverless Framework with WSGI</h2> <p> The <a href='https://www.serverless.com/plugins/serverless-wsgi' target='_blank' rel="nofollow">docs</a> describe Serverless WSGI as: </p> <div class="quote"> Serverless plugin to deploy WSGI applications (Flask/Django/Pyramid etc.) and bundle Python packages. </div> <p> I am concerned that the Serverless architecture requires an <a href='https://www.serverless.com/pricing/fair-use-policy/' target='_blank' rel="nofollow">ongoing runtime dependency</a> on the viability and good will of Serverless, Inc. Any hiccup on their part will immediately be felt by all their users. It would make me nervous to base daily operational infrastructure on this. </p> <!-- endregion --> <!-- #region --> <h3 id="bintray">Bintray and JCenter Went <i>Poof!</i></h3> <p> I do not want to rely upon online services from a software tool vendor to run my builds. The Scala community is still recovering from Bintray and JCenter shutting down. I had dozens of Scala libraries on Bintray. I do not plan to migrate them, they are gone from public access. </p> <div class="quote"> On February 3, 2021, JFrog announced that they will be shutting down Bintray and JCenter. A complete shutdown is planned for February 2022. <br><br> &ndash; <a href='https://blog.gradle.org/jcenter-shutdown' target='_blank' rel="nofollow">JCenter shutdown impact on Gradle builds</a> </div> <h3 id="free">Trading Autonomy for Minimal Convenience is a Poor Trade</h3> <p> Remember that free products are usually subject to change or termination without notice. Examples abound of many companies whose free (and non-free) products suddenly ceased. There is no need to assume this type of vulnerability, so I block my metaphoric ears to the siren sound that tempts trusting souls into assuming unnecessary dependencies, and I chose tooling that is completely under my control. </p> <div class="quote"> <h2>What happens if I exceed the fair use policy?</h2> <p> <i>From the <a href='https://www.serverless.com/pricing/' target='_blank' rel="nofollow">Serverless Pricing and Terms page</a>.</i> </p> We want to offer a lot of value for free so you can get your idea off the ground, before introducing any infrastructure cost. The intent of the fair use policy is to ensure that we can provide a high quality of service without incurring significant infrastructure costs. The great majority of users will fall well within range of the typical usage guidelines. While we reserve the right to throttle services if usage exceeds the fair use policy, we do not intend to do so as long as we can deliver a high quality of service without significant infrastructure costs. <br><br> If you anticipate your project will exceed these guidelines, please contact our support team. We’ll work with you on a plan which scales well. </div> <!-- endregion --> <h2 id="apprunner">AWS AppRunner</h2> <p> AWS just announced <a href='https://aws.amazon.com/apprunner/' target='_blank' rel="nofollow">AppRunner</a>. I wonder how suitable it is... </p> Merging a Remote File with a Local File 2021-04-12T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/12/merging-remote-file <!-- #region intro --> <p> Today I am once again re-installing WSL2 on one of my laptops. Seems that a Windows 10 installation&rsquo;s half-life is measured in months, after which time a reset is required. The reset preserves data, but not installed programs and not the WSL setup. </p> <p> When I set up an OS I often use a pre-existing system&rsquo;s files as templates for the new system&rsquo;s files. </p> <!-- endregion --> <!-- #region Meld --> <h2 id="meld">Meld</h2> <p> <a href='https://meldmerge.org/' target='_blank' rel="nofollow">Meld</a> is a fantastic, F/OSS file and directory merge tool. 2-way and 3-way merges are supported. Meld uses X Windows for its user interface. <a href='https://opticos.github.io/gwsl/' target='_blank' rel="nofollow">GWSL</a> makes it easy to run X apps on WSL and WSL2. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2e49fc0eefca'><button class='copyBtn' data-clipboard-target='#id2e49fc0eefca' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install meld</pre> </div> <p> Merging a remote file with a local file using Meld is easy once you know how. Unless the remote file system is mounted locally, Meld cannot be used to modify <i>remote</i> files and directories, just <i>local</i> files and directories. </p> <p> Following is the incantation I used to display my local <code>.profile</code> and interactively merge it with my profile on an Ubuntu Linux machine called <code>gojira</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida5d9d2e0f5d3'><button class='copyBtn' data-clipboard-target='#ida5d9d2e0f5d3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>meld ~/.profile &lt;(ssh mslinn@gojira cat .profile)&</pre> </div> <p> The above runs <code>ssh</code> in a subshell, logs in as <code>mslinn</code> to the machine called <code>gojira</code> and then displays the contents of <code>.profile</code> on <code>gojira</code>. Meld compares the output of <code>cat</code> with the local copy of <code>~/.profile</code>, and displays the differences: </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/mergeRemote/meld.svg" type="image/svg"> <!---<source srcset="/blog/images/mergeRemote/meld.avif" type="image/avif">--> <source srcset="/blog/images/mergeRemote/meld.webp" type="image/webp"> <source srcset="/blog/images/mergeRemote/meld.apng" type="image/apng"> <source srcset="/blog/images/mergeRemote/meld.png" type="image/png"> <source srcset="/blog/images/mergeRemote/meld.jpg" type="image/jpeg"> <source srcset="/blog/images/mergeRemote/meld.jpeg" type="image/jpeg"> <source srcset="/blog/images/mergeRemote/meld.jfif" type="image/jpeg"> <source srcset="/blog/images/mergeRemote/meld.pjpeg" type="image/jpeg"> <source srcset="/blog/images/mergeRemote/meld.pjp" type="image/jpeg"> <source srcset="/blog/images/mergeRemote/meld.gif" type="image/gif"> <source srcset="/blog/images/mergeRemote/meld.tif" type="image/tiff"> <source srcset="/blog/images/mergeRemote/meld.tiff" type="image/tiff"> <source srcset="/blog/images/mergeRemote/meld.bmp" type="image/bmp"> <source srcset="/blog/images/mergeRemote/meld.ico" type="image/x-icon"> <source srcset="/blog/images/mergeRemote/meld.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/mergeRemote/meld.png" style='width: 100%; ' /> </picture> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> Meld makes it easy to reconcile file versions. </p> <!-- endregion --> Visual Studio Code Workspace Settings 2021-04-11T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/11/svcode-workspace-settings <!-- #region intro --> <p> For me, the killer feature that Visual Studio Code is how it integrates the Windows user interface with working on WSL and WSL2. Programs residing on the active WSL OS image execute natively on that OS, while VSCode continues to run as a native Windows application. This is possible because VSCode installs a proxy on the target OS. The proxy does the bidding of the Windows executable. </p> <p> Getting a project to execute on the target OS instead of the host OS can be tricky. I have found that using a workspace to hold a collection of VSCode projects is very helpful, because the definition of the collection also defines how they are handled. </p> <p> WSL projects have different types of VSCode workspace entries than Windows entries do. They are easy to recognize and change once you know what to look for. The two possibile types of VSCode workspace project entries in a <code>.workspace</code> file are: </p> <ul> <li><b>WSL Project</b> &ndash; <code>"uri": "vscode-remote://wsl+ubuntu/path/to/vscode/project"</code></li> <li><b>Windows Project</b> &ndash; <code>"path": "C:\\path\\to\\vscode\\project"</code></li> </ul> <p> The following VSCode workspace file has both types of entries. For me, this is an error; I only want WSL projects. My task is to change the yellow highlighted Windows project and make it look like the other WSL projects. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>aw.workspace.code-workspace</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbb6992f4dd88'><button class='copyBtn' data-clipboard-target='#idbb6992f4dd88' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "folders": [ { "uri": "vscode-remote://wsl+ubuntu/var/sitesUbuntu/www.ancientwarmth.com" }, { "uri": "vscode-remote://wsl+ubuntu/var/work/django/django" }, { "uri": "vscode-remote://wsl+ubuntu/var/work/django/oscar" }, { "uri": "vscode-remote://wsl+ubuntu/var/work/ancientWarmth/ancientWarmth" }, { <span class="bg_yellow">"path": "../../var/work/django/main"</span> } ], "remoteAuthority": "wsl+Ubuntu", "settings": { "liveServer.settings.multiRootWorkspaceName": "www.mslinn.com", "python.pythonPath": "/var/work/django/oscar/bin/python", "git.ignoreLimitWarning": true, "sqltools.connections": [ { "previewLimit": 50, "server": "localhost", "port": 5432, "driver": "PostgreSQL", "name": "Ancient Warmth on Camille", "database": "ancient_warmth", "username": "postgres", "password": "hithere" } ] } }</pre> </div> <!-- endregion --> <p> All I need to do is change this entry: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>aw.workspace.code-workspace snippet</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc4b497601f56'><button class='copyBtn' data-clipboard-target='#idc4b497601f56' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class="bg_yellow">"path": "../..</span>/var/work/django/main"</pre> </div> <p>To:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>aw.workspace.code-workspace snippet</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' style='margin-bottom: 1em;' id='id4e1f6a302ad2'><button class='copyBtn' data-clipboard-target='#id4e1f6a302ad2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class="bg_yellow">"uri": "vscode-remote://wsl+ubuntu</span>/var/var/work/django/main"</pre> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> The modified entry will cause VSCode to launch the project from WSL, instead of Windows. </p> A Python Virtual Environment For Every Project 2021-04-09T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/09/python-venvs <!-- #region intro --> <p> Python virtual environments are cheap to make and use &ndash; unless you are unfortunate enough to program in native Windows. I have adopted the habit of making a Python virtual environment (<i>venv</i>) for each significant Python project, plus a default venv for trivial Python work. </p> <!-- endregion --> <!-- #region Why Virtualize Python --> <h2 id="why">Why Virtualize Python?</h2> <div class='imgWrapper imgFlex right' style='width: 40%; '> <picture class='imgPicture'> <source srcset="/blog/images/node/pushback.svg" type="image/svg"> <!---<source srcset="/blog/images/node/pushback.avif" type="image/avif">--> <source srcset="/blog/images/node/pushback.webp" type="image/webp"> <source srcset="/blog/images/node/pushback.apng" type="image/apng"> <source srcset="/blog/images/node/pushback.png" type="image/png"> <source srcset="/blog/images/node/pushback.jpg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.jpeg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.jfif" type="image/jpeg"> <source srcset="/blog/images/node/pushback.pjpeg" type="image/jpeg"> <source srcset="/blog/images/node/pushback.pjp" type="image/jpeg"> <source srcset="/blog/images/node/pushback.gif" type="image/gif"> <source srcset="/blog/images/node/pushback.tif" type="image/tiff"> <source srcset="/blog/images/node/pushback.tiff" type="image/tiff"> <source srcset="/blog/images/node/pushback.bmp" type="image/bmp"> <source srcset="/blog/images/node/pushback.ico" type="image/x-icon"> <source srcset="/blog/images/node/pushback.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/node/pushback.png" style='width: 100%; ' /> </picture> </div> <p> It is better to use virtualized user- and project-specific Python instances, instead of working with a system-wide installation of Python. This allows you to install and upgrade Python packages without using supervisor privileges. Also, virtualized instances allows you to work on many different independent Python projects at the same time, without package version collisions. </p> <p> <code>Docker</code> is over-sold. It adds unnecessary complexity to software projects. Instead of virtualizing the entire software environment, as <code>docker</code> attempts to do, virtualizing the Python programming environment as described in this article, <a href='/ruby/1000-ruby-setup.html'>Ruby <code>rbenv</code></a>, or <a href='/blog/2022/03/01/node-package-managers.html'><code>node.js nvm</code></a> are much easier and more productive approaches. </p> <p> I think <code>docker</code> has been pushed hard in the media because it is a gateway technology to <a href='https://www.infoworld.com/article/3223434/what-is-paas-platform-as-a-service-a-simpler-way-to-build-software-applications.html' target='_blank' rel="nofollow">PaSS</a>. This is a trend that PaSS vendors like AWS and Azure want to encourage, but <a href='https://www.datacenterdynamics.com/en/news/37signals-spent-more-than-3-million-on-the-cloud-in-2022-for-basecamp-and-hey' target='_blank' rel="nofollow">customers are pushing back</a>. </p> <!-- endregion --> <!-- #region Pythons venv Virtualization Module --> <h2 id="deprecated1">Python&rsquo;s <span class="code">venv</span> Virtualization Module</h2> <p> <code>Venv</code> is a tool to create isolated virtual Python environments. It has been included with Python since Python v3.3, which was released 11 years ago. </p> <p> <a href='https://peps.python.org/pep-0405/#motivation' target='_blank' rel="nofollow">PEP 405</a> specifies Python&rsquo;s <a href='https://docs.python.org/3/library/venv.html' target='_blank' rel="nofollow"><code>venv</code></a> virtualization module. </p> <!-- endregion --> <!-- #region Deprecated Virtualization Modules --> <h2 id="deprecated2">Deprecated Virtualization Modules</h2> <p> Python 3.6 was released 7 years ago. It deprecated the other virtualization modules, <a href='https://github.com/pyenv/pyenv' target='_blank' rel="nofollow"><code>pyenv</code></a> and <a href='https://virtualenv.pypa.io/en/latest/' target='_blank' rel="nofollow"><code>virtualenv</code></a>. Instead, use <a href='https://docs.python.org/3/library/venv.html' target='_blank' rel="nofollow"><code>venv</code></a>, as described in this article. </p> <!-- endregion --> <!-- #region Extra Installation Steps for Ubuntu --> <h2 id="installubuntu">Extra Installation Steps for Ubuntu</h2> <p> <code>Venv</code> was included with Python on Ubuntu until Ubuntu 23.04 It virtualizes all the common Python executables. </p> <p> Debian <a href='https://www.omgubuntu.co.uk/2023/04/pip-install-error-externally-managed-environment-fix' target='_blank' rel="nofollow">changed <code>pip</code>&rsquo;s behavior</a> as a result of <a href='https://peps.python.org/pep-0668/#implementation-notes' target='_blank' rel="nofollow">PEP 668 &ndash; Marking Python base environments as &ldquo;externally managed&rdquo;</a>. This affects Ubuntu because it is <a href='https://en.wikipedia.org/wiki/Downstream_(software_development)' target='_blank' rel="nofollow">downstream</a>. Starting with Ubuntu 23.04, you need to install <code>venv</code> by typing: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id42863aeb3357'><button class='copyBtn' data-clipboard-target='#id42863aeb3357' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.xx-venv</pre> </div> <!-- endregion --> <p> ... where <code>xx</code> is the minor version number of Python that is installed. </p> <p> For Python 3.12, provided with Ubuntu 24.04 (Noble Numbat), the following incantation is required: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8f46d6164ae4'><button class='copyBtn' data-clipboard-target='#id8f46d6164ae4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.12-venv</pre> </div> <p> For Python 3.11, provided with Ubuntu 23.10 (Mantic Minotaur), the following incantation is required: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id30e464756972'><button class='copyBtn' data-clipboard-target='#id30e464756972' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.11-venv</pre> </div> <p> Some software projects use <a href='https://help.ubuntu.com/community/MetaPackages' target='_blank' rel="nofollow">meta-packages</a> to specify version-agnostic dependencies. Perhaps Python on Ubuntu will do so in the future. </p> <p> In order to virtualize <code>pip</code>, <code>venv</code> needs to invoke <code>ensurepip</code>, which is <a href='https://askubuntu.com/a/897004/58760' target='_blank' rel="nofollow">not installed by default on Ubuntu</a>. On Ubuntu systems, the <code>ensurepip</code> command is provided by a package called <code>python3-pip</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide80c4b2603ee'><button class='copyBtn' data-clipboard-target='#ide80c4b2603ee' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3-pip</pre> </div> <p> You could install both of the above packages together, of course: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id83a7b8ec0562'><button class='copyBtn' data-clipboard-target='#id83a7b8ec0562' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.12-venv python3-pip</pre> </div> <!-- endregion --> <!-- #region Manually Creating a VEnv --> <h2 id="manual">Manually Creating a VEnv</h2> <p> The following demonstrates how to create a new virtual python environment in the <code>~/venv/default/</code> directory. Intermediate directories, such as <code>venv</code> in this example, will be created as required. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id93b18bd89e3d'><button class='copyBtn' data-clipboard-target='#id93b18bd89e3d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python3 -m venv ~/venv/default</pre> </div> <p> At this point, the virtual environment just contained executable images for Python. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8ccf2f1cad4e'><button class='copyBtn' data-clipboard-target='#id8ccf2f1cad4e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls ~/venv/default/** <span class='unselectable'>/home/mslinn/venv/default/lib64@ /home/mslinn/venv/default/pyvenv.cfg /home/mslinn/venv/default/bin: Activate.ps1 activate activate.csh activate.fish pip* pip3* pip3.10* python@ python3@ python3.10@ /home/mslinn/venv/default/include: /home/mslinn/venv/default/lib: python3.10/ </span></pre> </div> <!-- endregion --> <!-- #region Upgrading Python --> <h2 id="pyup">Upgrading Python</h2> <p> Re-installing the <code>venv</code> at a later date will update the version of Python in the <code>venv</code>. You will also need to install the appropriate <code>python3-venv</code> package when upgrading Python. </p> <p> Each time you (re)install a virtual environment, all <code>pip</code> packages are removed. To prevent the loss of your installed <code>pip</code> packages, use <code>pip freeze</code> before upgrading Ubuntu, so the pip packages are memorialized in <code>requirements.txt</code>. After upgrading Python, run <code>pip install</code> to re-install the <code>pip</code> packages. </p> <p> The following incantation runs <code>pip freeze</code> on every directory under <code>~/venv</code>, and creates a file called <code>requirements.txt</code> in each <code>venv</code> directory: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfa569691ce07'><button class='copyBtn' data-clipboard-target='#idfa569691ce07' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>find ~/venv -maxdepth 1 -mindepth 1 -type d -print0 | while IFS= read -r -d '' DIR; do source "$DIR/bin/activate" pip freeze > "$DIR/requirements.txt" done</pre> </div> <p class="alert rounded shadow"> The above incantation should be performed for each of your Python venvs before you upgrade Ubuntu, because sometimes Python venvs do not work after an upgrade. The next step should fix Python venvs that broke during an Ubuntu upgrade. </p> <p> Here is an example showing the commands that must be typed to upgrade Python from v3.11 to v3.12, which you will have to do when upgrading from Ubuntu 23.10 to Ubuntu 24.04. Scroll the code window to see all the commands: </p> <!-- #region pre --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id57334f69d9e1'><button class='copyBtn' data-clipboard-target='#id57334f69d9e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.12-venv <span class='unselectable'>Reading package lists... Done Building dependency tree... Done Reading state information... Done The following NEW packages will be installed: python3.12-venv 0 upgraded, 1 newly installed, 0 to remove and 4 not upgraded. Need to get 5676 B of archives. After this operation, 28.7 kB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu noble/universe amd64 python3.12-venv amd64 3.12.3-1 [5676 B] Fetched 5676 B in 0s (31.1 kB/s) Selecting previously unselected package python3.12-venv. (Reading database ... 444385 files and directories currently installed.) Preparing to unpack .../python3.12-venv_3.12.3-1_amd64.deb ... Unpacking python3.12-venv (3.12.3-1) ... Setting up python3.12-venv (3.12.3-1) ... Scanning processes...<br> No services need to be restarted.<br> No containers need to be restarted.<br> No user sessions are running outdated binaries.<br> No VM guests are running outdated hypervisor (qemu) binaries on this host. </span><br> <span class='unselectable'>$ </span>find ~/venv -maxdepth 1 -type d -print0 | while IFS= read -r -d '' DIR; do python3 -m venv "$DIR" source "$DIR/bin/activate" pip install -r "$DIR/requirements.txt" done</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region VEnvs are Nearly Free --> <h2 id="free">VEnvs are Nearly Free</h2> <p> The cost of a venv is virtually free for all OSes except native Windows. This is because on all OSes except native Windows, the executable images are linked by default, so they do not require much storage space. </p> <p> The <code>ls</code> command below shows that the <code>python</code> program in the <code>default</code> venv is linked to <code>/usr/bin/python3.10</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf00f3e6376d4'><button class='copyBtn' data-clipboard-target='#idf00f3e6376d4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -go ~/venv/default/bin/python <span class='unselectable'>lrwxrwxrwx 1 18 Apr 9 06:01 <span class="bg_yellow">/home/mslinn/venv/default/bin/python -> python3*</span> </span></pre> </div> <!-- endregion --> <!-- #region Create a VEnv for Every Python Project --> <h2 id="standard">Create a VEnv for Every Python Project</h2> <p> My projects are stored under the directory pointed to by <code>$work</code>. </p> <p> My standard procedure when making a Python project called <code>$work/blah</code> is to also create a venv for it at within the project, at <code>$work/blah/.venv/</code>. I add an entry to <code>.gitignore</code> that merely consists of a line that says <code>.venv/</code>. </p> <p> A bash alias could be defined called <code>blah</code> that makes the project directory current and activates the venv: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.bash_aliases</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' style='margin-bottom: 1em;' id='id1c9d01264741'><button class='copyBtn' data-clipboard-target='#id1c9d01264741' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>alias blah="cd $work/blah; source ./venv/bin/activate"</pre> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> Now you could type <code>blah</code> at a shell prompt, and you would be working on that project. Boom! </p> <!-- endregion --> <!-- #region Deactivate a VEnv --> <h2 id="deactivate">Deactivate a VEnv</h2> <p> Stop using venvs with <code>deactivate</code>. Notice that the prompt changes. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9688e87b9010'><button class='copyBtn' data-clipboard-target='#id9688e87b9010' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>(aw) $ </span>deactivate <span class='unselectable'>$ </span></pre> </div> <!-- endregion --> <!-- #region Directory-Locked Python Virtualization --> <h2 id="virt" class="clear">Directory-Locked Python Virtualization</h2> <p> After setting up a Python virtual environment, a quick examination of the <code>pip</code> script shows that it is hard-coded to the directory that it was made for: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idab1814ff24a3'><button class='copyBtn' data-clipboard-target='#idab1814ff24a3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>head -n 1 ~/venv/aw/bin/pip <span class='unselectable'><span class="bg_yellow">#!/home/mslinn/venv/aw/bin/python</span> </span></pre> </div> <p> For virtualized environments, such as Docker, this means that a Python virtual environment created without Docker can only be used within a Docker image if the path to it is the same from within the Docker image as when it was created. </p> <!-- endregion --> <!-- #region Poetry and Python Virtual Environments --> <h2 id="pvenv">Poetry and Python Virtual Environments</h2> <!-- #region implicit --> <p> Python venvs are a good way to isolate Python programs from each other, so their dependencies do not clash. </p> <p> The two best-known package managers for Python are <a href='https://pypi.org/project/pip/' target='_blank' rel="nofollow"><code>PIP</code></a> and <a href='https://python-poetry.org/' target='_blank' rel="nofollow">Poetry</a>. <code>PIP</code> is bundled with Python, while Poetry needs to be explicitly installed. </p> <p> Python venvs use links to provide virtualized Python commands. Inside each <code>venv</code>, you will find links to programs like <code>python</code>, <code>pip</code> and library dependencies for every project that installs dependencies while the <code>venv</code> is active. </p> <p> Poetry can: </p> <ol> <li>Create a venv in <code>.venv</code></li> <li>Install specified library dependencies</li> <li>Run the program using the venv dependencies</li> <li> <a href='https://www.freecodecamp.org/news/how-to-build-and-publish-python-packages-with-poetry/' target='_blank' rel="nofollow">Publish</a> the Python package. </li> </ol> <!-- endregion --> <!-- #region Installing Poetry --> <h3 id="pi">Installing Poetry</h3> <p> If you are not careful, installing Poetry could be a <a href='https://en.wikipedia.org/wiki/Chicken_or_the_egg' target='_blank' rel="nofollow">chicken-and-egg</a> race condition. You do not want Poetry to be a dependency of the <code>venv</code> that it manages, because it is designed to manage the libraries within the <code>venv</code>. </p> <p> Poetry has a custom installer that creates a dedicated virtual environment for itself, and operates independently from other Python-related mechanisms. This isolated environment prevents unintentional upgrades or removals of dependencies, allowing Poetry to manage its own dependencies properly. </p> <p> While it is possible to install Poetry using PIP within a venv, it is better to install Poetry system-wide. The <a href='#poetry_setup'>Setup</a> section below discusses how to do that. </p> <p> PIP dependencies are often specified in a file called <code>requirements.txt</code>. This file ensures that a Python project uses the correct versions of dependencies by listing all the packages and their versions. Poetry uses a file called <code>poetry.lock</code> for the same purpose. In addition to the names and versions of the dependencies, <code>poetry.lock</code> also specifies the hashes of the dependant source files. </p> <!-- endregion --> <!-- #region Poetry Help Messages --> <h3 id="poetry_help">Poetry Help Messages</h3> <p> The top-level help message for Poetry is: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb4ca901a212d'><button class='copyBtn' data-clipboard-target='#idb4ca901a212d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry --help <span class='unselectable'>Description: Lists commands.<br/> Usage: list [options] [--] [&lt;namespace&gt;]<br/> Arguments: namespace The namespace name<br/> Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. --no-plugins Disables plugins. --no-cache Disables Poetry source caches. -C, --directory=DIRECTORY The working directory for the Poetry command (defaults to the current working directory). -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.<br/> Help: The list command lists all commands:<br/> poetry list<br/> You can also display the commands for a specific namespace:<br/> poetry list test </span></pre> </div> <!-- endregion --> <p> The Poetry commands are: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idddebb6cf9860'><button class='copyBtn' data-clipboard-target='#idddebb6cf9860' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry list <span class='unselectable'>Poetry (version 1.7.1)<br/> Usage: command [options] [arguments]<br/> Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. --no-plugins Disables plugins. --no-cache Disables Poetry source caches. -C, --directory=DIRECTORY The working directory for the Poetry command (defaults to the current working directory). -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.<br/> Available commands: about Shows information about Poetry. add Adds a new dependency to pyproject.toml. build Builds a package, as a tarball and a wheel by default. check Validates the content of the pyproject.toml file and its consistency with the poetry.lock file. config Manages configuration settings. export Exports the lock file to alternative formats. help Displays help for a command. init Creates a basic pyproject.toml file in the current directory. install Installs the project dependencies. list Lists commands. lock Locks the project dependencies. new Creates a new Python project at &lt;path&gt;. publish Publishes a package to a remote repository. remove Removes a package from the project dependencies. run Runs a command in the appropriate environment. search Searches for packages on remote repositories. shell Spawns a shell within the virtual environment. show Shows information about packages. update Update the dependencies as according to the pyproject.toml file. version Shows the version of the project or bumps it when a valid bump rule is provided.<br/> cache cache clear Clears a Poetry cache by name. cache list List Poetry&#39;s caches.<br/> debug debug info Shows debug information. debug resolve Debugs dependency resolution.<br/> env env info Displays information about the current environment. env list Lists all virtualenvs associated with the current project. env remove Remove virtual environments associated with the project. env use Activates or creates a new virtualenv for the current project.<br/> self self add Add additional packages to Poetry&#39;s runtime environment. self install Install locked packages (incl. addons) required by this Poetry installation. self lock Lock the Poetry installation&#39;s system requirements. self remove Remove additional packages from Poetry&#39;s runtime environment. self show Show packages from Poetry&#39;s runtime environment. self show plugins Shows information about the currently installed plugins. self update Updates Poetry to the latest version.<br/> source source add Add source configuration for project. source remove Remove source configured for the project. source show Show information about sources configured for the project. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Detecting a venv --> <h3 id="detect">Detecting a Venv</h3> <p class="alert rounded shadow"> Active virtual environments suppress Poetry&rsquo;s behavior. </p> <p> For example: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfe3172244bb0'><button class='copyBtn' data-clipboard-target='#idfe3172244bb0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry shell <span class='unselectable'>Virtual environment already activated: /home/mslinn/venv/default </span></pre> </div> <!-- endregion --> <p> If you want to use Poetry on a project, deactivate any active virtual environment before attempting to work on the project. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8a001a75bc0b'><button class='copyBtn' data-clipboard-target='#id8a001a75bc0b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>deactivate</pre> </div> <!-- endregion --> <p> Here is a fragment of a script that detects if a venv is active, and disables it if so: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Bash script fragment</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id72eb3b07640f'><button class='copyBtn' data-clipboard-target='#id72eb3b07640f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Disable any activated virtual environment VENV="$( dirname "$( which virtualenv)" )" if [ -d "$VENV" ]; then source "$VENV/activate" && deactivate; fi</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Setup --> <h3 id="poetry_setup">Setup</h3> <!-- #region implicit --> <p> The optimal process for setting up Poetry for use in a project is: </p> <ol> <li> Install Poetry, preferably system-wide. I used <code>apt</code> to install the <code>poetry</code> package.<br> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idec4764b3fbc8'><button class='copyBtn' data-clipboard-target='#idec4764b3fbc8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3-poetry</pre> </div> <!-- endregion --> </li> <li> Configure Poetry to manage the libraries within the <code>venv</code>. The <a href='#poetry_config'><code>poetry config</code> help message</a> is shown below. </li> <li> Use Poetry to install the library dependencies. </li> <li> Use Poetry to run the program using the dependencies in the venv, and/or to spawn a command-line subshell that provides access to the dependencies managed by Poetry. </li> </ol> <!-- endregion --> <!-- #region misc poetry help messages --> <p id="poetry_config"> The help information for the <code>poetry config</code> command is: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0d8d6be160e2'><button class='copyBtn' data-clipboard-target='#id0d8d6be160e2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry config --help <span class='unselectable'>Description: Manages configuration settings.<br/> Usage: config [options] [--] [&lt;key&gt; [&lt;value&gt;...]]<br/> Arguments: key Setting key. value Setting value.<br/> Options: --list List configuration settings. --unset Unset configuration setting. --local Set/Get from the project&#39;s local configuration. -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. --no-plugins Disables plugins. --no-cache Disables Poetry source caches. -C, --directory=DIRECTORY The working directory for the Poetry command (defaults to the current working directory). -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.<br/> Help: This command allows you to edit the poetry config settings and repositories.<br/> To add a repository:<br/> poetry config repositories.foo https://bar.com/simple/<br/> To remove a repository (repo is a short alias for repositories):<br/> poetry config --unset repo.foo </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region Initializing the Poetry .venv --> <h3 id="pini">Initializing the Poetry <span class="code">.venv</span></h3> <p> Imagine you have a Python project stored in <code>$my_project</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb1a97e68c5e0'><button class='copyBtn' data-clipboard-target='#idb1a97e68c5e0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd $my_project</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region poetry install --> <h3 id="pinst"><span class="code">poetry install</span></h3> <p> The <code>poetry install</code> subcommand installs dependencies specified in <code>poetry.lock</code>, within the venv. </p> <p id="poetry_install"> The help information for the <code>poetry install</code> command is: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id083698250855'><button class='copyBtn' data-clipboard-target='#id083698250855' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry install --help <span class='unselectable'>Description: Installs the project dependencies.<br/> Usage: install [options]<br/> Options: --without=WITHOUT The dependency groups to ignore. (multiple values allowed) --with=WITH The optional dependency groups to include. (multiple values allowed) --only=ONLY The only dependency groups to include. (multiple values allowed) --no-dev Do not install the development dependencies. (Deprecated) --sync Synchronize the environment with the locked packages and the specified groups. --no-root Do not install the root package (the current project). --no-directory Do not install any directory path dependencies; useful to install dependencies without source code, e.g. for caching of Docker layers) --dry-run Output the operations but do not execute anything (implicitly enables --verbose). --remove-untracked Removes packages not present in the lock file. (Deprecated) -E, --extras=EXTRAS Extra sets of dependencies to install. (multiple values allowed) --all-extras Install all extra dependencies. --only-root Exclude all dependencies. --compile Compile Python source files to bytecode. (This option has no effect if modern-installation is disabled because the old installer always compiles.) -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. --no-plugins Disables plugins. --no-cache Disables Poetry source caches. -C, --directory=DIRECTORY The working directory for the Poetry command (defaults to the current working directory). -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.<br/> Help: The install command reads the poetry.lock file from the current directory, processes it, and downloads and installs all the libraries and dependencies outlined in that file. If the file does not exist it will look for pyproject.toml and do the same.<br/> poetry install<br/> By default, the above command will also install the current project. To install only the dependencies and not including the current project, run the command with the --no-root option like below:<br/> poetry install --no-root </span></pre> </div> <!-- endregion --> <p> Here is an example of using <code>poetry install</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id62102dbe2a6a'><button class='copyBtn' data-clipboard-target='#id62102dbe2a6a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry install --with local <span class='unselectable'>Creating virtualenv private-gpt in /mnt/f/work/my_project/.venv </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Created Files --> <h3 id="pinst">Created Files</h3> <p> The first time that Poetry does something, e.g. when the <code>poetry install</code> or <code>poetry shell</code> subcommands are run, Poetry creates a file in the project directory called <code>poetry.toml</code>. As an example, if your Python project is stored in <code>/mnt/<wbr>c/<wbr>work/<wbr>my_project/</code>, <code>poetry.toml</code> will contain: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/mnt/c/work/my_project/poetry.toml</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbcc431ddc59d'><button class='copyBtn' data-clipboard-target='#idbcc431ddc59d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[virtualenvs] create = true in-project = true path = "/mnt/c/work/my_project/.venv"</pre> </div> <!-- endregion --> <p> A file called <code>.venv/pyvenv.cfg</code> is also created, which looks like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/mnt/c/work/my_project/.venv/pyvenv.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id798ad1d48481'><button class='copyBtn' data-clipboard-target='#id798ad1d48481' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>home = /usr/bin implementation = CPython version_info = 3.11.6.final.0 virtualenv = 20.24.1+ds include-system-site-packages = false base-prefix = /usr base-exec-prefix = /usr base-executable = /usr/bin/python3 prompt = my_project-py3.11</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region poetry run --> <h3 id="prun"><span class="code">poetry run</span></h3> <p> The <code>poetry run</code> subcommand runs Python programs and scripts with the virtual environment managed by Poetry. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf461053a3e3c'><button class='copyBtn' data-clipboard-target='#idf461053a3e3c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry run python my_program</pre> </div> <!-- endregion --> <p> If the first line of a script is the <a href='https://stackoverflow.com/a/19305076/553865' target='_blank' rel="nofollow">shebang for running Python</a>, like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>scripts/setup shebang</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcdbdbbd91242'><button class='copyBtn' data-clipboard-target='#idcdbdbbd91242' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/usr/bin/env python3</pre> </div> <!-- endregion --> <p> ... then we do not need to specify <code>python</code> or <code>python3</code> on the command line. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide0c019c59539'><button class='copyBtn' data-clipboard-target='#ide0c019c59539' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry run my_script_containg_shebang</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region poetry shell --> <h3 id="poetry_shell"><span class="code">poetry shell</span></h3> <p> You could <a href='https://python-poetry.org/docs/basic-usage/#activating-the-virtual-environment' target='_blank' rel="nofollow">spawn a subshell</a> that provides access to the dependencies managed by Poetry with the <code>poetry shell</code> subcommand: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6f1e050860f8'><button class='copyBtn' data-clipboard-target='#id6f1e050860f8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry shell <span class='unselectable'>Creating virtualenv my_project in /mnt/f/work/evelyn/my_project/.venv Spawning shell within /mnt/f/work/my_project/.venv . /mnt/f/work/my_project/.venv/bin/activate . /mnt/f/work/my_project/.venv/bin/activate </span></pre> </div> <!-- endregion --> <p> To deactivate the virtual environment, and exit this new shell, type <kbd>CTRL</kbd>-<kbd>D</kbd> or <code>exit</code>. </p> <!-- endregion --> <!-- #region poetry show --tree --> <h3 id="poetry_tree"><span class="code">poetry show --tree</span></h3> <p> You can view all the dependencies for a Poetry project like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0589d5514cc8'><button class='copyBtn' data-clipboard-target='#id0589d5514cc8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry show --tree <span class='unselectable'>black 22.12.0 The uncompromising code formatter. ├── click >=8.0.0 │ └── colorama * ├── mypy-extensions >=0.4.3 ├── pathspec >=0.9.0 └── platformdirs >=2 boto3 1.34.2 The AWS SDK for Python ├── botocore >=1.34.2,<1.35.0 │ ├── jmespath >=0.7.1,<2.0.0 │ ├── python-dateutil >=2.1,<3.0.0 │ │ └── six >=1.5 │ └── urllib3 >=1.25.4,<2.1 ├── jmespath >=0.7.1,<2.0.0 └── s3transfer >=0.9.0,<0.10.0 └── botocore >=1.33.2,<2.0a.0 ├── jmespath >=0.7.1,<2.0.0 ├── python-dateutil >=2.1,<3.0.0 │ └── six >=1.5 └── urllib3 >=1.25.4,<2.1 chromadb 0.4.20 Chroma. ├── bcrypt >=4.0.1 ├── chroma-hnswlib 0.7.3 │ └── numpy * ├── fastapi >=0.95.2 │ ├── anyio >=3.7.1,<4.0.0 │ │ ├── idna >=2.8 │ │ └── sniffio >=1.1 │ ├── email-validator >=2.0.0 │ │ ├── dnspython >=2.0.0 │ │ └── idna >=2.0.0 (circular dependency aborted here) │ ├── httpx >=0.23.0 │ │ ├── anyio * (circular dependency aborted here) │ │ ├── certifi * │ │ ├── h2 >=3,<5 │ │ │ ├── hpack >=4.0,<5 │ │ │ └── hyperframe >=6.0,<7 │ │ ├── httpcore ==1.* │ │ │ ├── certifi * (circular dependency aborted here) │ │ │ └── h11 >=0.13,<0.15 │ │ ├── idna * (circular dependency aborted here) │ │ └── sniffio * (circular dependency aborted here) │ ├── itsdangerous >=1.1.0 │ ├── jinja2 >=2.11.2 │ │ └── markupsafe >=2.0 │ ├── orjson >=3.2.1 │ ├── pydantic >=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0 │ │ ├── annotated-types >=0.4.0 │ │ ├── pydantic-core 2.14.5 │ │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ │ └── typing-extensions >=4.6.1 (circular dependency aborted here) │ ├── pydantic-extra-types >=2.0.0 │ │ └── pydantic >=2.0.3 (circular dependency aborted here) │ ├── pydantic-settings >=2.0.0 │ │ ├── pydantic >=2.3.0 (circular dependency aborted here) │ │ └── python-dotenv >=0.21.0 │ ├── python-multipart >=0.0.5 │ ├── pyyaml >=5.3.1 │ ├── starlette >=0.27.0,<0.28.0 │ │ └── anyio >=3.4.0,<5 (circular dependency aborted here) │ ├── typing-extensions >=4.5.0 (circular dependency aborted here) │ ├── ujson >=4.0.1,<4.0.2 || >4.0.2,<4.1.0 || >4.1.0,<4.2.0 || >4.2.0,<4.3.0 || >4.3.0,<5.0.0 || >5.0.0,<5.1.0 || >5.1.0 │ └── uvicorn >=0.12.0 │ ├── click >=7.0 │ │ └── colorama * │ ├── colorama >=0.4 (circular dependency aborted here) │ ├── h11 >=0.8 (circular dependency aborted here) │ ├── httptools >=0.5.0 │ ├── python-dotenv >=0.13 (circular dependency aborted here) │ ├── pyyaml >=5.1 (circular dependency aborted here) │ ├── uvloop >=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1 │ ├── watchfiles >=0.13 │ │ └── anyio >=3.0.0 (circular dependency aborted here) │ └── websockets >=10.4 ├── grpcio >=1.58.0 ├── importlib-resources * ├── kubernetes >=28.1.0 │ ├── certifi >=14.05.14 │ ├── google-auth >=1.0.1 │ │ ├── cachetools >=2.0.0,<6.0 │ │ ├── pyasn1-modules >=0.2.1 │ │ │ └── pyasn1 >=0.4.6,<0.6.0 │ │ └── rsa >=3.1.4,<5 │ │ └── pyasn1 >=0.1.3 (circular dependency aborted here) │ ├── oauthlib >=3.2.2 │ ├── python-dateutil >=2.5.3 │ │ └── six >=1.5 │ ├── pyyaml >=5.4.1 │ ├── requests * │ │ ├── certifi >=2017.4.17 (circular dependency aborted here) │ │ ├── charset-normalizer >=2,<4 │ │ ├── idna >=2.5,<4 │ │ └── urllib3 >=1.21.1,<3 │ ├── requests-oauthlib * │ │ ├── oauthlib >=3.0.0 (circular dependency aborted here) │ │ └── requests >=2.0.0 (circular dependency aborted here) │ ├── six >=1.9.0 (circular dependency aborted here) │ ├── urllib3 >=1.24.2,<2.0 (circular dependency aborted here) │ └── websocket-client >=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0 ├── mmh3 >=4.0.1 ├── numpy >=1.22.5 ├── onnxruntime >=1.14.1 │ ├── coloredlogs * │ │ └── humanfriendly >=9.1 │ │ └── pyreadline3 * │ ├── flatbuffers * │ ├── numpy >=1.21.6 │ ├── packaging * │ ├── protobuf * │ └── sympy * │ └── mpmath >=0.19 ├── opentelemetry-api >=1.2.0 │ ├── deprecated >=1.2.6 │ │ └── wrapt >=1.10,<2 │ └── importlib-metadata >=6.0,<7.0 │ └── zipp >=0.5 ├── opentelemetry-exporter-otlp-proto-grpc >=1.2.0 │ ├── backoff >=1.10.0,<3.0.0 │ ├── deprecated >=1.2.6 │ │ └── wrapt >=1.10,<2 │ ├── googleapis-common-protos >=1.52,<2.0 │ │ └── protobuf >=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0 │ ├── grpcio >=1.0.0,<2.0.0 │ ├── opentelemetry-api >=1.15,<2.0 │ │ ├── deprecated >=1.2.6 (circular dependency aborted here) │ │ └── importlib-metadata >=6.0,<7.0 │ │ └── zipp >=0.5 │ ├── opentelemetry-exporter-otlp-proto-common 1.21.0 │ │ ├── backoff >=1.10.0,<3.0.0 (circular dependency aborted here) │ │ └── opentelemetry-proto 1.21.0 │ │ └── protobuf >=3.19,<5.0 (circular dependency aborted here) │ ├── opentelemetry-proto 1.21.0 (circular dependency aborted here) │ └── opentelemetry-sdk >=1.21.0,<1.22.0 │ ├── opentelemetry-api 1.21.0 (circular dependency aborted here) │ ├── opentelemetry-semantic-conventions 0.42b0 │ └── typing-extensions >=3.7.4 ├── opentelemetry-instrumentation-fastapi >=0.41b0 │ ├── opentelemetry-api >=1.12,<2.0 │ │ ├── deprecated >=1.2.6 │ │ │ └── wrapt >=1.10,<2 │ │ └── importlib-metadata >=6.0,<7.0 │ │ └── zipp >=0.5 │ ├── opentelemetry-instrumentation 0.42b0 │ │ ├── opentelemetry-api >=1.4,<2.0 (circular dependency aborted here) │ │ ├── setuptools >=16.0 │ │ └── wrapt >=1.0.0,<2.0.0 (circular dependency aborted here) │ ├── opentelemetry-instrumentation-asgi 0.42b0 │ │ ├── asgiref >=3.0,<4.0 │ │ ├── opentelemetry-api >=1.12,<2.0 (circular dependency aborted here) │ │ ├── opentelemetry-instrumentation 0.42b0 (circular dependency aborted here) │ │ ├── opentelemetry-semantic-conventions 0.42b0 │ │ └── opentelemetry-util-http 0.42b0 │ ├── opentelemetry-semantic-conventions 0.42b0 (circular dependency aborted here) │ └── opentelemetry-util-http 0.42b0 (circular dependency aborted here) ├── opentelemetry-sdk >=1.2.0 │ ├── opentelemetry-api 1.21.0 │ │ ├── deprecated >=1.2.6 │ │ │ └── wrapt >=1.10,<2 │ │ └── importlib-metadata >=6.0,<7.0 │ │ └── zipp >=0.5 │ ├── opentelemetry-semantic-conventions 0.42b0 │ └── typing-extensions >=3.7.4 ├── overrides >=7.3.1 ├── posthog >=2.4.0 │ ├── backoff >=1.10.0 │ ├── monotonic >=1.5 │ ├── python-dateutil >2.1 │ │ └── six >=1.5 │ ├── requests >=2.7,<3.0 │ │ ├── certifi >=2017.4.17 │ │ ├── charset-normalizer >=2,<4 │ │ ├── idna >=2.5,<4 │ │ └── urllib3 >=1.21.1,<3 │ └── six >=1.5 (circular dependency aborted here) ├── pulsar-client >=3.1.0 │ └── certifi * ├── pydantic >=1.9 │ ├── annotated-types >=0.4.0 │ ├── pydantic-core 2.14.5 │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ └── typing-extensions >=4.6.1 (circular dependency aborted here) ├── pypika >=0.48.9 ├── pyyaml >=6.0.0 ├── requests >=2.28 │ ├── certifi >=2017.4.17 │ ├── charset-normalizer >=2,<4 │ ├── idna >=2.5,<4 │ └── urllib3 >=1.21.1,<3 ├── tenacity >=8.2.3 ├── tokenizers >=0.13.2 │ └── huggingface-hub >=0.16.4,<1.0 │ ├── filelock * │ ├── fsspec >=2023.5.0 │ │ ├── aiohttp <4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1 │ │ │ ├── aiosignal >=1.1.2 │ │ │ │ └── frozenlist >=1.1.0 │ │ │ ├── attrs >=17.3.0 │ │ │ ├── frozenlist >=1.1.1 (circular dependency aborted here) │ │ │ ├── multidict >=4.5,<7.0 │ │ │ └── yarl >=1.0,<2.0 │ │ │ ├── idna >=2.0 │ │ │ └── multidict >=4.0 (circular dependency aborted here) │ │ └── requests * │ │ ├── certifi >=2017.4.17 │ │ ├── charset-normalizer >=2,<4 │ │ ├── idna >=2.5,<4 (circular dependency aborted here) │ │ └── urllib3 >=1.21.1,<3 │ ├── packaging >=20.9 │ ├── pyyaml >=5.1 │ ├── requests * (circular dependency aborted here) │ ├── tqdm >=4.42.1 │ │ └── colorama * │ └── typing-extensions >=3.7.4.3 ├── tqdm >=4.65.0 │ └── colorama * ├── typer >=0.9.0 │ ├── click >=7.1.1,<9.0.0 │ │ └── colorama * │ ├── colorama >=0.4.3,<0.5.0 (circular dependency aborted here) │ ├── rich >=10.11.0,<14.0.0 │ │ ├── markdown-it-py >=2.2.0 │ │ │ └── mdurl >=0.1,<1.0 │ │ └── pygments >=2.13.0,<3.0.0 │ ├── shellingham >=1.3.0,<2.0.0 │ └── typing-extensions >=3.7.4.3 ├── typing-extensions >=4.5.0 └── uvicorn >=0.18.3 ├── click >=7.0 │ └── colorama * ├── colorama >=0.4 (circular dependency aborted here) ├── h11 >=0.8 ├── httptools >=0.5.0 ├── python-dotenv >=0.13 ├── pyyaml >=5.1 ├── uvloop >=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1 ├── watchfiles >=0.13 │ └── anyio >=3.0.0 │ ├── idna >=2.8 │ └── sniffio >=1.1 └── websockets >=10.4 fastapi 0.103.2 FastAPI framework, high performance, easy to learn, fast to code, ready for production ├── anyio >=3.7.1,<4.0.0 │ ├── idna >=2.8 │ └── sniffio >=1.1 ├── email-validator >=2.0.0 │ ├── dnspython >=2.0.0 │ └── idna >=2.0.0 ├── httpx >=0.23.0 │ ├── anyio * │ │ ├── idna >=2.8 │ │ └── sniffio >=1.1 │ ├── certifi * │ ├── h2 >=3,<5 │ │ ├── hpack >=4.0,<5 │ │ └── hyperframe >=6.0,<7 │ ├── httpcore ==1.* │ │ ├── certifi * (circular dependency aborted here) │ │ └── h11 >=0.13,<0.15 │ ├── idna * (circular dependency aborted here) │ └── sniffio * (circular dependency aborted here) ├── itsdangerous >=1.1.0 ├── jinja2 >=2.11.2 │ └── markupsafe >=2.0 ├── orjson >=3.2.1 ├── pydantic >=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0 │ ├── annotated-types >=0.4.0 │ ├── pydantic-core 2.14.5 │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ └── typing-extensions >=4.6.1 (circular dependency aborted here) ├── pydantic-extra-types >=2.0.0 │ └── pydantic >=2.0.3 │ ├── annotated-types >=0.4.0 │ ├── pydantic-core 2.14.5 │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ └── typing-extensions >=4.6.1 (circular dependency aborted here) ├── pydantic-settings >=2.0.0 │ ├── pydantic >=2.3.0 │ │ ├── annotated-types >=0.4.0 │ │ ├── pydantic-core 2.14.5 │ │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ │ └── typing-extensions >=4.6.1 (circular dependency aborted here) │ └── python-dotenv >=0.21.0 ├── python-multipart >=0.0.5 ├── pyyaml >=5.3.1 ├── starlette >=0.27.0,<0.28.0 │ └── anyio >=3.4.0,<5 │ ├── idna >=2.8 │ └── sniffio >=1.1 ├── typing-extensions >=4.5.0 ├── ujson >=4.0.1,<4.0.2 || >4.0.2,<4.1.0 || >4.1.0,<4.2.0 || >4.2.0,<4.3.0 || >4.3.0,<5.0.0 || >5.0.0,<5.1.0 || >5.1.0 └── uvicorn >=0.12.0 ├── click >=7.0 │ └── colorama * ├── colorama >=0.4 (circular dependency aborted here) ├── h11 >=0.8 ├── httptools >=0.5.0 ├── python-dotenv >=0.13 ├── pyyaml >=5.1 ├── uvloop >=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1 ├── watchfiles >=0.13 │ └── anyio >=3.0.0 │ ├── idna >=2.8 │ └── sniffio >=1.1 └── websockets >=10.4 injector 0.21.0 Injector - Python dependency injection framework, inspired by Guice llama-index 0.9.3 Interface between LLMs and your data ├── aiohttp >=3.8.6,<4.0.0 │ ├── aiosignal >=1.1.2 │ │ └── frozenlist >=1.1.0 │ ├── attrs >=17.3.0 │ ├── frozenlist >=1.1.1 (circular dependency aborted here) │ ├── multidict >=4.5,<7.0 │ └── yarl >=1.0,<2.0 │ ├── idna >=2.0 │ └── multidict >=4.0 (circular dependency aborted here) ├── aiostream >=0.5.2,<0.6.0 │ └── typing-extensions * ├── beautifulsoup4 >=4.12.2,<5.0.0 │ └── soupsieve >1.2 ├── dataclasses-json >=0.5.7,<0.6.0 │ ├── marshmallow >=3.18.0,<4.0.0 │ │ └── packaging >=17.0 │ └── typing-inspect >=0.4.0,<1 │ ├── mypy-extensions >=0.3.0 │ └── typing-extensions >=3.7.4 ├── deprecated >=1.2.9.3 │ └── wrapt >=1.10,<2 ├── fsspec >=2023.5.0 │ ├── aiohttp <4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1 │ │ ├── aiosignal >=1.1.2 │ │ │ └── frozenlist >=1.1.0 │ │ ├── attrs >=17.3.0 │ │ ├── frozenlist >=1.1.1 (circular dependency aborted here) │ │ ├── multidict >=4.5,<7.0 │ │ └── yarl >=1.0,<2.0 │ │ ├── idna >=2.0 │ │ └── multidict >=4.0 (circular dependency aborted here) │ └── requests * │ ├── certifi >=2017.4.17 │ ├── charset-normalizer >=2,<4 │ ├── idna >=2.5,<4 (circular dependency aborted here) │ └── urllib3 >=1.21.1,<3 ├── httpx * │ ├── anyio * │ │ ├── idna >=2.8 │ │ └── sniffio >=1.1 │ ├── certifi * │ ├── h2 >=3,<5 │ │ ├── hpack >=4.0,<5 │ │ └── hyperframe >=6.0,<7 │ ├── httpcore ==1.* │ │ ├── certifi * (circular dependency aborted here) │ │ └── h11 >=0.13,<0.15 │ ├── idna * (circular dependency aborted here) │ └── sniffio * (circular dependency aborted here) ├── nest-asyncio >=1.5.8,<2.0.0 ├── nltk >=3.8.1,<4.0.0 │ ├── click * │ │ └── colorama * │ ├── joblib * │ ├── regex >=2021.8.3 │ └── tqdm * │ └── colorama * (circular dependency aborted here) ├── numpy * ├── openai >=1.1.0 │ ├── anyio >=3.5.0,<5 │ │ ├── idna >=2.8 │ │ └── sniffio >=1.1 │ ├── distro >=1.7.0,<2 │ ├── httpx >=0.23.0,<1 │ │ ├── anyio * (circular dependency aborted here) │ │ ├── certifi * │ │ ├── h2 >=3,<5 │ │ │ ├── hpack >=4.0,<5 │ │ │ └── hyperframe >=6.0,<7 │ │ ├── httpcore ==1.* │ │ │ ├── certifi * (circular dependency aborted here) │ │ │ └── h11 >=0.13,<0.15 │ │ ├── idna * (circular dependency aborted here) │ │ └── sniffio * (circular dependency aborted here) │ ├── pydantic >=1.9.0,<3 │ │ ├── annotated-types >=0.4.0 │ │ ├── pydantic-core 2.14.5 │ │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ │ └── typing-extensions >=4.6.1 (circular dependency aborted here) │ ├── sniffio * (circular dependency aborted here) │ ├── tqdm >4 │ │ └── colorama * │ └── typing-extensions >=4.5,<5 (circular dependency aborted here) ├── optimum >=1.13.2,<2.0.0 │ ├── coloredlogs * │ │ └── humanfriendly >=9.1 │ │ └── pyreadline3 * │ ├── datasets * │ │ ├── aiohttp * │ │ │ ├── aiosignal >=1.1.2 │ │ │ │ └── frozenlist >=1.1.0 │ │ │ ├── attrs >=17.3.0 │ │ │ ├── frozenlist >=1.1.1 (circular dependency aborted here) │ │ │ ├── multidict >=4.5,<7.0 │ │ │ └── yarl >=1.0,<2.0 │ │ │ ├── idna >=2.0 │ │ │ └── multidict >=4.0 (circular dependency aborted here) │ │ ├── dill >=0.3.0,<0.3.8 │ │ ├── fsspec >=2021.11.1 │ │ │ ├── aiohttp <4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1 (circular dependency aborted here) │ │ │ └── requests * │ │ │ ├── certifi >=2017.4.17 │ │ │ ├── charset-normalizer >=2,<4 │ │ │ ├── idna >=2.5,<4 (circular dependency aborted here) │ │ │ └── urllib3 >=1.21.1,<3 │ │ ├── huggingface-hub >=0.14.0,<1.0.0 │ │ │ ├── filelock * │ │ │ ├── fsspec >=2023.5.0 (circular dependency aborted here) │ │ │ ├── packaging >=20.9 │ │ │ ├── pyyaml >=5.1 │ │ │ ├── requests * (circular dependency aborted here) │ │ │ ├── tqdm >=4.42.1 │ │ │ │ └── colorama * │ │ │ └── typing-extensions >=3.7.4.3 │ │ ├── multiprocess * │ │ │ └── dill >=0.3.7 (circular dependency aborted here) │ │ ├── numpy >=1.17 │ │ ├── packaging * (circular dependency aborted here) │ │ ├── pandas * │ │ │ ├── numpy >=1.23.2,<2 (circular dependency aborted here) │ │ │ ├── python-dateutil >=2.8.2 │ │ │ │ └── six >=1.5 │ │ │ ├── pytz >=2020.1 │ │ │ └── tzdata >=2022.1 │ │ ├── pyarrow >=8.0.0 │ │ │ └── numpy >=1.16.6 (circular dependency aborted here) │ │ ├── pyyaml >=5.1 (circular dependency aborted here) │ │ ├── requests >=2.19.0 (circular dependency aborted here) │ │ ├── tqdm >=4.62.1 (circular dependency aborted here) │ │ └── xxhash * │ ├── datasets >=1.2.1 (circular dependency aborted here) │ ├── evaluate * │ │ ├── datasets >=2.0.0 (circular dependency aborted here) │ │ ├── dill * (circular dependency aborted here) │ │ ├── fsspec >=2021.05.0 (circular dependency aborted here) │ │ ├── huggingface-hub >=0.7.0 (circular dependency aborted here) │ │ ├── multiprocess * (circular dependency aborted here) │ │ ├── numpy >=1.17 (circular dependency aborted here) │ │ ├── packaging * (circular dependency aborted here) │ │ ├── pandas * (circular dependency aborted here) │ │ ├── requests >=2.19.0 (circular dependency aborted here) │ │ ├── responses <0.19 │ │ │ ├── requests >=2.0,<3.0 (circular dependency aborted here) │ │ │ └── urllib3 >=1.25.10 (circular dependency aborted here) │ │ ├── tqdm >=4.62.1 (circular dependency aborted here) │ │ └── xxhash * (circular dependency aborted here) │ ├── huggingface-hub >=0.8.0 (circular dependency aborted here) │ ├── numpy * (circular dependency aborted here) │ ├── onnx * │ │ ├── numpy * (circular dependency aborted here) │ │ └── protobuf >=3.20.2 │ ├── onnxruntime >=1.11.0 │ │ ├── coloredlogs * (circular dependency aborted here) │ │ ├── flatbuffers * │ │ ├── numpy >=1.21.6 (circular dependency aborted here) │ │ ├── packaging * (circular dependency aborted here) │ │ ├── protobuf * (circular dependency aborted here) │ │ └── sympy * │ │ └── mpmath >=0.19 │ ├── packaging * (circular dependency aborted here) │ ├── protobuf >=3.20.1 (circular dependency aborted here) │ ├── sympy * (circular dependency aborted here) │ ├── torch >=1.9 │ │ ├── filelock * (circular dependency aborted here) │ │ ├── fsspec * (circular dependency aborted here) │ │ ├── jinja2 * │ │ │ └── markupsafe >=2.0 │ │ ├── networkx * │ │ ├── nvidia-cublas-cu12 12.1.3.1 │ │ ├── nvidia-cuda-cupti-cu12 12.1.105 │ │ ├── nvidia-cuda-nvrtc-cu12 12.1.105 │ │ ├── nvidia-cuda-runtime-cu12 12.1.105 │ │ ├── nvidia-cudnn-cu12 8.9.2.26 │ │ │ └── nvidia-cublas-cu12 * (circular dependency aborted here) │ │ ├── nvidia-cufft-cu12 11.0.2.54 │ │ ├── nvidia-curand-cu12 10.3.2.106 │ │ ├── nvidia-cusolver-cu12 11.4.5.107 │ │ │ ├── nvidia-cublas-cu12 * (circular dependency aborted here) │ │ │ ├── nvidia-cusparse-cu12 * │ │ │ │ └── nvidia-nvjitlink-cu12 * │ │ │ └── nvidia-nvjitlink-cu12 * (circular dependency aborted here) │ │ ├── nvidia-cusparse-cu12 12.1.0.106 (circular dependency aborted here) │ │ ├── nvidia-nccl-cu12 2.18.1 │ │ ├── nvidia-nvtx-cu12 12.1.105 │ │ ├── sympy * (circular dependency aborted here) │ │ ├── triton 2.1.0 │ │ │ └── filelock * (circular dependency aborted here) │ │ └── typing-extensions * (circular dependency aborted here) │ └── transformers >=4.26.0 │ ├── accelerate >=0.21.0 │ │ ├── huggingface-hub * (circular dependency aborted here) │ │ ├── numpy >=1.17 (circular dependency aborted here) │ │ ├── packaging >=20.0 (circular dependency aborted here) │ │ ├── psutil * │ │ ├── pyyaml * (circular dependency aborted here) │ │ ├── safetensors >=0.3.1 │ │ └── torch >=1.10.0 (circular dependency aborted here) │ ├── filelock * (circular dependency aborted here) │ ├── huggingface-hub >=0.19.3,<1.0 (circular dependency aborted here) │ ├── numpy >=1.17 (circular dependency aborted here) │ ├── packaging >=20.0 (circular dependency aborted here) │ ├── protobuf * (circular dependency aborted here) │ ├── pyyaml >=5.1 (circular dependency aborted here) │ ├── regex !=2019.12.17 │ ├── requests * (circular dependency aborted here) │ ├── safetensors >=0.3.1 (circular dependency aborted here) │ ├── sentencepiece >=0.1.91,<0.1.92 || >0.1.92 │ ├── tokenizers >=0.14,<0.19 │ │ └── huggingface-hub >=0.16.4,<1.0 (circular dependency aborted here) │ ├── torch >=1.10,<1.12.0 || >1.12.0 (circular dependency aborted here) │ └── tqdm >=4.27 (circular dependency aborted here) ├── pandas * │ ├── numpy >=1.23.2,<2 │ ├── python-dateutil >=2.8.2 │ │ └── six >=1.5 │ ├── pytz >=2020.1 │ └── tzdata >=2022.1 ├── sentencepiece >=0.1.99,<0.2.0 ├── sqlalchemy >=1.4.49 │ ├── greenlet !=0.4.17 │ └── typing-extensions >=4.2.0 ├── tenacity >=8.2.0,<9.0.0 ├── tiktoken >=0.3.3 │ ├── regex >=2022.1.18 │ └── requests >=2.26.0 │ ├── certifi >=2017.4.17 │ ├── charset-normalizer >=2,<4 │ ├── idna >=2.5,<4 │ └── urllib3 >=1.21.1,<3 ├── transformers >=4.34.0,<5.0.0 │ ├── accelerate >=0.21.0 │ │ ├── huggingface-hub * │ │ │ ├── filelock * │ │ │ ├── fsspec >=2023.5.0 │ │ │ │ ├── aiohttp <4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1 │ │ │ │ │ ├── aiosignal >=1.1.2 │ │ │ │ │ │ └── frozenlist >=1.1.0 │ │ │ │ │ ├── attrs >=17.3.0 │ │ │ │ │ ├── frozenlist >=1.1.1 (circular dependency aborted here) │ │ │ │ │ ├── multidict >=4.5,<7.0 │ │ │ │ │ └── yarl >=1.0,<2.0 │ │ │ │ │ ├── idna >=2.0 │ │ │ │ │ └── multidict >=4.0 (circular dependency aborted here) │ │ │ │ └── requests * │ │ │ │ ├── certifi >=2017.4.17 │ │ │ │ ├── charset-normalizer >=2,<4 │ │ │ │ ├── idna >=2.5,<4 (circular dependency aborted here) │ │ │ │ └── urllib3 >=1.21.1,<3 │ │ │ ├── packaging >=20.9 │ │ │ ├── pyyaml >=5.1 │ │ │ ├── requests * (circular dependency aborted here) │ │ │ ├── tqdm >=4.42.1 │ │ │ │ └── colorama * │ │ │ └── typing-extensions >=3.7.4.3 │ │ ├── numpy >=1.17 │ │ ├── packaging >=20.0 (circular dependency aborted here) │ │ ├── psutil * │ │ ├── pyyaml * (circular dependency aborted here) │ │ ├── safetensors >=0.3.1 │ │ └── torch >=1.10.0 │ │ ├── filelock * (circular dependency aborted here) │ │ ├── fsspec * (circular dependency aborted here) │ │ ├── jinja2 * │ │ │ └── markupsafe >=2.0 │ │ ├── networkx * │ │ ├── nvidia-cublas-cu12 12.1.3.1 │ │ ├── nvidia-cuda-cupti-cu12 12.1.105 │ │ ├── nvidia-cuda-nvrtc-cu12 12.1.105 │ │ ├── nvidia-cuda-runtime-cu12 12.1.105 │ │ ├── nvidia-cudnn-cu12 8.9.2.26 │ │ │ └── nvidia-cublas-cu12 * (circular dependency aborted here) │ │ ├── nvidia-cufft-cu12 11.0.2.54 │ │ ├── nvidia-curand-cu12 10.3.2.106 │ │ ├── nvidia-cusolver-cu12 11.4.5.107 │ │ │ ├── nvidia-cublas-cu12 * (circular dependency aborted here) │ │ │ ├── nvidia-cusparse-cu12 * │ │ │ │ └── nvidia-nvjitlink-cu12 * │ │ │ └── nvidia-nvjitlink-cu12 * (circular dependency aborted here) │ │ ├── nvidia-cusparse-cu12 12.1.0.106 (circular dependency aborted here) │ │ ├── nvidia-nccl-cu12 2.18.1 │ │ ├── nvidia-nvtx-cu12 12.1.105 │ │ ├── sympy * │ │ │ └── mpmath >=0.19 │ │ ├── triton 2.1.0 │ │ │ └── filelock * (circular dependency aborted here) │ │ └── typing-extensions * (circular dependency aborted here) │ ├── filelock * (circular dependency aborted here) │ ├── huggingface-hub >=0.19.3,<1.0 (circular dependency aborted here) │ ├── numpy >=1.17 (circular dependency aborted here) │ ├── packaging >=20.0 (circular dependency aborted here) │ ├── protobuf * │ ├── pyyaml >=5.1 (circular dependency aborted here) │ ├── regex !=2019.12.17 │ ├── requests * (circular dependency aborted here) │ ├── safetensors >=0.3.1 (circular dependency aborted here) │ ├── sentencepiece >=0.1.91,<0.1.92 || >0.1.92 │ ├── tokenizers >=0.14,<0.19 │ │ └── huggingface-hub >=0.16.4,<1.0 (circular dependency aborted here) │ ├── torch >=1.10,<1.12.0 || >1.12.0 (circular dependency aborted here) │ └── tqdm >=4.27 (circular dependency aborted here) ├── typing-extensions >=4.5.0 ├── typing-inspect >=0.8.0 │ ├── mypy-extensions >=0.3.0 │ └── typing-extensions >=3.7.4 └── urllib3 <2 mypy 1.7.1 Optional static typing for Python ├── mypy-extensions >=1.0.0 └── typing-extensions >=4.1.0 pre-commit 2.21.0 A framework for managing and maintaining multi-language pre-commit hooks. ├── cfgv >=2.0.0 ├── identify >=1.0.0 ├── nodeenv >=0.11.1 │ └── setuptools * ├── pyyaml >=5.1 └── virtualenv >=20.10.0 ├── distlib >=0.3.7,<1 ├── filelock >=3.12.2,<4 └── platformdirs >=3.9.1,<5 pypdf 3.17.2 A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files pytest 7.4.3 pytest: simple powerful testing with Python ├── colorama * ├── iniconfig * ├── packaging * └── pluggy >=0.12,<2.0 pytest-asyncio 0.21.1 Pytest support for asyncio └── pytest >=7.0.0 ├── colorama * ├── iniconfig * ├── packaging * └── pluggy >=0.12,<2.0 pytest-cov 3.0.0 Pytest plugin for measuring coverage. ├── coverage >=5.2.1 └── pytest >=4.6 ├── colorama * ├── iniconfig * ├── packaging * └── pluggy >=0.12,<2.0 python-multipart 0.0.6 A streaming multipart parser for Python pyyaml 6.0.1 YAML parser and emitter for Python qdrant-client 1.7.0 Client library for the Qdrant vector search engine ├── grpcio >=1.41.0 ├── grpcio-tools >=1.41.0 │ ├── grpcio >=1.60.0 │ ├── protobuf >=4.21.6,<5.0dev │ └── setuptools * ├── httpx >=0.14.0 │ ├── anyio * │ │ ├── idna >=2.8 │ │ └── sniffio >=1.1 │ ├── certifi * │ ├── h2 >=3,<5 │ │ ├── hpack >=4.0,<5 │ │ └── hyperframe >=6.0,<7 │ ├── httpcore ==1.* │ │ ├── certifi * (circular dependency aborted here) │ │ └── h11 >=0.13,<0.15 │ ├── idna * (circular dependency aborted here) │ └── sniffio * (circular dependency aborted here) ├── numpy >=1.21 ├── portalocker >=2.7.0,<3.0.0 │ └── pywin32 >=226 ├── pydantic >=1.10.8 │ ├── annotated-types >=0.4.0 │ ├── pydantic-core 2.14.5 │ │ └── typing-extensions >=4.6.0,<4.7.0 || >4.7.0 │ └── typing-extensions >=4.6.1 (circular dependency aborted here) └── urllib3 >=1.26.14,<2.0.0 ruff 0.1.8 An extremely fast Python linter and code formatter, written in Rust. types-pyyaml 6.0.12.12 Typing stubs for PyYAML watchdog 3.0.0 Filesystem events monitoring </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region poetry update --> <h3 id="poetry_update"><span class="code">poetry update</span></h3> <p> You can update all the dependencies for a Poetry project like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1ad452a19374'><button class='copyBtn' data-clipboard-target='#id1ad452a19374' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>poetry update</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region For Further Reading --> <h2 id="further">For Further Reading</h2> <p> <a href='https://mitelman.engineering/blog/python-best-practice/automating-python-best-practices-for-a-new-project/' target='_blank' rel="nofollow">Python Best Practices for a New Project in 2021</a>. This article is already becoming dated. YMMV. </p> <!-- endregion --> <!-- #region Summary --> <h2 id="summary">Summary</h2> <ul> <li>Demonstrated how to make an alias for working with Python virtual environments (<i>venvs</i>) that are coupled with Python projects.</li> <li>Deactivating the current venv was demonstrated using the <code>deactivate</code> command, provided with every venv.</li> <li>Locked directories mean that Python virtual environments should normally only be created in the same environment they are intended to be used.</li> </ul> <!-- endregion --> Escaping HTML on Clipboard From a Windows Hot Key via WSL 2021-04-03T00:00:00-04:00 https://mslinn.github.io/blog/2021/04/03/escape-html-clipboard <!-- #region intro --> <p> I frequently show HTML source code when I write. That HTML must be escaped prior to displaying it on a web page. </p> <!-- endregion --> <!-- #region Script to Apply HTML Escape to Clipboard --> <h2 id="bash_script">Script to Apply HTML Escape to Clipboard</h2> <p> This bash script applies an HTML escape conversion to the contents of the system clipboard. If you have WSL on your machine, you could store it on the WSL file system, for example in <code>~/.local/bin/escapeHtml</code>. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,escapeHtml' download='escapeHtml' title='Click on the file name to download the file'>escapeHtml</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idc6991f5d91bd"><button class='copyBtn' data-clipboard-target='#idc6991f5d91bd'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # SPDX-License-Identifier: Apache-2.0 function help &#123; echo "$(basename $0) - Escapes HTML with entities. Reads from STDIN or pipe, or converts the clipboard. Result is copied to the clipboard. " exit 1 &#125; function filesAreLinked &#123; "$1" -ef "$2" &#125; function checkDependencies &#123; if [ -z `which recode` ]; then yes | sudo apt install recode; fi # See https://github.com/sindresorhus/clipboard-cli if [ -z `which clipboard` ]; then if [ "$( filesAreLinked /bin/npm /usr/local/lib/node_modules/npm/bin/npm-cli.js )" ]; then # No nodejs venv sudo -H npm install --global clipboard-cli else # nodejs venv npm install --global clipboard-cli fi fi &#125; if [ "$1" == -h ]; then help; fi checkDependencies if [ -t 0 ]; then # Not reading from a terminal HTML="$( clipboard )" else # Reading from stdin or pipe # See https://stackoverflow.com/a/32365596/553865 HTML=$(cat; echo x) HTML=$&#123;HTML%x&#125; # Strip the trailing x fi RESULT="$( recode utf8..html &lt;&lt;&lt; "$HTML" )" echo "$RESULT" | sed "s/&amp;#13;//g" | sed "s/'/\&amp;#39;/g" | clipboard echo "$( echo "$RESULT" | wc -l ) lines have been placed on the clipboard." </pre> <!-- endregion --> <!-- #region Using the Script --> <h2 id="use">Using the Script</h2> <ol> <li>Select some text in any document or anywhere that text can be selected.</li> <li>Run <code>escapeHtml</code> on the same machine. If you have Windows with WSL you can run the script there, or run it in native Windows, does not matter.</li> <li>Paste escaped HTML into your target document.</li> </ol> <!-- endregion --> <!-- #region Hot Key Trigger --> <h2 id="trigger">Hot Key Trigger</h2> <p> Trigger the script with a hot key via your OS's facilities. This section just discusses how native Windows hot keys can be used to trigger this script running in WSL. </p> <ol> <li>Right-click in a folder</li> <li>Select <b>New</b> / <b>Shortcut</b></li> <li> For <b>Type the location of the item</b>, type:<br><br> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Type the location of the item</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id10120544741c'><button class='copyBtn' data-clipboard-target='#id10120544741c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>%windir%\System32\wsl.exe ~/.local/bin/escapeHtml/escapeHtml</pre> </div> </li> <li>Click <b>Change icon</b> and select a retro icon for this shortcut.</li> <li>Click on <b>Apply</b>.</li> <li>Click on <b>Next</b>.</li> <li> When prompted for <b>Type a name for this shortcut</b>, save as <code>HTML Escape Clip to Clip</code>. </li> <li>Click on <b>Finish</b>.</li> <li>Right-click on the new shortcut and click in <b>Shortcut key</b></li> <li>I used <kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>A</kbd>.</li> <li>Click on <b>OK</b>.</li> </ol> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> You can now quickly copy HTML from any source to the clipboard, apply an HTML escape conversion to the clipboard contents, and then paste the escaped HTML into an editor. </p> <!-- endregion --> Microsoft Visual Studio Code Notes 2021-03-22T00:00:00-04:00 https://mslinn.github.io/blog/2021/03/22/vscode-notes <!-- #region launching --> <h2 id="launch">Launching VSCode</h2> <!-- #region Opening A File --> <h3 id="launch1">Opening A File</h3> <p> To open a file, simply provide the file name on the command line. For example, to open <code>abd/xyz/my_func.c</code>, type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5113b04fc515'><button class='copyBtn' data-clipboard-target='#id5113b04fc515' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>code abd/xyz/my_func.c</pre> </div> <p> If VSCode has at least one window open, the top-most window will open a new tab containing the desired file. Otherwise, VSCode will launch, with the desired file displayed in an editor pane. </p> <p> This works with relative and absolute file names. </p> <p> You can type the incantation into any command console, such as <code>CMD</code>, PowerShell or <code>bash</code>. You can also use the console built into VSCode, which can be toggled with <kbd>CTRL</kbd>-<kbd>~</kbd>, or you can use an external console such as <a href='https://www.microsoft.com/store/productId/9N8G5RFZ9XK3' target='_blank' rel="nofollow">Microsoft Terminal</a>. </p> <!-- endregion --> <!-- #region Opening A File At a Line --> <h3 id="launch1">Opening A File At a Line</h3> <p> To open a file at a given line number, use the <code>-g</code> option on the command line, and follow the file name with a colon and the line number. For example, to open <code>abd/xyz/my_func.c</code> at line 123, type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id87b0e30c77ff'><button class='copyBtn' data-clipboard-target='#id87b0e30c77ff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>code -g abd/xyz/my_func.c:123</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region default key bindings --> <h2 id="keys">Useful Default Key Bindings</h2> <dl> <dt><kbd>CTRL</kbd>+<kbd>~</kbd></dt> <dd> Toggle visibility of the current <b>Terminal</b> pane in the lower panel. </dd> <dt><kbd>CTRL</kbd>+<kbd>B</kbd></dt> <dd> Toggle visibility of the Explorer view, which contains the files and directories for the VSCode project. </dd> <dt><kbd>CTRL</kbd>+<kbd>J</kbd></dt> <dd> Toggle visibility of the lower panel, containing <b>Problems</b>, <b>Output</b>, <b>Debug Console</b>, <b>Terminal</b>, <b>GitLens</b> and <b>Watch</b>. </dd> <dt><kbd>CTRL</kbd>+<kbd>K</kbd> &nbsp; <kbd>M</kbd></dt> <dd> Set the language mode for the file displayed in the editor. </dd> <dt><kbd>CTRL</kbd>+<kbd>K</kbd> &nbsp; <kbd>CTRL</kbd>+<kbd>S</kbd></dt> <dd> <b>Display / edit the <a href='https://code.visualstudio.com/docs/getstarted/keybindings' target='_blank' rel="nofollow">Keyboard Shortcuts</a> definitions.</b><br> You can filter the keybindings by pressing <kbd>ALT</kbd>+<kbd>K</kbd> or clicking the icon of the little keyboard at the top right of the Keyboard Shortcut page, then press the keys that you want to see the key binding for. \ The keyboard icon starts keystroke recording mode. Recording mode is sticky; each time you revisit the Keyboard Shortcuts tab you can just press the keys you are interested in to see their bindings. Step by step: <ol> <li> When you press the <kbd>CTRL</kbd> key you will see <code>"ctrl"</code> displayed, and recording mode continues to listen to what you type. Don't do this right now, but FYI, if you toggle keystroke recording mode now, and then remove the quotes around <code>"ctrl"</code>, you will see a sorted list of all the key chords bound to <kbd>CTRL</kbd>. </li> <li> Next, when you add the <kbd>Shift</kbd> key to the key chord, you then see <code>"ctrl+shift"</code> displayed. Don't do this right now, but FYI, if you toggle keystroke recording mode now, and then remove the quotes, you will see a sorted list of all the key chords bound to <kbd>CTRL</kbd>+<kbd>Shift</kbd>. </li> <li>Finally, adding the <kbd>=</kbd> key to the key chord shows all the commands bound to <kbd>CTRL</kbd>+<kbd>Shift</kbd>+<kbd>=</kbd>.</li> </ol> </dd> <dt><kbd>CTRL</kbd>+<kbd>K</kbd>+<kbd>0</kbd> (zero)</dt> <dd>Completely fold the active editor contents.</dd> <dt><kbd>CTRL</kbd>+<kbd>K</kbd>+<kbd>1</kbd> (one)</dt> <dd>Fold level 1 the active editor contents.</dd> <dt><kbd>CTRL</kbd>+<kbd>K</kbd>+<kbd>2</kbd></dt> <dd>Fold level 2 the active editor contents.</dd> <dt><kbd>CTRL</kbd>+<kbd>B</kbd></dt> <dd>Toggle side bar visibility.</dd> <dt><kbd>CTRL</kbd>+<kbd>P</kbd></dt> <dd> Show names of recently opened tabs, which might contain files to edit, or might be VSCode settings, or VSCode key bindings, etc. Click on a tab name to open it. This key binding is bound to <b>Go to File</b>, which is somewhat logical but not the most descriptive name. </dd> <dt><kbd>CTRL</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></dt> <dd>Open the <a href='https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette' target='_blank' rel="nofollow">Command Palette</a>.</dd> <dt><kbd>CTRL</kbd>+<kbd>L</kbd> &nbsp; <kbd>G</kbd></dt> <dd> Open the active (currently edited) file on GitHub in the default web browser. Requires the <a href='https://marketplace.visualstudio.com/items?itemName=sysoev.vscode-open-in-github' target='_blank' rel="nofollow">Open in GitHub</a> extension. </dd> <dt><kbd>CTRL</kbd>+<kbd>,</kbd> (comma)</dt> <dd>Open the <a href='https://code.visualstudio.com/docs/getstarted/settings' target='_blank' rel="nofollow">Settings</a> tab.</dd> <dt><kbd>CTRL</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></dt> <dd>Reopen the most recently closed editor tab.</dd> <dt><kbd>ALT</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd></dt> <dd>Format the current file.</dd> </dl> <!-- endregion --> <!-- #region annoying side bar --> <h2 id="reveal">Annoying Side Bar Auto Reveal</h2> <!-- #region Problem --> <h3 id="reveal_problem">Problem</h3> <p> When editing a file, if you <kbd>CTRL</kbd>+<kbd>click</kbd> on a function, method or class name defined in a dependency, the dependency's folder will be expanded in the side bar. Some dependencies are deeply nested, which means that the side bar expands quite a lot. In order to close the folders in the side bar it is necessary to go all the way back to the top of that folder, which is annoying and wastes time. </p> <!-- endregion --> <!-- #region Solution --> <h3 id="reveal_solution">Solution</h3> <p> In settings, look for <b>Explorer: Auto Reveal</b>, which controls whether the explorer should automatically reveal and select files when opening them. </p> <p> To do that, bring up settings with <kbd>CTRL</kbd>+<kbd>,</kbd> (comma), and then type <code>reveal ex</code> into the filter. </p> <p> The default value is <code>true</code>. Change the value to <code>false</code>. </p> <!-- endregion --> <!-- #region Bonus: Reveal Active File in Side Bar --> <h3 id="reveal_bonus">Bonus: Reveal Active File in Side Bar</h3> <p> To scroll to a file that you are editing in the list of files in the side bar, right-click on the file's tab, then select <b>Reveal in side bar</b>. </p> <p> Even better, define a keyboard shortcut to do this: </p> <ol> <li> Bring up the <b>Keyboard Shortcuts</b> definitions by typing <kbd>CTRL</kbd>+<kbd>K</kbd> &nbsp; <kbd>CTRL</kbd>+<kbd>S</kbd>. </li> <li>Type <code>reveal side</code> into the search bar.</li> <li>Double-click on <b>File: Reveal Active File in Side Bar</b>.</li> <li> For my desired key shortcut, I pressed <kbd>CTRL</kbd>+<kbd>Shift</kbd>+<kbd>ALT</kbd>+<kbd>R</kbd>, then pressed <kbd>Enter</kbd>. </li> </ol> <!-- endregion --> <!-- endregion --> <!-- #region Extensions --> <h2 id="extensions">Extensions</h2> <!-- #region implicit --> <ul> <li> <a href='https://marketplace.visualstudio.com/items?itemName=tkcandrade.code-annotation' target='_blank' rel="nofollow">Code Annotation</a> </li> <li> <a href='https://marketplace.visualstudio.com/items?itemName=YuTengjing.open-in-external-app' target='_blank' rel="nofollow">Open in External App</a> </li> <li> <a href='https://mitelman.engineering/blog/python-best-practice/automating-python-best-practices-for-a-new-project/#code-analysis-with-flake8-linter' target='_blank' rel="nofollow">Flake 8</a> </li> </ul> <!-- endregion --> <!-- #region extensions --> <h3 id="extdir">Extension Directory</h3> <p> Extensions are stored in <code>~/.vscode/extensions/</code>. </p> <!-- endregion --> <!-- endregion --> <!-- #region settings --> <h2 id="settings">Settings</h2> <!-- #region implicit --> <p> <b>User settings</b> are stored in <code>%AppData%\Code\User\</code> on Windows, in files called <code>settings.json</code>, <code>keybindings.json</code>, <code>syncLocalSettings.json</code> and <code>tasks.json</code>. </p> <p> <b>Project settings</b> are stored in <code>.vscode/</code> on all platforms, in files called <code>launch.json</code>, <code>settings.json</code> and <code>tasks.json</code>. </p> <p> <b>Workspace settings</b> are stored in files with a <code>.code-workspace</code> filetype. They can be painful to work with because paths in them are OS-dependent. </p> <!-- endregion --> <!-- #region Open Windows Directory From File Explorer --> <h3 id="windirexp">Open Windows Directory From File Explorer</h3> <p> The ability to have Visual Studio Code open a Windows directory from File Explorer is useful. When you install VS Code for Windows, you can add an <b>Open with Code</b> action to the Windows Explorer file context menu by enabling the two options that are highlighted in the image below. </p> <div class='imgWrapper imgFlex center' style='width: 550px; '> <picture class='imgPicture'> <source srcset="/blog/images/vscode/VSCodeInstall.svg" type="image/svg"> <!---<source srcset="/blog/images/vscode/VSCodeInstall.avif" type="image/avif">--> <source srcset="/blog/images/vscode/VSCodeInstall.webp" type="image/webp"> <source srcset="/blog/images/vscode/VSCodeInstall.apng" type="image/apng"> <source srcset="/blog/images/vscode/VSCodeInstall.png" type="image/png"> <source srcset="/blog/images/vscode/VSCodeInstall.jpg" type="image/jpeg"> <source srcset="/blog/images/vscode/VSCodeInstall.jpeg" type="image/jpeg"> <source srcset="/blog/images/vscode/VSCodeInstall.jfif" type="image/jpeg"> <source srcset="/blog/images/vscode/VSCodeInstall.pjpeg" type="image/jpeg"> <source srcset="/blog/images/vscode/VSCodeInstall.pjp" type="image/jpeg"> <source srcset="/blog/images/vscode/VSCodeInstall.gif" type="image/gif"> <source srcset="/blog/images/vscode/VSCodeInstall.tif" type="image/tiff"> <source srcset="/blog/images/vscode/VSCodeInstall.tiff" type="image/tiff"> <source srcset="/blog/images/vscode/VSCodeInstall.bmp" type="image/bmp"> <source srcset="/blog/images/vscode/VSCodeInstall.ico" type="image/x-icon"> <source srcset="/blog/images/vscode/VSCodeInstall.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/vscode/VSCodeInstall.png" style='width: 100%; ' /> </picture> </div> <p> To add the ability without reinstalling VSCode, save the following file, then double-click on it. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,vscode_open_dir.reg' download='vscode_open_dir.reg' title='Click on the file name to download the file'>vscode_open_dir.reg</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id3975cd66d26f"><button class='copyBtn' data-clipboard-target='#id3975cd66d26f'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Windows Registry Editor Version 5.00 ; Open files [HKEY_CLASSES_ROOT\*\shell\Open with VS Code] @="Edit with VS Code" "Icon"="%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe,0" [HKEY_CLASSES_ROOT\*\shell\Open with VS Code\command] @="\"%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe\" \"%1\"" ; This will make it appear when you right click ON a folder ; The "Icon" line can be removed if you don't want the icon to appear [HKEY_CLASSES_ROOT\Directory\shell\vscode] @="Open in VS Code" "Icon"="\"%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe\",0" [HKEY_CLASSES_ROOT\Directory\shell\vscode\command] @="\"%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe\" \"%1\"" ; This will make it appear when you right click INSIDE a folder ; The "Icon" line can be removed if you don't want the icon to appear [HKEY_CLASSES_ROOT\Directory\Background\shell\vscode] @="Open in VS Code" "Icon"="\"%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe\",0" [HKEY_CLASSES_ROOT\Directory\Background\shell\vscode\command] @="\"%LocalAppData%\\Programs\\Microsoft VS Code\\Code.exe\" \"%V\"" </pre> <p> There is no need to reboot your computer. Now you should see a new entry when you right-click on a directory: </p> <div class='imgWrapper imgFlex center' style='width: 300px; '> <picture class='imgPicture'> <source srcset="/blog/images/vscode/file_explorer_context_menu.svg" type="image/svg"> <!---<source srcset="/blog/images/vscode/file_explorer_context_menu.avif" type="image/avif">--> <source srcset="/blog/images/vscode/file_explorer_context_menu.webp" type="image/webp"> <source srcset="/blog/images/vscode/file_explorer_context_menu.apng" type="image/apng"> <source srcset="/blog/images/vscode/file_explorer_context_menu.png" type="image/png"> <source srcset="/blog/images/vscode/file_explorer_context_menu.jpg" type="image/jpeg"> <source srcset="/blog/images/vscode/file_explorer_context_menu.jpeg" type="image/jpeg"> <source srcset="/blog/images/vscode/file_explorer_context_menu.jfif" type="image/jpeg"> <source srcset="/blog/images/vscode/file_explorer_context_menu.pjpeg" type="image/jpeg"> <source srcset="/blog/images/vscode/file_explorer_context_menu.pjp" type="image/jpeg"> <source srcset="/blog/images/vscode/file_explorer_context_menu.gif" type="image/gif"> <source srcset="/blog/images/vscode/file_explorer_context_menu.tif" type="image/tiff"> <source srcset="/blog/images/vscode/file_explorer_context_menu.tiff" type="image/tiff"> <source srcset="/blog/images/vscode/file_explorer_context_menu.bmp" type="image/bmp"> <source srcset="/blog/images/vscode/file_explorer_context_menu.ico" type="image/x-icon"> <source srcset="/blog/images/vscode/file_explorer_context_menu.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/vscode/file_explorer_context_menu.png" style='width: 100%; ' /> </picture> </div> <p> Thanks to Erick Petrucelli and Walid Bousseta on <a href='https://stackoverflow.com/a/71711472/553865' target='_blank' rel="nofollow">StackOverflow</a> for this procedure. </p> <!-- endregion --> <!-- endregion --> Command-Line AWS Utilities 2021-03-22T00:00:00-04:00 https://mslinn.github.io/blog/2021/03/22/command-line-aws-utilities <!-- #region --> <p> Here are some command-line utilities I have written for AWS. They are dependent on <a href='https://aws.amazon.com/cli/' target='_blank' rel="nofollow">aws cli</a>. You can <a href='/mslinn_aws.tar'>download all of these utilities</a> in tar format. Extract them into the current directory like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id59d8ad5e8ca9'><button class='copyBtn' data-clipboard-target='#id59d8ad5e8ca9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tar xf mslinn_aws.tar</pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsCfInvalidate"><span class="code">awsCfInvalidate</span></h2> <p> Given a CloudFront distribution ID, invalidate the distribution. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsCfInvalidate' download='awsCfInvalidate' title='Click on the file name to download the file'>awsCfInvalidate</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idfe304c4cf64c"><button class='copyBtn' data-clipboard-target='#idfe304c4cf64c'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Invalidate the CloudFront distribution for the given ID. If no distribution with the given ID exists, the empty string is returned and the return code is 2. A message is printed asynchronously to the console when the invalidation completes. Syntax: $(basename $0) distId Syntax: awsCfS3Dist www.mslinn.com | $(basename $0) " exit 1 &#125; function waitForInvalidation &#123; echo "Waiting for invalidation $2 to complete." aws cloudfront wait invalidation-completed \ --distribution-id "$1" \ --id "$2" echo "Invalidation $2 has completed." &#125; if [ "$1" == -h ]; then help; fi if [ "$1" ]; then DIST_ID="$1" shift elif [ ! -t 0 ]; then read -r DIST_ID fi if [ -z "$DIST_ID" ]; then help 'Error: No CloudFront distribution ID was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi JSON="$( aws cloudfront create-invalidation \ --distribution-id "$DIST_ID" \ --paths "/*" )" INVALIDATION_ID="$( jq -r .Invalidation.Id &lt;&lt;&lt; "$JSON" )" waitForInvalidation "$DIST_ID" "$INVALIDATION_ID" &amp; </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id300eddb3bf04'><button class='copyBtn' data-clipboard-target='#id300eddb3bf04' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfInvalidate E2P5S6OYKQNB6B <span class='unselectable'>Waiting for invalidation IFOPKECU4YYHD to complete. </span> <span class='unselectable'><i>... do other things ...</i> </span> <span class='unselectable'>$ </span><span class='unselectable'>Invalidation IFOPKECU4YYHD has completed. </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id37c61264b88a'><button class='copyBtn' data-clipboard-target='#id37c61264b88a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfS3Dist www.mslinn.com | awsCfInvalidate <span class='unselectable'>Waiting for invalidation IFOPKECU4YYHD to complete. </span> <span class='unselectable'><i>... do other things ...</i> </span> <span class='unselectable'>$ </span><span class='unselectable'>Invalidation IFOPKECU4YYHD has completed. </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsCfS3Dist"><span class="code">awsCfS3Dist</span></h2> <p> Given an S3 bucket name, return the CloudFront distribution JSON. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsCfS3Dist' download='awsCfS3Dist' title='Click on the file name to download the file'>awsCfS3Dist</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idd51866e8c7b8"><button class='copyBtn' data-clipboard-target='#idd51866e8c7b8'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Obtain the CloudFront distribution JSON for an S3 bucket. If no S3 bucket with the given name exists, the empty string is returned and the return code is 2. Syntax: $(basename $0) bucketName Syntax: echo bucketName | $(basename $0) " exit 1 &#125; if [ "$1" == -h ]; then help; fi if [ "$1" ]; then BUCKET_NAME="$1" shift elif [ ! -t 0 ]; then read -r BUCKET_NAME fi if [ -z "$BUCKET_NAME" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi if [ "$( aws s3api head-bucket --bucket $BUCKET_NAME 2> >(grep -i 'Not Found') )" ]; then >&amp;2 echo "Error: Bucket $BUCKET_NAME does not exist." exit 2 fi DIST_ID="$( awsCfS3DistId "$BUCKET_NAME" )" if [ -z "$DIST_ID" ]; then exit 2; fi aws cloudfront get-distribution-config --id "$DIST_ID" </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9a104ea8f808'><button class='copyBtn' data-clipboard-target='#id9a104ea8f808' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfS3Dist www.mslinn.com <span class='unselectable'>{ "ETag": "E1DIZUSLMOLXKP", "DistributionConfig": { "CallerReference": "1454487160038", "Aliases": { "Quantity": 2, "Items": [ "www.mslinn.com", "mslinn.com" ] }, "DefaultRootObject": "index.html", "Origins": { "Quantity": 1, "Items": [ { "Id": "S3-www.mslinn.com", "DomainName": "www.mslinn.com.s3-website-us-east-1.amazonaws.com", "OriginPath": "", "CustomHeaders": { "Quantity": 0 }, "CustomOriginConfig": { "HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "http-only", "OriginSslProtocols": { "Quantity": 3, "Items": [ "TLSv1", "TLSv1.1", "TLSv1.2" ] }, "OriginReadTimeout": 30, "OriginKeepaliveTimeout": 5 }, "ConnectionAttempts": 3, "ConnectionTimeout": 10 } ] }, "OriginGroups": { "Quantity": 0 }, "DefaultCacheBehavior": { "TargetOriginId": "S3-www.mslinn.com", "TrustedSigners": { "Enabled": false, "Quantity": 0 }, "ViewerProtocolPolicy": "redirect-to-https", "AllowedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ], "CachedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ] } }, "SmoothStreaming": false, "Compress": true, "LambdaFunctionAssociations": { "Quantity": 0 }, "FieldLevelEncryptionId": "", "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6" }, "CacheBehaviors": { "Quantity": 0 }, "CustomErrorResponses": { "Quantity": 2, "Items": [ { "ErrorCode": 403, "ResponsePagePath": "", "ResponseCode": "", "ErrorCachingMinTTL": 60 }, { "ErrorCode": 404, "ResponsePagePath": "", "ResponseCode": "", "ErrorCachingMinTTL": 60 } ] }, "Comment": "", "Logging": { "Enabled": false, "IncludeCookies": false, "Bucket": "", "Prefix": "" }, "PriceClass": "PriceClass_All", "Enabled": true, "ViewerCertificate": { "ACMCertificateArn": "arn:aws:acm:us-east-1:031372724784:certificate/2be42926-829c-4db9-be7d-a72e951256d4", "SSLSupportMethod": "sni-only", "MinimumProtocolVersion": "TLSv1", "Certificate": "arn:aws:acm:us-east-1:031372724784:certificate/2be42926-829c-4db9-be7d-a72e951256d4", "CertificateSource": "acm" }, "Restrictions": { "GeoRestriction": { "RestrictionType": "none", "Quantity": 0 } }, "WebACLId": "", "HttpVersion": "http1.1", "IsIPV6Enabled": false } } </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idacdf9c60b551'><button class='copyBtn' data-clipboard-target='#idacdf9c60b551' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo www.mslinn.com | awsCfS3Dist <span class='unselectable'>{ "ETag": "E1DIZUSLMOLXKP", "DistributionConfig": { "CallerReference": "1454487160038", "Aliases": { "Quantity": 2, "Items": [ "www.mslinn.com", "mslinn.com" ] }, "DefaultRootObject": "index.html", "Origins": { "Quantity": 1, "Items": [ { "Id": "S3-www.mslinn.com", "DomainName": "www.mslinn.com.s3-website-us-east-1.amazonaws.com", "OriginPath": "", "CustomHeaders": { "Quantity": 0 }, "CustomOriginConfig": { "HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "http-only", "OriginSslProtocols": { "Quantity": 3, "Items": [ "TLSv1", "TLSv1.1", "TLSv1.2" ] }, "OriginReadTimeout": 30, "OriginKeepaliveTimeout": 5 }, "ConnectionAttempts": 3, "ConnectionTimeout": 10 } ] }, "OriginGroups": { "Quantity": 0 }, "DefaultCacheBehavior": { "TargetOriginId": "S3-www.mslinn.com", "TrustedSigners": { "Enabled": false, "Quantity": 0 }, "ViewerProtocolPolicy": "redirect-to-https", "AllowedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ], "CachedMethods": { "Quantity": 2, "Items": [ "HEAD", "GET" ] } }, "SmoothStreaming": false, "Compress": true, "LambdaFunctionAssociations": { "Quantity": 0 }, "FieldLevelEncryptionId": "", "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6" }, "CacheBehaviors": { "Quantity": 0 }, "CustomErrorResponses": { "Quantity": 2, "Items": [ { "ErrorCode": 403, "ResponsePagePath": "", "ResponseCode": "", "ErrorCachingMinTTL": 60 }, { "ErrorCode": 404, "ResponsePagePath": "", "ResponseCode": "", "ErrorCachingMinTTL": 60 } ] }, "Comment": "", "Logging": { "Enabled": false, "IncludeCookies": false, "Bucket": "", "Prefix": "" }, "PriceClass": "PriceClass_All", "Enabled": true, "ViewerCertificate": { "ACMCertificateArn": "arn:aws:acm:us-east-1:031372724784:certificate/2be42926-829c-4db9-be7d-a72e951256d4", "SSLSupportMethod": "sni-only", "MinimumProtocolVersion": "TLSv1", "Certificate": "arn:aws:acm:us-east-1:031372724784:certificate/2be42926-829c-4db9-be7d-a72e951256d4", "CertificateSource": "acm" }, "Restrictions": { "GeoRestriction": { "RestrictionType": "none", "Quantity": 0 } }, "WebACLId": "", "HttpVersion": "http1.1", "IsIPV6Enabled": false } } </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsCfS3DistId"><span class="code">awsCfS3DistId</span></h2> <p> Given an S3 bucket name, return the CloudFront distribution ID. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsCfS3DistId' download='awsCfS3DistId' title='Click on the file name to download the file'>awsCfS3DistId</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idab53cf9b7a7e"><button class='copyBtn' data-clipboard-target='#idab53cf9b7a7e'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Obtain the CloudFront distribution ID for an S3 bucket. If no S3 bucket with the given name exists, the empty string is returned and the return code is 2. Syntax: $(basename $0) bucketName Syntax: echo bucketName | $(basename $0) " exit 1 &#125; if [ "$1" == -h ]; then help; fi if [ "$1" ]; then BUCKET_NAME="$1" shift elif [ ! -t 0 ]; then read -r BUCKET_NAME fi if [ -z "$BUCKET_NAME" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi if [ "$( aws s3api head-bucket --bucket $BUCKET_NAME 2> >(grep -i 'Not Found') )" ]; then >&amp;2 echo "Error: Bucket $BUCKET_NAME does not exist." exit 2 fi DIST_ID="$( aws cloudfront list-distributions \ --query "DistributionList.Items[*].&#123;id:Id,origin:Origins.Items[0].Id&#125;[?origin=='S3-$BUCKET_NAME'].id" \ --output text )" if [ -z "$DIST_ID" ]; then exit 2; fi echo "$DIST_ID" </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide99613d1f214'><button class='copyBtn' data-clipboard-target='#ide99613d1f214' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfS3DistId www.mslinn.com <span class='unselectable'>E2P5S6OYKQNB6B </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide69bcf766274'><button class='copyBtn' data-clipboard-target='#ide69bcf766274' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo www.mslinn.com | awsCfS3DistId <span class='unselectable'>E2P5S6OYKQNB6B </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsCfS3MakeDist"><span class="code">awsCfS3MakeDist</span></h2> <p> Creates a CloudFront distribution for the given bucket name. Returns the new distribution's ID. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsCfS3MakeDist' download='awsCfS3MakeDist' title='Click on the file name to download the file'>awsCfS3MakeDist</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id9434623bb5fd"><button class='copyBtn' data-clipboard-target='#id9434623bb5fd'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Make a new CloudFront distribution for the given S3 bucket name. Returns the new distribution's ID. Syntax: $(basename $0) bucketName Syntax: echo bucketName | $(basename $0) " exit 1 &#125; function doesDistributionExist &#123; DIST_ID="$( awsCfS3Dist "$BUCKET_NAME" )" if [ "$DIST_ID" ]; then echo true; fi &#125; function createDist &#123; read -r -d '' NEW_DIST_JSON &lt;&lt;EOF &#123; "CallerReference": "$BUCKET_NAME", "Aliases": &#123; "Quantity": 0 &#125;, "DefaultRootObject": "index.html", "Origins": &#123; "Quantity": 1, "Items": [ &#123; "Id": "$BUCKET_NAME", "DomainName": "$BUCKET_NAME.s3.amazonaws.com", "S3OriginConfig": &#123; "OriginAccessIdentity": "" &#125; &#125; ] &#125;, "DefaultCacheBehavior": &#123; "TargetOriginId": "$BUCKET_NAME", "ForwardedValues": &#123; "QueryString": true, "Cookies": &#123; "Forward": "none" &#125; &#125;, "TrustedSigners": &#123; "Enabled": false, "Quantity": 0 &#125;, "ViewerProtocolPolicy": "redirect-to-https", "MinTTL": 3600 &#125;, "CacheBehaviors": &#123; "Quantity": 0 &#125;, "Comment": "", "Logging": &#123; "Enabled": false, "IncludeCookies": true, "Bucket": "", "Prefix": "" &#125;, "PriceClass": "PriceClass_All", "Enabled": true &#125; EOF NEW_DIST_RESULT_JSON = "$( aws cloudfront create-distribution --distribution-config "$NEW_DIST_JSON" )" DISTRIBUTION_ID="$( jq -r '.Distribution.Id' &lt;&lt;&lt; "$NEW_DIST_RESULT_JSON" )" echo "$DISTRIBUTION_ID" &#125; if [ "$1" == -h ]; then help; fi if [ -t 0 ]; then if [ -z "$1" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi BUCKET_NAME="$1" shift else read -r BUCKET_NAME fi if [ -z "$BUCKET_NAME" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi if [ "$( aws s3api head-bucket --bucket $BUCKET_NAME 2> >(grep -i 'Not Found') )" ]; then >&amp;2 echo "Error: Bucket $BUCKET_NAME does not exist." exit 2 fi if [ "$(doesDistributionExist)" ]; then >&amp;2 echo "Error: a CloudFront distibution already exists for S3 bucket $BUCKET_NAME" exit 3 fi createDist </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2feadd66a080'><button class='copyBtn' data-clipboard-target='#id2feadd66a080' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfS3MakeDist my_bucket <span class='unselectable'>E2P5S6OYKQNB6B </span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddae29fa94171'><button class='copyBtn' data-clipboard-target='#iddae29fa94171' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo my_bucket | awsCfS3MakeDist <span class='unselectable'>E2P5S6OYKQNB6B </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsS3Mb"><span class="code">awsS3Mb</span></h2> <p> Make a new S3 bucket with the given name in the default AWS region. If the <code>--public-read</code> option is provided, set the ACL to <code>public-read</code> </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsS3Mb' download='awsS3Mb' title='Click on the file name to download the file'>awsS3Mb</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idb71dab98969d"><button class='copyBtn' data-clipboard-target='#idb71dab98969d'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Make a new S3 bucket with the given name in the default AWS region. Syntax: $(basename $0) bucketName [OPTIONS] Syntax: echo bucketName | $(basename $0) [OPTIONS] Options are: --public-read Set bucket ACL to public-read " exit 1 &#125; if [ "$1" == -h ]; then help; fi if [ "$1" == "--public-read" ]; then ACL="public-read" shift fi if [ "$1" ]; then BUCKET_NAME="$1" shift elif [ ! -t 0 ]; then read -r BUCKET_NAME fi if [ -z "$BUCKET_NAME" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi aws s3 mb s3://$BUCKET_NAME if [ "$ACL" ]; then aws s3api put-bucket-acl --bucket $BUCKET_NAME --acl $ACL fi </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbe6ab8456e00'><button class='copyBtn' data-clipboard-target='#idbe6ab8456e00' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsS3Mb my_bucket</pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf0f32667634d'><button class='copyBtn' data-clipboard-target='#idf0f32667634d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsS3Mb my_bucket --public-read</pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id606cf88298bc'><button class='copyBtn' data-clipboard-target='#id606cf88298bc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo my_bucket | awsS3Mb --public-read</pre> </div> <!-- endregion --> <!-- #region --> <h2 id="awsS3Website"><span class="code">awsS3Website</span></h2> <p> Enable an S3 bucket to be a website. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,awsS3Website' download='awsS3Website' title='Click on the file name to download the file'>awsS3Website</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id0558e63f7bcc"><button class='copyBtn' data-clipboard-target='#id0558e63f7bcc'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function help &#123; printf "$1$(basename $0) - Enable an S3 bucket to be a website. Syntax: $(basename $0) bucketName Syntax: echo bucketName | $(basename $0) " exit 1 &#125; if [ "$1" == -h ]; then help; fi if [ "$1" ]; then BUCKET_NAME="$1" shift elif [ ! -t 0 ]; then read -r BUCKET_NAME fi if [ -z "$BUCKET_NAME" ]; then help 'Error: No S3 bucket name was specified.\n\n'; fi if [ "$1" ]; then help 'Error: Too many arguments provided.\n\n'; fi aws s3 website s3://$BUCKET_NAME \ --index-document index.html \ --error-document 404.html </pre> <p> Example usages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id396022778774'><button class='copyBtn' data-clipboard-target='#id396022778774' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsS3Website my_bucket</pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id53e2fa6c4234'><button class='copyBtn' data-clipboard-target='#id53e2fa6c4234' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo my_bucket | awsS3Website</pre> </div> <!-- endregion --> CORS on AWS S3 and Cloudfront 2021-03-21T00:00:00-04:00 https://mslinn.github.io/blog/2021/03/21/cors-aws <!-- #region intro --> <p> This article shows how to enable CORS on an AWS S3 bucket with AWS CLI, then modify the bucket&rsquo;s CloudFront distribution. In preparing this article, I found that the <a href='https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html' target='_blank' rel="nofollow">AWS S3 CORS documentation</a> needs to be read in conjunction with how <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors' target='_blank' rel="nofollow">AWS CloudFront can be configured to handle CORS</a>. </p> <p> I used one origin for testing. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd0837c0f59a9'><button class='copyBtn' data-clipboard-target='#idd0837c0f59a9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ORIGIN=ancientwarmth.com<br> <span class='unselectable'>$ </span>JSON_FILE=cors.json</pre> </div> <p> The CORS configuration for the AWS S3 bucket will be stored in the file pointed to by <code>JSON_FILE</code>. </p> <!-- endregion --> <!-- #region Define the AWS S3 Bucket CORS Configuration --> <h2 id="defS3Cors">Define the AWS S3 Bucket CORS Configuration</h2> <p> This configuration (in JSON format) contains 1 rule: </p> <ol> <li>Allow <code>GET</code> HTTP methods from anywhere.</li> </ol> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>cors.json</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida9d95137948c'><button class='copyBtn' data-clipboard-target='#ida9d95137948c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "CORSRules": [ { "AllowedHeaders": [], "AllowedMethods": [ "GET" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [] } ] }</pre> </div> <p> You can read about CORS configuration in the <a href='https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html#cors-example-1' target='_blank' rel="nofollow">AWS documentation</a>. </p> <!-- endregion --> <!-- #region Set the AWS S3 Bucket CORS Configuration --> <h2 id="setS3Cors">Set the AWS S3 Bucket CORS Configuration</h2> <p> It is easy to set the CORS configuration for an AWS S3 bucket using AWS CLI&rsquo;s <a href='https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-cors.html' target='_blank' rel="nofollow"><code>aws s3api put-bucket-cors</code> subcommand</a>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id283bbe4bd1c8'><button class='copyBtn' data-clipboard-target='#id283bbe4bd1c8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>BUCKET=assets.ancientwarmth.com<br> <span class='unselectable'>$ </span>aws s3api put-bucket-cors \ --bucket $BUCKET \ --cors-configuration "file://$JSON_FILE"</pre> </div> <!-- endregion --> <!-- #region Test the AWS S3 Bucket CORS Configuration --> <h2 id="testS3Cors">Test the AWS S3 Bucket CORS Configuration</h2> <p> Now it is time to test the S3 bucket&rsquo;s CORS configuration using <code>curl</code>. I defined a bash function to peform the test to save typing. You can use it by first copy/pasting the code below into a shell prompt, then calling the function with the proper arguments, as shown. The function requires 3 arguments: the request origin, the URL of an asset in an AWS S3 bucket, and an HTTP method (which must be in UPPPER CASE). </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4d9f2cb075df'><button class='copyBtn' data-clipboard-target='#id4d9f2cb075df' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>function testCors { if [ -z "$1" ]; then echo "Error: No origin was provided"; exit 1; fi if [ -z "$2" ]; then echo "Error: No URL to test was provided"; exit 1; fi if [ "$3" ]; then METHOD="$3"; else METHOD=GET; fi<br> curl -I -X OPTIONS \ --no-progress-meter \ -H "Origin: $1" \ -H "Access-Control-Request-Method: $METHOD" \ "$2" 2>&1 | \ grep --color=never 'Access-Control' }</pre> </div> <p> The JSON file for testing CORS was <code><a href='https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html#path-argument-type' target='_blank' rel="nofollow">s3://</a>$BUCKET/testCors.json</code>: </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,testCors.json' download='testCors.json' title='Click on the file name to download the file'>testCors.json</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id35f726ddba30"><button class='copyBtn' data-clipboard-target='#id35f726ddba30'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&#123; "key1": "value1", "key2": "value2" &#125; </pre> <p> We will know if CORS is set up properly by receiving a header containing <code>Access-Control-Allow-Origin: *</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id13ffb2b688e1'><button class='copyBtn' data-clipboard-target='#id13ffb2b688e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>URL="https://s3.amazonaws.com/$BUCKET/testCors.json"<br> <span class='unselectable'>$ </span>testCors $ORIGIN $URL GET <span class='unselectable'><span class="bg_yellow">Access-Control-Allow-Origin: *</span> Access-Control-Allow-Methods: GET Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method </span></pre> </div> <p> The origin worked when the bucket is accessed via a <code>GET</code> method sent to its <code>s3.amazonaws.com</code> DNS alias (yay!). </p> <p> CORScanner (<a href="/blog/2021/03/20/cors.html#corscanner">discussed in a previous article</a>) reported no issues: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf48175bd8a39'><button class='copyBtn' data-clipboard-target='#idf48175bd8a39' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cors -u s3.amazonaws.com/assets.ancientwarmth.com/testCors.json <span class='unselectable'>Starting CORS scan... Finished CORS scanning... </span></pre> </div> <!-- endregion --> <!-- #region CloudFront --> <h2 id="cf">CloudFront</h2> <p> I have not worked through the process of using AWS CLI to obtain a JSON object describing the distribution, and then changing some properties and writing it back. So until that happy day comes, here are 2 screen shots of the <a href='https://console.aws.amazon.com/cloudfront/home' target='_blank' rel="nofollow">AWS CloudFront web console</a> showing the settings. The first screen shot shows the <b>Behaviors</b> tab of the top-level details of the <code>assets.ancientwarmth.com</code> CloudFront distribution. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/aws/cfBehaviorCors0.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/cfBehaviorCors0.avif" type="image/avif">--> <source srcset="/blog/images/aws/cfBehaviorCors0.webp" type="image/webp"> <source srcset="/blog/images/aws/cfBehaviorCors0.apng" type="image/apng"> <source srcset="/blog/images/aws/cfBehaviorCors0.png" type="image/png"> <source srcset="/blog/images/aws/cfBehaviorCors0.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors0.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors0.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors0.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors0.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors0.gif" type="image/gif"> <source srcset="/blog/images/aws/cfBehaviorCors0.tif" type="image/tiff"> <source srcset="/blog/images/aws/cfBehaviorCors0.tiff" type="image/tiff"> <source srcset="/blog/images/aws/cfBehaviorCors0.bmp" type="image/bmp"> <source srcset="/blog/images/aws/cfBehaviorCors0.ico" type="image/x-icon"> <source srcset="/blog/images/aws/cfBehaviorCors0.cur" type="image/x-icon"> <img alt='CloudFront / Edit Distribution / Behaviors <br> About to click on <b>Edit</b> (default behavior)' class="imgImg rounded shadow" src="/blog/images/aws/cfBehaviorCors0.png" style='width: 100%; ' title='CloudFront / Edit Distribution / Behaviors <br> About to click on <b>Edit</b> (default behavior)' /> </picture> <figcaption class='imgFigCaption '> CloudFront / Edit Distribution / Behaviors <br> About to click on <b>Edit</b> (default behavior) </figcaption> </figure> </div> <p> My application does not require users to upload anything, so everything in the S3 bucket is truly static. Thus I have no need to <code>PUT</code>, <code>POST</code> or <code>DELETE</code> HTTP methods for the AWS S3 content. I have not seen a good explanation of why enabling <code>OPTIONS</code> HTTP methods is necessary, but every person on Stack Overflow who got CORS to work with AWS S3 says this was necessary. With that in mind, I set the following for the next screen shot: </p> <ul> <li><b>Viewer Protocol Policy:</b> <code>Redirect HTTP to HTTPS</code></li> <li><b>Allowed HTTP Methods:</b> <code>GET, HEAD, OPTIONS</code></li> <li><b>Cached HTTP Methods:</b> Enable <code>OPTIONS</code></li> <li><b>Use a cache policy and origin request policy:</b> (default is Use legacy cache settings, which is usually undesirable)</li> <li><b>Cache Policy:</b> <code>Managed-CachingOptimized</code></li> <li><b>Origin Request Policy:</b> <code>Managed-CORS-S3Origin</code></li> </ul> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/aws/cfBehaviorCors1.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/cfBehaviorCors1.avif" type="image/avif">--> <source srcset="/blog/images/aws/cfBehaviorCors1.webp" type="image/webp"> <source srcset="/blog/images/aws/cfBehaviorCors1.apng" type="image/apng"> <source srcset="/blog/images/aws/cfBehaviorCors1.png" type="image/png"> <source srcset="/blog/images/aws/cfBehaviorCors1.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors1.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors1.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors1.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/cfBehaviorCors1.gif" type="image/gif"> <source srcset="/blog/images/aws/cfBehaviorCors1.tif" type="image/tiff"> <source srcset="/blog/images/aws/cfBehaviorCors1.tiff" type="image/tiff"> <source srcset="/blog/images/aws/cfBehaviorCors1.bmp" type="image/bmp"> <source srcset="/blog/images/aws/cfBehaviorCors1.ico" type="image/x-icon"> <source srcset="/blog/images/aws/cfBehaviorCors1.cur" type="image/x-icon"> <img alt='Editing default CloudFront distribution behavior' class="imgImg rounded shadow" src="/blog/images/aws/cfBehaviorCors1.png" style='width: 100%; ' title='Editing default CloudFront distribution behavior' /> </picture> <figcaption class='imgFigCaption '> Editing default CloudFront distribution behavior </figcaption> </figure> </div> <h3 id="cfManagedCorsS3OriginPolicy">Managed CORS S3 Origin Poligy</h3> <p> AWS CloudFront's <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html' target='_blank' rel="nofollow">managed origin request policy</a> called <code>Managed-CORS-S3Origin</code> includes the headers that enable cross-origin resource sharing (CORS) requests when the origin is an Amazon S3 bucket. This policy's settings are: </p> <ul> <li><b>Query strings included in origin requests</b>: None</li> <li><b>Headers included in origin requests</b>: <ul> <li><code>Origin</code></li> <li><code>Access-Control-Request-Headers</code></li> <li><code>Access-Control-Request-Method</code></li> </ul> </li> <li><b>Cookies included in origin requests</b>: None</li> </ul> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.svg" type="image/svg"> <!---<source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.avif" type="image/avif">--> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.webp" type="image/webp"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.apng" type="image/apng"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.png" type="image/png"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.jpg" type="image/jpeg"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.jpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.jfif" type="image/jpeg"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.pjp" type="image/jpeg"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.gif" type="image/gif"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.tif" type="image/tiff"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.tiff" type="image/tiff"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.bmp" type="image/bmp"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.ico" type="image/x-icon"> <source srcset="/blog/images/aws/cfManagedCorsS3OriginPolicy.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/aws/cfManagedCorsS3OriginPolicy.png" style='width: 100%; ' /> </picture> </div> <!-- endregion --> <!-- #region --> <h2 id="wait">Wait or Invalidate</h2> <p> Whenever you make a configuration change to a CloudFront distribution, or the contents change, the distributed assets will not reflect those changes until the next CloudFront invalidation. Automatic invalidations take 20 minutes. You can invalidate manually for near-instant gratification. I use my <a href='/blog/2021/03/22/command-line-aws-utilities.html#awsCfInvalidate'>AWS command-line utilities</a> to invalidate manually: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id50cf97552bbd'><button class='copyBtn' data-clipboard-target='#id50cf97552bbd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>awsCfS3DistId $BUCKET | awsCfInvalidate</pre> </div> <p> Now the grand finale: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' style='margin-bottom: 1em;' id='idcf52b5ca1e9c'><button class='copyBtn' data-clipboard-target='#idcf52b5ca1e9c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>testCors $ORIGIN $URL GET <span class='unselectable'>Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method </span></pre> </div> <span style='font-size: 3em; float: right; margin-left: 5px;;'>&#x1F601;</span> <p> The presence of the <code>Access-Control-Allow-Origin</code> header indicates that CORS allowed the data file to be transferred from the content server (AWS S3/CloudFront) to the origin server (the command line). </p> <!-- endregion --> Cross-Origin Resource Sharing (CORS) 2021-03-20T00:00:00-04:00 https://mslinn.github.io/blog/2021/03/20/cors <!-- #region intro --> <p> Many have tried to explain CORS, but most have not provided a clear explanation. I am going to try, then I will refer to explanations by others, who also provide examples. </p> <!-- endregion --> <!-- #region Origin and Origin Server --> <h2 id="origin">Origin and Origin Server</h2> <p> A website is delivered to web browsers from an <i>origin server</i>, or <i>origin</i> for short. The origin server is principally responsible for generating web pages. </p> <p> An origin is a combination of 3 things: </p> <ol> <li>A scheme (<code>http</code>, <code>https</code>, etc.)</li> <li>A (sub)domain, for example <code>localhost</code>, <code>blah.com</code> or <code>assets.blah.com</code>.</li> <li>A port, for example 80, 443, 8000, etc.</li> </ol> <p> All three things must match in order for two URLs to be considered to be from the same origin. For example: </p> <table class="table"> <tr> <th>URL 1</th> <th>URL 2</th> <th>Same Origin?</th> </tr> <tr> <td><code>http://blah.com</code></td> <td><code>http<span class="bg_yellow">s</span>://blah.com</code></td> <th>No</th> </tr> <tr> <td><code>https://blah.com</code></td> <td><code>https://<span class="bg_yellow">assets.</span>blah.com</code></td> <th>No</th> </tr> <tr> <td><code>https://blah.com</code></td> <td><code>https://blah.com<span class="bg_yellow">/path/page.html</span></code></td> <th>Yes</th> </tr> </table> <!-- endregion --> <!-- #region Content Servers --> <h2 id="contentServer">Content Servers</h2> <p> In this article, I use the term <i>content server</i> to refer to sources of online information other than the origin server. Resources referenced by a web page, such as images, JavaScript, CSS, and data might be provided by the origin server, or they might come from a content server. </p> <p> Because every server has by definition a different origin, content servers always have a different origin than the origin server. Static resources (resources that do not change) are often served by <i>content delivery networks</i> (CDNs), which are also content servers. </p> <p> The Cross-Origin Resource Sharing (CORS) standard controls if a web page can load resources from content servers. Content servers are in charge of their content; they decide which origin servers they wish to co-operate with. When CORS support is properly configured, content servers include HTTP headers into their responses that tell a web browser if those resources may be read by the web page being constructed. </p> <p class="alert rounded shadow"> <i>Proxy servers</i> incorporate content from other servers into their output. The content from <i>proxied sites</i> are incorporated into proxy server&rsquo;s output. </p> <p> Here is a more detailed set of definitions: </p> <p class="alert rounded shadow"> A <i>proxy</i> is a person or process serving as an authorized agent or substitute for another. In computer science, a more specific term is <i>forward proxy</i>. <br><br> A <i>proxy server</i> is a server process that acts as an intermediary between a client requesting a resource, and the process that provides the resource. <br><br> A <i>reverse proxy</i> is a process that sits in front of other processes, and forwards client requests to them. The term <i>forwarding process</i> is similar to <i>reverse proxy</i>. <br><br> The definitions for <i>proxy</i>, <i>forward proxy</i> and <i>reverse proxy</i> all sound identical. The key difference between a reverse proxy and a forward proxy is that a <b>forward proxy enables computers isolated on a private network</b> to connect to the public internet, while a <b>reverse proxy enables computers on the internet</b> to access a private subnet. </p> <p> Data is a special type of resource. CORS restricts how data is exchanged between the web page delivered to the web browser from the origin server and content servers. In particular, JSON and XML data communicated to and from content servers requires CORS authorization. Furthermore, requests (from the web browser) that send JSON, XML and other data formats to content servers also require CORS authorization. </p> <div class="pullQuote">Content servers are in charge of their content; they decide which origin servers they wish to co-operate with.</div> <!-- endregion --> <!-- #region Nginx Proxy Configuration --> <h3 id="nginx">Nginx Proxy Configuration</h3> <p> Nginx is a content server, and an nginx website can configured to allow its content to be proxied by other servers. Other nginx websites could be configured as proxy servers. </p> <p class="alert rounded shadow"> CORS headers emitted by the proxied website determine which proxy servers are allowed to consume their content. </p> <p> For example, imagine a website running at <code>http://localhost:9000</code>. The following configuration that would allow that server&rsquo;s content to be proxied by any another server. Specifically, the highlighted snippet grants permission to any other server to <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin' target='_blank' rel="nofollow">include the content into their web pages</a>. All of the <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers' target='_blank' rel="nofollow">proxied website&rsquo;s headers are allowed to be passed through</a> by the proxy server, if it is configured to do so. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Nginx configuration for proxied server on port 9000</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcd61aa2f1d61'><button class='copyBtn' data-clipboard-target='#idcd61aa2f1d61' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>server { listen 9000; listen [::]:9000;<br> server_name localhost;<br> location / { root /var/www/blah/blah; index index.html;<br> # First attempt to serve request as file, then as directory, then fall back to displaying a 404. try_files $uri $uri/ =404;<br> <span class="bg_yellow">add_header 'Access-Control-Allow-Origin' '*' always;</span> <span class="bg_yellow">add_header 'Access-Control-Allow-Headers' '*' always;</span> } }</pre> </div> <!-- endregion --> <p> Continuing our imaginary setup, imagine a publicly accessible proxy server that needs to incorporate the content from the local server on port 9000. The configuration for the proxy server would, at a minimum, require something like the following. This configuration causes the proxied content to be included into the public server&rsquo;s content. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Nginx proxy server configuration</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id43fefedb752d'><button class='copyBtn' data-clipboard-target='#id43fefedb752d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443;<br> server_name scalacourses.com www.scalacourses.com; ssl_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/fullchain.pem; ssl_certificate_key /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/privkey.pem; ssl_trusted_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/chain.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;<br> root /var/www/html; index index.html; # This gets served if the proxied website is down<br> location / { <span class="bg_yellow">proxy_pass http://localhost:9000;</span> } }</pre> </div> <!-- endregion --> <p> For further reading, please see my article entitled <a href='/blog/2022/07/08/reverse-proxy.html'>Using Nginx As a Reverse Proxy With SSL</a>. </p> <!-- endregion --> <!-- #region Content-Type Header --> <h2 id="ctype">Content-Type Header</h2> <p> The <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type' target='_blank' rel="nofollow"><code>Content-Type</code> header</a> is used to indicate the <a href='https://developer.mozilla.org/en-US/docs/Glossary/MIME_type' target='_blank' rel="nofollow"><code>media type</code></a> of the resource. The old name <i>MIME type</i> has been replaced by <i>media type</i>. <a href='https://www.iana.org/assignments/media-types/media-types.xhtml' target='_blank' rel="nofollow">Here is a list of media types.</a> </p> <p> Media types with names that start with <code>application</code> require CORS authentication if they are delivered from content servers, for example <code>application/json</code> and <code>application/javascript</code>. </p> <p> As well, a few media types with names that start with <code>text</code> require CORS authentication if they are delivered from content servers, for example <code>text/xml</code> and <code>text/xml-external-parsed-entity</code>. </p> <!-- endregion --> <!-- #region Further Reading --> <h2 id="resources">Further Reading</h2> <h3 id="Kosaka">Mariko Kosaka</h3> <p> Mariko Kosaka has written an easy-to-understand article describing CORS, and provides a simple but effective working Express website for demonstration. </p> <div class="quote"> The same-origin policy tells the browser to block cross-origin requests. When you want to get a public resource from a different origin, the resource-providing server needs to tell the browser &lsquo;This origin where the request is coming from can access my resource&rsquo;. The browser remembers that and allows cross-origin resource sharing. <br><br> <span style="font-style: normal"> &nbsp; &ndash; <a href='https://web.dev/cross-origin-resource-sharing/' target='_blank' rel="nofollow">Mariko Kosaka</a></span> </div> <!-- endregion --> <!-- #region Derric Gilling and MDN --> <h3 id="Gilling">Derric Gilling and MDN</h3> <p> Derric Gilling has written a more in-depth yet very approachable article describing CORS. I've paraphrased his quoting of the Mozilla Developer Network documentation into the following: </p> <div class="quote"> CORS is a security mechanism that allows a web page from one domain or Origin to access a resource with a different domain (a cross-domain request). CORS is a relaxation of the same-origin policy implemented in modern browsers. Without features like CORS, websites are restricted to accessing resources from the same origin through what is known as same-origin policy. <br><br> Any CORS request has to be preflighted if:<br> <ul> <li>It uses methods other than <code>GET</code>, <code>HEAD</code> or <code>POST</code>.</li> <li> If POST is used to send request data with a <code>Content-Type</code> other than <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code>, or <code>text/plain</code>. Examples: <ul> <li> A <code>POST</code> request sends an XML payload to the server; this requires the <code>Content-Type</code> header is set either to <code>application/xml</code> or <code>text/xml</code>. </li> <li> A website makes an AJAX call that <code>POST</code>s JSON data to a REST API, this requires the <code>Content-Type</code> header is set to <code>application/json</code>. </li> </ul> </li> </ul> <span style="font-style: normal">&nbsp; &ndash; <a href='https://www.moesif.com/blog/technical/cors/Authoritative-Guide-to-CORS-Cross-Origin-Resource-Sharing-for-REST-APIs/#how-cors-works/' target='_blank' rel="nofollow">Derric Gilling</a> <br> &nbsp; &ndash; <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests' target='_blank' rel="nofollow">Mozilla Developer Network</a> </span> </div> <!-- endregion --> <!-- #region Preflight Requests --> <h3 id="preflight">Preflight Requests</h3> <p> CORS preflight requests effectively double the latency of user requests for <a href='https://developer.mozilla.org/en-US/docs/Glossary/CRUD' target='_blank' rel="nofollow">CRUD actions</a>. Client-side and server-side caching can help reduce this overhead for many circumstances. In <a href='https://www.mslinn.com/blog/2021/04/14/serverless-ecommerce.html#cf' target='_blank' rel="nofollow">another article</a> I discuss how to use a CDN with multiple origin servers to completely eliminate the need for preflight requests. </p> <p> For additional background, please see: </p> <ul> <li><a href='https://www.rehanvdm.com/serverless/cloudfront-reverse-proxy-api-gateway-to-prevent-cors/index.html' target='_blank' rel="nofollow">CloudFront reverse proxy API Gateway to prevent CORS</a> by Rehan van der Merwe</li> <li><a href='https://httptoolkit.tech/blog/cache-your-cors/' target='_blank' rel="nofollow">Cache your CORS, for performance & profit</a> by Tim Perry</li> </ul> <!-- endregion --> <!-- #region KeyCDN --> <h3 id="KeyCDN">KeyCDN</h3> <p> KeyCDN has an even more in-depth yet still very approachable <a href='https://www.keycdn.com/support/cors' target='_blank' rel="nofollow">article describing CORS</a>. </p> <!-- endregion --> <!-- #region CORScanner --> <h2 id="corscanner">CORScanner</h2> <!-- #region implicit --> <p> <a href='https://github.com/chenjj/CORScanner' target='_blank' rel="nofollow">CORScanner</a> is a popular tool for detecting CORS misconfiguration. It is a Python module that can be executed as a shell command. Install CORScanner like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id83455019f1b9'><button class='copyBtn' data-clipboard-target='#id83455019f1b9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pip install cors</pre> </div> <p> The above adds a new executable called <code>cors</code> in the same directory where your <code>python</code> command resides. <p> <p> The <code>cors</code> documentation <a href='https://www.merriam-webster.com/thesaurus/conflate#verb' target='_blank' rel="nofollow">conflates</a> the words URL and origin. Everywhere the word <code>URL</code> appears in the documentation, the word <code>origin</code> should be assumed. </p> <!-- endregion --> <!-- #region Example: Check Domain --> <h3 id="scannEx">Example: Check Domain</h3> <p>Use the <code>-u</code> option to specify an origin to test:</p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd4979a50d481'><button class='copyBtn' data-clipboard-target='#idd4979a50d481' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cors -u api.github.com <span class='unselectable'>Starting CORS scan... Finished CORS scanning... </span></pre> </div> <!-- endregion --> <p> To enable more debug info, use the <code>-v</code> option more than once. We can see that specifying <code>https</code> restricts testing to that <code>scheme</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8f7fbdbec2c3'><button class='copyBtn' data-clipboard-target='#id8f7fbdbec2c3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cors -vv -u https://api.github.com <span class='unselectable'>Starting CORS scan... 2021-03-21 09:55:58 INFO Start checking reflect_origin for https://api.github.com 2021-03-21 09:55:58 INFO nothing found for {url: https://api.github.com, origin: https://evil.com, type: reflect_origin} 2021-03-21 09:55:58 INFO Start checking prefix_match for https://api.github.com 2021-03-21 09:55:58 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com.evil.com, type: prefix_match} 2021-03-21 09:55:58 INFO Start checking suffix_match for https://api.github.com 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://evilgithub.com, type: suffix_match} 2021-03-21 09:55:59 INFO Start checking trust_null for https://api.github.com 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: null, type: trust_null} 2021-03-21 09:55:59 INFO Start checking include_match for https://api.github.com 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://ithub.com, type: include_match} 2021-03-21 09:55:59 INFO Start checking not_escape_dot for https://api.github.com 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://api.githubacom, type: not_escape_dot} 2021-03-21 09:55:59 INFO Start checking custom_third_parties for https://api.github.com 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://whatever.github.io, type: custom_third_parties} 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: http://jsbin.com, type: custom_third_parties} 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://codepen.io, type: custom_third_parties} 2021-03-21 09:55:59 INFO nothing found for {url: https://api.github.com, origin: https://jsfiddle.net, type: custom_third_parties} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: http://www.webdevout.net, type: custom_third_parties} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://repl.it, type: custom_third_parties} 2021-03-21 09:56:00 INFO Start checking special_characters_bypass for https://api.github.com 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com_.evil.com, type: special_characters_bypass} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com-.evil.com, type: special_characters_bypass} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&quot;.evil.com, type: special_characters_bypass} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com{.evil.com, type: special_characters_bypass} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com}.evil.com, type: special_characters_bypass} 2021-03-21 09:56:00 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com^.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%60.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com!.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com~.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com`.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com;.evil.com, type: special_characters_bypass} 2021-03-21 09:56:01 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com|.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&apos;.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com(.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com).evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com*.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com,.evil.com, type: special_characters_bypass} 2021-03-21 09:56:02 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com$.evil.com, type: special_characters_bypass} 2021-03-21 09:56:03 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com=.evil.com, type: special_characters_bypass} 2021-03-21 09:56:03 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 09:56:03 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%0b.evil.com, type: special_characters_bypass} 2021-03-21 09:56:03 INFO Start checking trust_any_subdomain for https://api.github.com 2021-03-21 09:56:03 INFO nothing found for {url: https://api.github.com, origin: https://evil.api.github.com, type: trust_any_subdomain} 2021-03-21 09:56:03 INFO Start checking https_trust_http for https://api.github.com 2021-03-21 09:56:03 INFO nothing found for {url: https://api.github.com, origin: http://api.github.com, type: https_trust_http} Finished CORS scanning... </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Example: Check Origin --> <h3 id="scannEx2">Example: Check Origin</h3> <p> To check CORS misconfigurations of an origin: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4f0433629bbf'><button class='copyBtn' data-clipboard-target='#id4f0433629bbf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cors -vvu https://api.github.com/users/mslinn/repos <span class='unselectable'>Starting CORS scan... 2021-03-21 10:08:49 INFO Start checking reflect_origin for https://api.github.com 2021-03-21 10:08:49 INFO nothing found for {url: https://api.github.com, origin: https://evil.com, type: reflect_origin} 2021-03-21 10:08:49 INFO Start checking prefix_match for https://api.github.com 2021-03-21 10:08:49 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com.evil.com, type: prefix_match} 2021-03-21 10:08:49 INFO Start checking suffix_match for https://api.github.com 2021-03-21 10:08:49 INFO nothing found for {url: https://api.github.com, origin: https://evilgithub.com, type: suffix_match} 2021-03-21 10:08:49 INFO Start checking trust_null for https://api.github.com 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: null, type: trust_null} 2021-03-21 10:08:50 INFO Start checking include_match for https://api.github.com 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: https://ithub.com, type: include_match} 2021-03-21 10:08:50 INFO Start checking not_escape_dot for https://api.github.com 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: https://api.githubacom, type: not_escape_dot} 2021-03-21 10:08:50 INFO Start checking custom_third_parties for https://api.github.com 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: https://whatever.github.io, type: custom_third_parties} 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: http://jsbin.com, type: custom_third_parties} 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: https://codepen.io, type: custom_third_parties} 2021-03-21 10:08:50 INFO nothing found for {url: https://api.github.com, origin: https://jsfiddle.net, type: custom_third_parties} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: http://www.webdevout.net, type: custom_third_parties} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://repl.it, type: custom_third_parties} 2021-03-21 10:08:51 INFO Start checking special_characters_bypass for https://api.github.com 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com_.evil.com, type: special_characters_bypass} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com-.evil.com, type: special_characters_bypass} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&quot;.evil.com, type: special_characters_bypass} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com{.evil.com, type: special_characters_bypass} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com}.evil.com, type: special_characters_bypass} 2021-03-21 10:08:51 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com^.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%60.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com!.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com~.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com`.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com;.evil.com, type: special_characters_bypass} 2021-03-21 10:08:52 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com|.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&apos;.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com(.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com).evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com*.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com,.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com$.evil.com, type: special_characters_bypass} 2021-03-21 10:08:53 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com=.evil.com, type: special_characters_bypass} 2021-03-21 10:08:54 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:08:54 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%0b.evil.com, type: special_characters_bypass} 2021-03-21 10:08:54 INFO Start checking trust_any_subdomain for https://api.github.com 2021-03-21 10:08:54 INFO nothing found for {url: https://api.github.com, origin: https://evil.api.github.com, type: trust_any_subdomain} 2021-03-21 10:08:54 INFO Start checking https_trust_http for https://api.github.com 2021-03-21 10:08:54 INFO nothing found for {url: https://api.github.com, origin: http://api.github.com, type: https_trust_http} Finished CORS scanning... </span></pre> </div> <!-- endregion --> <p> If a <code>scheme</code> is not specified, then both <code>http</code> and <code>https</code> are tested: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8ba9bc4ca32'><button class='copyBtn' data-clipboard-target='#idc8ba9bc4ca32' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cors -vvu api.github.com/users/mslinn/repos <span class='unselectable'>Starting CORS scan... 2021-03-21 10:03:30 INFO Start checking reflect_origin for http://api.github.com 2021-03-21 10:03:30 INFO Start checking reflect_origin for https://api.github.com 2021-03-21 10:03:30 INFO nothing found for {url: https://api.github.com, origin: https://evil.com, type: reflect_origin}2021-03-21 10:03:30 INFO Start checking prefix_match for https://api.github.com 2021-03-21 10:03:30 INFO nothing found for {url: http://api.github.com, origin: http://evil.com, type: reflect_origin} 2021-03-21 10:03:30 INFO Start checking prefix_match for http://api.github.com 2021-03-21 10:03:30 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com.evil.com, type: prefix_match} 2021-03-21 10:03:30 INFO Start checking suffix_match for https://api.github.com 2021-03-21 10:03:30 INFO nothing found for {url: https://api.github.com, origin: https://evilgithub.com, type: suffix_match} 2021-03-21 10:03:30 INFO Start checking trust_null for https://api.github.com 2021-03-21 10:03:30 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com.evil.com, type: prefix_match} 2021-03-21 10:03:30 INFO Start checking suffix_match for http://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: null, type: trust_null} 2021-03-21 10:03:31 INFO Start checking include_match for https://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: http://api.github.com, origin: http://evilgithub.com, type: suffix_match} 2021-03-21 10:03:31 INFO Start checking trust_null for http://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: https://ithub.com, type: include_match}2021-03-21 10:03:31 INFO Start checking not_escape_dot for https://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: https://api.githubacom, type: not_escape_dot} 2021-03-21 10:03:31 INFO Start checking custom_third_parties for https://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: http://api.github.com, origin: null, type: trust_null} 2021-03-21 10:03:31 INFO Start checking include_match for http://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: https://whatever.github.io, type: custom_third_parties} 2021-03-21 10:03:31 INFO nothing found for {url: http://api.github.com, origin: http://ithub.com, type: include_match} 2021-03-21 10:03:31 INFO Start checking not_escape_dot for http://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: http://jsbin.com, type: custom_third_parties} 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: https://codepen.io, type: custom_third_parties} 2021-03-21 10:03:31 INFO nothing found for {url: http://api.github.com, origin: http://api.githubacom, type: not_escape_dot} 2021-03-21 10:03:31 INFO Start checking custom_third_parties for http://api.github.com 2021-03-21 10:03:31 INFO nothing found for {url: https://api.github.com, origin: https://jsfiddle.net, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: http://api.github.com, origin: https://whatever.github.io, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: http://www.webdevout.net, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://repl.it, type: custom_third_parties} 2021-03-21 10:03:32 INFO Start checking special_characters_bypass for https://api.github.com 2021-03-21 10:03:32 INFO nothing found for {url: http://api.github.com, origin: http://jsbin.com, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com_.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com-.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: http://api.github.com, origin: https://codepen.io, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&quot;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com{.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: http://api.github.com, origin: https://jsfiddle.net, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com}.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:03:32 INFO nothing found for {url: http://api.github.com, origin: http://www.webdevout.net, type: custom_third_parties} 2021-03-21 10:03:32 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com^.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: http://api.github.com, origin: https://repl.it, type: custom_third_parties} 2021-03-21 10:03:33 INFO Start checking special_characters_bypass for http://api.github.com 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%60.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com!.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com_.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com~.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com`.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com-.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com&quot;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com|.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com{.evil.com, type: special_characters_bypass} 2021-03-21 10:03:33 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com&qpos;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com(.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com}.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com).evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com*.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com,.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com^.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com$.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com=.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com%60.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com!.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO nothing found for {url: https://api.github.com, origin: https://api.github.com%0b.evil.com, type: special_characters_bypass} 2021-03-21 10:03:34 INFO Start checking trust_any_subdomain for https://api.github.com 2021-03-21 10:03:35 INFO nothing found for {url: https://api.github.com, origin: https://evil.api.github.com, type: trust_any_subdomain} 2021-03-21 10:03:35 INFO Start checking https_trust_http for https://api.github.com 2021-03-21 10:03:35 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com~.evil.com, type: special_characters_bypass} 2021-03-21 10:03:35 INFO nothing found for {url: https://api.github.com, origin: http://api.github.com, type: https_trust_http} 2021-03-21 10:03:35 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com`.evil.com, type: special_characters_bypass} 2021-03-21 10:03:35 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:35 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com|.evil.com, type: special_characters_bypass} 2021-03-21 10:03:35 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com&.evil.com, type: special_characters_bypass} 2021-03-21 10:03:36 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com&apos;.evil.com, type: special_characters_bypass} 2021-03-21 10:03:36 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com(.evil.com, type: special_characters_bypass} 2021-03-21 10:03:36 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com).evil.com, type: special_characters_bypass} 2021-03-21 10:03:36 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com*.evil.com, type: special_characters_bypass} 2021-03-21 10:03:37 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com,.evil.com, type: special_characters_bypass} 2021-03-21 10:03:38 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com$.evil.com, type: special_characters_bypass} 2021-03-21 10:03:38 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com=.evil.com, type: special_characters_bypass} 2021-03-21 10:03:38 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com+.evil.com, type: special_characters_bypass} 2021-03-21 10:03:38 INFO nothing found for {url: http://api.github.com, origin: http://api.github.com%0b.evil.com, type: special_characters_bypass} 2021-03-21 10:03:38 INFO Start checking trust_any_subdomain for http://api.github.com 2021-03-21 10:03:39 INFO nothing found for {url: http://api.github.com, origin: http://evil.api.github.com, type: trust_any_subdomain} Finished CORS scanning... </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> AWS S3 and CloudFront SSL 2021-03-19T00:00:00-04:00 https://mslinn.github.io/blog/2021/03/19/aws-ssl <style> .sslScenario { border: thin solid grey; border-radius: 4px; padding: 8px; } </style> <p> SSL certificates need to match the domain they are served from. </p> <p> AWS uses one of several SSL certificates, depending on the <a href='https://docs.aws.amazon.com/AmazonS3/latest/dev-retired/UsingBucket.html#access-bucket-intro' target='_blank' rel="nofollow">domain that an asset is requested from</a>. </p> <ul> <li> AWS S3 applies an SSL certificate for <code>https</code> requests. The SSL certificate chosen depends on the bucket endpoint used: <code>s3.amazonaws.com</code>, <code>*.s3.amazonaws.com</code>, or <code>s3.<i>region</i>.amazonaws.com</code>. </li> <li> AWS CloudFront will apply your custom SSL certificate (for example, a wildcard certificate such as <code>*.ancientwarmth.com</code>) for <code>https</code> requests to the CNAME for that distribution, otherwise it will apply the wildcard SSL certificate for <code>*.cloudfront.net</code>. </li> </ul> <h2 id="examples">Example <span class="code">SSL</span> URLs</h2> <p> My AWS S3 bucket called <code>assets.ancientwarmth.com</code> is served via a CloudFront distribution with URL <code>d1bci9l8cjf24o.cloudfront.net</code> that applies a wildcard SSL certificate for <code>*.ancientwarmth.com</code> that I created using <a href='https://aws.amazon.com/certificate-manager/' target='_blank' rel="nofollow">AWS Certificate Manager</a>. I defined a CNAME called <code>assets.ancientwarmth.com</code> for that same CloudFront distribution using Route 53. </p> <p> All of the following URLs can be used to access my content, providing the SSL certificate matches the requested domain. </p> <p class="sslScenario"> <b>URL:</b> <code>https://d1bci9l8cjf24o.cloudfront.net</code><br> <b>Origin Type:</b> CloudFront distribution<br> <b>SSL certificate origin:</b> <code>*.cloudfront.net</code><br> <b>Valid SSL certificate?</b> Yes. </p> <p class="sslScenario"> <b>URL:</b> <code>https://assets.ancientwarmth.com</code><br> <b>Origin Type:</b> CloudFront distribution<br> <b>SSL certificate origin:</b> <code>*.ancientwarmth.com</code><br> <b>Valid SSL certificate?</b> Yes. (I created this wildcard certificate using Route 53.) </p> <p class="sslScenario"> <b>S3 path-style URL:</b> <code>https://s3.us-east-1.amazonaws.com/assets.ancientwarmth.com</code><br> <b>Origin Type:</b> S3 bucket<br> <b>SSL certificate origin:</b> <code>s3.us-east-1.amazonaws.com</code><br> <b>Valid SSL certificate?</b> Yes. </p> <p class="sslScenario"> <b>S3 dot URL:</b> <code>https://assets.ancientwarmth.com.s3.amazonaws.com</code><br> <b>Origin Type:</b> S3 bucket<br> <b>SSL certificate origin:</b> <code>*.s3.amazonaws.com</code><br> <b>Valid SSL certificate?</b> No, does not match URL (wildcards only match one subdomain). </p> <p class="sslScenario"> <b>S3 dot Region URL:</b> <code>https://assets.ancientwarmth.com.s3.us-east-1.amazonaws.com</code><br> <b>Origin Type:</b> S3 bucket<br> <b>SSL certificate origin:</b> <code>s3.amazonaws.com</code><br> <b>Valid SSL certificate?</b> No, does not match URL. </p> <h2 id="curl">Testing with <span class="code">curl</span></h2> <p> <code>Curl</code> is often used to test SSL requests. In the following <code>curl</code> commands, the <a href='https://curl.se/docs/manpage.html#-I' target='_blank' rel="nofollow"><code>-I</code> option</a> just fetches the headers, and the <a href='https://curl.se/docs/manpage.html#-v' target='_blank' rel="nofollow"><code>-v</code> option</a> provides verbose output. You can see the SSL certificate negotation. </p> <p> Fetching an asset from a CloudFront distribution using the AWS <code>*.cloudfront.net</code> wildcard SSL certificate: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbb265953710a'><button class='copyBtn' data-clipboard-target='#idbb265953710a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -Iv \ https://d1bci9l8cjf24o.cloudfront.net/js/jquery.modal.min.js <span class='unselectable'>* Trying 52.85.149.22:443... * TCP_NODELAY set * Connected to d1bci9l8cjf24o.cloudfront.net (52.85.149.22) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 * ALPN, server accepted to use h2 * Server certificate: * subject: CN=*.cloudfront.net * start date: Feb 22 00:00:00 2021 GMT * expire date: Feb 21 23:59:59 2022 GMT * subjectAltName: host &quot;d1bci9l8cjf24o.cloudfront.net&quot; matched cert&apos;s &quot;*.cloudfront.net&quot; * issuer: C=US; O=DigiCert Inc; CN=DigiCert Global CA G2 * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x5585dcfaf7e0) > HEAD /js/jquery.modal.min.js HTTP/2 > Host: d1bci9l8cjf24o.cloudfront.net > user-agent: curl/7.68.0 > accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS == 128)! < HTTP/2 200 HTTP/2 200 < content-type: application/javascript content-type: application/javascript < content-length: 4953 content-length: 4953 < date: Sat, 20 Mar 2021 14:11:34 GMT date: Sat, 20 Mar 2021 14:11:34 GMT < last-modified: Sat, 20 Mar 2021 03:14:08 GMT last-modified: Sat, 20 Mar 2021 03:14:08 GMT < etag: &quot;c8f50397e0560719c62a35318f413e16&quot; etag: &quot;c8f50397e0560719c62a35318f413e16&quot; < accept-ranges: bytes accept-ranges: bytes < server: AmazonS3 server: AmazonS3 < x-cache: Miss from cloudfront x-cache: Miss from cloudfront < via: 1.1 0712e4ad4264127dfcb76a114b130495.cloudfront.net (CloudFront) via: 1.1 0712e4ad4264127dfcb76a114b130495.cloudfront.net (CloudFront) < x-amz-cf-pop: IAD89-C3 x-amz-cf-pop: IAD89-C3 < x-amz-cf-id: hWrjwajqqkI9-rJnK1BSQqkX9DPXIlZJLfa28UaIeze7taBP5kqMNg== x-amz-cf-id: hWrjwajqqkI9-rJnK1BSQqkX9DPXIlZJLfa28UaIeze7taBP5kqMNg==<br> < * Connection #0 to host d1bci9l8cjf24o.cloudfront.net left intact </span></pre> </div> <p> Fetching an asset from a CloudFront distribution using my <code>*.ancientwarmth.com</code> wildcard SSL certificate: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id24c06940f34e'><button class='copyBtn' data-clipboard-target='#id24c06940f34e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -Iv \ https://assets.ancientwarmth.com/js/jquery.modal.min.js <span class='unselectable'>modal.min.js> https://assets.ancientwarmth.com/js/jquery.modal.min.js * Trying 13.226.36.16:443... * TCP_NODELAY set * Connected to assets.ancientwarmth.com (13.226.36.16) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 * ALPN, server accepted to use h2 * Server certificate: * subject: CN=*.ancientwarmth.com * start date: Mar 14 00:00:00 2021 GMT * expire date: Apr 12 23:59:59 2022 GMT * subjectAltName: host &quot;assets.ancientwarmth.com&quot; matched cert&apos;s &quot;*.ancientwarmth.com&quot; * issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x5599720157e0) > GET /js/jquery.modal.min.js HTTP/2 > Host: assets.ancientwarmth.com > user-agent: curl/7.68.0 > accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS == 128)! < HTTP/2 200 < content-type: application/javascript < content-length: 4953 < date: Sat, 20 Mar 2021 12:34:25 GMT < last-modified: Sat, 20 Mar 2021 03:14:08 GMT < etag: &quot;c8f50397e0560719c62a35318f413e16&quot; < accept-ranges: bytes < server: AmazonS3 < x-cache: Hit from cloudfront < via: 1.1 4e3df844337032b56b8434990b0f76ca.cloudfront.net (CloudFront) < x-amz-cf-pop: EWR53-C2 < x-amz-cf-id: 17Dxn6QqtK6JfkJwFnESVYsG-Cbzu6H-sOTWcGDpznGcpjIZbhJDRA== < age: 5195<br> < * Connection #0 to host assets.ancientwarmth.com left intact </span></pre> </div> <p> Fetching an asset from an S3 bucket using an AWS SSL certificate for all S3 buckets in region <code>us-east-1</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcc0acfc20dd6'><button class='copyBtn' data-clipboard-target='#idcc0acfc20dd6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -Iv \ https://s3.us-east-1.amazonaws.com/assets.ancientwarmth.com/js/jquery.modal.min.js <span class='unselectable'>* Trying 52.216.24.46:443... * TCP_NODELAY set * Connected to s3.us-east-1.amazonaws.com (52.216.24.46) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 * ALPN, server did not agree to a protocol * Server certificate: * subject: C=US; ST=Washington; L=Seattle; O=Amazon.com, Inc.; CN=s3.amazonaws.com * start date: Aug 4 00:00:00 2020 GMT * expire date: Aug 9 12:00:00 2021 GMT * subjectAltName: host &quot;s3.us-east-1.amazonaws.com&quot; matched cert&apos;s &quot;s3.us-east-1.amazonaws.com&quot; * issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=DigiCert Baltimore CA-2 G2 * SSL certificate verify ok. > GET /assets.ancientwarmth.com/js/jquery.modal.min.js HTTP/1.1 > Host: s3.us-east-1.amazonaws.com > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < x-amz-id-2: xIXUHy7YBpjZaF+cpGoSAwNvC5+NrmM5pmJM8nInI6weEkbht350xSPC9+yOBJrGs9GY0hn2V7Y= < x-amz-request-id: JM2K8HR109JNMMB1 < Date: Sat, 20 Mar 2021 14:03:56 GMT < Last-Modified: Sat, 20 Mar 2021 03:14:08 GMT < ETag: &quot;c8f50397e0560719c62a35318f413e16&quot; < Accept-Ranges: bytes < Content-Type: application/javascript < Content-Length: 4953 < Server: AmazonS3 < * Connection #0 to host s3.us-east-1.amazonaws.com left intact </span></pre> </div> <p> Test the date range of the certificate with this incantation: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3b67b351b95e'><button class='copyBtn' data-clipboard-target='#id3b67b351b95e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://ancientwarmth.com -vI --stderr - | grep 'date:' <span class='unselectable'>* start date: Nov 25 17:46:07 2022 GMT * expire date: Feb 23 17:46:06 2023 GMT </span></pre> </div> Pretty JSON Reduces Errors and Fatigue 2021-02-23T00:00:00-05:00 https://mslinn.github.io/blog/2021/02/23/pretty-json <p> I've been using <a href='https://stedolan.github.io/jq/' target='_blank' rel="nofollow">jq</a> to format my JSON for years. It is easy to format a JSON document, just pass it through <code>jq</code> without any options or arguments. Notice, however, that a lot of vertical space is wasted using this formatting: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id87bd25173ba6'><button class='copyBtn' data-clipboard-target='#id87bd25173ba6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>$ jq < blog/colors.json { "colors": [ { "color": "black", "hex": "#000", "rgb": [ 0, 0, 0 ] }, { "color": "red", "hex": "#f00", "rgb": [ 255, 0, 0 ] }, { "color": "yellow", "hex": "#ff0", "rgb": [ 255, 255, 0 ] }, { "color": "green", "hex": "#0f0", "rgb": [ 0, 255, 0 ] }, { "color": "cyan", "hex": "#0ff", "rgb": [ 0, 255, 255 ] }, { "color": "blue", "hex": "#00f", "rgb": [ 0, 0, 255 ] }, { "color": "magenta", "hex": "#f0f", "rgb": [ 255, 0, 255 ] }, { "color": "white", "hex": "#fff", "rgb": [ 255, 255, 255 ] } ] }</pre> </div> <p> After reading <a href='http://www.ohler.com/dev/pretty.html' target='_blank' rel="nofollow">The Pretty JSON Revolution</a> I decided to try the program the article mentioned, <code>oj</code>. <code>oj</code> is a Go program. Here is how I compiled it on Ubuntu: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id733c008b8b51'><button class='copyBtn' data-clipboard-target='#id733c008b8b51' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install golang-go <span class='unselectable'>$ </span>go get github.com/ohler55/ojg <span class='unselectable'>$ </span>go get github.com/ohler55/ojg/cmd/oj</pre> </div> <p> By default, compiled go projects are placed in the <code>~/go/bin/</code> directory. Here is how I added that directory the the <code>PATH</code>, and made an alias for invoking the program with the proper options for maximum prettiness: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide3895997270d'><button class='copyBtn' data-clipboard-target='#ide3895997270d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo "$HOME/go/bin/:$PATH" >> ~/.bashrc <span class='unselectable'>$ </span>echo "alias pprint='oj -i 2 -s -p 80.3'" >> ~/.bash_aliases <span class='unselectable'>$ </span>source ~/.bashrc</pre> </div> <p> Pretty-printing the JSON in <code>colors.json</code> with <code>oj</code> is easy: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id692b3e819b79'><button class='copyBtn' data-clipboard-target='#id692b3e819b79' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pprint colors.json <span class='unselectable'>{ "colors": [ {"color": "black", "hex": "#000", "rgb": [0, 0, 0]}, {"color": "red", "hex": "#f00", "rgb": [255, 0, 0]}, {"color": "yellow", "hex": "#ff0", "rgb": [255, 255, 0]}, {"color": "green", "hex": "#0f0", "rgb": [0, 255, 0]}, {"color": "cyan", "hex": "#0ff", "rgb": [0, 255, 255]}, {"color": "blue", "hex": "#00f", "rgb": [0, 0, 255]}, {"color": "magenta", "hex": "#f0f", "rgb": [255, 0, 255]}, {"color": "white", "hex": "#fff", "rgb": [255, 255, 255]} ] } </span></pre> </div> <p> I like it! </p> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/34wJt3pRY0w" frameborder="0" allowfullscreen></iframe></div> <p style="text-align: center"> <i>Give it to Mikey. He won't eat it. He hates everything!</i> </p> <p> I will continue to use <a href='https://stedolan.github.io/jq/manual/' target='_blank' rel="nofollow"><code>jq</code> for queries</a>, but I'll use <code>oj</code> for pretty-printing from now on. </p> JavaScript Named Arguments and Class Constructors 2021-02-11T00:00:00-05:00 https://mslinn.github.io/blog/2021/02/11/javascript-named-arguments <p> Named arguments make a program safe from errors caused by changes to method arguments. JavaScript named arguments can appear in any order. Default values for parameters allow an API to evolve gracefully without runtime errors. </p> <p> Building on the article entitled <a href='https://afontcu.medium.com/cool-javascript-9-named-arguments-functions-that-get-and-return-objects-337b6f8cfa07' target='_blank' rel="nofollow">Cool JavaScript 9: Named arguments — Functions that get and return Objects</a>, this article shows how JavaScript class constructors can use named arguments, optionally define default values for parameters, and conveniently inflate new class instances from JSON. </p> <p> In this article I use Node.js for convenience, however the code shown will run in all modern web browsers. </p> <h2 id="stdArgs">JavaScript Class Definition Encapsulating Properties</h2> <p> Let&rsquo;s quickly review how to define a JavaScript class and instantiate an instance. Here is a simple JavaScript / ECMAScript 6 class that encapsulates two properties: <code>id</code> and <code>parts</code>. The constructor merely lists the names of the parameters, which happen to be the same as the names of the class properties. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8dad16942fb3'><button class='copyBtn' data-clipboard-target='#id8dad16942fb3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span> js <span class='unselectable'>Welcome to Node.js v12.18.2. Type ".help" for more information. > </span>class Ingredient { <span class='unselectable'>... </span> constructor(id, parts) { <span class='unselectable'>..... </span> this.id = id; <span class='unselectable'>..... </span> this.parts = parts; <span class='unselectable'>..... </span> } <span class='unselectable'>... </span>} <span class='unselectable'>undefined </span></pre> </div> <p> New <code>Ingredient</code> instances can be created using this familiar syntax: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf9f9e44ddfe8'><button class='copyBtn' data-clipboard-target='#idf9f9e44ddfe8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredient = new Ingredient("123", 10); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredient <span class='unselectable'>Ingredient { id: '123', parts: 10 } </span></pre> </div> <h2 id="lits">Object Literals</h2> <p> Object literals look like JSON objects, but without quotes around property names. For example, the following defines an object literal called <code>lit</code> with 2 properties, called <code>id</code> and <code>parts</code>, with values <code>"123"</code> and <code>10</code>, respectively. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1ae96394c5b3'><button class='copyBtn' data-clipboard-target='#id1ae96394c5b3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span> var lit = {id: "123", parts: 10}; <span class='unselectable'>undefined </span> <span class='unselectable'>$ </span>lit <span class='unselectable'>{ id: '123', parts: 10 } </span> <span class='unselectable'>> </span>lit.id <span class='unselectable'>'123' </span> <span class='unselectable'>> </span>lit.parts <span class='unselectable'>10 </span></pre> </div> <h2 id="jsonArgs">Use Object Literals to Define Arguments</h2> <p> We can define a class similar to <code>Ingredient</code>, but with the arguments replaced by a something that looks like an object literal without values. For want of a better term, I call this an <i>object name literal</i>. The following class definition encapsulates the same two properties as before as an object name literal. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id48f4bf850cd3'><button class='copyBtn' data-clipboard-target='#id48f4bf850cd3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>class IngredientX { <span class='unselectable'>... </span> constructor(<span class="bg_yellow">{id, parts}</span>) { <span class='unselectable'>..... </span> this.id = id; <span class='unselectable'>..... </span> this.parts = parts; <span class='unselectable'>..... </span> } <span class='unselectable'>... </span>} <span class='unselectable'>undefined </span></pre> </div> <p> New <code>IngredientX</code> instances can be created from an object literal: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5b38a5b0b2c3'><button class='copyBtn' data-clipboard-target='#id5b38a5b0b2c3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredientX1 = new IngredientX(<span class="bg_yellow">{id: "123", parts: 10 }</span>); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientX1 <span class='unselectable'>IngredientX { id: '123', parts: 10 } </span></pre> </div> <p> Because the <code>IngredientX</code> class definition requires an object name literal (or a JSON object, more on that later) to provide constructor arguments, constructor invocations must specify the names of each parameter being passed to the constructor arguments. This has the benefit of making your software more robust in the face of changing method signatures. </p> <p> Caution: new <code>IngredientX</code> instances cannot be created from scalar arguments. JavaScript gives no error or warning if you do not: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1186084f9970'><button class='copyBtn' data-clipboard-target='#id1186084f9970' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredientX2 = new IngredientX("123", 10); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientX2 <span class='unselectable'>IngredientX { id: <span class="bg_yellow">undefined</span>, parts: <span class="bg_yellow">undefined</span> } </span></pre> </div> <h2 id="jsonArgs">JSON Object Can Be Supplied Instead of Object Literals</h2> <p> JSON objects can be provided as arguments instead of object literals. This is extremely handy. Replacing several arguments with a JSON object would possibly be the most significant improvement in robustness that could be made to a JavaScript project. The number of runtime errors encountered as a code base evolves would be greatly reduced. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7b72e40e552c'><button class='copyBtn' data-clipboard-target='#id7b72e40e552c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredientX3 = new IngredientX({ <span class='unselectable'>... </span> <span class="bg_yellow">"id"</span>: "123", <span class='unselectable'>... </span> <span class="bg_yellow">"parts"</span>: 10 <span class='unselectable'>... </span> }); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientX3 <span class='unselectable'>IngredientX { id: '123', parts: 10 } </span></pre> </div> <h2 id="jsonArgs">Arguments and Parameters Can Be Provided In Any Order</h2> <p> This definition of <code>ingredientX4</code> is identical to the definition of <code>ingredientX3</code>, even though the order of the arguments has been reversed: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcd62d975f16e'><button class='copyBtn' data-clipboard-target='#idcd62d975f16e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredientX4 = new IngredientX({ <span class='unselectable'>... </span> <span class="bg_yellow">"parts"</span>: 10, <span class='unselectable'>... </span> <span class="bg_yellow">"id"</span>: "123" <span class='unselectable'>... </span> }); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientX4 <span class='unselectable'>IngredientX { id: '123', parts: 10 } </span></pre> </div> <p> The parameters in the function or method declaration are also insensitive to ordering: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9bd771691a75'><button class='copyBtn' data-clipboard-target='#id9bd771691a75' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>class IngredientXReordered { <span class='unselectable'>... </span> constructor(<span class="bg_yellow">{parts, id}</span>) { <span class='unselectable'>..... </span> this.parts = parts; <span class='unselectable'>..... </span> this.id = id; <span class='unselectable'>..... </span> } <span class='unselectable'>... </span>} <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>var ingredientX5 = new IngredientXReordered({ <span class='unselectable'>... </span> <span class="bg_yellow">"parts"</span>: 10, <span class='unselectable'>... </span> <span class="bg_yellow">"id"</span>: "123" <span class='unselectable'>... </span> }); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientX5 <span class='unselectable'>IngredientXReordered { id: '123', parts: 10 } </span></pre> </div> <h2 id="litArgs">Object Literals Can Be Used With Any Method</h2> <p> Object literals / named arguments can be used to define the signature of any function or method, not just class constructors. For example: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide1dc32c9b58a'><button class='copyBtn' data-clipboard-target='#ide1dc32c9b58a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>class IngredientY { <span class='unselectable'>... </span> constructor({id, parts}) { <span class='unselectable'>..... </span> this.id = id; <span class='unselectable'>..... </span> this.parts = parts; <span class='unselectable'>..... </span> } <span class='unselectable'>... </span> <span class='unselectable'>... </span> mix(<span class="bg_yellow">{duration, intensity}</span>) { <span class='unselectable'>... </span> console.log(`Shake for ${duration} hours at intensity ${intensity}.`); <span class='unselectable'>... </span> } <span class='unselectable'>... </span> } <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>var ingredientY = new IngredientY({id: "123", parts: 10 }); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientY.mix(<span class="bg_yellow">{duration: 2.5, intensity: 2}</span>); <span class='unselectable'>Shake for 2.5 hours at intensity 2. </span> <span class='unselectable'>undefined </span></pre> </div> <h2 id="jsonArgs">Default Values for Named Arguments</h2> <p> To make this example more interesting, the default value for <code>id</code> will be generated as a GUID. <a href='https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid' target='_blank' rel="nofollow">Here are some other GUID implementations</a>, but the best implementations have dependencies and that would just make the article more complex than necessary. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id74125a0a654c'><button class='copyBtn' data-clipboard-target='#id74125a0a654c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>function uuidv4() { <span class='unselectable'>... </span> return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, <span class='unselectable'>... </span> function(c) { <span class='unselectable'>..... </span> var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); <span class='unselectable'>..... </span> return v.toString(16); <span class='unselectable'>..... </span> }); <span class='unselectable'>... </span>} <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>uuidv4() <span class='unselectable'>'b13137c1-1598-42ca-9498-c1502e5405ed' </span></pre> </div> <p> A JavaScript object literal or JSON object must be passed to a method whose parameters were defined by object literal names. If a name/value pair is not provided in the argument, then the default parameter value is used. Some examples should help demonstrate how this works: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8cfb42ea0ac'><button class='copyBtn' data-clipboard-target='#idc8cfb42ea0ac' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>class IngredientZ { <span class='unselectable'>... </span> constructor({id<span class="bg_yellow">=uuidv4()</span>, parts<span class="bg_yellow">=10</span>}) { <span class='unselectable'>..... </span> this.id = id; <span class='unselectable'>..... </span> this.parts = parts; <span class='unselectable'>..... </span> } <span class='unselectable'>... </span> <span class='unselectable'>... </span> mix({duration<span class="bg_yellow">=1.2</span>, intensity<span class="bg_yellow">=6</span>}) { <span class='unselectable'>... </span> console.log(`Shake for ${duration} hours at intensity ${intensity}.`); <span class='unselectable'>... </span> } <span class='unselectable'>... </span> } <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>var ingredientZ1 = new IngredientZ(<span class="bg_yellow">{parts: 4}</span>); <span class='unselectable'>undefined </span> <span class='unselectable'>> </span>ingredientZ1 <span class='unselectable'>IngredientZ { id: <span class="bg_yellow">'4290dc1a-4f4c-4579-9e27-39b68085ad97'</span>, parts: <span class="bg_yellow">4</span> } </span> <span class='unselectable'>undefined </span></pre> </div> <p> Empty objects are allowed as arguments. All this means is that default values are used for all parameters of the object name literal. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6ae534e37628'><button class='copyBtn' data-clipboard-target='#id6ae534e37628' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>var ingredientZ2 = new IngredientZ(<span class="bg_yellow">{}</span>); <span class='unselectable'>undefined </span> > ingredientZ2 <span class='unselectable'>IngredientZ { id: '9e70dc12-1f4c-3579-6a17-49a68385bf73', parts: 10 } </span> <span class='unselectable'>> </span>ingredientZ2.mix(<span class="bg_yellow">{}</span>); <span class='unselectable'>Shake for 2.5 hours at intensity 2. </span></pre> </div> <p> Missing objects result in a syntax error. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf1ea29d7f2cd'><button class='copyBtn' data-clipboard-target='#idf1ea29d7f2cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>> </span>ingredientZ2.mix<span class="bg_yellow">()</span>; <span class='unselectable'>Uncaught TypeError: Cannot read property 'id' of undefined at new IngredientZ2 (repl:3:17) {% noselect undefined </span></pre> </div> <h2 id="info">For More Information</h2> <p> For more information, please see <a href='https://exploringjs.com/impatient-js/ch_callables.html#named-parameters' target='_blank' rel="nofollow">JavaScript for impatient programmers (ES2021 edition)</a>. </p> JavaScript Linter Configuration 2021-02-08T00:00:00-05:00 https://mslinn.github.io/blog/2021/02/08/js-linter-config <!-- #region intro --> <p> Using a lint tool can really help improve your code in a hurry. I am using <a href='https://jshint.com' target='_blank' rel="nofollow">JSHint</a> for a project that has a big JavaScript file that needs some love. </p> <!-- endregion --> <!-- #region .jshintrc --> <h2 id=".jshintrc"><span class="code">.jshintrc</span></h2> <p> All modern web browsers support at least the version of JavaScript that conforms to <a href='https://en.wikipedia.org/wiki/ECMAScript#6th_Edition_%E2%80%93_ECMAScript_2015' target='_blank' rel="nofollow">ECMAScript 6th Edition</a>, also known as ECMAScript 2015. Neither the documentation for the Atom <a href='https://github.com/AtomLinter/linter-jshint' target='_blank' rel="nofollow">linter-jshint</a> plugin nor <a href='https://jshint.com/docs/' target='_blank' rel="nofollow">JSHint</a> itself explicitly state that in order to work with the version of JavaScript supported by all modern web browsers, you need to provide a JSON formatted configuration file that sets the <code>esversion</code> property to 6, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>.jshintrc</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida2274c8001be'><button class='copyBtn' data-clipboard-target='#ida2274c8001be' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{ "esversion": 6 }</pre> </div> <p> If you do not do this, then JSHint will indicate errors if it encounters class definitions, for example. </p> <p> I put <code>.jshintrc</code> in the top-level directory of my project. </p> <p> I created a <a href='https://github.com/mslinn/linter-jshint/pull/1' target='_blank' rel="nofollow">pull request</a> for the `linter-jshint` GitHub project so this documentation would be included. </p> <!-- endregion --> <!-- #region Reading from stdin --> <h2 id="stdin">Reading from <span class="code">stdin</span></h2> <p> The <a href='https://jshint.com/docs/cli/' target='_blank' rel="nofollow">JSHint CLI docs</a> say: </p> <div class="quote"> If a file path is a dash (-) then JSHint will read from standard input. </div> <p> I needed to preprocess my JavaScript source files before invoking JSHint. Because JSHint can read from standard input, there is no need to write the preprocessed file contents to a temporary file. </p> <!-- endregion --> <!-- #region Removing Jekyll Front Matter for JSHint --> <h2 id="stdin">Removing Jekyll Front Matter for JSHint</h2> <p> Jekyll can process any text file, including JavaScript files, if they contain front matter markers. This is useful for invoking Jekyll plugins and/or using Liquid expressions. My big JavaScript file has some information injected into it when Jekyll generates the site. </p> <p> Front matter is marked (delimited by) by two lines at the top of a file, consisting of three dashes, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6e73c1e32bd6'><button class='copyBtn' data-clipboard-target='#id6e73c1e32bd6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>--- ---</pre> </div> <p> Here is how the empty front matter can be stripped from <code>myfile.js</code> so JSHint can inspect the remaining lines: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc0752b211c02'><button class='copyBtn' data-clipboard-target='#idc0752b211c02' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sed '/---/d' < myfile.js | jshint -</pre> </div> <!-- endregion --> Functional and Non-Functional E-Commerce Requirements 2021-02-02T00:00:00-05:00 https://mslinn.github.io/blog/2021/02/02/ecommerce-requirements <!-- #region intro --> <p> In a <a href='/blog/2021/01/30/opencart-postgres.html'>previous article</a>, I described how I searched for an <a href='https://www.google.com/search?q=open+source+shopping+cart' target='_blank' rel="nofollow">open-source shopping cart</a> and the disappointing software options that I found. These searches yielded software projects that had begun 20 years ago and were generally of low quality. </p> <p> The businesses that manage these projects use a failed business model for open source, namely software-as-a-service (SaaS), without much added value. The hosted products have not changed much since they were first established, and they have not kept up with the relentless advances in computer technology. Those businesses derive some additional revenue from customizing the open-source projects. </p> <p> I decided to be more rigorous in my requirements analysis for a shopping cart with good coupon support so that I could find more suitable options. I now know that the cart must have: </p> <ul> <li>Strong discount/coupon support.</li> <li>Support for SKUs defined on-the-fly by customers as they interact with the website.</li> <li>Integration with various payment processors.</li> </ul> <p> Before I share my reviews of candidate shopping carts with you, let&rsquo;s first discuss the state of the e-commerce market, functional and non-functional requirements, the master/detail pattern, and some technology trends for business software. </p> <!-- endregion --> <!-- #region E-Commerce Is Hot, Hot, Hot --> <h2 id="hot">E-Commerce Is Hot, Hot, Hot</h2> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <a href='https://www.digitalcommerce360.com/article/us-ecommerce-sales/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/django/usEcommerce.svg" type="image/svg"> <!---<source srcset="/blog/images/django/usEcommerce.avif" type="image/avif">--> <source srcset="/blog/images/django/usEcommerce.webp" type="image/webp"> <source srcset="/blog/images/django/usEcommerce.apng" type="image/apng"> <source srcset="/blog/images/django/usEcommerce.png" type="image/png"> <source srcset="/blog/images/django/usEcommerce.jpg" type="image/jpeg"> <source srcset="/blog/images/django/usEcommerce.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/usEcommerce.jfif" type="image/jpeg"> <source srcset="/blog/images/django/usEcommerce.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/usEcommerce.pjp" type="image/jpeg"> <source srcset="/blog/images/django/usEcommerce.gif" type="image/gif"> <source srcset="/blog/images/django/usEcommerce.tif" type="image/tiff"> <source srcset="/blog/images/django/usEcommerce.tiff" type="image/tiff"> <source srcset="/blog/images/django/usEcommerce.bmp" type="image/bmp"> <source srcset="/blog/images/django/usEcommerce.ico" type="image/x-icon"> <source srcset="/blog/images/django/usEcommerce.cur" type="image/x-icon"> <img alt='US ecommerce grows 44% in 2020<br />Online spending was $861 billion: 21% of total retail sales' class="imgImg rounded shadow" src="/blog/images/django/usEcommerce.png" style='width: 100%; ' title='US ecommerce grows 44% in 2020<br />Online spending was $861 billion: 21% of total retail sales' /> </picture> </a> <figcaption class='imgFigCaption halfsize'> <a href="https://www.digitalcommerce360.com/article/us-ecommerce-sales/" target='_blank' > US ecommerce grows 44% in 2020<br />Online spending was $861 billion: 21% of total retail sales </a> </figcaption> </figure> </div> <div class='imgWrapper imgBlock center' style='width: 75%; '> <figure> <a href='https://www.amazon.com/gp/product/0062292986/ref=as_li_qf_sp_asin_il_tl' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/ecommerceGrowth.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/ecommerceGrowth.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/ecommerceGrowth.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.png" type="image/png"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/ecommerceGrowth.cur" type="image/x-icon"> <img alt=' US e-commerce vs. retail sales, 2010-2020<br /> Source: Digital Commerce 360, U.S. Department of Commerce; January 2021 ' class="imgImg rounded shadow" src="/blog/images/ecommerce/ecommerceGrowth.png" style='width: 100%; ' title=' US e-commerce vs. retail sales, 2010-2020<br /> Source: Digital Commerce 360, U.S. Department of Commerce; January 2021 ' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.amazon.com/gp/product/0062292986/ref=as_li_qf_sp_asin_il_tl" target='_blank' > US e-commerce vs. retail sales, 2010-2020<br /> Source: Digital Commerce 360, U.S. Department of Commerce; January 2021 </a> </figcaption> </figure> </div> <p> There are a huge number of e-commerce sites, and that market is experiencing strong growth in part due to the COVID-19 epidemic. E-commerce has <a href='https://www.amazon.com/gp/product/0062292986/ref=as_li_qf_sp_asin_il_tl' target='_blank' rel="nofollow">entered Main Street</a>, as per Geoffrey Moore&rsquo;s technology adoption lifecycle. </p> <div class='imgWrapper imgBlock center' style='width: 35%; '> <figure> <a href='https://www.amazon.com/gp/product/0062292986/ref=as_li_qf_sp_asin_il_tl' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/crossingTheChasm.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/crossingTheChasm.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/crossingTheChasm.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/crossingTheChasm.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/crossingTheChasm.png" type="image/png"> <source srcset="/blog/images/ecommerce/crossingTheChasm.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/crossingTheChasm.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/crossingTheChasm.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/crossingTheChasm.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/crossingTheChasm.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/crossingTheChasm.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/crossingTheChasm.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/crossingTheChasm.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/crossingTheChasm.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/crossingTheChasm.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/crossingTheChasm.cur" type="image/x-icon"> <img alt='&ldquo;Crossing the Chasm&rdquo;, which introduced the technology adoption lifecycle' class="imgImg rounded shadow" src="/blog/images/ecommerce/crossingTheChasm.png" style='width: 100%; ' title='&ldquo;Crossing the Chasm&rdquo;, which introduced the technology adoption lifecycle' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.amazon.com/gp/product/0062292986/ref=as_li_qf_sp_asin_il_tl" target='_blank' > &ldquo;Crossing the Chasm&rdquo;, which introduced the technology adoption lifecycle </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region Functional requirements --> <h2 id="nfrs">Functional Requirements</h2> <div class='quote'> <div class='quoteText clearfix'> Functional requirements describe what the system must or must not do, and can be thought of in terms of how the system responds to inputs. Functional requirements usually define if/then behaviors and include calculations, data input, and business processes. <br><br> Functional requirements are features that allow the system to function as intended. In other words, if the functional requirements are not met, the system will not work. Functional requirements are product features that focus on user requirements. </div><div class='quoteAttribution'> &nbsp;&ndash; <a href='https://qracorp.com/functional-vs-non-functional-requirements/' rel='nofollow' target='_blank'>Paraphrased from QRA Corp&rsquo;s definition</a></div> </div> <p> Functionally, a shopping cart is well understood, yet to be certain that a shopping cart actually works properly, test data and test procedures would need to be designed to verify that all the corner cases were exercised. That would be a significant amount of work to do properly, but at least the work would be rather straightforward. </p> <!-- endregion --> <!-- #region Non-functional requirements (NFRs) --> <h2 id="nfrs">Non-functional requirements (NFRs)</h2> <p> Non-functional requirements relate to user expectations and include security, reliability, performance, maintainability, scalability, and usability. </p> <div class='quote'> <p> An NFR is a requirement that specifies criteria that can be used to judge the operation of a system, rather than specific behaviors. <br /><br /> The plan for implementing NFRs is detailed in the system architecture because they are usually architecturally significant requirements. <br /><br /> Broadly, functional requirements define what a system is supposed to <b>do</b>, and NFRs define how a system is supposed to <b>be</b>. <br /><br /> NFRs can be divided into two main categories: </p> <ul> <li>Execution qualities, such as safety, security and usability, which are observable during operation (at run time).</li> <li>Evolution qualities, such as testability, maintainability, extensibility and scalability, which are embodied in the static structure of the system.</li> </ul> <span class='quoteAttribution'> &nbsp;&ndash; <a href='https://en.wikipedia.org/wiki/Non-functional_requirement' rel='nofollow' target='_blank'>Paraphrased excerpts from the Wikipedia article on NFRs</a></span> </div> <p> I need a shopping cart that has been properly tested, is flexible to configure, and is easy to use. These non-functional requirements are more important to me than the mechanics of how the cart was built or the technology that was used, especially for a proof of concept. In my reviews of shopping cart candidates, I will focus on how well they fulfill these NFRs. </p> <p> I also want to use technology that is current, properly maintained, full-featured and has a future. </p> <!-- endregion --> <!-- #region Master-Detail Structures, Interfaces and Reporting --> <h2 id="master-detail">Master-Detail Structures, Interfaces and Reporting</h2> <p> Shopping carts are a classic example of a master-detail structure. The user interface of a shopping cart must exploit the master-detail paradigm effectively. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://www.oracle.com/webfolder/ux/middleware/alta/patterns/MasterDetail.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/MasterDetail.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/MasterDetail.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/MasterDetail.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/MasterDetail.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/MasterDetail.png" type="image/png"> <source srcset="/blog/images/ecommerce/MasterDetail.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/MasterDetail.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/MasterDetail.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/MasterDetail.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/MasterDetail.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/MasterDetail.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/MasterDetail.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/MasterDetail.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/MasterDetail.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/MasterDetail.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/MasterDetail.cur" type="image/x-icon"> <img alt='Image from Oracle Alta UI Patterns: Master-Detail' class="imgImg rounded shadow" src="/blog/images/ecommerce/MasterDetail.png" style='width: 100%; ' title='Image from Oracle Alta UI Patterns: Master-Detail' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://www.oracle.com/webfolder/ux/middleware/alta/patterns/MasterDetail.html" target='_blank' > Image from Oracle Alta UI Patterns: Master-Detail </a> </figcaption> </figure> </div> <div class='quote'> <p> A master–detail relationship is a one-to-many type of relationship. </p> <p> Examples of a master-detail relationship are: </p> <ul> <li>A set of purchase orders and a set of line items belonging to each purchase order.</li> <li>An expense report with a set of expense line items or a department with a list of employees belonging to it.</li> </ul> <p> An application can use this master-detail relationship to enable users to navigate through the purchase order data and see the detail data for line items only related to the master purchase order selected. </p> <span class='quoteAttribution'> &nbsp;&ndash; <a href='https://en.wikipedia.org/wiki/Master%E2%80%93detail_interface' rel='nofollow' target='_blank'>From the Wikipedia article on master/detail data models</a></span> </div> <p> In the book <a href='https://www.oreilly.com/library/view/sap-businessobjects-bi/9780071773126/lev1sec120.html' target='_blank'>SAP BusinessObjects BI 4.0 The Complete Reference 3/E</a>, authors Cindi Howson and Elizabeth Newbould provide this rather abstract definition of <i>master/detail reports</i>: </p> <p class="quote"> A master/detail report is a particular kind of report in which a dimension value (master) is used to group data (detail) into separate sections. Master/detail reports allow you to analyze and format data for each unique master data value. </p> <p> Page 13 of <a href='https://books.google.ca/books?id=wgBMWCvTEHQC&pg=PA13&dq=First+normal+form+removes+repetition+by+creating+one-to-many+relationships&hl=en&sa=X&ved=2ahUKEwjB-5_NrMvuAhWCElkFHV0aA-MQ6AEwAHoECAEQAg#v=onepage&q&f=false' target='_blank' rel="nofollow">Oracle&reg; Performance Tuning for 10gR2, Second Edition</a> by Gavin JT Powell has a more concrete definition: </p> <p class="quote"> <b>First normal form removes repetition by creating one-to-many relationships.</b> Data repeated many times in one entity is removed to a subset entity, which becomes the container for the removed repeating data. Each row in the subset entity will contain a single reference to each row in the original entity. The original entity will then contain only nonduplicated data. This one-to-many relationship is commonly known as a master-detail relationship, where repeating columns are removed to a new entity. The new entity gets a primary key consisting of a composite of the primary key in the master entity and a unique identifier (within each master primary key) on the detail entity. </p> <p> Master-detail is such a common architectural pattern that most business software vendors provide support for it. Other vendors include <a href='https://www.ibm.com/support/knowledgecenter/SSEP7J_11.1.0/com.ibm.swg.ba.cognos.ug_cr_rptstd.doc/t_cr_rptstd_modrep_create_master_detail_relationship.html#cr_rptstd_modrep_create_master_detail_relationship' target='_blank'>IBM</a>, <a href='https://docs.oracle.com/cd/E15586_01/web.1111/b31974/web_masterdetail.htm' target='_blank'>Oracle</a>, <a href='https://help.salesforce.com/articleView?id=sf.overview_of_custom_object_relationships.htm&type=5' target='_blank'>Salesforce</a>, and <a href='https://channel9.msdn.com/Blogs/OneCode/How-to-create-a-master-detail-ListBox-in-universal-Windows-apps' target='_blank'>Microsoft</a>. Apple&rsquo;s iPad is used as a client for master-detail applications so often that the iOS SDK even provides a <a href='https://www.oreilly.com/library/view/beginning-ios-5/9781118144251/ch004-sec007.html' target='_blank'>Master-Detail Application template</a>. The master/detail pattern for Google Android applications <a href='https://github.com/lurbas/MaterialMasterDetail' target='_blank' rel="nofollow">can be implemented using the Material Design</a> visual language. </p> <!-- endregion --> <!-- #region Master-Detail Frameworks --> <h2 id="master-detail-frameworks">Master-Detail Frameworks</h2> <p> Shopping carts must have engaging and effective implementations for master/detail patterns throughout: in the user interface, in the design of reports, in the data structures, in the types of software modules and their interfaces to each other, and in the internal data flow. The best-known web framework explicitly designed to support the master/detail pattern is <a href='https://rubyonrails.org/' target='_blank'>Ruby on Rails</a>. Of course, many other web frameworks can support master/detail. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <a href='https://rubyonrails.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/Ruby-on-Rails.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.png" type="image/png"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/Ruby-on-Rails.cur" type="image/x-icon"> <img alt='Ruby on Rails' class="imgImg rounded shadow" src="/blog/images/ecommerce/Ruby-on-Rails.png" style='width: 100%; ' title='Ruby on Rails' /> </picture> </a> <figcaption class='imgFigCaption halfsize'> <a href="https://rubyonrails.org/" target='_blank' > Ruby on Rails </a> </figcaption> </figure> </div> <!-- endregion --> <!-- #region Momentum --> <h2 id="momentum">Momentum</h2> <!-- #region Web Servers --> <h3 id="webserver">Web Servers</h3> <p> Web servers are usually placed in front of e-commerce servers for scalability and security reasons. <a href='https://www.netcraft.com/about/' target='_blank' rel="nofollow">Netcraft</a> has been reporting on web server deployments since 1995. The <a href='https://news.netcraft.com/archives/2021/01/28/january-2021-web-server-survey.html' target='_blank' rel="nofollow">Netcraft January 2021 Web Server Survey</a> shows some interesting trends. </p> <div class='imgWrapper imgFlex center' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/ecommerce/wss-active-share.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/wss-active-share.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/wss-active-share.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/wss-active-share.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/wss-active-share.png" type="image/png"> <source srcset="/blog/images/ecommerce/wss-active-share.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-active-share.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-active-share.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-active-share.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-active-share.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-active-share.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/wss-active-share.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/wss-active-share.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/wss-active-share.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/wss-active-share.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/wss-active-share.cur" type="image/x-icon"> <img alt='Web server developer market share by server type.<br />Apache <code>httpd</code> is losing ground, as developers move to Microsoft and nginx.' class="imgImg rounded shadow" src="/blog/images/ecommerce/wss-active-share.png" style='width: 100%; ' title='Web server developer market share by server type.<br />Apache <code>httpd</code> is losing ground, as developers move to Microsoft and nginx.' /> </picture> <figcaption class='imgFigCaption '> Web server developer market share by server type.<br />Apache <code>httpd</code> is losing ground, as developers move to Microsoft and nginx. </figcaption> </figure> </div> <div class='imgWrapper imgFlex center' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/ecommerce/wss-top-1m-share.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/wss-top-1m-share.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/wss-top-1m-share.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.png" type="image/png"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/wss-top-1m-share.cur" type="image/x-icon"> <img alt='For the busiest 1 million websites, Microsoft has taken the lead.' class="imgImg rounded shadow" src="/blog/images/ecommerce/wss-top-1m-share.png" style='width: 100%; ' title='For the busiest 1 million websites, Microsoft has taken the lead.' /> </picture> <figcaption class='imgFigCaption '> For the busiest 1 million websites, Microsoft has taken the lead. </figcaption> </figure> </div> <p> Microsoft&rsquo;s <a href='https://dotnet.microsoft.com/apps/aspnet' target='_blank' rel="nofollow">ASP.NET</a> web framework is free, and web hosting is free (to a point), but those enticements fulfill their purpose by locking in customers to Microsoft's software stack and tools. The nopCommerce e-commerce server is free, very capable, and has enjoyed terrific adoption. This technology might be a good business decision for e-commerce sites that have typical requirements, but not if significant customization or integration with non-Microsoft services becomes important. This article was written by an experienced developer: <a href='https://www.freecodecamp.org/news/i-built-a-web-api-with-express-flask-aspnet/' target='_blank' rel="nofollow">I rebuilt the same web API using Express, Flask, and ASP.NET. Here&rsquo;s what I found</a>. </p> <!-- endregion --> <!-- #region Computer Languages --> <h3 id="language">Computer Languages</h3> <div class='imgWrapper imgFlex center' style=' '> <figure> <a href='https://github.com/nasa/fprime' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/mars_drone.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/mars_drone.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/mars_drone.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/mars_drone.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/mars_drone.png" type="image/png"> <source srcset="/blog/images/ecommerce/mars_drone.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/mars_drone.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/mars_drone.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/mars_drone.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/mars_drone.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/mars_drone.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/mars_drone.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/mars_drone.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/mars_drone.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/mars_drone.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/mars_drone.cur" type="image/x-icon"> <img alt='Python runs Mars drone <i>Ingenuity</i>.' class="imgImg rounded shadow" src="/blog/images/ecommerce/mars_drone.png" style='width: 100%; ' title='Python runs Mars drone <i>Ingenuity</i>.' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://github.com/nasa/fprime" target='_blank' > Python runs Mars drone <i>Ingenuity</i>. </a> </figcaption> </figure> </div> <p> Of all computer languages, Python has arguably the most momentum at present. Ruby on Rails is terrific, but its market share has dropped dramatically since 2011, and without that framework, the Ruby language would probably be much less important than it is. <a href='https://python.org' target='_blank' rel="nofollow">Python</a> has been growing in all directions for a very long time. Microsoft&rsquo;s C# language, which requires a .NET compatible runtime, is also popular but has never had the momentum that Python has. </p> <p> The following graph compares the popularity of the Python Django library for making web applications against the popularity of the Ruby on Rails framework for the time period 2009 to the present day. As you can see, in 2011, Ruby on Rails was at its peak popularity; at that time, it was 300% more popular than Django. Now the situation is reversed: today, Django is 400% more popular than Ruby on Rails. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <a href='https://insights.stackoverflow.com/trends?tags=django%2Cruby-on-rails' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/django/django_vs_ror.svg" type="image/svg"> <!---<source srcset="/blog/images/django/django_vs_ror.avif" type="image/avif">--> <source srcset="/blog/images/django/django_vs_ror.webp" type="image/webp"> <source srcset="/blog/images/django/django_vs_ror.apng" type="image/apng"> <source srcset="/blog/images/django/django_vs_ror.png" type="image/png"> <source srcset="/blog/images/django/django_vs_ror.jpg" type="image/jpeg"> <source srcset="/blog/images/django/django_vs_ror.jpeg" type="image/jpeg"> <source srcset="/blog/images/django/django_vs_ror.jfif" type="image/jpeg"> <source srcset="/blog/images/django/django_vs_ror.pjpeg" type="image/jpeg"> <source srcset="/blog/images/django/django_vs_ror.pjp" type="image/jpeg"> <source srcset="/blog/images/django/django_vs_ror.gif" type="image/gif"> <source srcset="/blog/images/django/django_vs_ror.tif" type="image/tiff"> <source srcset="/blog/images/django/django_vs_ror.tiff" type="image/tiff"> <source srcset="/blog/images/django/django_vs_ror.bmp" type="image/bmp"> <source srcset="/blog/images/django/django_vs_ror.ico" type="image/x-icon"> <source srcset="/blog/images/django/django_vs_ror.cur" type="image/x-icon"> <img alt='Stack Overflow Trends: Django vs Ruby on Rails' class="imgImg rounded shadow" src="/blog/images/django/django_vs_ror.png" style='width: 100%; ' title='Stack Overflow Trends: Django vs Ruby on Rails' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://insights.stackoverflow.com/trends?tags=django%2Cruby-on-rails" target='_blank' > Stack Overflow Trends: Django vs Ruby on Rails </a> </figcaption> </figure> </div> <p> The one thing about Python that I like better than all other languages is the <a href='https://www.python.org/dev/peps/pep-0206/#id3' target='_blank' rel="nofollow">&ldquo;batteries included&rdquo;</a> feature-driven approach. This means that Python projects can aspire to more ambitious goals for a given amount of programmer effort as compared to projects implemented in other languages. That was a significant factor in my decision to focus on Python-powered semi-custom shopping carts instead of investigating shopping carts programmed with other computer languages. Ruby on Rails would doubtless provide an excellent foundation for shopping carts, but I think Python is a better strategic choice for me at now in the world of open source. </p> <p> This is not a decision I would have made in years past. Python's runtime consumes a lot more electrical power than the runtimes of other computer languages. However, <a href='https://docs.python.org/3/whatsnew/3.11.html' target='_blank' rel="nofollow">Python 3.11 is 25% faster than previus versions.</a> As available computing power continues to grow year-on-year within devices everywhere, the &ldquo;Python runtime tax&rdquo; has become much easier to bear. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <a href='https://python.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/python.svg" type="image/svg"> <!---<source srcset="/blog/images/python.avif" type="image/avif">--> <source srcset="/blog/images/python.webp" type="image/webp"> <source srcset="/blog/images/python.apng" type="image/apng"> <source srcset="/blog/images/python.png" type="image/png"> <source srcset="/blog/images/python.jpg" type="image/jpeg"> <source srcset="/blog/images/python.jpeg" type="image/jpeg"> <source srcset="/blog/images/python.jfif" type="image/jpeg"> <source srcset="/blog/images/python.pjpeg" type="image/jpeg"> <source srcset="/blog/images/python.pjp" type="image/jpeg"> <source srcset="/blog/images/python.gif" type="image/gif"> <source srcset="/blog/images/python.tif" type="image/tiff"> <source srcset="/blog/images/python.tiff" type="image/tiff"> <source srcset="/blog/images/python.bmp" type="image/bmp"> <source srcset="/blog/images/python.ico" type="image/x-icon"> <source srcset="/blog/images/python.cur" type="image/x-icon"> <img alt='The Python Language' class="imgImg rounded shadow" src="/blog/images/python.png" style='width: 100%; ' title='The Python Language' /> </picture> </a> <figcaption class='imgFigCaption halfsize'> <a href="https://python.org/" target='_blank' > The Python Language </a> </figcaption> </figure> </div> <!-- endregion --> <!-- endregion --> <!-- #region Beyond Open Source --> <h2 id="open">Beyond Open Source</h2> <p> Most open-source software suffers from limited funding and conflicts of interest. This seriously impacts long-term viability. I am happy to consider all appropriate technology, and I am not fixated on open-source options. Two major technical components need to be considered: </p> <ul> <li>E-commerce framework: this dictates the computer language and runtime library.</li> <li>Database: the e-commerce framework often dictates the choice of database.</li> </ul> <!-- endregion --> <!-- #region Monolithic vs Serverless Architectures --> <h2 id="arch">Monolithic vs Serverless Architectures</h2> <!-- #region Modular Yet Monolithic Architectures --> <h3 id="serverless">Modular Yet Monolithic Architectures</h3> <div class='imgWrapper imgFlex center' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/ecommerce/easter_island.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/easter_island.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/easter_island.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/easter_island.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/easter_island.png" type="image/png"> <source srcset="/blog/images/ecommerce/easter_island.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/easter_island.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/easter_island.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/easter_island.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/easter_island.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/easter_island.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/easter_island.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/easter_island.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/easter_island.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/easter_island.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/easter_island.cur" type="image/x-icon"> <img alt='Monoliths on Easter Island' class="imgImg rounded shadow" src="/blog/images/ecommerce/easter_island.png" style='width: 100%; ' title='Monoliths on Easter Island' /> </picture> <figcaption class='imgFigCaption '> Monoliths on Easter Island </figcaption> </figure> </div> <p> Traditionally, software has been built by first constructing functional modules, then combining the modules into a complete program (the monolith). For e-commerce, entire programs are deployed to production by adding configuration information and integrating with external services, such as payment processors. </p> <p> This works well; however, the result is a centralized computing resource that has a fixed capability and resource requirements. The expense of operating the software does not change much, regardless of the traffic volume. </p> <p> To handle periods of heavy traffic, complex mechanisms are required to scale up transactional capacity. Conversely, during periods of light traffic, the minimum cost to maintain the system in an operational state can be significant, especially for a startup company. </p> <!-- endregion --> <!-- #region Serverless Architecture --> <h3 id="serverless">Serverless Architecture</h3> <div class='imgWrapper imgFlex center' style=' '> <figure> <a href='https://martinfowler.com/articles/serverless.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/serverless_fowler.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/serverless_fowler.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/serverless_fowler.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/serverless_fowler.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/serverless_fowler.png" type="image/png"> <source srcset="/blog/images/ecommerce/serverless_fowler.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/serverless_fowler.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/serverless_fowler.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/serverless_fowler.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/serverless_fowler.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/serverless_fowler.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/serverless_fowler.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/serverless_fowler.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/serverless_fowler.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/serverless_fowler.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/serverless_fowler.cur" type="image/x-icon"> <img alt='Martin Fowler on Serverless Architectures' class="imgImg rounded shadow" src="/blog/images/ecommerce/serverless_fowler.png" style='width: 100%; ' title='Martin Fowler on Serverless Architectures' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://martinfowler.com/articles/serverless.html" target='_blank' > Martin Fowler on Serverless Architectures </a> </figcaption> </figure> </div> <p> One of the big architectural advances recently has been the introduction of <i>serverless computing</i>. It provides near-infinite scalability without paying for unused resources. <a href='https://www.cloudflare.com/learning/serverless/what-is-serverless/' target='_blank' rel="nofollow">CloudFlare has a good definition</a>: </p> <p class="quote"> Serverless computing is a method of providing backend services on an as-needed basis. A serverless provider allows users to write and deploy code without worrying about the underlying infrastructure. A company that gets backend services from a serverless vendor is charged based on their computation and does not have to reserve and pay for a fixed amount of bandwidth or number of servers, as the service is auto-scaling. Note that despite the name serverless, physical servers are still used, but developers do not need to be aware of them. </p> <p> Vendors that provide serverless computing platforms include <a href='https://aws.amazon.com/lambda/' target='_blank' rel="nofollow">AWS Lambda</a>, <a href='https://azure.microsoft.com/services/functions/' target='_blank' rel="nofollow">Azure Functions</a>, <a href='https://workers.cloudflare.com/' target='_blank' rel="nofollow">CloudFlare Workers</a>, <a href='https://cloud.google.com/functions' target='_blank' rel="nofollow">Google Cloud Functions</a> </p> <p> The <a href='https://www.serverless.com' target='_blank' rel="nofollow">Serverless Framework</a> is a language- and platform-agnostic framework. Languages supported include Node.js, Python, Java, Go, C#, Ruby, Swift, Kotlin, PHP, Scala, & F#. Platforms supported include <a href='https://www.serverless.com/framework/docs/providers/' target='_blank' rel="nofollow">Alibaba Cloud, AWS, Microsoft Azure, Fn Project, Google Cloud Platform, Apache OpenWhisk, CloudFlare Workers, Knative, Kubeless, Spotinst and Tencent Cloud</a>. The <a href='https://github.com/serverless/serverless' target='_blank' rel="nofollow">GitHub project</a> has the code. </p> <!-- endregion --> <!-- endregion --> <!-- #region Responsive Web Pages --> <h2 id="responsive">Responsive Web Pages</h2> <div class='imgWrapper imgFlex center' style=' '> <figure> <a href='https://medium.com/level-up-web/best-practices-of-responsive-web-design-6da8578f65c4' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/ecommerce/responsive_web.svg" type="image/svg"> <!---<source srcset="/blog/images/ecommerce/responsive_web.avif" type="image/avif">--> <source srcset="/blog/images/ecommerce/responsive_web.webp" type="image/webp"> <source srcset="/blog/images/ecommerce/responsive_web.apng" type="image/apng"> <source srcset="/blog/images/ecommerce/responsive_web.png" type="image/png"> <source srcset="/blog/images/ecommerce/responsive_web.jpg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/responsive_web.jpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/responsive_web.jfif" type="image/jpeg"> <source srcset="/blog/images/ecommerce/responsive_web.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ecommerce/responsive_web.pjp" type="image/jpeg"> <source srcset="/blog/images/ecommerce/responsive_web.gif" type="image/gif"> <source srcset="/blog/images/ecommerce/responsive_web.tif" type="image/tiff"> <source srcset="/blog/images/ecommerce/responsive_web.tiff" type="image/tiff"> <source srcset="/blog/images/ecommerce/responsive_web.bmp" type="image/bmp"> <source srcset="/blog/images/ecommerce/responsive_web.ico" type="image/x-icon"> <source srcset="/blog/images/ecommerce/responsive_web.cur" type="image/x-icon"> <img alt='Image from &lsquo;Best Practices of Responsive Web Design&rsquo; by Bradley Nice' class="imgImg rounded shadow" src="/blog/images/ecommerce/responsive_web.png" style='width: 100%; ' title='Image from &lsquo;Best Practices of Responsive Web Design&rsquo; by Bradley Nice' /> </picture> </a> <figcaption class='imgFigCaption '> <a href="https://medium.com/level-up-web/best-practices-of-responsive-web-design-6da8578f65c4" target='_blank' > Image from &lsquo;Best Practices of Responsive Web Design&rsquo; by Bradley Nice </a> </figcaption> </figure> </div> <p> <a href='https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design' target='_blank' rel="nofollow">Responsive web pages</a> alter their layout and appearance to suit different screen widths and resolutions. Many of the shopping carts I looked at did not support responsive web pages. </p> <!-- endregion --> <!-- #region Next: Check Out Django Shopping Carts --> <h2 id="next">Next: Check Out Django Shopping Carts</h2> <p> Python, and therefore Django, is winning. I admit that I like the Django slogan: &ldquo;Django &ndash; The web framework for perfectionists with deadlines&rdquo;. When an option dominates the competition, you would need an exceptional reason to consider other options. My functional and non-functional requirements are mainstream, so next I will check out the leading option: <a href='/django/'>Django</a>. </p> <!-- endregion --> OpenCart - Postgres - ngnix - Ubuntu 2021-01-30T00:00:00-05:00 https://mslinn.github.io/blog/2021/01/30/opencart-postgres <!-- #region intro --> <p> I needed a shopping cart that has good coupon and discount support and flexible pricing. My requirements were unique in that each item in the cart might be a custom product, with the price computed according to a formula on the server. The store had few standard SKUs. </p> <p> I started to look into three options for obtaining a shopping cart with good coupon support: building it myself, using a commercial product, or customizing an open-source project. This article is the story of the &lsquo;customize an open source&rsquo; track. </p> <!-- endregion --> <!-- #region OpenCart --> <h2 id="opencart">OpenCart</h2> <p> I wanted to evaluate the leading open-source shopping cart contender by installing it on a development machine and giving it real data. OpenCart is renowned as one of the better open-source shopping carts available today. As with many open-source projects, the company that provides the source code has a conflict of interest: if they make installing and configuring the software effortless, then their revenue would be much less than if they had a cadre of interested but frustrated developers. I looked at the hosting options and did not like their value or customizability. </p> <p> OpenCart shows its age by using MySQL and its descendants, like Maria. Long ago, I moved on from MySQL to Postgres, and I have been pleased with Postgres. I decided to try to make OpenCart run on Postgres and Ubuntu. I knew before I started that OpenCart&rsquo;s support for Postgres was weak. </p> <div class="pullQuoteFull"> Spoiler: <a href='https://www.theregister.com/2023/11/24/opencart_vulnerability_dispute/' target='_blank' rel="nofollow">OpenCart would fail a quality review</a> </div> <p> If you find watching videos of gory slow-motion accidents entertaining, please read on. </p> <!-- endregion --> <!-- #region Unit Tests --> <h2 id="tests">Unit Tests</h2> <p> The first thing I look for when evaluating software for possible incorporation into a project is unit tests. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id39aff831c690'><button class='copyBtn' data-clipboard-target='#id39aff831c690' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>git grep -Ii test \ ":(exclude)*.css" \ ":(exclude)*.yml" \ ":(exclude)*.js" <span class='unselectable'>$ </span>git grep -IiE 'phptest|Codeception' \ ":(exclude)*.css" \ ":(exclude)*.yml" \ ":(exclude)*.js"</pre> </div> <!-- endregion --> <p> There were no tests and no references to PHPUnit or Codeception. This was a big black mark against OpenCart. </p> <!-- endregion --> <!-- #region Install Dependencies --> <h2 id="deps">Install Dependencies</h2> <p> If you need information about PostgreSQL, <a href='https://www.postgresql.org/' target='_blank' rel="nofollow">here is the mother ship</a>. This is a good description of <a href='https://www.digitalocean.com/community/tutorials/how-to-install-postgresql-on-ubuntu-20-04-quickstart' target='_blank' rel="nofollow">how to install Postgres</a>. </p> <p> I use PGAdmin when I want a graphical interface to PostgreSQL. Installation instructions are <a href='https://www.pgadmin.org/download/pgadmin-4-apt/' target='_blank' rel="nofollow">here</a>. In a nutshell: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id05e8aa9ac336'><button class='copyBtn' data-clipboard-target='#id05e8aa9ac336' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | \ sudo apt-key add <span class='unselectable'>$ </span>sudo sh -c \ 'echo "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list && apt update' <span class='unselectable'>$ </span>yes | sudo apt install pgadmin4</pre> </div> <!-- endregion --> <p> I installed the rest of the dependencies like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2e50c66e0ec4'><button class='copyBtn' data-clipboard-target='#id2e50c66e0ec4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install \ postgresql postgresql-contrib \ software-properties-common lynx \ php7.4 php-fpm php-gd php-curl php-postgre php-zip \ php5-pgsql</pre> </div> <!-- endregion --> <p> <code>phpenmod</code> is a Debian / Ubuntu command for enabling PHP extensions. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9fc40dff010c'><button class='copyBtn' data-clipboard-target='#id9fc40dff010c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo phpenmod pgsql</pre> </div> <p> I verified that the desired PHP extensions were installed typing <code>php -m</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id083f6aaee37d'><button class='copyBtn' data-clipboard-target='#id083f6aaee37d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>php -m | grep pg <span class='unselectable'>pdo_pgsql pgsql</span></pre> </div> <!-- endregion --> <!-- #region Figuring Out Problems --> <h2 id="debug">Figuring Out Problems</h2> <p> I also installed the PHP debugger when I realized I needed to do some debugging. IntelliJ (which is the big brother of PHP Storm) did a terrific job of providing debug capability for command-line PHP. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id97e691717faf'><button class='copyBtn' data-clipboard-target='#id97e691717faf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install php-xdebug</pre> </div> <p> I opened a new console and continuously viewed the <code>nginx</code>, PHP and PostgreSQL error logs. This was a big help whenever I needed to figure out problems. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id20f0ab85e115'><button class='copyBtn' data-clipboard-target='#id20f0ab85e115' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tail -f \ /var/log/nginx/*.log \ /var/log/php*.log \ /var/log/postgresql/postgresql-12*.log</pre> </div> <!-- endregion --> <!-- #region Configure nginx --> <h2 id="nginx_config">Configure <span class='code'>nginx</span></h2> <p> The only change I made to <code>/etc/nginx/nginx.conf</code> was to change the default MIME type from <code>application/octet-stream</code> to <code>text/html</code>. The change is highlighted in yellow; scroll the code to see it. </p> <!-- #region /etc/nginx/nginx.conf --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nginx/nginx.conf</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id876bf13ff68e'><button class='copyBtn' data-clipboard-target='#id876bf13ff68e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; # multi_accept on; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; types_hash_maxsize 2048; # server_tokens off; # server_names_hash_bucketsize 64; # server_name_in_redirect off; include /etc/nginx/mime.types; <span style="background-color: yellow">#default_type application/octet-stream;</span> <span style="background-color: yellow">default_type text/html;</span> ## # SSL Settings ## ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## # Logging Settings ## access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log notice; ## # Gzip Settings ## gzip on; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } #mail { # # See sample authentication script at: # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript # # # auth_http localhost/auth.php; # # pop3_capabilities "TOP" "USER"; # # imap_capabilities "IMAP4rev1" "UIDPLUS"; # # server { # listen localhost:110; # protocol pop3; # proxy on; # } # # server { # listen localhost:143; # protocol imap; # proxy on; # } #}</pre> </div> <!-- endregion --> <p> I deleted <code>/etc/nginx/sites-enabled/default</code> and replaced it with <code>/etc/nginx/sites-enabled/php</code> so PHP files would be parsed properly, no matter what directory they resided in. </p> <!-- #region /etc/nginx/sites-enabled/php --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nginx/sites-enabled/php</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id83ccf678c50f'><button class='copyBtn' data-clipboard-target='#id83ccf678c50f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># See https://tecadmin.net/setup-nginx-php-fpm-on-ubuntu-20-04/ server { listen 80; root /var/www/html; index index.php index.html index.htm; server_name example.com; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; } }</pre> </div> <!-- endregion --> <p> Now it was time to restart <code>nginx</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5e5f257049ce'><button class='copyBtn' data-clipboard-target='#id5e5f257049ce' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl restart nginx.service</pre> </div> <h2 id="restart">Verify Services Are Running</h2> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide1c2595992d6'><button class='copyBtn' data-clipboard-target='#ide1c2595992d6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl status nginx <span class='unselectable'>● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2021-01-29 16:58:04 EST; 15h ago Docs: man:nginx(8) Main PID: 1584845 (nginx) Tasks: 9 (limit: 38389) Memory: 10.0M CGroup: /system.slice/nginx.service ├─1584845 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; ├─1584846 nginx: worker process ├─1584847 nginx: worker process ├─1584848 nginx: worker process ├─1584849 nginx: worker process ├─1584850 nginx: worker process ├─1584851 nginx: worker process ├─1584852 nginx: worker process └─1584853 nginx: worker process Jan 29 16:58:04 localhost systemd[1]: Starting A high performance web server and a reverse proxy server... Jan 29 16:58:04 localhost systemd[1]: Started A high performance web server and a reverse proxy server.</span></pre> </div> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id139a5daf4847'><button class='copyBtn' data-clipboard-target='#id139a5daf4847' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl status php7.4-fpm <span class='unselectable'>● php7.4-fpm.service - The PHP 7.4 FastCGI Process Manager Loaded: loaded (/lib/systemd/system/php7.4-fpm.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2021-01-30 08:32:36 EST; 10min ago Docs: man:php-fpm7.4(8) Process: 3430785 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/7.4/fp> Main PID: 3430782 (php-fpm7.4) Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec" Tasks: 3 (limit: 38389) Memory: 8.1M CGroup: /system.slice/php7.4-fpm.service ├─3430782 php-fpm: master process (/etc/php/7.4/fpm/php-fpm.conf) ├─3430783 php-fpm: pool www └─3430784 php-fpm: pool www Jan 30 08:32:36 localhost systemd[1]: Starting The PHP 7.4 FastCGI Process Manager... Jan 30 08:32:36 localhost systemd[1]: Started The PHP 7.4 FastCGI Process Manager. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Verify PHP Works --> <h2 id="restart">Verifing That PHP Works</h2> <p> I made the file <code>/var/www/html/info.php</code>, which is very common when working with PHP. Just ensure that it does not appear on your production site, or hackers will know more about your website than they should. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/var/www/html/info.php</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idefbd2c342c51'><button class='copyBtn' data-clipboard-target='#idefbd2c342c51' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;?php phpinfo(); ?></pre> </div> <p> Now, I verified that PHP worked by viewing information about the setup. The <code>-I</code> option causes <code>curl</code> to just return HTML headers, not the HTML body. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2415d563dfda'><button class='copyBtn' data-clipboard-target='#id2415d563dfda' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -I http://localhost/info.php <span class='unselectable'>HTTP/1.1 200 OK Server: nginx/1.18.0 (Ubuntu) Date: Sat, 30 Jan 2021 18:50:30 GMT Content-Type: text/html; charset=UTF-8 Connection: keep-alive </span></pre> </div> <p> View <code>info.php</code> in a web browser to see the details. Lynx is good for that from the command line: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6328cbd244de'><button class='copyBtn' data-clipboard-target='#id6328cbd244de' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lynx http://localhost/info.php</pre> </div> <!-- endregion --> <!-- #region Set Up PostgreSQL --> <h2 id="postgres_setup">Set Up PostgreSQL</h2> <p> I only changed the value of <code>listen_addresses</code> in <code>postgresql.conf</code>. Again, this change is highlighted in yellow; scroll down to see it. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/postgresql/12/main/postgresql.conf</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfff3f016d15e'><button class='copyBtn' data-clipboard-target='#idfff3f016d15e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># ----------------------------- # PostgreSQL configuration file # ----------------------------- # # This file consists of lines of the form: # # name = value # # (The "=" is optional.) Whitespace may be used. Comments are introduced with # "#" anywhere on a line. The complete list of parameter names and allowed # values can be found in the PostgreSQL documentation. # # The commented-out settings shown in this file represent the default values. # Re-commenting a setting is NOT sufficient to revert it to the default value; # you need to reload the server. # # This file is read on server startup and when the server receives a SIGHUP # signal. If you edit the file on a running system, you have to SIGHUP the # server for the changes to take effect, run "pg_ctl reload", or execute # "SELECT pg_reload_conf()". Some parameters, which are marked below, # require a server shutdown and restart to take effect. # # Any parameter can also be given as a command-line option to the server, e.g., # "postgres -c log_connections=on". Some parameters can be changed at run time # with the "SET" SQL command. # # Memory units: kB = kilobytes Time units: ms = milliseconds # MB = megabytes s = seconds # GB = gigabytes min = minutes # TB = terabytes h = hours # d = days #------------------------------------------------------------------------------ # FILE LOCATIONS #------------------------------------------------------------------------------ # The default values of these variables are driven from the -D command-line # option or PGDATA environment variable, represented here as ConfigDir. data_directory = '/var/lib/postgresql/12/main' # use data in another directory # (change requires restart) hba_file = '/etc/postgresql/12/main/pg_hba.conf' # host-based authentication file # (change requires restart) ident_file = '/etc/postgresql/12/main/pg_ident.conf' # ident configuration file # (change requires restart) # If external_pid_file is not explicitly set, no extra PID file is written. external_pid_file = '/var/run/postgresql/12-main.pid' # write an extra PID file # (change requires restart) #------------------------------------------------------------------------------ # CONNECTIONS AND AUTHENTICATION #------------------------------------------------------------------------------ # - Connection Settings - <span style="background-color: yellow">listen_addresses = '*'</span> <span style="background-color: yellow">#listen_addresses = 'localhost' # what IP address(es) to listen on;</span> # comma-separated list of addresses; # defaults to 'localhost'; use '*' for all # (change requires restart) port = 5432 # (change requires restart) max_connections = 100 # (change requires restart) #superuser_reserved_connections = 3 # (change requires restart) unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories # (change requires restart) #unix_socket_group = '' # (change requires restart) #unix_socket_permissions = 0777 # begin with 0 to use octal notation # (change requires restart) #bonjour = off # advertise server via Bonjour # (change requires restart) #bonjour_name = '' # defaults to the computer name # (change requires restart) # - TCP settings - # see "man 7 tcp" for details #tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; # 0 selects the system default #tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; # 0 selects the system default #tcp_keepalives_count = 0 # TCP_KEEPCNT; # 0 selects the system default #tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; # 0 selects the system default # - Authentication - #authentication_timeout = 1min # 1s-600s #password_encryption = md5 # md5 or scram-sha-256 #db_user_namespace = off # GSSAPI using Kerberos #krb_server_keyfile = '' #krb_caseins_users = off # - SSL - #ssl = on #ssl_ca_file = '' ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' #ssl_crl_file = '' ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers #ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_min_protocol_version = 'TLSv1' #ssl_max_protocol_version = '' #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off #------------------------------------------------------------------------------ # RESOURCE USAGE (except WAL) #------------------------------------------------------------------------------ # - Memory - shared_buffers = 128MB # min 128kB # (change requires restart) #huge_pages = try # on, off, or try # (change requires restart) #temp_buffers = 8MB # min 800kB #max_prepared_transactions = 0 # zero disables the feature # (change requires restart) # Caution: it is not advisable to set max_prepared_transactions nonzero unless # you actively intend to use prepared transactions. #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: # mmap # sysv # windows # (change requires restart) dynamic_shared_memory_type = posix # the default is the first option # supported by the operating system: # posix # sysv # windows # mmap # (change requires restart) # - Disk - #temp_file_limit = -1 # limits per-process temp file space # in kB, or -1 for no limit # - Kernel Resources - #max_files_per_process = 1000 # min 25 # (change requires restart) # - Cost-Based Vacuum Delay - #vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) #vacuum_cost_page_hit = 1 # 0-10000 credits #vacuum_cost_page_miss = 10 # 0-10000 credits #vacuum_cost_page_dirty = 20 # 0-10000 credits #vacuum_cost_limit = 200 # 1-10000 credits # - Background Writer - #bgwriter_delay = 200ms # 10-10000ms between rounds #bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables #bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round #bgwriter_flush_after = 512kB # measured in pages, 0 disables # - Asynchronous Behavior - #effective_io_concurrency = 1 # 1-1000; 0 disables prefetching #max_worker_processes = 8 # (change requires restart) #max_parallel_maintenance_workers = 2 # taken from max_parallel_workers #max_parallel_workers_per_gather = 2 # taken from max_parallel_workers #parallel_leader_participation = on #max_parallel_workers = 8 # maximum number of max_worker_processes that # can be used in parallel operations #old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate # (change requires restart) #backend_flush_after = 0 # measured in pages, 0 disables #------------------------------------------------------------------------------ # WRITE-AHEAD LOG #------------------------------------------------------------------------------ # - Settings - #wal_level = replica # minimal, replica, or logical # (change requires restart) #fsync = on # flush data to disk for crash safety # (turning this off can cause # unrecoverable data corruption) #synchronous_commit = on # synchronization level; # off, local, remote_write, remote_apply, or on #wal_sync_method = fsync # the default is the first option # supported by the operating system: # open_datasync # fdatasync (default on Linux) # fsync # fsync_writethrough # open_sync #full_page_writes = on # recover from partial page writes #wal_compression = off # enable compression of full-page writes #wal_log_hints = off # also do full page writes of non-critical updates # (change requires restart) #wal_init_zero = on # zero-fill new WAL files #wal_recycle = on # recycle WAL files #wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers # (change requires restart) #wal_writer_delay = 200ms # 1-10000 milliseconds #wal_writer_flush_after = 1MB # measured in pages, 0 disables #commit_delay = 0 # range 0-100000, in microseconds #commit_siblings = 5 # range 1-1000 # - Checkpoints - #checkpoint_timeout = 5min # range 30s-1d max_walsize = 1GB min_walsize = 80MB #checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 #checkpoint_flush_after = 256kB # measured in pages, 0 disables #checkpoint_warning = 30s # 0 disables # - Archiving - #archive_mode = off # enables archiving; off, on, or always # (change requires restart) #archive_command = '' # command to use to archive a logfile segment # placeholders: %p = path of file to archive # %f = file name only # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' #archive_timeout = 0 # force a logfile segment switch after this # number of seconds; 0 disables # - Archive Recovery - # These are only used in recovery mode. #restore_command = '' # command to use to restore an archived logfile segment # placeholders: %p = path of file to restore # %f = file name only # e.g. 'cp /mnt/server/archivedir/%f %p' # (change requires restart) #archive_cleanup_command = '' # command to execute at every restartpoint #recovery_end_command = '' # command to execute at completion of recovery # - Recovery Target - # Set these only when performing a targeted recovery. #recovery_target = '' # 'immediate' to end recovery as soon as a # consistent state is reached # (change requires restart) #recovery_target_name = '' # the named restore point to which recovery will proceed # (change requires restart) #recovery_target_time = '' # the time stamp up to which recovery will proceed # (change requires restart) #recovery_target_xid = '' # the transaction ID up to which recovery will proceed # (change requires restart) #recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed # (change requires restart) #recovery_target_inclusive = on # Specifies whether to stop: # just after the specified recovery target (on) # just before the recovery target (off) # (change requires restart) #recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID # (change requires restart) #recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' # (change requires restart) #------------------------------------------------------------------------------ # REPLICATION #------------------------------------------------------------------------------ # - Sending Servers - # Set these on the master and on any standby that will send replication data. #max_wal_senders = 10 # max number of walsender processes # (change requires restart) #wal_keep_segments = 0 # in logfile segments; 0 disables #wal_sender_timeout = 60s # in milliseconds; 0 disables #max_replication_slots = 10 # max number of replication slots # (change requires restart) #track_commit_timestamp = off # collect timestamp of transaction commit # (change requires restart) # - Master Server - # These settings are ignored on a standby server. #synchronous_standby_names = '' # standby servers that provide sync rep # method to choose sync standbys, number of sync standbys, # and comma-separated list of application_name # from standby(s); '*' = all #vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed # - Standby Servers - # These settings are ignored on a master server. #primary_conninfo = '' # connection string to sending server # (change requires restart) #primary_slot_name = '' # replication slot on sending server # (change requires restart) #promote_trigger_file = '' # file name whose presence ends recovery #hot_standby = on # "off" disallows queries during recovery # (change requires restart) #max_standby_archive_delay = 30s # max delay before canceling queries # when reading WAL from archive; # -1 allows indefinite delay #max_standby_streaming_delay = 30s # max delay before canceling queries # when reading streaming WAL; # -1 allows indefinite delay #wal_receiver_status_interval = 10s # send replies at least this often # 0 disables #hot_standby_feedback = off # send info from standby to prevent # query conflicts #wal_receiver_timeout = 60s # time that receiver waits for # communication from master # in milliseconds; 0 disables #wal_retrieve_retry_interval = 5s # time to wait before retrying to # retrieve WAL after a failed attempt #recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery # - Subscribers - # These settings are ignored on a publisher. #max_logical_replication_workers = 4 # taken from max_worker_processes # (change requires restart) #max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers #------------------------------------------------------------------------------ # QUERY TUNING #------------------------------------------------------------------------------ # - Planner Method Configuration - #enable_bitmapscan = on #enable_hashagg = on #enable_hashjoin = on #enable_indexscan = on #enable_indexonlyscan = on #enable_material = on #enable_mergejoin = on #enable_nestloop = on #enable_parallel_append = on #enable_seqscan = on #enable_sort = on #enable_tidscan = on #enable_partitionwise_join = off #enable_partitionwise_aggregate = off #enable_parallel_hash = on #enable_partition_pruning = on # - Planner Cost Constants - #seq_page_cost = 1.0 # measured on an arbitrary scale #random_page_cost = 4.0 # same scale as above #cpu_tuple_cost = 0.01 # same scale as above #cpu_index_tuple_cost = 0.005 # same scale as above #cpu_operator_cost = 0.0025 # same scale as above #parallel_tuple_cost = 0.1 # same scale as above #parallel_setup_cost = 1000.0 # same scale as above #jit_above_cost = 100000 # perform JIT compilation if available # and query more expensive than this; # -1 disables #jit_inline_above_cost = 500000 # inline small functions if query is # more expensive than this; -1 disables #jit_optimize_above_cost = 500000 # use expensive JIT optimizations if # query is more expensive than this; # -1 disables #min_parallel_table_scansize = 8MB #min_parallel_index_scansize = 512kB #effective_cachesize = 4GB # - Genetic Query Optimizer - #geqo = on #geqo_threshold = 12 #geqo_effort = 5 # range 1-10 #geqo_poolsize = 0 # selects default based on effort #geqo_generations = 0 # selects default based on effort #geqo_selection_bias = 2.0 # range 1.5-2.0 #geqo_seed = 0.0 # range 0.0-1.0 # - Other Planner Options - #default_statistics_target = 100 # range 1-10000 #constraint_exclusion = partition # on, off, or partition #cursor_tuple_fraction = 0.1 # range 0.0-1.0 #from_collapse_limit = 8 #join_collapse_limit = 8 # 1 disables collapsing of explicit # JOIN clauses #force_parallel_mode = off #jit = on # allow JIT compilation #plan_cache_mode = auto # auto, force_generic_plan or # force_custom_plan #------------------------------------------------------------------------------ # REPORTING AND LOGGING #------------------------------------------------------------------------------ # - Where to Log - #log_destination = 'stderr' # Valid values are combinations of # stderr, csvlog, syslog, and eventlog, # depending on platform. csvlog # requires logging_collector to be on. # This is used when logging to stderr: #logging_collector = off # Enable capturing of stderr and csvlog # into log files. Required to be on for # csvlogs. # (change requires restart) # These are only used if logging_collector is on: #log_directory = 'log' # directory where log files are written, # can be absolute or relative to PGDATA #log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, # can include strftime() escapes #log_file_mode = 0600 # creation mode for log files, # begin with 0 to use octal notation #log_truncate_on_rotation = off # If on, an existing log file with the # same name as the new log file will be # truncated rather than appended to. # But such truncation only occurs on # time-driven rotation, not on restarts # or size-driven rotation. Default is # off, meaning append to existing files # in all cases. #log_rotation_age = 1d # Automatic rotation of logfiles will # happen after that time. 0 disables. #log_rotationsize = 10MB # Automatic rotation of logfiles will # happen after that much log output. # 0 disables. # These are relevant when logging to syslog: #syslog_facility = 'LOCAL0' #syslog_ident = 'postgres' #syslog_sequence_numbers = on #syslog_split_messages = on # This is only relevant when logging to eventlog (win32): # (change requires restart) #event_source = 'PostgreSQL' # - When to Log - #log_min_messages = warning # values in order of decreasing detail: # debug5 # debug4 # debug3 # debug2 # debug1 # info # notice # warning # error # log # fatal # panic #log_min_error_statement = error # values in order of decreasing detail: # debug5 # debug4 # debug3 # debug2 # debug1 # info # notice # warning # error # log # fatal # panic (effectively off) #log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements # and their durations, > 0 logs only # statements running at least this number # of milliseconds #log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements # are logged regardless of their duration. 1.0 logs all # statements from all transactions, 0.0 never logs. # - What to Log - #debug_print_parse = off #debug_print_rewritten = off #debug_print_plan = off #debug_pretty_print = on #log_checkpoints = off #log_connections = off #log_disconnections = off #log_duration = off #log_error_verbosity = default # terse, default, or verbose messages #log_hostname = off log_line_prefix = '%m [%p] %q%u@%d ' # special values: # %a = application name # %u = username # %d = database name # %r = remote host and port # %h = remote host # %p = process ID # %t = timestamp without milliseconds # %m = timestamp with milliseconds # %n = timestamp with milliseconds (as a Unix epoch) # %i = command tag # %e = SQL state # %c = session ID # %l = session line number # %s = session start timestamp # %v = virtual transaction ID # %x = transaction ID (0 if none) # %q = stop here in non-session # processes # %% = '%' # e.g. '<%u%%%d> ' #log_lock_waits = off # log lock waits >= deadlock_timeout #log_statement = 'none' # none, ddl, mod, all #log_replication_commands = off #log_temp_files = -1 # log temporary files equal or larger # than the specified size in kilobytes; # -1 disables, 0 logs all temp files log_timezone = 'America/New_York' #------------------------------------------------------------------------------ # PROCESS TITLE #------------------------------------------------------------------------------ cluster_name = '12/main' # added to process titles if nonempty # (change requires restart) #update_process_title = on #------------------------------------------------------------------------------ # STATISTICS #------------------------------------------------------------------------------ # - Query and Index Statistics Collector - #track_activities = on #track_counts = on #track_io_timing = off #track_functions = none # none, pl, all #track_activity_querysize = 1024 # (change requires restart) stats_temp_directory = '/var/run/postgresql/12-main.pg_stat_tmp' # - Monitoring - #log_parser_stats = off #log_planner_stats = off #log_executor_stats = off #log_statement_stats = off #------------------------------------------------------------------------------ # AUTOVACUUM #------------------------------------------------------------------------------ #autovacuum = on # Enable autovacuum subprocess? 'on' # requires track_counts to also be on. #log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and # their durations, > 0 logs only # actions running at least this number # of milliseconds. #autovacuum_max_workers = 3 # max number of autovacuum subprocesses # (change requires restart) #autovacuum_naptime = 1min # time between autovacuum runs #autovacuum_vacuum_threshold = 50 # min number of row updates before # vacuum #autovacuum_analyze_threshold = 50 # min number of row updates before # analyze #autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum #autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze #autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum # (change requires restart) #autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age # before forced vacuum # (change requires restart) #autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for # autovacuum, in milliseconds; # -1 means use vacuum_cost_delay #autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for # autovacuum, -1 means use # vacuum_cost_limit #------------------------------------------------------------------------------ # CLIENT CONNECTION DEFAULTS #------------------------------------------------------------------------------ # - Statement Behavior - #client_min_messages = notice # values in order of decreasing detail: # debug5 # debug4 # debug3 # debug2 # debug1 # log # notice # warning # error #search_path = '"$user", public' # schema names #row_security = on #default_tablespace = '' # a tablespace name, '' uses the default #temp_tablespaces = '' # a list of tablespace names, '' uses # only default tablespace #default_table_access_method = 'heap' #check_function_bodies = on #default_transaction_isolation = 'read committed' #default_transaction_read_only = off #default_transaction_deferrable = off #session_replication_role = 'origin' #statement_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled #idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled #vacuum_freeze_min_age = 50000000 #vacuum_freeze_table_age = 150000000 #vacuum_multixact_freeze_min_age = 5000000 #vacuum_multixact_freeze_table_age = 150000000 #vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples # before index cleanup, 0 always performs # index cleanup #bytea_output = 'hex' # hex, escape #xmlbinary = 'base64' #xmloption = 'content' #gin_fuzzy_search_limit = 0 #gin_pending_list_limit = 4MB # - Locale and Formatting - datestyle = 'iso, mdy' #intervalstyle = 'postgres' timezone = 'America/New_York' #timezone_abbreviations = 'Default' # Select the set of available time zone # abbreviations. Currently, there are # Default # Australia (historical usage) # India # You can create your own file in # share/timezonesets/. #extra_float_digits = 1 # min -15, max 3; any value >0 actually # selects precise output mode #client_encoding = sql_ascii # actually, defaults to database # encoding # These settings are initialized by initdb, but they can be changed. lc_messages = 'en_US.UTF-8' # locale for system error message # strings lc_monetary = 'en_US.UTF-8' # locale for monetary formatting lc_numeric = 'en_US.UTF-8' # locale for number formatting lc_time = 'en_US.UTF-8' # locale for time formatting # default configuration for text search default_text_search_config = 'pg_catalog.english' # - Shared Library Preloading - #shared_preload_libraries = '' # (change requires restart) #local_preload_libraries = '' #session_preload_libraries = '' #jit_provider = 'llvmjit' # JIT library to use # - Other Defaults - #dynamic_library_path = '$libdir' #------------------------------------------------------------------------------ # LOCK MANAGEMENT #------------------------------------------------------------------------------ #deadlock_timeout = 1s #max_locks_per_transaction = 64 # min 10 # (change requires restart) #max_pred_locks_per_transaction = 64 # min 10 # (change requires restart) #max_pred_locks_per_relation = -2 # negative values mean # (max_pred_locks_per_transaction # / -max_pred_locks_per_relation) - 1 #max_pred_locks_per_page = 2 # min 0 #------------------------------------------------------------------------------ # VERSION AND PLATFORM COMPATIBILITY #------------------------------------------------------------------------------ # - Previous PostgreSQL Versions - #array_nulls = on #backslash_quote = safe_encoding # on, off, or safe_encoding #escape_string_warning = on #lo_compat_privileges = off #operator_precedence_warning = off #quote_all_identifiers = off #standard_conforming_strings = on #synchronize_seqscans = on # - Other Platforms and Clients - #transform_null_equals = off #------------------------------------------------------------------------------ # ERROR HANDLING #------------------------------------------------------------------------------ #exit_on_error = off # terminate session on any error? #restart_after_crash = on # reinitialize after backend crash? #data_sync_retry = off # retry or panic on failure to fsync # data? # (change requires restart) #------------------------------------------------------------------------------ # CONFIG FILE INCLUDES #------------------------------------------------------------------------------ # These options allow settings to be loaded from files other than the # default postgresql.conf. Note that these are directives, not variable # assignments, so they can usefully be given more than once. include_dir = 'conf.d' # include files ending in '.conf' from # a directory, e.g., 'conf.d' #include_if_exists = '...' # include file only if it exists #include = '...' # include file #------------------------------------------------------------------------------ # CUSTOMIZED OPTIONS #------------------------------------------------------------------------------</pre> </div> <!-- endregion --> <p> All of my changes, highlighted in yellow, are at the bottom of <code>pg_hba.conf</code>. Scroll down to see them. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/postgresql/12/main/pg_hba.conf</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id12ec04a02ce2'><button class='copyBtn' data-clipboard-target='#id12ec04a02ce2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># PostgreSQL Client Authentication Configuration File # =================================================== # # Refer to the "Client Authentication" section in the PostgreSQL # documentation for a complete description of this file. A short # synopsis follows. # # This file controls: which hosts are allowed to connect, how clients # are authenticated, which PostgreSQL usernames they can use, which # databases they can access. Records take one of these forms: # # local DATABASE USER METHOD [OPTIONS] # host DATABASE USER ADDRESS METHOD [OPTIONS] # hostssl DATABASE USER ADDRESS METHOD [OPTIONS] # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] # hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] # hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] # # (The uppercase items must be replaced by actual values.) # # The first field is the connection type: "local" is a Unix-domain # socket, "host" is either a plain or SSL-encrypted TCP/IP socket, # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a # non-SSL TCP/IP socket. Similarly, "hostgssenc" uses a # GSSAPI-encrypted TCP/IP socket, while "hostnogssenc" uses a # non-GSSAPI socket. # # DATABASE can be "all", "sameuser", "samerole", "replication", a # database name, or a comma-separated list thereof. The "all" # keyword does not match "replication". Access to replication # must be enabled in a separate record (see example below). # # USER can be "all", a username, a group name prefixed with "+", or a # comma-separated list thereof. In both the DATABASE and USER fields # you can also write a file name prefixed with "@" to include names # from a separate file. # # ADDRESS specifies the set of hosts the record matches. It can be a # host name, or it is made up of an IP address and a CIDR mask that is # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that # specifies the number of significant bits in the mask. A host name # that starts with a dot (.) matches a suffix of the actual host name. # Alternatively, you can write an IP address and netmask in separate # columns to specify the set of hosts. Instead of a CIDR-address, you # can write "samehost" to match any of the server's own IP addresses, # or "samenet" to match any address in any subnet that the server is # directly connected to. # # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", # "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". # Note that "password" sends passwords in clear text; "md5" or # "scram-sha-256" are preferred since they send encrypted passwords. # # OPTIONS are a set of options for the authentication in the format # NAME=VALUE. The available options depend on the different # authentication methods -- refer to the "Client Authentication" # section in the documentation for a list of which options are # available for which authentication methods. # # Database and usernames containing spaces, commas, quotes and other # special characters must be quoted. Quoting one of the keywords # "all", "sameuser", "samerole" or "replication" makes the name lose # its special character, and just match a database or username with # that name. # # This file is read on server startup and when the server receives a # SIGHUP signal. If you edit the file on a running system, you have to # SIGHUP the server for the changes to take effect, run "pg_ctl reload", # or execute "SELECT pg_reload_conf()". # # Put your actual configuration here # ---------------------------------- # # If you want to allow non-local connections, you need to add more # "host" records. In that case you will also need to make PostgreSQL # listen on a non-local interface via the listen_addresses # configuration parameter, or via the -i or -h command line switches. # DO NOT DISABLE! # If you change this first entry you will need to make sure that the # database superuser can access the database using some other method. # Noninteractive access to all databases is required during automatic # maintenance (custom daily cronjobs, replication, and similar tasks). # # Database administrative login by Unix domain socket <span style="background-color: yellow">local all postgres md5</span> # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only <span style="background-color: yellow">local all all peer</span> # IPv4 local connections: <span style="background-color: yellow">host all all 0.0.0.0/0 md5</span> <span style="background-color: yellow">host all all 0.0.0.0/32 md5</span> # IPv6 local connections: host all all ::1/128 md5 # Allow replication connections from localhost, by a user with the # replication privilege. local replication all peer host replication all 127.0.0.1/32 md5 host replication all ::1/128 md5</pre> </div> <!-- endregion --> <p> Now that PostgreSQL was configured, I restarted it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id09b2056c5713'><button class='copyBtn' data-clipboard-target='#id09b2056c5713' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl restart postgresql</pre> </div> <!-- endregion --> <!-- #region Connecting PostgreSQL to PHP --> <h2 id="together">Connecting PostgreSQL to PHP</h2> <p> I created a new database called <code>opencart</code> with this command: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc09d22977377'><button class='copyBtn' data-clipboard-target='#idc09d22977377' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>psql -U postgres -c "create database opencart;" <span class='unselectable'>CREATE DATABASE Time: 1057.887 ms (00:01.058)</span></pre> </div> <p> I entered PHP interactive mode to verify that PHP could connect properly to the new PostgreSQL database. This command sequence just creates a simple table and deletes it. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id721c958cd57f'><button class='copyBtn' data-clipboard-target='#id721c958cd57f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>php -a <span class='unselectable'>Interactive mode enabled php > </span>pg_connect("host=localhost dbname=opencart user=postgres password=hithere"); <span class='unselectable'>php > </span>pg_query("create table test(id integer)"); <span class='unselectable'>php > </span>pg_query("drop table test"); <span class='unselectable'>php > </span>exit</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Configuring OpenCart --> <h2 id="opencart_configuration1">Configuring OpenCart</h2> <p> The two configuration files that OpenCart provides are empty. They need to be renamed before OpenCart can be installed. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida6e06eaedeb4'><button class='copyBtn' data-clipboard-target='#ida6e06eaedeb4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo mv /work/ecommerce/opencart/upload/config{-dist,}.php <span class='unselectable'>$ </span>sudo mv /work/ecommerce/opencart/upload/admin/config{-dist,}.php</pre> </div> <p> These files will contain configuration information after the OpenCart <code>admin</code> user configures the system. The files also need to have their owner or group set to the same user that the web server runs as. For <code>nginx</code>, this username and group are both called <code>www-data</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfccb8b48a70a'><button class='copyBtn' data-clipboard-target='#idfccb8b48a70a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>find . -name config.php -exec sudo chown www-data:dev {} \;</pre> </div> <p> I dislike the idea of having a web application modify its configuration data while running. This is inherently insecure. However, many PHP programs from the era in which OpenCart was originally written operated that way. I have always been acutely uncomfortable with this practice. </p> <p> Equally distasteful to me was the hack that PHP programmers often do to support multi-tenant web applications where users who self-administer their sites have limited storage options (20 years ago, this was an issue; the rest of the world has moved on): storing logs within the program file structure. This is insecure. OpenCart logs belong in <code>/var/log/opencart</code>. I did not modify the code; instead, I rolled my eyes and made the log files in <code>opencart/upload/system/storage/logs/</code> group writable. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5a4ea35d2d57'><button class='copyBtn' data-clipboard-target='#id5a4ea35d2d57' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo chmod g+w opencart/upload/system/storage/logs/*.log</pre> </div> <!-- endregion --> <!-- #region Installing OpenCart --> <h2 id="install">Installing OpenCart</h2> <!-- #region Web-Based Installer --> <h3 id="web-install">Web-Based Installer</h3> <p> The web-based OpenCart installer is fragile and <a href='https://github.com/opencart/opencart/commits/master/upload/install/index.php' target='_blank' rel="nofollow">not well maintained</a>. It dies near the end of its work when attempting to install using a Postgres database. </p> <p> Clicking on <code><a href='http://http://localhost/upload/install/' target='_blank' rel="nofollow"><code>http://localhost/upload/install/</code></a></code> starts the web-based OpenCart installation process by displaying the GNU license agreement from 2007. The installation fails on page 3. This problem was first reported on <a href='https://github.com/opencart/opencart/issues/7521' target='_blank' rel="nofollow">July 15, 2019</a>, but it was never addressed. </p> <p> Completing the installation only requires that the database be set up. <code>system/helper/db_schema.php</code> contains PHP code for defining the database schema using MySQL, and the SQL to populate the database is found in <code>upload/install/opencart.sql</code>. </p> <p> At this point, I gave up and tried the command-line installer. </p> <!-- endregion --> <!-- #region Command-Line Installer --> <h3 id="cli-install">Command-Line Installer</h3> <!-- #region implicit --> <p> OpenCart has a command-line installer, which is not mentioned in the online installation documentation. I always prefer to use a command-line installer, if possible, because any problems encountered are easier to diagnose and fix than with web-based installers. </p> <p> In contrast to the publicly promoted web-based installer, the command-line installer appears to be <a href='https://github.com/opencart/opencart/commits/master/upload/install/cli_install.php' target='_blank' rel="nofollow">well maintained</a> for and by the current authors, who obviously also operate OpenCart Cloud (more on that in a minute). Once again, we see the inherent conflict of interest in traditional open-source software. </p> <p> Here is a sample command line for installing OpenCart. The script should be run from the <code>opencart/upload/install</code> directory. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide6a474c58165'><button class='copyBtn' data-clipboard-target='#ide6a474c58165' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>php cli_install.php install \ --db_database opencart \ --db_driver postgre \ --db_hostname localhost \ --db_password postgres_password \ --db_port 5432 \ --db_username postgres \ --email email@example.com \ --http_server http://localhost/opencart/ \ --password admin_password \ --username admin \ | lynx -stdin</pre> </div> <!-- endregion --> <p> I needed to provide proper values for the following options: </p> <dl> <dt><code>--db_database</code></dt> <dd> There is no default value for this option. It makes sense to name the database <code>opencart</code>, but one might have reasons to give it another name. </dd> <dt><code>--db_hostname</code></dt> <dd>The database might not run on the same network node as OpenCart's web server.</dd> <dt><code>--db_password</code></dt> <dd>Friends do not let friends use empty passwords, even on personal machines.</dd> <dt><code>--db_port</code></dt> <dd>I usually use the default PostgreSQL port, 5432.</dd> <dt><code>--db_username</code></dt> <dd>It is more secure to not use the <code>postgres</code> default username.</dd> <dt><code>--email</code></dt> <dd>Email address of the OpenCart administrator.</dd> <dt><code>--http_server</code></dt> <dd>More than just the domain name, this option also specifies the protocol, HTTP port and the path to the OpenCart directory on the web server.</dd> <dt><code>--password</code></dt> <dd>OpenCart admin user password.</dd> </dl> <!-- endregion --> <!-- #region The --db-prefix Option --> <h4 id="db_prefix">The <span class="code">--db-prefix</span> Option</h4> <p> The above omits the <code>--db_prefix</code> option, whose default value is <code>oc_</code>. This is because the installer uses <code>upload/install/opencart.sql</code>, which is hard-coded to use the default value. </p> <!-- endregion --> <!-- #region The --cloud Option --> <h4 id="cloud">The <span class="code">--cloud</span> Option</h4> <p> The above also omits the <code>--cloud</code> option. This option has no documentation. After looking at the source code, I think this parameter is exclusively for <a href='https://www.opencart.com/index.php?route=cloud/landing' target='_blank' rel="nofollow">OpenCart Cloud</a> installations. This means that most people could omit the option because its value defaults to 0, which means the installation is not intended for Open Cloud. </p> <p> Why do I think that? <a href='https://github.com/opencart/opencart/blob/master/upload/install/cli_install.php' target='_blank' rel="nofollow">Looking at the code</a>, I see this parameter suppresses database configuration and the saving of configuration information. Also, cloud installations require that the admin user password be pre-hashed, which suggests to me that this script can be initiated from another installation script used by Open Cloud. </p> <!-- endregion --> <!-- endregion --> <!-- #region Using Default Values --> <h3 id="cloud">Using Default Values</h3> <p> If you are installing on a development machine, it is likely to run both the Postgres database and the PHP website, and the software is likely to be set up using default values. Assuming that the PostgreSQL username is the default, <code>postgres</code>, and the database is called <code>opencart</code>, you just need to specify the following parameters: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id04efe2cb6e57'><button class='copyBtn' data-clipboard-target='#id04efe2cb6e57' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>php cli_install.php install \ --db_driver postgre \ --db_database opencart \ --db_password postgres_password \ --db_port 5432 \ --db_username postgres \ --email email@example.com \ --http_server http://localhost/opencart/ \ --password admin_password \ | lynx -stdin</pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Patching Source Code --> <h3 id="patch">Patching Source Code</h3> <!-- #region implicit --> <p> I ran <code>cli_install.php</code>, found problems, fixed them, reran <code>cli_install.php</code>, found more problems, fixed them, etc. etc. </p> <p> Clearly, no one has ever run <code>cli_install.php</code> to completion when <code>--cloud</code> option was set to 0. <p> <!-- endregion --> <!-- #region Defining Constants --> <h4 id="constants">Defining Constants</h4> <p> The people who use <code>cli_install.php</code> provide values for two constants before the program runs. I found some code in <code>upload/index.php</code> that defined them. Using those statements as a guide, I added some lines after line 57 of <code>upload/system/startup.php</code> so these constants were defined: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd0845d4ce00c'><button class='copyBtn' data-clipboard-target='#idd0845d4ce00c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// mslinn added: define('DIR_EXTENSION', DIR_OPENCART . 'extension/'); define('HTTP_SERVER', 'file:' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/.\\') . '/'); // end mslinn</pre> </div> <!-- endregion --> <!-- #region Checking the Postgres Driver --> <h4 id="db_driver">Checking The Postgres Driver</h4> <p> Clearly no-one has ever tried to install using the PostgreSQL driver before. I had to modify line 198 of <code>upload/install/cli_install.php</code> to add a check, highlighted in yellow, for the PostgreSQL driver extension: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8eb26de7468b'><button class='copyBtn' data-clipboard-target='#id8eb26de7468b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>if (!extension_loaded('mysqli') <span style="background-color: yellow">&& !extension_loaded('pgsql')</span>) { $error .= 'ERROR: MySQLi extension needs to be loaded for OpenCart to work!' . "\n"; }</pre> </div> <!-- endregion --> <!-- endregion --> <!-- endregion --> <!-- #region Running the Command-Line Installer --> <h2 id="results">Running the Command-Line Installer</h2> <p> The command-line installer spewed out miles and miles of HTML (mostly the GNU license) and died with an error message. So, I fixed the problem and reran it. It died somewhere else with a different error. So, I fixed that problem too and ran it again. It died yet again somewhere else with yet another error. And again, and again, and again... </p> <p> The last problem I found before quitting was an error message resulting from the command-line installer attempting to rewrite an HTML header. Really! A command-line installer does not need to present HTML to the user. This command-line installer program is clearly a cheap hack. </p> <!-- endregion --> <!-- #region Evaluation Results --> <h2 id="stop">Evaluation Results</h2> <p> At this point, I felt that I now had a good idea of the quality of this open-source project: </p> <div class="pullQuoteFull"> OpenCart is very poorly constructed </div> <p> The business model for the company that stewards OpenCart is also clear: hire cheap programmers of modest ability and charge for their time by the hour. OpenCart is not something I would want to base an e-commerce business on. </p> <p> Since OpenCart is supposedly the best open-source shopping cart today, <a href='/django/index.html'>I will next look into</a> building something just for me that provides me with a competitive advantage. </p> <!-- endregion --> <!-- #region Cleaning Up --> <!-- endregion --> Installing a New SSH Key on AWS EC2 with User Data 2020-10-27T00:00:00-04:00 https://mslinn.github.io/blog/2020/10/27/installing-a-new-ssh-key-on-awc-ec2-with-user-data <!-- #region --> <p> For some reason the <code>ssh</code> certificates that AWS generated for me 3 years ago are no longer recognized by Ubuntu 20.10. This article shows how to create new certificates and push them to an AWS server that was just upgraded to Ubuntu 20.01, and now cannot be logged into. I decided to use OpenSSH to generate the new keypairs instead of AWS to generate the keypairs because the current problem stems from <a href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html' target='_blank' rel="nofollow">AWS-generated keys</a> gradually becoming incompatible with OpenSSH servers. </p> <p> This article describes the following: </p> <ol> <li>Tracking down the problem</li> <li>Create a new <code>ssh</code> certificate keypair.</li> <li>Even though the system cannot accept logins, the new <code>ssh</code> public key must be copied to the <code>ubuntu</code> user&rsquo;s <code>~/.ssh</code> directory on the problem server. This is done by defining a user data script on the server instance prior to booting it.</li> <li>Log into the problem server using the new certificates.</li> <li>Complete the upgrade to XUbuntu 20.10.</li> <li>Remove the user data script from the problem server instance.</li> </ol> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="discovery">Discovery</h2> <p> I was able to log into another of my machines (<code>gojira</code>), so first I wanted to know if the problem machines (<code>va</code> and <code>france</code>) had OpenSSH configured differently. I used the <a href='https://linux.die.net/man/1/comm' target='_blank' rel="nofollow">comm</a> Linux utility to perform the comparison. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idae7f61bf1f14'><button class='copyBtn' data-clipboard-target='#idae7f61bf1f14' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>for x in cipher mac key kex; do comm -3 <(ssh -Q $x france|sort) <(ssh -Q $x gojira|sort) done <span class='unselectable'>$ </span>for x in cipher mac key kex; do comm -3 <(ssh -Q $x france|sort) <(ssh -Q $x gojira|sort) done</pre> </div> <p> So the problem was not OpenSSH configuration per se. Next I wondered if the ssh connections to the problem machines were different somehow from the ssh connection to the working machine. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id90f619b7ebad'><button class='copyBtn' data-clipboard-target='#id90f619b7ebad' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>comm -3 <(ssh -Gva|sort) <(ssh -G gojira|sort) <span class='unselectable'>hostname gojira hostname va identityfile ~/.ssh/id_rsa identityfile ~/.ssh/sslTest user mslinn user ubuntu </span></pre> </div> <p>So the only differences were the hostnames and the keys offered.</p> <p> One of the problem machines, <code>france</code>, resided on <a href='https://scaleway.com' target='_blank' rel="nofollow"><code>scaleway</code></a>. I used the most recently available <a href='https://medium.com/@moul/scaleway-bootscript-simple-kernel-management-for-your-c1-server-de0c301de721' target='_blank' rel="nofollow">bootscript</a> to launch the server and examined <code>/var/log/auth.log</code>. I found this: <code>sshd[27025]: Unable to negotiate with 205.185.123.173 port 40555: no matching key exchange method found. Their offer: diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1 [preauth]</code> </p> <p> This error message is produced by OpenSSH 7.0+. <a href='https://www.openssh.com/txt/release-7.0' target='_blank' rel="nofollow">The release notes say</a> &ldquo;Support for the 1024-bit diffie-hellman-group1-sha1 key exchange is disabled by default at run-time. It may be re-enabled using the instructions at <code><a href='https://http://www.openssh.com/legacy.html' target='_blank' rel="nofollow"><code>http://www.openssh.com/legacy.html</code></a></code>&rdquo; </p> <p> So it seems that the version of OpenSSH installed with Ubuntu 20.10 rejects my old keys. The release notes for the new version of OpenSSH also indicate that OpenSSH 7.1 will be even stricter: </p> <ul> <li> &ldquo;This focus of this release is primarily to deprecate weak, legacy and/or unsafe cryptography&rdquo; </li> <li> &ldquo;Refusing all RSA keys smaller than 1024 bits (the current minimum is 768 bits)&rdquo; </li> <li> &ldquo;Several ciphers will be disabled by default: <code>blowfish-cbc</code>, <code>cast128-cbc</code>, all <code>arcfour</code> variants and the <code>rijndael-cbc</code> aliases for AES.&rdquo; </li> <li> &ldquo;MD5-based <code>HMAC</code> algorithms will be disabled by default.&rdquo; </li> </ul> <p> Clearly, I need to generate better <code>ssh</code> keys. </p> <p> The version of OpenSSH installed by Ubuntu 20.10 is 8.3: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf002ef2d4753'><button class='copyBtn' data-clipboard-target='#idf002ef2d4753' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>$ sshd -V <span class='unselectable'>unknown option -- V OpenSSH_<span class="bg_yellow">8.3p1</span> Ubuntu-1, OpenSSL 1.1.1f 31 Mar 2020 usage: sshd [-46DdeiqTt] [-C connection_spec] [-c host_cert_file] [-E log_file] [-f config_file] [-g login_grace_time] [-h host_key_file] [-o option] [-p port] [-u len]</span> $ ssh -V <span class='unselectable'>OpenSSH_<span class="bg_yellow">8.3p1</span> Ubuntu-1, OpenSSL 1.1.1f 31 Mar 2020 </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="setup">Setup</h2> <h3 class="numbered" id="setupAwsCli">AWS CLI</h3> <p> I prefer to use the AWS CLI instead of the <a href='https://console.aws.amazon.com' target='_blank' rel="nofollow">web console</a>. Installation instructions are <a href='https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html' target='_blank' rel="nofollow">here</a>. This article uses the AWS CLI exclusively in favor of the AWS web console. </p> <p> The code in the remainder of this article references an AWS EC2 instance, whose id I stored in <code>AWS_PROBLEM_INSTANCE_ID</code>. The <a href='/blog/2020/10/25/rescuing-catastrophic-upgrades-to-ubuntu-20_10.html#info'>previous article</a> shows how I did that. </p> <h3 class="numbered" id="setupJq"><span class="code">jq</span></h3> <p> I also use <a href='https://stedolan.github.io/jq/' target='_blank' rel="nofollow">jq</a> for parsing JSON in the bash shell. Install it on Debian-style Linux distros such as Ubuntu like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida77da87faa28'><button class='copyBtn' data-clipboard-target='#ida77da87faa28' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install jq</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="nameKeys">Name the New Keypair</h2> <p> I wanted to make new <code>ecdsa</code> keys because <a href='https://goteleport.com/blog/comparing-ssh-keys/' target='_blank' rel="nofollow">this algorithm is the currently accepted best practice</a> for commercial security concerns. <code>ecdsa</code> stands for <a href='https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm' target='_blank' rel="nofollow">Elliptic Curve Digital Signature Algorithm</a>. </p> <p> Unfortunately, AWS EC2 only accepts <code>RSA</code> keys. The name of the new key pair will be of the form <code>~/.ssh/rsa-YYYY-MM-DD</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id264db75d1d19'><button class='copyBtn' data-clipboard-target='#id264db75d1d19' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_KEY_PAIR_FILE="$HOME/.ssh/rsa-$( date '+%Y-%m-%d-%M-%S' )" <span class='unselectable'>$ </span>echo $AWS_KEY_PAIR_FILE <span class='unselectable'>/home/mslinn/.ssh/rsa-2020-11-04-08-24-22 </span></pre> </div> <p> The new public key will be called <code>~/.ssh/rsa-2020-11-04-08-24-22.pub</code> and the new private key will be called <code>~/.ssh/rsa-2020-11-04-08-24-22</code>. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="createKeys">Create a New Keypair</h2> <ol> <li type='a'>This is how I would have created a new <code>ECDSA</code> keypair, if AWS supported that type of encryption. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb338795de693'><button class='copyBtn' data-clipboard-target='#idb338795de693' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ssh-keygen -b 512 -C "mslinn@mslinn.com" -f "$AWS_KEY_PAIR_FILE" -P "" -t ecdsa <span class='unselectable'>Generating public/private ecdsa key pair. Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-08-24-22 Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-08-24-22.pub The key fingerprint is: SHA256:HEKjAA1GZxHbpwqjm85DXQpQEeIWrcjZ6fl84RHQaHE mslinn@mslinn.com The key&rsquo;s randomart image is: +---[RSA 512]----+ |=O*Bo.*E | |+.=oo*.o | |+o+.+.o.. | |o= o .o+ . | | o+ +. S | |..o=. o | |o .o . o | |.+ o o | |+o. . | +----[SHA256]-----+ $ </span>chmod 400 $AWS_KEY_PAIR_FILE</pre> </div> </li> <li type='a'> Instead, I created a new <code>RSA</code> keypair like this: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idec5f4ca2c76e'><button class='copyBtn' data-clipboard-target='#idec5f4ca2c76e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ssh-keygen -b 2048 -C "mslinn@mslinn.com" -f "$AWS_KEY_PAIR_FILE" -m PEM -P "" -t rsa <span class='unselectable'>Generating public/private rsa key pair. Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-08-24-22 Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-08-24-22.pub The key fingerprint is: SHA256:bQScX0UMn0xGDorxSvElMZzwMyyk7hgs2FNbshBNenA mslinn@mslinn.com The key&rsquo;s randomart image is: +---[RSA 2048]----+ | ooE .*++o+** | | =. ooXo=.B.. | | o + o +.X. = | | o = * . =.o | |. + = . S o | | o + . | | . . | | | | | +----[SHA256]-----+ $ </span>chmod 400 $AWS_KEY_PAIR_FILE</pre> </div> </li> <li type='a'> I would have liked to copy the keypair to the problem system using <code>ssh-copy-id</code>, but that only works when login is possible. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id66237ee949ff'><button class='copyBtn' data-clipboard-target='#id66237ee949ff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ssh-copy-id -i $AWS_KEY_PAIR_FILE ubuntu@$AWS_PROBLEM_IP</pre> </div> </li> <li type='a'> Instead, I decided to paste the public key into an AWS user data script and execute that script on the problem server the next time it booted. The purpose of the script is to copy the new public key that was just made to <code>~/.ssh/</code> on the problem server. This is the user data script I wrote to install the new public key, called <code>rescue_ubuntu2010.sh</code>: <br /> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,rescue_ubuntu2010.sh' download='rescue_ubuntu2010.sh' title='Click on the file name to download the file'>rescue_ubuntu2010.sh</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id4a4ac6881ee1"><button class='copyBtn' data-clipboard-target='#id4a4ac6881ee1'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash KEY_FILE_NAME=/home/ubuntu/.ssh/rsa-2020-11-03.pub cat > "$KEY_FILE_NAME" &lt;&lt;EOF ssh-rsa ABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFAABCDEFA== mslinn@mslinn.com EOF chown ubuntu: /home/ubuntu/.ssh/* chmod 400 "$KEY_FILE_NAME" cat "$KEY_FILE_NAME" >> /home/ubuntu/.ssh/authorized_keys </pre> The script runs on the problem server as <code>root</code> next time the system boots, and it reboots the server on the last line. </li> <li type='a'> The script need to be converted into base 64, in a file called <code>rescue_ubuntu2010.b64</code>. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id06ef78b24a9e'><button class='copyBtn' data-clipboard-target='#id06ef78b24a9e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>base64 rescue_ubuntu2010.sh > rescue_ubuntu2010.b64</pre> </div> </li> <li type='a'> The problem EC2 instance can be shut down like this: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id06aa0b6a7f92'><button class='copyBtn' data-clipboard-target='#id06aa0b6a7f92' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 stop-instances --instance-id $AWS_PROBLEM_INSTANCE_ID <span class='unselectable'>$ </span>aws ec2 wait instance-stopped --instance-ids $AWS_PROBLEM_INSTANCE_ID</pre> </div> </li> <li type='a'> With the problem EC2 instance stopped, its user data was set to the base64-encoded version of the rescue script. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7c1d51181815'><button class='copyBtn' data-clipboard-target='#id7c1d51181815' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 modify-instance-attribute \ --instance-id $AWS_PROBLEM_INSTANCE_ID \ --attribute userData \ --value file://rescue_ubuntu2010.b64</pre> </div> </li> <li type='a'> Now the problem EC2 instance can be restarted. The script will add the new key to <code>/home/ubuntu/.ssh/authorized_keys</code> and login should be possible. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id89259116b92c'><button class='copyBtn' data-clipboard-target='#id89259116b92c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 start-instances --instance-id $AWS_PROBLEM_INSTANCE_ID <span class='unselectable'>{ "StartingInstances": [ { "CurrentState": { "Code": 0, "Name": "pending" }, "InstanceId": "i-d3b03954", "PreviousState": { "Code": 80, "Name": "stopped" } } ] }</span> <span class='unselectable'>$ </span>aws ec2 wait instance-running --instance-ids $AWS_PROBLEM_INSTANCE_ID</pre> </div> </li> </ol> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="resetData">Reset User Data for Next Time</h2> <p> Next time the problem server is stopped, clear the user data so it is not provided the next time the server restarts. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcf9ad095ac27'><button class='copyBtn' data-clipboard-target='#idcf9ad095ac27' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 modify-instance-attribute \ --instance-id $AWS_PROBLEM_INSTANCE_ID \ --user-data Value=</pre> </div> <!-- endregion --> Rescuing a Catastrophic Upgrade to Ubuntu 20.10 2020-10-25T00:00:00-04:00 https://mslinn.github.io/blog/2020/10/25/rescuing-catastrophic-upgrades-to-ubuntu-20_10 <!-- #region --> <p> The upgrade from Ubuntu 20.04 to 20.10 has been especially problematic for each of the half-dozen XUbuntu systems that I manage. One important server that I run on <a href='https://www.scaleway.com' target='_blank' rel="nofollow">Scaleway</a> became unresponsive and would not boot shortly after starting the installation, and another important server on <a href='https://aws.amazon.com' target='_blank' rel="nofollow">AWS</a> ran fine, but did not allow logins. </p> <p> This article details what I did to recover the AWS server using a standard <a href='https://en.wikipedia.org/wiki/Unix-like' target='_blank' rel="nofollow">*nix</a> procedure that any competent system administrator would be comfortable with: <a href='https://wiki.archlinux.org/title/chroot' target='_blank' rel="nofollow"><code>chroot</code></a>. </p> <p> Before Linux had <a href='https://wiki.archlinux.org/title/Cgroups' target='_blank' rel="nofollow"><code>cgroups</code></a>, we used <code>chroot</code> and its close cousin, <code>jail</code>. I used <code>chroot</code> for the technical basis of <a href="/blog/index.html#Zamples">Zamples</a> back in 2001. </p> <p> Because the <code>chroot</code> environment will be set up in a way that it shares the rescue system&rsquo;s <code>/var/run</code> directory, the rescue system should have all upgrades in place and should be rebooted if <code>/var/run/reboot-required</code> exists. </p> <p> AWS also provides a tool called <a href='https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-ec2rescue.html' target='_blank' rel="nofollow"><code>EC2Rescue</code></a>, which does a complicated series of actions to accomplish something similar. <a href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html' target='_blank' rel="nofollow">Here is additional documentation.</a> I find the AWS documentation is frequently obtuse, and the approach taken by most AWS products and tools is extremely general. Consequently I often find myself wasting a lot of time trying to get things to work. I don&rsquo;t subscribe to AWS support; if I had subscribed to expensive enterprise-level support, complete with an AWS expert to hold my hand while I attempted to resurrect the server, I might have tried using <code>EC2Rescue</code>. On the other hand, when pressed with an emergency, I prefer to lean on tried-and-true methods like <code>chroot</code>. </p> <p> This article concludes with two Bash scripts to automate the details. I wrote the second script in late January 2022, approximately 2 years after this article was first published. The second script is less user-friendly, but more fine-grained. That is a reasonable tradeoff, and both approaches have merit. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="setup">Setup</h2> <h3 class="numbered" id="setupAwsCli">AWS CLI</h3> <p> I prefer to use the AWS CLI instead of the <a href='https://console.aws.amazon.com' target='_blank' rel="nofollow">web console</a>. Installation instructions are <a href='https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html' target='_blank' rel="nofollow">here</a>. This article uses the AWS CLI exclusively in favor of the AWS web console. </p> <h3 class="numbered" id="setupJq"><span class="code">jq</span></h3> <p> I also use <a href='https://stedolan.github.io/jq/' target='_blank' rel="nofollow">jq</a> for parsing JSON in the bash shell. Install it on Debian-style Linux distros such as Ubuntu like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6fc17fb3bb79'><button class='copyBtn' data-clipboard-target='#id6fc17fb3bb79' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install jq</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="info">Discover information about the Problem EC2 instance</h2> <h3 class="numbered" id="ec2Info">Getting the AWS EC2 Instance Information</h3> <p> Because my problem EC2 instance has a tag called <code>Name</code> with <code>Value</code> <code>production</code>, I was able to easily obtain a JSON representation of all the information about it. I stored the JSON in an environment variable called <code>AWS_EC2_PRODUCTION</code>. </p> <p> The results are shown in unselectable text. This is so you can easily use this sample code yourself. You can copy the code to run into your clipboard. Just click on the little copy icon at the top right hand corner of the scrolling code display area. Because the prompt and the results and are unselectable, your clipboard will only pick up the code you need to paste in order to run the code example yourself. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0f0558b917b8'><button class='copyBtn' data-clipboard-target='#id0f0558b917b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_EC2_PRODUCTION="$( aws ec2 describe-instances | \ jq '.Reservations[].Instances[] | select((.Tags[]?.Key=="Name") and (.Tags[]?.Value=="production"))' )" <span class='unselectable'>$ </span>echo "$AWS_EC2_PRODUCTION" <span class='unselectable'>{ "AmiLaunchIndex": 0, "ImageId": "ami-e29b9388", "InstanceId": "i-825eb905", "InstanceType": "t2.small", "KeyName": "sslTest", "LaunchTime": "2017-10-12T16:24:14.000Z", "Monitoring": { "State": "disabled" }, "Placement": { "AvailabilityZone": "us-east-1c", "GroupName": "", "Tenancy": "default" }, "PrivateDnsName": "ip-10-0-0-201.ec2.internal", "PrivateIpAddress": "10.0.0.201", "ProductCodes": [], "PublicDnsName": "", "PublicIpAddress": "52.207.225.143", "State": { "Code": 16, "Name": "running" }, "StateTransitionReason": "", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "Architecture": "x86_64", "BlockDeviceMappings": [ { "DeviceName": "/dev/sda1", "Ebs": { "AttachTime": "2016-04-05T19:07:17.000Z", "DeleteOnTermination": true, "Status": "attached", "VolumeId": "vol-1c8903b4" } } ], "ClientToken": "GykZz1459883236367", "EbsOptimized": false, "Hypervisor": "xen", "NetworkInterfaces": [ { "Association": { "IpOwnerId": "amazon", "PublicDnsName": "", "PublicIp": "52.207.225.143" }, "Attachment": { "AttachTime": "2016-04-05T19:07:16.000Z", "AttachmentId": "eni-attach-a58bd15f", "DeleteOnTermination": true, "DeviceIndex": 0, "Status": "attached" }, "Description": "Primary network interface", "Groups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "Ipv6Addresses": [], "MacAddress": "0a:a4:be:1b:8e:eb", "NetworkInterfaceId": "eni-fa4f65bb", "OwnerId": "031372724784", "PrivateIpAddress": "10.0.0.201", "PrivateIpAddresses": [ { "Association": { "IpOwnerId": "amazon", "PublicDnsName": "", "PublicIp": "52.207.225.143" }, "Primary": true, "PrivateIpAddress": "10.0.0.201" } ], "SourceDestCheck": true, "Status": "in-use", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "InterfaceType": "interface" } ], "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SecurityGroups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "SourceDestCheck": true, "Tags": [ { "Key": "Name", "Value": "production" } ], "VirtualizationType": "hvm", "CpuOptions": { "CoreCount": 1, "ThreadsPerCore": 1 }, "CapacityReservationSpecification": { "CapacityReservationPreference": "open" }, "HibernationOptions": { "Configured": false }, "MetadataOptions": { "State": "applied", "HttpTokens": "optional", "HttpPutResponseHopLimit": 1, "HttpEndpoint": "enabled" } }</span></pre> </div> <h3 class="numbered" id="problemIdGet">Getting the AWS EC2 Problem Instance Id</h3> <p> The instance ID for the problem EC2 instance can be extracted from the JSON returned by the preceding results easily: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd3ed774302f4'><button class='copyBtn' data-clipboard-target='#idd3ed774302f4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_PROBLEM_INSTANCE_ID="$( jq -r .InstanceId &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" <span class='unselectable'>$ </span>echo "$AWS_PROBLEM_INSTANCE_ID" <span class='unselectable'>i-825eb905 </span></pre> </div> <h3 class="numbered" id="problemIdIP">Getting the AWS EC2 Problem Instance IP Address</h3> <p> The IP address for the problem EC2 instance can be extracted from the JSON returned by the preceding results easily: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf225b9e21cb2'><button class='copyBtn' data-clipboard-target='#idf225b9e21cb2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_PROBLEM_IP="$( jq -r .PublicIpAddress &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" <span class='unselectable'>$ </span>echo "$AWS_PROBLEM_IP" <span class='unselectable'>52.207.225.143</span></pre> </div> <h3 class="numbered" id="problemAZ">Getting the AWS EC2 Problem Availability Zone</h3> <p> The AWS availability zone for the problem EC2 instance can be extracted from the JSON returned by the preceding results easily: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6c53cd34d3e2'><button class='copyBtn' data-clipboard-target='#id6c53cd34d3e2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_AVAILABILITY_ZONE="$( jq -r .Placement.AvailabilityZone &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" <span class='unselectable'>$ </span>echo "$AWS_AVAILABILITY_ZONE" <span class='unselectable'>us-east-1c </span></pre> </div> <h3 class="numbered" id="volumeId">Getting the AWS EC2 Problem Volume ID</h3> <p> The following command line extracts the volume id of the problem server&rsquo;s system drive into an environment variable called <code>$AWS_PROBLEM_VOLUME_ID</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc3486c633f72'><button class='copyBtn' data-clipboard-target='#idc3486c633f72' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_PROBLEM_VOLUME_ID="$( jq -r '.BlockDeviceMappings[].Ebs.VolumeId' &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" <span class='unselectable'>$ </span>echo "$AWS_PROBLEM_VOLUME_ID" <span class='unselectable'>vol-1c8903b4 </span></pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="snapshotProblem">Make a Snapshot of the Problem Server</h2> <p> One approach, which would be living dangerously, would be to mount the system volume of the problem server on another server, set up <code>chroot</code>, attempt to repair the drive image, remount the repaired drive on the problem server, and reboot the server. I am never that optimistic. Things invariably go wrong. Instead, we will take a snapshot of the problem drive, turn the snapshot into a volume, repair the volume, swap in the repaired volume on the problem system, and reboot that system. </p> <p> It is better to shut down the EC2 instance before making a snapshot, however a snapshot can be taken whenever the server is idling. We will need to shut down the server anyway, so that could be done now, or at the last minute. </p> <p> I made a snapshot with a tag called <code>Name</code> with the value like <code>production 2020-10-25</code> and saved the snapshot id in an environment variable called <code>AWS_PROBLEM_SNAPSHOT_ID</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3381fe49da26'><button class='copyBtn' data-clipboard-target='#id3381fe49da26' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_PROBLEM_SNAPSHOT_ID="$( aws ec2 create-snapshot --volume-id "$AWS_PROBLEM_VOLUME_ID" \ --description "production `date '+%Y-%m-%d'`" \ --tag-specifications "ResourceType=snapshot,Tags=[{Key=Created, Value=`date '+%Y-%m-%d'`},{Key=Name, Value=\"Broken do-release-upgrade 20.{04,10\"}]" | \ jq -r .SnapshotId )" <span class='unselectable'>$ </span>echo "$AWS_PROBLEM_SNAPSHOT_ID" <span class='unselectable'>snap-0a856be1f58b8a856 </span> <span class='unselectable'>$ </span>aws ec2 wait snapshot-completed --snapshot-ids "$AWS_PROBLEM_SNAPSHOT_ID"</pre> </div> <p> Snapshots only take a few minutes to complete. The <code>aws ec2 wait</code> command blocks until the specified operation finishes. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="rescueVolue">Create Rescue Volume From Snapshot</h2> <p> Once the snapshot process has completed, create a new volume from the snapshot. The default volume type is <a href='https://aws.amazon.com/ebs/features/#Amazon_EBS_volume_types' target='_blank' rel="nofollow"><code>gp2</code></a>. We&rsquo;ll refer to this volume as <code>$AWS_RESCUE_VOLUME_ID</code>. It is important to create the volume in the same availability zone as the problem EC2 instance so that it can easily be attached. This command applies a tag called <code>Name</code>, with the value <code>rescue</code>, for easy identification. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb0217ff87086'><button class='copyBtn' data-clipboard-target='#idb0217ff87086' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_RESCUE_VOLUME_ID="$( aws ec2 create-volume \ --availability-zone $AWS_AVAILABILITY_ZONE \ --snapshot-id $AWS_PROBLEM_SNAPSHOT_ID \ --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=rescue}]' | \ jq -r .VolumeId )" <span class='unselectable'>$ </span>echo "$AWS_RESCUE_VOLUME_ID" <span class='unselectable'>vol-0e20fd22d2dc5a933 </span> <span class='unselectable'>$ </span>aws ec2 wait volume-available --volume-id "$AWS_RESCUE_VOLUME_ID"</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="snapshotRescue">Use an EC2 Spot Instance For the Rescue Server</h2> <p> Now that the rescue volume is <code>available</code>, we need to mount it on a server, which I&rsquo;ll call the rescue server. We&rsquo;ll refer to the server where the rescue volume is prepared via its instance id, saved as <code>AWS_EC2_RESCUE_ID</code>. You can either create a new EC2 instance for this purpose, or use an existing EC2 instance. </p> <p> The rescue server does not need to be anything special; a tiny virtual machine of any description will do fine. However, some rescue operations will be much easier if the type of operating system is the same as that on the problem drive. <a href='https://www.mslinn.com/blog/2020/10/24/ec2-spot-instances-cli.html' target='_blank' rel="nofollow">Yesterday</a> I blogged about how to find a suitable AMI, and determine its <code>image-id</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7a80a2b4dc3f'><button class='copyBtn' data-clipboard-target='#id7a80a2b4dc3f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_AMI="$( aws ec2 describe-images \ --owners 099720109477 \ --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-━━━━━???-━━━━━???-amd64-server-━━━━━???" \ "Name=state,Values=available" \ --query "reverse(sort_by(Images, &CreationDate))[:1]" | \ jq -r '.[0]' )" <span class='unselectable'>$ </span>echo "$AWS_AMI" <span class='unselectable'>{ "Architecture": "x86_64", "CreationDate": "2020-10-30T14:07:42.000Z", "ImageId": "ami-0c71ec98278087e60", "ImageLocation": "099720109477/ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030", "ImageType": "machine", "Public": true, "OwnerId": "099720109477", "PlatformDetails": "Linux/UNIX", "UsageOperation": "RunInstances", "State": "available", "BlockDeviceMappings": [ { "DeviceName": "/dev/sda1", "Ebs": { "DeleteOnTermination": true, "SnapshotId": "snap-00bf581086dd686e5", "VolumeSize": 8, "VolumeType": "gp2", "Encrypted": false } }, { "DeviceName": "/dev/sdb", "VirtualName": "ephemeral0" }, { "DeviceName": "/dev/sdc", "VirtualName": "ephemeral1" } ], "Description": "Canonical, Ubuntu, 20.10, amd64 groovy image build on 2020-10-30", "EnaSupport": true, "Hypervisor": "xen", "Name": "ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030", "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SriovNetSupport": "simple", "VirtualizationType": "hvm" } </span></pre> </div> <p> Now let's extract the ID of the AMI image and save it as <code>AWS_AMI_ID</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1fdf2a61dafe'><button class='copyBtn' data-clipboard-target='#id1fdf2a61dafe' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_AMI_ID="$( jq -r '.[0].ImageId' &lt;&lt;&lt; "$AWS_AMI" )" <span class='unselectable'>$ </span>echo "$AWS_AMI_ID" <span class='unselectable'>ami-0c71ec98278087e60 </span></pre> </div> <p> Volumes can be attached to running and stopped server instances. The load on the rescue server will likely be light and short-lived. An EC2 spot instance is ideal, and only costs two cents per hour! The spot instance will likely only be needed for 15 minutes. I specified my VPC id as <code>SubnetId</code>, the security group <code>sg-4cbc6f35</code> and the <code>AvailabilityZone</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id465a76820bc0'><button class='copyBtn' data-clipboard-target='#id465a76820bc0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_EC2_RESCUE="$( aws ec2 run-instances \ --image-id "$AWS_AMI_ID" \ --instance-market-options '{ "MarketType": "spot" }' \ --instance-type t2.medium \ --key-name rsa-2020-11-03.pub \ --network-interfaces '[ { "DeviceIndex": 0, "Groups": ["sg-4cbc6f35"], "SubnetId": "subnet-49de033f", "DeleteOnTermination": true, "AssociatePublicIpAddress": true } ]' \ --placement '{ "AvailabilityZone": "us-east-1c" }' )" <span class='unselectable'>$ </span>echo "$AWS_EC2_RESCUE" <span class='unselectable'>{ "Groups": [], "Instances": [ { "AmiLaunchIndex": 0, "ImageId": "ami-0dba2cb6798deb6d8", "InstanceId": "i-012a54aefcd333de9", "InstanceType": "t2.small", "KeyName": "rsa-2020-11-03.pub", "LaunchTime": "2020-11-03T23:19:50.000Z", "Monitoring": { "State": "disabled" }, "Placement": { "AvailabilityZone": "us-east-1c", "GroupName": "", "Tenancy": "default" }, "PrivateDnsName": "ip-10-0-0-210.ec2.internal", "PrivateIpAddress": "10.0.0.210", "ProductCodes": [], "PublicDnsName": "", "State": { "Code": 0, "Name": "pending" }, "StateTransitionReason": "", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "Architecture": "x86_64", "BlockDeviceMappings": [], "ClientToken": "026583fb-c94e-4bca-bdd2-8dcdcaa3aae9", "EbsOptimized": false, "EnaSupport": true, "Hypervisor": "xen", "InstanceLifecycle": "spot", "NetworkInterfaces": [ { "Attachment": { "AttachTime": "2020-11-03T23:19:50.000Z", "AttachmentId": "eni-attach-04feb4d36cf5c6792", "DeleteOnTermination": true, "DeviceIndex": 0, "Status": "attaching" }, "Description": "", "Groups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "Ipv6Addresses": [], "MacAddress": "0a:6d:ba:c5:65:4b", "NetworkInterfaceId": "eni-09ef90920cfb29dd9", "OwnerId": "031372724784", "PrivateIpAddress": "10.0.0.210", "PrivateIpAddresses": [ { "Primary": true, "PrivateIpAddress": "10.0.0.210" } ], "SourceDestCheck": true, "Status": "in-use", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "InterfaceType": "interface" } ], "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SecurityGroups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "SourceDestCheck": true, "SpotInstanceRequestId": "sir-rrs9gm3j", "StateReason": { "Code": "pending", "Message": "pending" }, "VirtualizationType": "hvm", "CpuOptions": { "CoreCount": 1, "ThreadsPerCore": 1 }, "CapacityReservationSpecification": { "CapacityReservationPreference": "open" }, "MetadataOptions": { "State": "pending", "HttpTokens": "optional", "HttpPutResponseHopLimit": 1, "HttpEndpoint": "enabled" } } ], "OwnerId": "031372724784", "ReservationId": "r-0d45e1919e7bad5c9" } </span></pre> </div> <p> We can use <code>jq</code> to extract the EC2 <code>InstanceId</code> of the spot instance: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide4440ae3b6f7'><button class='copyBtn' data-clipboard-target='#ide4440ae3b6f7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_SPOT_INSTANCE_ID="$( jq -r '.Instances[].InstanceId' &lt;&lt;&lt; "$AWS_EC2_RESCUE" )" <span class='unselectable'>$ </span>echo "$AWS_SPOT_INSTANCE_ID" <span class='unselectable'>ami-0dba2cb6798deb6d8 </span></pre> </div> <p> We need to retrieve the IP address of the newly created EC2 spot instance. This instance will disappear (terminate) once it shuts down, so do not reboot it. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id809fc6c16394'><button class='copyBtn' data-clipboard-target='#id809fc6c16394' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 describe-instances --instance-ids "$AWS_SPOT_INSTANCE_ID" <span class='unselectable'>{ "Reservations": [ { "Groups": [], "Instances": [ { "AmiLaunchIndex": 0, "ImageId": "ami-0dba2cb6798deb6d8", "InstanceId": "i-012a54aefcd333de9", "InstanceType": "t2.small", "KeyName": "rsa-2020-11-03.pub", "LaunchTime": "2020-11-03T23:19:50.000Z", "Monitoring": { "State": "disabled" }, "Placement": { "AvailabilityZone": "us-east-1c", "GroupName": "", "Tenancy": "default" }, "PrivateDnsName": "ip-10-0-0-210.ec2.internal", "PrivateIpAddress": "10.0.0.210", "ProductCodes": [], "PublicDnsName": "", "PublicIpAddress": "54.242.88.254", "State": { "Code": 16, "Name": "running" }, "StateTransitionReason": "", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "Architecture": "x86_64", "BlockDeviceMappings": [ { "DeviceName": "/dev/sda1", "Ebs": { "AttachTime": "2020-11-03T23:19:51.000Z", "DeleteOnTermination": true, "Status": "attached", "VolumeId": "vol-0c44c8c009d1fafda" } } ], "ClientToken": "026583fb-c94e-4bca-bdd2-8dcdcaa3aae9", "EbsOptimized": false, "EnaSupport": true, "Hypervisor": "xen", "InstanceLifecycle": "spot", "NetworkInterfaces": [ { "Association": { "IpOwnerId": "amazon", "PublicDnsName": "", "PublicIp": "54.242.88.254" }, "Attachment": { "AttachTime": "2020-11-03T23:19:50.000Z", "AttachmentId": "eni-attach-04feb4d36cf5c6792", "DeleteOnTermination": true, "DeviceIndex": 0, "Status": "attached" }, "Description": "", "Groups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "Ipv6Addresses": [], "MacAddress": "0a:6d:ba:c5:65:4b", "NetworkInterfaceId": "eni-09ef90920cfb29dd9", "OwnerId": "031372724784", "PrivateIpAddress": "10.0.0.210", "PrivateIpAddresses": [ { "Association": { "IpOwnerId": "amazon", "PublicDnsName": "", "PublicIp": "54.242.88.254" }, "Primary": true, "PrivateIpAddress": "10.0.0.210" } ], "SourceDestCheck": true, "Status": "in-use", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "InterfaceType": "interface" } ], "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SecurityGroups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "SourceDestCheck": true, "SpotInstanceRequestId": "sir-rrs9gm3j", "VirtualizationType": "hvm", "CpuOptions": { "CoreCount": 1, "ThreadsPerCore": 1 }, "CapacityReservationSpecification": { "CapacityReservationPreference": "open" }, "HibernationOptions": { "Configured": false }, "MetadataOptions": { "State": "applied", "HttpTokens": "optional", "HttpPutResponseHopLimit": 1, "HttpEndpoint": "enabled" } } ], "OwnerId": "031372724784", "ReservationId": "r-0d45e1919e7bad5c9" } ] }</span></pre> </div> <!-- <p> It is possible that the work necessary to rescue the problem disk image might make changes to the rescue system. The rescue system should therefore have a snapshot taken before going any further. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3e2f8e9d806e'><button class='copyBtn' data-clipboard-target='#id3e2f8e9d806e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_RESCUE_SNAPSHOT_ID="$( aws ec2 create-snapshot --volume-id $AWS_RESCUE_VOLUME_ID %} \ --description "production `date '+%Y-%m-%d'`" \ --tag-specifications "ResourceType=snapshot,Tags=[{Key=Created, Value=`date '+%Y-%m-%d'`},{Key=Name, Value=\"Broken do-release-upgrade 20.{04,10\"}]" | \ jq -r .SnapshotId )" $ echo "$AWS_RESCUE_SNAPSHOT_ID" <span class='unselectable'>snap-0a856be1f58b8359a </span> <span class='unselectable'>$ </span>aws ec2 wait snapshot-completed --snapshot-ids $AWS_RESCUE_SNAPSHOT_ID</pre> </div> --> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="mount">Mount the Rescue Volume On the Rescue Server</h2> <p> We need to select a device name to be assigned to the rescue disk once it is attached to an EC2 instance. The available names depend on what names are already in use on the rescue server. After logging into the rescue server, I ran the <code>lsblk</code> Linux command to see the available disk devices and their mount points. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf1c9156d64a7'><button class='copyBtn' data-clipboard-target='#idf1c9156d64a7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lsblk <span class="unselectable">NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT loop1 7:1 0 53.1M 1 loop /snap/lxd/10984 loop2 7:2 0 88.4M 1 loop /snap/core/7169 loop3 7:3 0 97.8M 1 loop /snap/core/10185 loop4 7:4 0 53.1M 1 loop /snap/lxd/11348 xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part /</span></pre> </div> <p> The lsblk output does not show full device paths, instead, the <code>/dev/</code> prefix is omitted. With that in mind we can see that the only <code>disk</code> device on the rescue server is <code>/dev/xvda</code>, and its only partition called <code>/dev/xvda1</code> is mounted on the root directory. Because Linux drives are normally named sequentially, we should name the rescue disk <code>/dev/xvdb</code>. Let&rsquo;s define an environment variable called <code>AWS_RESCUE_DRIVE</code> to memorialize that decision. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd0ed8b21e888'><button class='copyBtn' data-clipboard-target='#idd0ed8b21e888' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_RESCUE_DRIVE=/dev/xvdb</pre> </div> <p> The <code>aws ec2 attach-volume</code> command will attach the rescue volume to the rescue server. It automatically selects an appropriate device name for the rescue volume, which in the following example is <code>/dev/xvdb</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id15391390e8b8'><button class='copyBtn' data-clipboard-target='#id15391390e8b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_ATTACH_VOLUME="$( aws ec2 attach-volume \ --device $AWS_RESCUE_DRIVE \ --instance-id $AWS_EC2_RESCUE_ID \ --volume-id $AWS_RESCUE_VOLUME_ID )" <span class='unselectable'>$ </span>echo "$AWS_ATTACH_VOLUME" <span class='unselectable'>{ "AttachTime": "2020-10-26T14:34:55.222Z", "InstanceId": "i-d3b03954", "VolumeId": "vol-0e20fd22d2dc5a933", "State": "attaching", "Device": "/dev/xvdb" }</span> <span class='unselectable'>$ </span>aws ec2 wait volume-in-use --volume-id "$AWS_RESCUE_VOLUME_ID"</pre> </div> <p> The details of the mounted rescue drive are provided by <code>fdisk -l</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0be02bfe61e7'><button class='copyBtn' data-clipboard-target='#id0be02bfe61e7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo fdisk -l | sed -n -e '/xvdb/,$p' <span class='unselectable'>Disk /dev/xvdb: 12 GiB, 12884901888 bytes, 25165824 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x00000000 Device Boot Start End Sectors Size Id Type /dev/xvdb1 * 16065 25165790 25149726 12G 83 Linux</span></pre> </div> <p> Now it is time to mount the rescue drive on the rescue server. Ubuntu has a directory called <code>/mnt</code> whose purpose is to act as a mount point: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id51d5a4b866bc'><button class='copyBtn' data-clipboard-target='#id51d5a4b866bc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo mount /dev/xvdb1 /mnt</pre> </div> <p> Let&rsquo;s confirm that the drive is mounted: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf0cdc4618d77'><button class='copyBtn' data-clipboard-target='#idf0cdc4618d77' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>df -h | grep '^/dev/' | grep -v '^/dev/loop' <span class='unselectable'>/dev/xvda1 7.8G 6.3G 1.1G 86% / /dev/xvdb1 12G 9.0G 2.2G 82% /mnt</span></pre> </div> <p> The last line shows that this drive is mounted on <code>/mnt</code> and it is 82% full. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="chroot">Set Up a <span class='code'>chroot</span> to Establish an Environment for Making Repairs</h2> <p> We need to mount some more file systems before we perform the <code>chroot</code>. The following mounts the rescue server&rsquo;s <code>/dev</code>, <code>/dev/shm</code>, <code>/sys</code>, and <code>/run</code> to the same paths within the rescue volume. Because programs like <code>do-release-upgrade</code> need a <code>tty</code>, I also mount <code>devtps</code> and <code>proc</code>. These mounts only last until the next server reboot. After all the mounts the <code>chroot</code> is issued. </p> <p class="warning rounded shadow"> <b>Warning</b> - mounting <code>/run</code> and then updating the system on the rescue disk from within a chroot may change the host system&rsquo;s <code>/run</code> contents; if the package managers (<code>apt</code> and <code>dpkg</code>) get out of sync with the actual state on the host system you won&rsquo;t be able to update the host system until you restore the host system&rsquo;s image from the snapshot that we made <a href='#snapshotRescue'>earlier</a>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id173c4052fc8e'><button class='copyBtn' data-clipboard-target='#id173c4052fc8e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo mount -o bind /dev /mnt/dev <span class='unselectable'>$ </span>sudo mount -o bind /dev/shm /mnt/dev/shm <span class='unselectable'>$ </span>sudo mount -o bind /sys /mnt/sys <span class='unselectable'>$ </span>sudo mount -o bind /run /mnt/run <span class='unselectable'>$ </span>sudo mount -t proc proc /mnt/proc <span class='unselectable'>$ </span>sudo mount -t devpts devpts /mnt/dev/pts <span class='unselectable'>$ </span>sudo chroot /mnt <span class='unselectable'>root@ip-10-0-0-189:/#</span></pre> </div> <p> Notice how the prompt changed after the <code>chroot</code>. That is your clue that it is active. </p> <!-- <p> I edited <code>/etc/hosts</code> in the <code>chroot</code> to add the name of the system to the entry for <code>localhost</code> (<code>127.0.0.1</code>): </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idffd1205dfbaa'><button class='copyBtn' data-clipboard-target='#idffd1205dfbaa' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>127.0.0.1 localhost ip-10-0-0-189</pre> </div> --> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="fix">Correct the Problem</h2> <p> This step depends on whatever is wrong. I won&rsquo;t bore you with the problem I had. </p> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="unmount">Unmount the New Volume</h2> <p> Exit the <code>chroot</code> and unmount the rescue volume from the rescue server. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id31da6f0704e7'><button class='copyBtn' data-clipboard-target='#id31da6f0704e7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'># </span>exit <span class='unselectable'>$ </span>sudo umount /mnt/dev <span class='unselectable'>$ </span>sudo umount /mnt/dev/shm <span class='unselectable'>$ </span>sudo umount /mnt/sys <span class='unselectable'>$ </span>sudo umount /mnt/run <span class='unselectable'>$ </span>sudo umount /mnt/proc <span class='unselectable'>$ </span>sudo umount /mnt/dev/pts <span class='unselectable'>$ </span>sudo umount /mnt</pre> </div> <p> Detach the rescue volume from the rescue server. This can be done from any machine that is configured with <code>aws cli</code> for use with your account credentials. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd331d3e0f01c'><button class='copyBtn' data-clipboard-target='#idd331d3e0f01c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 detach-volume --volume-id $AWS_RESCUE_VOLUME_ID <span class='unselectable'>$ </span>aws ec2 wait volume-available --volume-id $AWS_RESCUE_VOLUME_ID</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="unmount">Unmount the Problem Volume</h2> <p> The problem server must be shut down for this to work. Detach the problem volume from the problem server. This can be done from any machine that is configured with <code>aws cli</code> for use with your account credentials. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id57af77c32ccf'><button class='copyBtn' data-clipboard-target='#id57af77c32ccf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 stop-instances --instance-id $AWS_PROBLEM_INSTANCE_ID <span class='unselectable'>$ </span>aws ec2 wait instance-stopped --instance-ids $AWS_PROBLEM_INSTANCE_ID <span class='unselectable'>$ </span>aws ec2 detach-volume --volume-id $AWS_PROBLEM_VOLUME_ID <span class='unselectable'>$ </span>aws ec2 wait volume-available --volume-id $AWS_PROBLEM_VOLUME_ID</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="replace">Replace the Problem Volume</h2> <p> Now it is time to replace the problem volume containing the problem boot drive on the problem system with the newly created volume. BTW, AWS EC2 always refers to boot drives as <code>/dev/sda1</code>, even when the device has a different name, such as <code>/dev/xvdb1</code>. </p> <h3 id="replaceSystemVolume"><span>replaceSystemVolume</span> Bash function</h3> <p> This Bash function detaches the volume containing the current boot drive of an EC2 instance and replaces it with another volume. If the EC2 instance is running then it is first stopped. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,replaceSystemVolume' download='replaceSystemVolume' title='Click on the file name to download the file'>replaceSystemVolume</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id7584bd73cae1"><button class='copyBtn' data-clipboard-target='#id7584bd73cae1'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash function replaceSystemVolume &#123; # $1 - EC2 instance id # $2 - new volume to mount as system boot drive export EC2_INSTANCE="$( aws ec2 describe-instances --instance-ids "$1" | \ jq -r ".Reservations[].Instances[0]" )" export EC2_NAME="$( jq -r ".Tags[] | select(.Key==\"Name\") | .Value" &lt;&lt;&lt; "$EC2_INSTANCE" )" export ATTACHED_VOLUME_ID="$( jq -r ".BlockDeviceMappings[].Ebs.VolumeId" &lt;&lt;&lt; "$EC2_INSTANCE" )" if [[ "$ATTACHED_VOLUME_ID" == "$2" ]]; then >&amp;2 echo "VolumeId $2 is already attached to EC2 instance $1" exit 1 fi export EC2_STATE="$( jq -r ".State.Name" &lt;&lt;&lt; "$EC2_INSTANCE" )" if [ "$EC2_STATE" == running ]; then echo "Stopping EC2 instance $1" aws ec2 stop-instances --instance-ids "$1" aws ec2 wait instance-stopped --instance-ids "$1" fi aws ec2 detach-volume --volume-id "$ATTACHED_VOLUME_ID" aws ec2 wait volume-available --volume-id "$ATTACHED_VOLUME_ID" aws ec2 attach-volume \ --device /dev/sda1 \ --instance-id "$1" \ --volume-id "$2" aws ec2 wait volume-in-use --volume-id "$2" aws ec2 start-instances --instance-ids "$1" aws ec2 wait instance-started --instance-ids "$1" &#125; set -e replaceSystemVolume "$@" </pre> <p> Here is how to use it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7be9300efa8c'><button class='copyBtn' data-clipboard-target='#id7be9300efa8c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>replaceSystemVolume "$AWS_PROBLEM_INSTANCE_ID" "$AWS_RESCUE_VOLUME_ID"</pre> </div> <p> Preview 2 instance id is <code>AWS_EC2_RESCUE_ID</code>. Replace rescue volume on preview with preview's original volume: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb829e08f2c4f'><button class='copyBtn' data-clipboard-target='#idb829e08f2c4f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>replaceSystemVolume "$AWS_EC2_RESCUE_ID" "$AWS_PREVIEW_VOLUME_ID"</pre> </div> <!-- endregion --> <!-- #region --> <h2 class="numbered" id="boot">Boot the problem system</h2> <p> Boot the problem system and verify the problem is solved. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5cb32f22c73d'><button class='copyBtn' data-clipboard-target='#id5cb32f22c73d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 start-instances --instance-ids $AWS_PROBLEM_INSTANCE_ID <span class='unselectable'>$ </span>aws ec2 wait instance-started --instance-ids $AWS_PROBLEM_INSTANCE_ID</pre> </div> <!-- endregion --> <!-- #region --> <h2 id="acknowledgements">Acknowledgements</h2> <p> This article was inspired by <a href='https://www.rootusers.com/how-to-repair-an-aws-ec2-instance-without-console' target='_blank'>this excellent article</a>, which uses the AWS web console to achieve similar results. </p> <!-- endregion --> Working With EC2 Spot Instances From AWS CLI 2020-10-24T00:00:00-04:00 https://mslinn.github.io/blog/2020/10/24/ec2-spot-instances-cli <!-- #region intro --> <p> AWS EC2 <code>T2.medium</code> spot instances <a href='https://aws.amazon.com/ec2/spot/pricing/' target='_blank' rel="nofollow">cost less than 2 cents per hour</a> for Linux and can be created very easily from the command line. They self-destruct once shut down. These powerful virtual machines can do an incredible amount of work in an hour for less than 2 cents! </p> <p> This article shows how all this can be done via the command line. I also provide an <a href='#createEc2Spot'>interactive bash script</a> for automating the process of obtaining and releasing an EC2 spot instance. </p> <p> Once again this article uses <a href='https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html' target='_blank' rel="nofollow"><code>AWS CLI</code></a> and <a href='https://stedolan.github.io/jq/' target='_blank' rel="nofollow"><code>jq</code></a>. </p> <!-- endregion --> <!-- #region Create and Import a New Keypair --> <h2 class="numbered" id="nameKeys">Create and Import a New Keypair</h2> <p> I want to create a new temporary ssh keypair that will just be used for this spot instance. The name of the new key pair will be of the form <code>~/.ssh/rsa-YYYY-MM-DD-mm-ss</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfdc49740fd8b'><button class='copyBtn' data-clipboard-target='#idfdc49740fd8b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )"<br> <span class='unselectable'>$ </span>echo "$AWS_KEY_PAIR_NAME" <span class='unselectable'>rsa-2020-11-04-10-43-54 </span><br> <span class='unselectable'>$ </span>AWS_KEY_PAIR_FILE="~/.ssh/$AWS_KEY_PAIR_NAME"<br> <span class='unselectable'>$ </span>echo "$AWS_KEY_PAIR_FILE" <span class='unselectable'>~/.ssh/rsa-2020-11-04-10-43-54 </span></pre> </div> <!-- endregion --> <p> Now we can make the keypair. AWS EC2 does not accept keys longer than 2048 bits. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idafee340cda87'><button class='copyBtn' data-clipboard-target='#idafee340cda87' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa <span class='unselectable'>Generating public/private rsa key pair. Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54 Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54.pub The key fingerprint is: SHA256:bQScX0UMn0xGDorxSvElMZzwMyyk7hgs2FNbshBNenA mslinn@mslinn.com The key&rsquo;s randomart image is: +---[RSA 2048]----+ | ooE .*++o+** | | =. ooXo=.B.. | | o + o +.X. = | | o = * . =.o | |. + = . S o | | o + . | | . . | | | | | +----[SHA256]-----+</span></pre> </div> <!-- endregion --> <p> The new public key will be stored in <code>~/.ssh/2020-11-04-10-43-54.pub</code> and the new private key will be stored in <code>~/.ssh/2020-11-04-10-43-54</code>. </p> <p> Now set the permissions for the key. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd59199b91bd1'><button class='copyBtn' data-clipboard-target='#idd59199b91bd1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>chmod 400 "$AWS_KEY_PAIR_FILE"</pre> </div> <p> Now we can import the key pair into AWS: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddc08340b8112'><button class='copyBtn' data-clipboard-target='#iddc08340b8112' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 import-key-pair \ --key-name "$AWS_KEY_PAIR_NAME" \ --public-key-material "fileb://${AWS_KEY_PAIR_FILE}.pub" <span class='unselectable'>{ "KeyFingerprint": "c7:76:90:53:17:d0:fc:ba:45:dd:93:d2:93:03:c2:19", "KeyName": "2020-11-04-10-43-54", "KeyPairId": "key-092a2306ec3f4aff6" }</span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Select an AMI --> <h2 class="numbered" id="ami">Select an AMI</h2> <p> New <a href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html' target='_blank' rel="nofollow">AMIs</a> become available every day. You probably want your EC2 spot instance to be created from the most recent AMI that matches your needs. For most of my work I want an Ubuntu 64-bit Intel/AMD server distribution. <a href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html' target='_blank' rel="nofollow">AWS documentation</a> is helpful and gives us a head start in automating the AMI selection. </p> <p> The following incantation sets an environment variable called <code>AWS_AMI</code> to the details in JSON syntax of the AMI for the most recent 64-bit Ubuntu server release for Intel/AMD architecture. The <code>OwnerId</code> of Canonical, the publisher of Ubuntu, is <a href='https://ubuntu.com/server/docs/cloud-images/amazon-ec2' target='_blank' rel="nofollow"><code>099720109477</code></a>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id71cc50018aba'><button class='copyBtn' data-clipboard-target='#id71cc50018aba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_AMI="$( aws ec2 describe-images \ --owners <span class="bg_yellow">099720109477</span> \ --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-━━━━━???-━━━━━???-amd64-server-━━━━━???" \ "Name=state,Values=available" \ --query "reverse(sort_by(Images, &CreationDate))[:1]" | \ jq -r '.[0]' )"<br> <span class='unselectable'>$ </span>echo "$AWS_AMI" <span class='unselectable'>{ "Architecture": "x86_64", "CreationDate": "2020-10-30T14:07:42.000Z", "ImageId": "ami-0c71ec98278087e60", "ImageLocation": "099720109477/ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030", "ImageType": "machine", "Public": true, "OwnerId": "099720109477", "PlatformDetails": "Linux/UNIX", "UsageOperation": "RunInstances", "State": "available", "BlockDeviceMappings": [ { "DeviceName": "/dev/sda1", "Ebs": { "DeleteOnTermination": true, "SnapshotId": "snap-00bf581086dd686e5", "VolumeSize": 8, "VolumeType": "gp2", "Encrypted": false } }, { "DeviceName": "/dev/sdb", "VirtualName": "ephemeral0" }, { "DeviceName": "/dev/sdc", "VirtualName": "ephemeral1" } ], "Description": "Canonical, Ubuntu, 20.10, amd64 groovy image build on 2020-10-30", "EnaSupport": true, "Hypervisor": "xen", "Name": "ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030", "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SriovNetSupport": "simple", "VirtualizationType": "hvm" } </span></pre> </div> <!-- endregion --> <p> Now let's extract the ID of the AMI image and save it as <code>AWS_AMI_ID</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3679b9b87089'><button class='copyBtn' data-clipboard-target='#id3679b9b87089' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_AMI_ID="$( jq -r '.[0].ImageId' &lt;&lt;&lt; "$AWS_AMI" )" <span class='unselectable'>$ </span>echo "$AWS_AMI_ID" <span class='unselectable'>ami-0c71ec98278087e60 </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Create an EC2 Spot Instance --> <h2 class="numbered" id="create">Create an EC2 Spot Instance</h2> <p> For my work I often want my spot instance to be created in the same VPC subnet as my other resources, with the same security group. That is why the following environment variables are defined for the <code>Groups</code> and <code>SubnetId</code> values within the <code>network-interfaces</code> option, as well as the AWS region. The script at the end of this article offers an easier way of obtaining all these values. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id312b14f9ef2e'><button class='copyBtn' data-clipboard-target='#id312b14f9ef2e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_GROUP=sg-4cbc6f35<br> <span class='unselectable'>$ </span>AWS_SUBNET=subnet-49de033f<br> <span class='unselectable'>$ </span>AWS_ZONE=us-east-1c<br> <span class='unselectable'>$ </span>AWS_EC2_TYPE=t2.medium</pre> </div> <!-- endregion --> <p> The following creates an AWS EC2 spot instance with a public IP address and runs it. Details about the newly created spot instance are stored as JSON in <code>AWS_SPOT_INSTANCE</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id13c1512e368f'><button class='copyBtn' data-clipboard-target='#id13c1512e368f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_SPOT_INSTANCE="$( aws ec2 run-instances \ --image-id "$AWS_AMI_ID" \ --instance-market-options '{ "MarketType": "spot" }' \ --instance-type "$AWS_EC2_TYPE" \ --key-name "$AWS_KEY_PAIR_NAME" \ --network-interfaces "[ { \"DeviceIndex\": 0, \"Groups\": [\"$AWS_GROUP\"], \"SubnetId\": \"$AWS_SUBNET\", \"DeleteOnTermination\": true, \"AssociatePublicIpAddress\": true } ]" \ --placement "{ \"AvailabilityZone\": \"$AWS_ZONE\" }" | \ jq -r .Instances[0] )"<br> <span class='unselectable'>$ </span>echo "$AWS_SPOT_INSTANCE" <span class='unselectable'>{ "AmiLaunchIndex": 0, "ImageId": "ami-0dba2cb6798deb6d8", "InstanceId": "i-012a54aefcd333de9", "InstanceType": "t2.small", "KeyName": "rsa-2020-11-03.pub", "LaunchTime": "2020-11-03T23:19:50.000Z", "Monitoring": { "State": "disabled" }, "Placement": { "AvailabilityZone": "us-east-1c", "GroupName": "", "Tenancy": "default" }, "PrivateDnsName": "ip-10-0-0-210.ec2.internal", "PrivateIpAddress": "10.0.0.210", "ProductCodes": [], "PublicDnsName": "", "State": { "Code": 0, "Name": "pending" }, "StateTransitionReason": "", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "Architecture": "x86_64", "BlockDeviceMappings": [], "ClientToken": "026583fb-c94e-4bca-bdd2-8dcdcaa3aae9", "EbsOptimized": false, "EnaSupport": true, "Hypervisor": "xen", "InstanceLifecycle": "spot", "NetworkInterfaces": [ { "Attachment": { "AttachTime": "2020-11-03T23:19:50.000Z", "AttachmentId": "eni-attach-04feb4d36cf5c6792", "DeleteOnTermination": true, "DeviceIndex": 0, "Status": "attaching" }, "Description": "", "Groups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "Ipv6Addresses": [], "MacAddress": "0a:6d:ba:c5:65:4b", "NetworkInterfaceId": "eni-09ef90920cfb29dd9", "OwnerId": "031372724784", "PrivateIpAddress": "10.0.0.210", "PrivateIpAddresses": [ { "Primary": true, "PrivateIpAddress": "10.0.0.210" } ], "SourceDestCheck": true, "Status": "in-use", "SubnetId": "subnet-49de033f", "VpcId": "vpc-f16a0895", "InterfaceType": "interface" } ], "RootDeviceName": "/dev/sda1", "RootDeviceType": "ebs", "SecurityGroups": [ { "GroupName": "testSG", "GroupId": "sg-4cbc6f35" } ], "SourceDestCheck": true, "SpotInstanceRequestId": "sir-rrs9gm3j", "StateReason": { "Code": "pending", "Message": "pending" }, "VirtualizationType": "hvm", "CpuOptions": { "CoreCount": 1, "ThreadsPerCore": 1 }, "CapacityReservationSpecification": { "CapacityReservationPreference": "open" }, "MetadataOptions": { "State": "pending", "HttpTokens": "optional", "HttpPutResponseHopLimit": 1, "HttpEndpoint": "enabled" } } </span></pre> </div> <!-- endregion --> <p> Now extract the EC2 spot instance id and save it in <code>AWS_SPOT_ID</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5beeeb60cac3'><button class='copyBtn' data-clipboard-target='#id5beeeb60cac3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_SPOT_ID="$( jq -r .InstanceId <<< "$AWS_SPOT_INSTANCE" )"<br> <span class='unselectable'>$ </span>echo "$AWS_SPOT_ID" <span class='unselectable'>i-012a54aefcd333de9 </span></pre> </div> <!-- endregion --> <p> Wait for the instance to start. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfde58588be9c'><button class='copyBtn' data-clipboard-target='#idfde58588be9c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 wait instance-running --instance-ids "$AWS_SPOT_ID"</pre> </div> <!-- endregion --> <!-- #region Connect to the Spot Instance --> <h2 class="numbered" id="connect">Connect to the Spot Instance</h2> <p> In order to <code>ssh</code> into the spot instance we first need to discover its IP address, which is saved in <code>AWS_SPOT_IP</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id89e5ce08cdec'><button class='copyBtn' data-clipboard-target='#id89e5ce08cdec' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>AWS_SPOT_IP="$( aws ec2 describe-instances \ --instance-ids $AWS_SPOT_ID | \ jq -r .Reservations[0].Instances[0].PublicIpAddress )"<br> <span class='unselectable'>$ </span>echo "$AWS_SPOT_IP" <span class='unselectable'>54.242.88.254 </span></pre> </div> <!-- endregion --> <p> Now we can connect to the spot instance via <code>ssh</code>. The default userid for Ubuntu is <code>ubuntu</code>. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id303750f5d8cf'><button class='copyBtn' data-clipboard-target='#id303750f5d8cf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP" <span class='unselectable'>Warning: No xauth data; using fake authentication data for X11 forwarding. Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.11.0-1027-aws x86_64)<br> * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage<br> System information as of Thu Jan 27 20:28:22 UTC 2022<br> System load: 0.06 Processes: 113 Usage of /: 18.3% of 7.69GB Users logged in: 0 Memory usage: 5% IPv4 address for eth0: 10.0.0.29 Swap usage: 0%<br> 1 update can be applied immediately. To see these additional updates run: apt list --upgradable<br><br> The list of available updates is more than a week old. To check for new updates run: sudo apt update<br><br> The programs included with the Ubuntu system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.<br> Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law.<br> /usr/bin/xauth: file /home/ubuntu/.Xauthority does not exist To run a command as administrator (user "root"), use "sudo <command>". See "man sudo_root" for details.<br> ubuntu@ip-10-0-0-29:~$ </span></pre> </div> <!-- endregion --> <p> Do your work on the spot instance now. We'll disconnect and clean up next. </p> <!-- endregion --> <!-- #region Disconnect from the Spot Instance and Clean Up --> <h2 class="numbered" id="connect">Disconnect from the Spot Instance and Clean Up</h2> <!-- #region Using the Command Line --> <h3 class="numbered" id="consoleCheck">Using the Command Line</h3> <p> Once the spot instance stops it is automatically terminated. The instance will survive a <code>reboot</code>, but not a <code>halt</code>. From a prompt on the spot instance, type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>shell&nbsp;(Spot&nbsp;Instance)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4bc88edcf465'><button class='copyBtn' data-clipboard-target='#id4bc88edcf465' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo halt</pre> </div> <p> Back in the shell that launched the spot instance, wait for the spot instance to stop before cleaning up. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9ec587e82c1d'><button class='copyBtn' data-clipboard-target='#id9ec587e82c1d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 wait instance-stopped --instance-ids $AWS_SPOT_ID</pre> </div> <p> Delete the temporary <code>ssh</code> keypair we created. Copies exist on AWS and the local machine; we need to remove all of them, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id935abf5eb5db'><button class='copyBtn' data-clipboard-target='#id935abf5eb5db' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ec2 delete-key-pair --key-name $AWS_KEY_PAIR_NAME <span class='unselectable'>$ </span>rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub</pre> </div> <!-- endregion --> <!-- #region Checking With Web Console --> <h3 class="numbered" id="consoleCheck">Checking With Web Console</h3> <p> You can use the web console to verify that all the <a href='https://console.aws.amazon.com/ec2sp/v2/#/spot' target='_blank' rel="nofollow">spot instances</a> were shut down, and the <a href='https://console.aws.amazon.com/ec2/v2/home?#KeyPairs:' target='_blank' rel="nofollow">key pairs</a> were deleted. </p> <!-- endregion --> <!-- endregion --> <!-- #region Bash Script createEc2Spot --> <h2 class="numbered" id="createEc2Spot">Bash Script <span class="code">createEc2Spot</span></h2> <!-- #region Source Code --> <h3 class="numbered" id="createEc2Spot_code">Source Code</h3> <p> This script does everything discussed above, plus it prompts the user with default values for parameters unique to each invocation. Click on the name of the script and save it this script to a directory on your <code>PATH</code>. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,createEc2Spot' download='createEc2Spot' title='Click on the file name to download the file'>createEc2Spot</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id30f2640a1d9d"><button class='copyBtn' data-clipboard-target='#id30f2640a1d9d'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # Author: Mike Slinn mslinn@mslinn.com # Initial version 2020-1-25 # Last modified 2022-01-27 set -e function readWithDefault &#123; >&amp;2 printf "\n$1: " read -e -i "$2" VALUE echo "$VALUE" &#125; echo "The AWS EC2 spot instance needs to share settings with the existing EC2 instance you want to affect." echo "The easiest way to do this is to reference an EC2 instance with a Name tag, so it can be identified." echo "If there is no such EC2 instance, delete the default value in the next prompt, and you will be able to specify the details manually." AWS_EC2_NAME="$( readWithDefault "AWS EC2 Name tag value" production )" if [ "$AWS_EC2_NAME" ]; then AWS_EC2_PRODUCTION="$( aws ec2 describe-instances | \ jq ".Reservations[].Instances[] | select((.Tags[]?.Key==\"Name\") and (.Tags[]?.Value==\"$AWS_EC2_NAME\"))" )" AWS_GROUP="$( jq -r ".NetworkInterfaces[].Groups[].GroupId" &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" AWS_SUBNET="$( jq -r ".SubnetId" &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" AWS_ZONE="$( jq -r ".Placement.AvailabilityZone" &lt;&lt;&lt; "$AWS_EC2_PRODUCTION" )" else echo "Please answer a few questions so the AWS EC2 spot instance can be created." AWS_GROUP="$( readWithDefault "AWS security group" sg-4cbc6f35 )" AWS_SUBNET="$( readWithDefault "EC2 subnet" subnet-49de033f )" AWS_ZONE="$( readWithDefault "AWS availability zone" us-east-1c )" fi echo "EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance." AWS_EC2_TYPE="$( readWithDefault "EC2 machine type" t2.medium )" AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )" AWS_KEY_PAIR_FILE="$HOME/.ssh/$AWS_KEY_PAIR_NAME" ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa # -q chmod 400 "$AWS_KEY_PAIR_FILE" aws ec2 import-key-pair \ --key-name "$AWS_KEY_PAIR_NAME" \ --public-key-material "fileb://$AWS_KEY_PAIR_FILE.pub" echo "Searching for the latest 64-bit Intel/AMD Ubuntu AMI by Canonical." AWS_AMI="$( aws ec2 describe-images \ --owners 099720109477 \ --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-????????-????????-amd64-server-????????" \ "Name=state,Values=available" \ --query "reverse(sort_by(Images, &amp;CreationDate))[:1]" | \ jq -r '.[0]' )" echo "Obtaining the AMI image ID." AWS_AMI_ID="$( jq -r '.ImageId' &lt;&lt;&lt; "$AWS_AMI" )" echo "Creating the EC2 spot instance." AWS_SPOT_INSTANCE="$( aws ec2 run-instances \ --image-id $AWS_AMI_ID \ --instance-market-options '&#123; "MarketType": "spot" &#125;' \ --instance-type $AWS_EC2_TYPE \ --key-name $AWS_KEY_PAIR_NAME \ --network-interfaces "[ &#123; \"DeviceIndex\": 0, \"Groups\": [\"$AWS_GROUP\"], \"SubnetId\": \"$AWS_SUBNET\", \"DeleteOnTermination\": true, \"AssociatePublicIpAddress\": true &#125; ]" \ --placement "&#123; \"AvailabilityZone\": \"$AWS_ZONE\" &#125;" | \ jq -r .Instances[0] )" echo "Obtaining the EC2 spot instance ID." AWS_SPOT_ID="$( jq -r .InstanceId &lt;&lt;&lt; "$AWS_SPOT_INSTANCE" )" echo "Awaiting for the EC2 spot instance $AWS_SPOT_ID to enter the running state." aws ec2 wait instance-running --instance-ids $AWS_SPOT_ID echo "Obtaining the IP address of the new EC2 spot instance $AWS_SPOT_ID." AWS_SPOT_IP="$( aws ec2 describe-instances \ --instance-ids $AWS_SPOT_ID | \ jq -r .Reservations[0].Instances[0].PublicIpAddress )" echo "About to ssh to the EC2 spot instance as ubuntu@$AWS_SPOT_IP using $AWS_KEY_PAIR_FILE." echo "When you are done, type: sudo halt." echo "The spot instance will then terminate and be gone forever." echo "Any predefined resources, such as volumes that you attach will be freed." ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP" echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state." aws ec2 wait instance-stopped --instance-ids "$AWS_SPOT_ID" echo "The spot instance is no longer available. Deleting its keypair." aws ec2 delete-key-pair --key-name AWS_KEY_PAIR_NAME rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub </pre> <p>Make the script executable.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0580924b4343'><button class='copyBtn' data-clipboard-target='#id0580924b4343' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>chmod a+x createEc2Spot</pre> </div> <!-- endregion --> <!-- #region Sample Usage --> <h3 class="numbered" id="createEc2Spot_usage">Sample Usage</h3> <p> The script is easy to use: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id17873deff30c'><button class='copyBtn' data-clipboard-target='#id17873deff30c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>createEc2Spot <span class='unselectable'>The AWS EC2 spot instance needs to share settings with the existing EC2 instance you want to affect. The easiest way to do this is to reference an EC2 instance with a Name tag, so it can be identified. If there is no such EC2 instance, delete the default value in the next prompt, and you will be able to specify the details manually.<br> AWS EC2 Name tag value: </span>production<br> <span class='unselectable'>EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance. EC2 machine type: </span>t2.medium<br> <span class='unselectable'>Generating public/private rsa key pair. Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54 Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54.pub The key fingerprint is: SHA256:bQScX0UMn0xGDorxSvElMZzwMyyk7hgs2FNbshBNenA mslinn@mslinn.com The key&rsquo;s randomart image is: +---[RSA 2048]----+ | ooE .*++o+** | | =. ooXo=.B.. | | o + o +.X. = | | o = * . =.o | |. + = . S o | | o + . | | . . | | | | | +----[SHA256]-----+ "KeyFingerprint": "be:19:50:59:a1:83:ea:c1:91:1e:2f:d6:31:64:9a:c0", "KeyName": "rsa-2022-01-27-50-02", "KeyPairId": "key-072a3f0545864526a" } Searching for the latest 64-bit Intel/AMD Ubuntu AMI by Canonical. Obtaining the AMI image ID. Creating the EC2 spot instance. Obtaining the EC2 spot instance ID. Awaiting for the EC2 spot instance i-03d09e364ed15a448 to enter the running state. Obtaining the IP address of the new EC2 spot instance i-03d09e364ed15a448. 54.242.88.254 When you are done, type: sudo halt. The spot instance will then terminate and be gone forever. Any predefined resources, such as volumes that you attach will be freed. Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state. </span></pre> </div> <!-- endregion --> <p> Now do your work on the spot instance, and then run <code>sudo halt</code>. Once the spot instance shuts down, it is destroyed and the script cleans up. Do not try to reboot the spot instance, because that shuts it down and it goes away instead of coming back up. Now do your work on the spot instance. From a prompt on the spot instance, type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>shell&nbsp;(Spot&nbsp;Instance)</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9fa9f3da9264'><button class='copyBtn' data-clipboard-target='#id9fa9f3da9264' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo halt</pre> </div> <p> Back in the shell on your computer, you should see: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id685c31af9d51'><button class='copyBtn' data-clipboard-target='#id685c31af9d51' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo halt <span class='unselectable'>The spot instance is no longer available. Deleting its keypair. </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Bash Script aws_ec2_functions --> <h2 class="numbered" id="aws_ec2_functions">Bash Script <span class="code">aws_ec2_functions</span></h2> <h3 class="numbered" id="aws_ec2_functions_code">Source Code</h3> <p> This is another script does the same thing as the previous script, but in steps. Click on the name of the script and save it this script to a directory on your <code>PATH</code>. </p> <div class="codeLabel"><a href='data:text/plain;charset=UTF-8,aws_ec2_functions' download='aws_ec2_functions' title='Click on the file name to download the file'>aws_ec2_functions</a> </div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="ideb7dfd1bd2dc"><button class='copyBtn' data-clipboard-target='#ideb7dfd1bd2dc'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # Author: Mike Slinn mslinn@mslinn.com # Initial version 2022-01-28 # Last modified 2022-01-28 function readWithDefault &#123; # prompt user for a value, with a default >&amp;2 printf "\n$1: " read -e -i "$2" VALUE echo "$VALUE" &#125; function requires &#123; # Halts if any of the supplied arguments is not the name of a defined environment variable for ENV_VAR in "$@"; do if [ -z "$&#123;!ENV_VAR&#125;" ]; then echo "Error: $&#123;ENV_VAR&#125; is undefined." return 1 2> /dev/null || exit 1 #else # echo "$&#123;ENV_VAR&#125; has value $&#123;!ENV_VAR&#125;" fi done &#125; function attachVolumeToSpot &#123; requires AWS_SPOT_ID AWS_NEW_VOLUME_ID || return export AWS_ATTACH_VOLUME="$( aws ec2 attach-volume \ --device /dev/xvdh \ --instance-id $AWS_SPOT_ID \ --volume-id $AWS_NEW_VOLUME_ID )" aws ec2 wait volume-in-use --volume-id "$AWS_NEW_VOLUME_ID" export AWS_ATTACH_VOLUME_DEVICE="$( aws ec2 describe-volumes \ --volume-id "$AWS_NEW_VOLUME_ID" | \ jq -r .Volumes[0].Attachments[0].Device )" &#125; function chroot &#123; requires AWS_NEW_VOLUME_ID || return # Use the /tmp/mounter script built by attachVolumeToSpot aws ec2 detach-volume --volume-id $AWS_NEW_VOLUME_ID aws ec2 wait volume-available --volume-id $AWS_NEW_VOLUME_ID &#125; function copyScriptToSpot &#123; requires AWS_ATTACH_VOLUME_DEVICE cat >/tmp/mounter &lt;&lt;EOF sudo mount "$&#123;AWS_ATTACH_VOLUME_DEVICE&#125;1" /mnt sudo mount -o bind /dev /mnt/dev sudo mount -o bind /dev/shm /mnt/dev/shm sudo mount -o bind /sys /mnt/sys sudo mount -o bind /run /mnt/run sudo mount -t proc proc /mnt/proc sudo mount -t devpts devpts /mnt/dev/pts sudo chroot /mnt # If the user types 'exit' then this script continues. # Otherwise, if the user types 'sudo halt' this script is not needed, AWS does the cleanup. sudo umount /mnt/dev sudo umount /mnt/dev/shm sudo umount /mnt/sys sudo umount /mnt/run sudo umount /mnt/proc sudo umount /mnt/dev/pts sudo umount /mnt EOF set -xv chmod a+x /tmp/mounter scpToSpot /tmp/mounter mounter echo "About to ssh into the spot instance. Run ./mounter to enter chroot using the new volume" sshToSpot # Proves the drive is mounted: #df -h | grep '^/dev/' | grep -v '^/dev/loop' rm /tmp/mounter &#125; function deleteEc2SpotInstance &#123; requires AWS_KEY_PAIR_NAME AWS_SPOT_ID || return # Hope that this does not bomb out if the user types 'sudo halt' aws ec2 cancel-spot-instance-requests --spot-instance-request-ids "$AWS_SPOT_ID" echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state." aws ec2 wait instance-stopped --instance-ids "$AWS_SPOT_ID" echo "The spot instance is no longer available. Deleting its keypair." aws ec2 delete-key-pair --key-name AWS_KEY_PAIR_NAME rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub &#125; function findEc2 &#123; echo "The existing AWS EC2 instance needs a snapshot to be made, which will then be turned into a volume and then mounted on a new AWS EC2 spot instance." echo "This script looks for an EC2 instance with a Name tag, so it can be identified." export AWS_EC2_ORIGINAL_NAME="$( readWithDefault "AWS EC2 Name tag value" production )" export AWS_EC2_ORIGINAL="$( aws ec2 describe-instances | \ jq ".Reservations[].Instances[] | select((.Tags[]?.Key==\"Name\") and (.Tags[]?.Value==\"$AWS_EC2_ORIGINAL_NAME\"))" )" export AWS_ORIGINAL_EC2_INSTANCE_ID="$( jq -r .InstanceId &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" export AWS_ORIGINAL_EC2_IP="$( jq -r .PublicIpAddress &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" AWS_ORIGINAL_VOLUME_ID="$( jq -r '.BlockDeviceMappings[].Ebs.VolumeId' &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" export AWS_GROUP="$( jq -r ".NetworkInterfaces[].Groups[].GroupId" &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" export AWS_SUBNET="$( jq -r ".SubnetId" &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" export AWS_ZONE="$( jq -r .Placement.AvailabilityZone &lt;&lt;&lt; "$AWS_EC2_ORIGINAL" )" echo "Original EC2 instance $AWS_ORIGINAL_EC2_INSTANCE_ID is at IP address $AWS_ORIGINAL_EC2_IP, has EBS volume $AWS_ORIGINAL_VOLUME_ID, with security group $AWS_GROUP, in $AWS_SUBNET, in the $AWS_ZONE zone." &#125; function findSnapshot &#123; # Look for a snapshot previously created by this script export AWS_SNAPSHOT_ID="$( aws ec2 describe-snapshots \ --owner-ids self \ --filters Name=tag:Name,Values=TestScript | \ jq -r .Snapshots[].SnapshotId )" &#125; function findVolume &#123; # Look for a snapshot previously created by this script requires AWS_ZONE AWS_SNAPSHOT_ID || return export AWS_ATTACH_VOLUME_DEVICE="$( aws ec2 describe-volumes \ --filters Name=tag:Name,Values=TestScript | \ jq -r .Volumes[0].Attachments[0].Device )" &#125; function fn_help &#123; echo "Bash functions to make working with AWS EC2 easier via the command line. Source this file then call the functions: attachVolumeToSpot chroot copyScriptToSpot deleteEc2SpotInstance findEc2 findSnapshot findVolume latestUbuntuAmi makeEc2SpotInstance makeSnapshot makeVolumeFromSnapshot mountVolumeOnSpot scpToSpot sshToSpot Typical usage requires ' || true' to keep the terminal open if a problem occurs. This is unnecessary if you invoke from another bash script. source aws_ec2_functions findEc2 || true makeSnapshot || true makeVolumeFromSnapshot || true latestUbuntuAmi || true makeEc2SpotInstance || true attachVolumeToSpot || true copyScriptToSpot || true ... or, to perform all of the above: source aws_ec2_functions prepare_spot || true ... another way to perform all of the above: aws_ec2_functions run ... to pick up from a failed attempt, which created a snapshot and a volume, but did not make a spot instance, or the spot instance has been cancelled: findSnapshot || true findVolume || true latestUbuntuAmi || true makeEc2SpotInstance || true attachVolumeToSpot || true copyScriptToSpot || true " &#125; function latestUbuntuAmi &#123; export AWS_AMI="$( aws ec2 describe-images \ --owners 099720109477 \ --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-????????-????????-amd64-server-????????" \ "Name=state,Values=available" \ --query "reverse(sort_by(Images, &amp;CreationDate))[:1]" | \ jq -r '.[0]' )" export AWS_AMI_ID="$( jq -r '.ImageId' &lt;&lt;&lt; "$AWS_AMI" )" echo "The most recent Ubuntu AMI ID is $AWS_AMI_ID" &#125; function makeEc2SpotInstance &#123; requires AWS_AMI_ID AWS_GROUP AWS_SUBNET AWS_ZONE || return export AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )" export AWS_KEY_PAIR_FILE="$HOME/.ssh/$AWS_KEY_PAIR_NAME" ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa # -q chmod 400 "$AWS_KEY_PAIR_FILE" aws ec2 import-key-pair \ --key-name "$AWS_KEY_PAIR_NAME" \ --public-key-material "fileb://$AWS_KEY_PAIR_FILE.pub" echo "EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance." export AWS_EC2_TYPE="$( readWithDefault "EC2 machine type" t2.medium )" export AWS_SPOT_INSTANCE="$( aws ec2 run-instances \ --image-id $AWS_AMI_ID \ --instance-market-options '&#123; "MarketType": "spot" &#125;' \ --instance-type $AWS_EC2_TYPE \ --key-name $AWS_KEY_PAIR_NAME \ --network-interfaces "[ &#123; \"DeviceIndex\": 0, \"Groups\": [\"$AWS_GROUP\"], \"SubnetId\": \"$AWS_SUBNET\", \"DeleteOnTermination\": true, \"AssociatePublicIpAddress\": true &#125; ]" \ --placement "&#123; \"AvailabilityZone\": \"$AWS_ZONE\" &#125;" | \ jq -r .Instances[0] )" export AWS_SPOT_ID="$( jq -r .InstanceId &lt;&lt;&lt; "$AWS_SPOT_INSTANCE" )" echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the running state. This usually takes about 1 minute." aws ec2 wait instance-running --instance-ids "$AWS_SPOT_ID" export AWS_SPOT_IP="$( aws ec2 describe-instances \ --instance-ids $AWS_SPOT_ID | \ jq -r .Reservations[0].Instances[0].PublicIpAddress )" echo "The IP address of the new EC2 spot instance $AWS_SPOT_ID is $AWS_SPOT_IP." &#125; function makeSnapshot &#123; # Makes an AWS EC2 snapshot with name TestScript from AWS_ORIGINAL_VOLUME_ID requires AWS_ORIGINAL_VOLUME_ID || return COMPLETION_TIME="$(date --date="@$(($(date +%s)+120))" +"%H":"%M":"%S")" echo "Snapshots take about 2 minutes. This one should complete by $COMPLETION_TIME." export AWS_SNAPSHOT_ID="$( aws ec2 create-snapshot --volume-id "$AWS_ORIGINAL_VOLUME_ID" \ --description "production $( date '+%Y-%m-%d' )" \ --tag-specifications "ResourceType=snapshot,Tags=[&#123;Key=Created, Value=`date '+%Y-%m-%d'`&#125;,&#123;Key=Name, Value=\"TestScript\"&#125;]" | \ jq -r .SnapshotId )" aws ec2 wait snapshot-completed --snapshot-ids "$AWS_SNAPSHOT_ID" echo "Snapshot $AWS_SNAPSHOT_ID is complete." &#125; function makeVolumeFromSnapshot &#123; # Makes an AWS EC2 volume with name TestScript requires AWS_ZONE AWS_SNAPSHOT_ID || return export AWS_NEW_VOLUME_ID="$( aws ec2 create-volume \ --availability-zone $AWS_ZONE \ --snapshot-id $AWS_SNAPSHOT_ID \ --tag-specifications 'ResourceType=volume,Tags=[&#123;Key=Name,Value=TestScript&#125;]' | \ jq -r .VolumeId )" echo "Waiting for AWS volume $AWS_NEW_VOLUME_ID to become available. This usually takes about 20 seconds." aws ec2 wait volume-available --volume-id "$AWS_NEW_VOLUME_ID" echo "$AWS_NEW_VOLUME_ID" &#125; function prepare_spot &#123; # Run everything findEc2 makeSnapshot makeVolumeFromSnapshot latestUbuntuAmi makeEc2SpotInstance attachVolumeToSpot copyScriptToSpot &#125; function scpToSpot &#123; requires AWS_KEY_PAIR_FILE AWS_SPOT_IP || return scp -pi "$AWS_KEY_PAIR_FILE" "$1" "ubuntu@$AWS_SPOT_IP:$2" &#125; function sshToSpot &#123; requires AWS_KEY_PAIR_FILE AWS_SPOT_IP || return echo "When you are done, type: sudo halt." echo "The AWS EC2 spot instance will then terminate and be gone forever." echo "Any predefined resources, such as volumes that you attach will be freed." ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP" "$*" &#125; if [ "$1" == prepare_spot ]; then echo "Starting..." prepare_spot || true elif [ "$1" ]; then fn_help || true else echo "Type fn_help to obtain help information" fi </pre> <p>Make the script executable.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0860e42d7c6b'><button class='copyBtn' data-clipboard-target='#id0860e42d7c6b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>chmod a+x createEc2Spot</pre> </div> <h3 class="numbered" id="aws_ec2_functions_usage">Sample Usage</h3> <p> View the help like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4950b65259ea'><button class='copyBtn' data-clipboard-target='#id4950b65259ea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws_ec2_functions -h <span class='unselectable'>Bash functions to make working with AWS EC2 easier via the command line. Source this file then call the functions, listed alphabetically: attachVolumeToSpot chroot copyScriptToSpot deleteEc2SpotInstance findEc2 findSnapshot findVolume latestUbuntuAmi makeEc2SpotInstance makeSnapshot makeVolumeFromSnapshot mountVolumeOnSpot scpToSpot sshToSpot<br> Typical usage requires ' || true' to keep the terminal open if a problem occurs. This is unnecessary if you invoke from another bash script. source aws_ec2_functions findEc2 || true makeSnapshot || true makeVolumeFromSnapshot || true latestUbuntuAmi || true makeEc2SpotInstance || true attachVolumeToSpot || true copyScriptToSpot || true<br> ... or, to perform all of the above: source aws_ec2_functions prepare_spot || true<br> ... another way to perform all of the above: aws_ec2_functions run<br> ... to pick up from a failed attempt, which created a snapshot and a volume, but did not make a spot instance, or the spot instance has been cancelled: findSnapshot || true findVolume || true latestUbuntuAmi || true makeEc2SpotInstance || true attachVolumeToSpot || true copyScriptToSpot || true </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region Using the chroot --> <h2 class="numbered" id="chroot_usage">Using the chroot</h2> <p> Using either of the preceding two ways to set up the <code>chroot</code>, enter it using the <code>mounter</code> script: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>shell&nbsp;on&nbsp;Spot&nbsp;Instance</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id03af7baff8e3'><button class='copyBtn' data-clipboard-target='#id03af7baff8e3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>ubuntu@ip-10-0-0-193:~$ </span>ls <span class='unselectable'>mounter<br> ubuntu@ip-10-0-0-193:~$ </span>./mounter<br> <span class='unselectable'>ubuntu@ip-10-0-0-193:~$ </span>echo "127.0.1.1 $(hostname)" >> /etc/hosts<br> <span class='unselectable'>root@ip-10-0-0-193:/# </span>su ubuntu<br> <span class='unselectable'>ubuntu@ip-10-0-0-193:/$ </span># Do whatever you need to do<br> <span class='unselectable'>ubuntu@ip-10-0-0-193:/$ </span>sudo halt # Shut everything down</pre> </div> <!-- endregion --> <!-- endregion --> Scala-Style Lambda Function Placeholder Syntax in Python 3 2020-10-22T00:00:00-04:00 https://mslinn.github.io/blog/2020/10/22/scala-style-functional-programming-in-python-3 <p> Pipes are the ultimate in functional programming. It is clear when reading code that uses pipes that data is not mutated. Piping data into and out of lambda functions (and regular functions) is a succinct way of elegantly expressing a computation. This article discusses how to do this using similar syntax in Python 3, Scala 2 and Scala 3. </p> <p> After programming in Scala for more than ten years I have grown to appreciate Scala's implementation of lambda functions, including the ability to use the underscore character as a placeholder for variables. Scala 2.13 introduced pipelining between functions, which is rather like <a href='https://en.wikipedia.org/wiki/Unix-like' target='_blank' rel="nofollow">*nix</a> pipes between processes. </p> <p> Python 3 can also do something similar. This article demonstrates how to use <a href='https://github.com/sspipe/sspipe' target='_blank' rel="nofollow"><code>sspipe</code></a> and <a href='https://github.com/JulienPalard/Pipe' target='_blank' rel="nofollow">JulienPalard&rsquo;s <code>pipe</code></a> with Scala's underscore placeholder for <a href='https://stackoverflow.com/questions/29767310/pythons-lambda-with-underscore-for-an-argument' target='_blank' rel="nofollow">Python 3 lambda functions</a>. </p> <h2 id="pythonSetup">Python 3 Setup</h2> The key concept is to use this specific Python import: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7123b2e92a5a'><button class='copyBtn' data-clipboard-target='#id7123b2e92a5a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>from sspipe import p, px, px as _</pre> </div> <p> All the Python code examples that follow require this import. The Python code examples are modified versions of the <a href='https://github.com/sspipe/sspipe#examples' target='_blank' rel="nofollow"><code>sspipe</code> examples</a> to illustrate how to use underscores as placeholders. </p> <p> This import is unusual because it imports <code>px</code> twice: once as a normal import, and once aliased to <code>_</code>. I use the <code>_</code> alias to support Scala-like syntax, and I use <code>px</code> when I need to reference a parameter twice. Python is unlike Scala in that the Python compiler does not treat variables called <code>_</code> specially; those variables are merely called <code>_</code>. I could use <code>_</code> in Python code many times to refer to the same value, but a Scala programmer reading that code would expect that each reference to <code>_</code> would be another input parameter, not a regular variable reference. The examples that follow should make this clear. </p> <h3 id="installation">Python 3 Installation</h3> Install <code>sspipe</code> using <code>pip</code>: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1e60d35bba70'><button class='copyBtn' data-clipboard-target='#id1e60d35bba70' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pip install --upgrade sspipe</pre> </div> <h2 id="scalaSetup">Scala 2.13+ Setup</h2> <p> Scala 2.13 introduced <a href='https://www.scala-lang.org/api/current/scala/util/ChainingOps.html' target='_blank' rel="nofollow"><code>ChainingOps</code></a>, which adds chaining methods <code>tap</code> and <code>pipe</code> to every type. The key concept is to import the following prior to attempting the code examples below: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id87b081f3bbda'><button class='copyBtn' data-clipboard-target='#id87b081f3bbda' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>implicit class Smokin[A](val a: A) { import scala.util.chaining._ import scala.language.implicitConversions implicit def |>[B](f: (A) => B): B = a.pipe(f) }</pre> </div> <h2 id="examples">Python and Scala Usage Examples</h2> <h3 id="2-3">One Lambda Function and 1 Pipe</h3> <p> This Python example employs one lambda function and 1 pipe to add 2 to the number 5: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc2778d223483'><button class='copyBtn' data-clipboard-target='#idc2778d223483' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>5 | _ + 2 <span class='unselectable'>7 </span></pre> </div> <p> The Scala equivalent of the above is: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida131d734caa8'><button class='copyBtn' data-clipboard-target='#ida131d734caa8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>scala> </span>5 |> ((_: Int) + 2) <span class='unselectable'>val res0: Int = 7 </span></pre> </div> <h3 id="2-3">Two Lambda Functions and 2 Pipes</h3> <p> This Python example employs two lambda functions and 2 pipes to multiply the previous result by 5 and then add the previous result. Recall that I said that in Python, an underscore when used this way is the name of a normal variable and that the compiler does not treat underscores as placeholders for lambda parameters. A Scala programmer would complain about the following code, because they would expect that the second lambda function would require 2 inputs: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id48407ca863de'><button class='copyBtn' data-clipboard-target='#id48407ca863de' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>5 | _ + 2 | _ * 5 + _ <span class='unselectable'>42 </span></pre> </div> <p> A better way to write the above would be to use the special variable <code>px</code>, which was imported above. Now everyone either knows that <code>px</code> holds the piped value, or they complain about <code>px</code> being a magic variable. A possible solution to this complaint would be to alias <code>px</code> to a more descriptive name, such as <code>pipedValue</code> &mldr; which is still magical, but at least it is more descriptive. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8f19cacc4ba2'><button class='copyBtn' data-clipboard-target='#id8f19cacc4ba2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>5 | _ + 2 | px * 5 + px <span class='unselectable'>42 </span></pre> </div> The Scala equivalent of the above is: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf32b1a98b7bd'><button class='copyBtn' data-clipboard-target='#idf32b1a98b7bd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>scala> </span>5 |> ((_: Int) + 2) |> ((x: Int) => x * 5 + x) <span class='unselectable'>val res1: Int = 42 </span></pre> </div> <h3 id="2-3">Two Lambda Functions and 3 Pipes</h3> <p> This Python example employs 2 lambda functions and 3 pipes to add 10 to the even numbers from 0 to 5, exclusive. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4b90f8c3f9fc'><button class='copyBtn' data-clipboard-target='#id4b90f8c3f9fc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>( range(5) | p(filter, _ % 2 == 0) | p(map, _ + 10) | p(list) ) <span class='unselectable'>[10, 12, 14] </span></pre> </div> <p> Scala has a better way of performing this type of computation that does not require pipes or computation. It is better because it is simpler to understand. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4da3e38b3a80'><button class='copyBtn' data-clipboard-target='#id4da3e38b3a80' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>scala> </span>for { | x <- (0 until 5).toList if x % 2 == 0 | y = x + 10 | } yield y <span class='unselectable'>val res12: List[Int] = List(10, 12, 14) </span></pre> </div> <h2 id="other">Other examples of placeholder syntax</h2> <p> <code>NumPy</code> expressions (NumPy is Python-specific): </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id724059bcf156'><button class='copyBtn' data-clipboard-target='#id724059bcf156' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>range(10) | np.sin(_)+1 | p(plt.plot)</pre> </div> <p> Pandas expressions (Pandas is Python-specific): </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id13312b58a894'><button class='copyBtn' data-clipboard-target='#id13312b58a894' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>people_df | _.loc[_.age > 10, 'name']</pre> </div> <p> Solution for the <a href='https://projecteuler.net/problem=2' target='_blank' rel="nofollow">2nd Project Euler exercise</a>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idefa75f9cfadb'><button class='copyBtn' data-clipboard-target='#idefa75f9cfadb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>def fib(): a, b = 0, 1 while True: yield a a, b = b, a + b <span class='unselectable'>>>> </span>euler2 = ( fib() | p.where(_ % 2 == 0) | p.take_while(_ < 4000000) | p.add() ) <span class='unselectable'>>>> </span>euler2 4613732</pre> </div> <h2 id="dotty">Looking Ahead to Scala 3 (Dotty)</h2> <p> The next major version of Scala, due out in a few months, will probably allow a Scala 3 extension method to define the vertical bar as a method for more readabile code: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf4bb58ecc688'><button class='copyBtn' data-clipboard-target='#idf4bb58ecc688' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def [A,B](a: A) |(f: (A) => B): B = a.pipe(f) # Sample usage: val x = 5 | doSomething | doSomethingElse | doSomethingMore</pre> </div> <h2 id="scalacourses">To Learn More</h2> <p> My <a href='https://www.scalacourses.com/showCourse/40' target='_blank'>Introduction to Scala</a> course on ScalaCourses.com teaches Scala lambda functions. </p> Converting All Images in a Website to webp Format 2020-08-15T00:00:00-04:00 https://mslinn.github.io/blog/2020/08/15/converting-all-images-to-webp-format <!-- #region intro --> <!-- #region implicit --> <p> I first launched this website in 1996. Since then, it has been re-incarnated using many different technologies. Presently I use <a href='https://jekyllrb.com/' target='_blank' rel="nofollow">Jekyll</a> to assemble the site, then push the image to a web-enabled AWS S3 bucket that is edge-cached by an AWS CloudFront distribution. </p> <p> Until yesterday, the site contained images with a mixture of image formats. I decided to convert them all to the new <a href='https://developers.google.com/speed/webp' target='_blank' rel="nofollow"><code>webp</code></a> format. Because there are hundreds of images in over 120 web pages, I wrote a bash script called <code>toWebP</code> to do the work. This posting provides the <code>toWebP</code> script plus instructions on how you could use it for your website. </p> <p> The script converts image types <code>gif</code>, <code>jpg</code>, <code>jpeg</code>, <code>png</code>, <code>tif</code>, and <code>tiff</code>. It also modifies the HTML pages, CSS and SCSS that reference those images. </p> <p> The conversions are set for maximum fidelity (lossless where possible), and maximum compression. This means the images look great and load quickly. </p> <p> I also wrote <a href='#toPng'><code>toPng</code></a>, which works in a similar manner as <a href='#toWebp'><code>toWebp</code></a>. </p> <!-- endregion --> <!-- endregion --> <!-- #region caveat --> <h3 id="caveat">Caveat</h3> <p> The script assumes that all images are local to your website, which makes sense because the converted images need to be stored, and local storage is the only sensible option. It renames all references to images in HTML, CSS and SCSS files to <code>webp</code> format. If the images are remote (for example, on a CDN), they are not converted, but the image file types in the HTML, CSS and SCSS are adjusted anyway. I suppose I could fix the script, but I don&apos;t need to do that for myself. If someone needs that feature, go ahead and enhance the script... and please provide me the enhanced script, so I can update this articleing. </p> <!-- endregion --> <!-- #region prerequisites --> <h2 id="prerequisites">Prerequisites</h2> <p> You need to install the WebP package.<br> </p> <h3 id="mac">Mac</h3> <p> Use <a href='https://formulae.brew.sh/formula/webp' target='_blank' rel="nofollow">Homebrew</a> or <a href='https://ports.macports.org/?search=webp&search_by=name' target='_blank' rel="nofollow">Macports</a>. </p> <h3 id="ubuntu">Ubuntu (this is the default Linux distribution for Windows Subsystem for Linux)</h3> <p>At a shell prompt type:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide4c913868ecd'><button class='copyBtn' data-clipboard-target='#ide4c913868ecd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install webp</pre> </div> <!-- endregion --> <!-- #region running toWebp --> <h2 id="running">Running <span class="code">toWebp</span></h2> <p> The program may emit warnings when it runs. Those warnings can be safely ignored. </p> <p> Hopefully, your website is managed by git. I suggest that you commit your work before running the script. That way if something goes wrong you just have to type <code>git stash</code> to return your website to its previous state. </p> <h3 id="usage">Usage</h3> <p>The general form of the command to convert all images and modify the HTML pages that they are referenced from is:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8bffd1ab562e'><button class='copyBtn' data-clipboard-target='#id8bffd1ab562e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toWebp &lt;directoryName></pre> </div> <h3 id="examples">Examples</h3> <p>To convert the website (images, html, scss & css) rooted at the current directory, type:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9cc5412ef1f9'><button class='copyBtn' data-clipboard-target='#id9cc5412ef1f9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toWebp .</pre> </div> <p>To convert the website called <code>mySite</code> rooted under your home directory, type:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id52a243683896'><button class='copyBtn' data-clipboard-target='#id52a243683896' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toWebp ~/mySite</pre> </div> <p>To just convert 1 specific image to <code>webp</code>, type:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6fe274851be0'><button class='copyBtn' data-clipboard-target='#id6fe274851be0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toWebp images/blah.jpg</pre> </div> <h2 id="toWebp">The <span class="code">toWebP</span> Bash Script.</h2> <p> Put this file in one of the directories on your <code>PATH</code>, for example <code>/usr/local/bin</code>, or for Ubuntu users <code>~/.local/bin/</code>: </p> <div class="codeLabel">toWebp</div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idfa41ff7f0e16"><button class='copyBtn' data-clipboard-target='#idfa41ff7f0e16'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # SPDX-License-Identifier: Apache-2.0 shopt -s extglob export CMD="cwebp alpha_q 10 -exact -lossless -m 6 -short -q 100 -z 9" function help &#123; [[ "$1" ]] &amp;&amp; >&amp;2 printf "Error: $1\n\n"; >&amp;2 printf "$( basename $0 ) - Convert all images to webp files in place and update HTML, CSS and SCSS to suit Usage: toWebp directory|imageFileName Example: toWebp . # convert images, html, scss &amp; css in current directory tree Example: toWebp images/blah.jpg # just convert 1 specific image " &#125; function checkDependencies &#123; if [ -z "$( which cwebp )" ]; then echo "Installing webp" yes | sudo apt install webp fi &#125; function convertGifs &#123; for F in $( find "$1" -iname '*.gif' ); do TOF="$&#123;F%.*&#125;.webp" gif2webp -loop_compatibility -m 6 -mixed "$F" -q 100 -o "$TOF" done &#125; function convertMost &#123; # See "Extended pattern" extglob for Bash # https://wiki.bash-hackers.org/syntax/pattern#extended_pattern_language cd "$1" || exit 3 for F in $( listImages ); do TOF="$&#123;F%.*&#125;.webp" echo "Converting '$F' to '$TOF'" # Warning messages might be emitted, but don't worry $CMD "$F" -o "$TOF" if [ "$DELETE_OLD" ]; then rm "$F"; fi done cd - || exit &#125; function listImages &#123; # Does not return gifs because cwebp cannot handle them find . -iregex '.*\.\(jpg\|png\|jpeg\|tif\|tiff\)' -printf '%f\n' &#125; function renameImage &#123; sed -i "s,\b$1\b,.webp,g" "$2" &#125; function swapImages &#123; for F in $( find "$1" -iname '*.html' -o -iname '*.css' -o -iname '*.scss' ); do for X in .gif .jpg .jpeg .tif .tiff .png; do echo "Swapping $X images for .webp in '$F'" renameImage "$X" "$F" done done &#125; unset DELETE_OLD # TODO make this a command line option if [[ -f "$1" ]]; then # just convert a single image to webp F="$1" $CMD "$F" -o "$&#123;F%.*&#125;.webp" if [ "$DELETE_OLD" ]; then rm "$F"; fi elif [[ -d "$1" ]]; then # process all images, css and scss in directory shopt -s globstar convertMost "$1" convertGifs "$1" swapImages "$1" else help "You must either specify a valid file or a directory" fi </pre> <h3 id="chmod">Make it Executable</h3> <p> Remember to make the <code>toWebp</code> script executable before trying to use it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc43f8d2d32cd'><button class='copyBtn' data-clipboard-target='#idc43f8d2d32cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>chmod a+x /usr/local/bin/toWebp</pre> </div> <h3 id="helpToWebp">Help Message</h3> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd490bbc6c445'><button class='copyBtn' data-clipboard-target='#idd490bbc6c445' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toWebp <span class='unselectable'>Error: You must either specify a valid file or a directory toWebp - Convert all images to webp files in place and update HTML, CSS and SCSS to suit Usage: toWebp directory|imageFileName Example: toWebp . # convert images, html, scss & css in current directory tree Example: toWebp images/blah.jpg # just convert 1 specific image </span></pre> </div> <!-- endregion --> <!-- endregion --> <!-- #region toPng --> <h2 id="toPng">The <span class="code">toPng</span> Bash Script.</h2> <div class="codeLabel">toPng</div> <pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id318b8eb602e4"><button class='copyBtn' data-clipboard-target='#id318b8eb602e4'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash # Convert all webp images to png in place; does not update HTML, CSS or SCSS # Usage: toPng directory|imageFileName # Example: toPng . # convert images in current directory tree # Example: toPng images/blah.webp # just convert 1 specific image # # SPDX-License-Identifier: Apache-2.0 shopt -s extglob export CMD="dwebp" function help &#123; [[ "$1" ]] &amp;&amp; >&amp;2 printf "Error: $1\n\n"; >&amp;2 printf "$( basename $0 ) - Convert all images to webp files in place and update HTML, CSS and SCSS to suit Usage: $( basename $0 ) directory|imageFileName Example: $( basename $0 ) . # convert images, html, scss &amp; css in current directory tree Example: $( basename $0 ) images/blah.jpg # just convert 1 specific image " &#125; function checkDependencies &#123; if [ -z "$(which dwebp)" ]; then echo "Installing webp" yes | sudo apt install webp fi &#125; function listImages &#123; find . -type f -iname '*.webp' &#125; function convert &#123; # See "Extended pattern" extglob for Bash # https://wiki.bash-hackers.org/syntax/pattern#extended_pattern_language cd "$1" || exit for F in $( listImages ); do TOF="$&#123;F%.*&#125;.png" echo "Converting '$F' to '$TOF'" # Warning messages might be emitted, but don't worry $CMD "$F" -o "$TOF" if [ "$DELETE_OLD" ]; then rm "$F"; fi done cd - || exit &#125; # Set cwd to project root GIT_ROOT="$( git rev-parse --show-toplevel )" cd "$&#123;GIT_ROOT&#125;" || exit [[ -f _bin/loadConfigEnvVars ]] &amp;&amp; source _bin/loadConfigEnvVars; unset DELETE_OLD # TODO make this a command line option checkDependencies if [ -f "$1" ]; then # just convert a single image to png F="$1" $CMD "$F" -o "$&#123;F%.*&#125;.png" if [ "$DELETE_OLD" ]; then rm "$F"; fi elif [ -d "$1" ]; then # process all images, css and scss in directory convert "$1" else help "You must either specify a valid file or a directory." fi </pre> <h3 id="chmod">Make it Executable</h3> <p> Remember to make the <code>toWebp</code> script executable before trying to use it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5922ca430078'><button class='copyBtn' data-clipboard-target='#id5922ca430078' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>chmod a+x /usr/local/bin/toPng</pre> </div> <h3 id="helpToPng">Help Message</h3> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide038e3c8e637'><button class='copyBtn' data-clipboard-target='#ide038e3c8e637' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>toPng <span class='unselectable'>Error: You must either specify a valid file or a directory. toPng - Convert all images to webp files in place and update HTML, CSS and SCSS to suit. Usage: toWebp directory|imageFileName Example: toWebp . # convert images, html, scss & css in current directory tree Example: toWebp images/blah.jpg # just convert 1 specific image </span></pre> </div> <!-- endregion --> <!-- endregion --> Dotty (Scala 3 Preview) Presentation at Hopper, Montreal 2019-11-28T00:00:00-05:00 https://mslinn.github.io/blog/2019/11/28/dotty-scala-3-preview <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/dotty/dottyLambda_690x388.svg" type="image/svg"> <!---<source srcset="/blog/images/dotty/dottyLambda_690x388.avif" type="image/avif">--> <source srcset="/blog/images/dotty/dottyLambda_690x388.webp" type="image/webp"> <source srcset="/blog/images/dotty/dottyLambda_690x388.apng" type="image/apng"> <source srcset="/blog/images/dotty/dottyLambda_690x388.png" type="image/png"> <source srcset="/blog/images/dotty/dottyLambda_690x388.jpg" type="image/jpeg"> <source srcset="/blog/images/dotty/dottyLambda_690x388.jpeg" type="image/jpeg"> <source srcset="/blog/images/dotty/dottyLambda_690x388.jfif" type="image/jpeg"> <source srcset="/blog/images/dotty/dottyLambda_690x388.pjpeg" type="image/jpeg"> <source srcset="/blog/images/dotty/dottyLambda_690x388.pjp" type="image/jpeg"> <source srcset="/blog/images/dotty/dottyLambda_690x388.gif" type="image/gif"> <source srcset="/blog/images/dotty/dottyLambda_690x388.tif" type="image/tiff"> <source srcset="/blog/images/dotty/dottyLambda_690x388.tiff" type="image/tiff"> <source srcset="/blog/images/dotty/dottyLambda_690x388.bmp" type="image/bmp"> <source srcset="/blog/images/dotty/dottyLambda_690x388.ico" type="image/x-icon"> <source srcset="/blog/images/dotty/dottyLambda_690x388.cur" type="image/x-icon"> <img alt='Mike Slinn presents' class="imgImg liImg" src="/blog/images/dotty/dottyLambda_690x388.png" style='width: 100%; ' title='Mike Slinn presents' /> </picture> </div> <div style="text-align: center"> <p> Yesterday I presented <a href='https://www.meetup.com/lambda-montreal/events/266306046/' target='_blank'>Dotty (Scala 3 Preview)</a> to Lambda Montreal. </p> <p> The slides are <a href='https://www.slideshare.net/mslinn/dotty-scala-3-preview' target='_blank'>here</a>. </p> <p> The code is <a href='https://github.com/mslinn/dotty-example-project/' target='_blank'>here</a>. </p> <p> The video recording is <a href='https://www.youtube.com/watch?v=7S68TY0S2e0' target='_blank'>here</a>. </p> </div> <div class='imgWrapper imgFlex center quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/dotty/lambdaMontreal.svg" type="image/svg"> <!---<source srcset="/blog/images/dotty/lambdaMontreal.avif" type="image/avif">--> <source srcset="/blog/images/dotty/lambdaMontreal.webp" type="image/webp"> <source srcset="/blog/images/dotty/lambdaMontreal.apng" type="image/apng"> <source srcset="/blog/images/dotty/lambdaMontreal.png" type="image/png"> <source srcset="/blog/images/dotty/lambdaMontreal.jpg" type="image/jpeg"> <source srcset="/blog/images/dotty/lambdaMontreal.jpeg" type="image/jpeg"> <source srcset="/blog/images/dotty/lambdaMontreal.jfif" type="image/jpeg"> <source srcset="/blog/images/dotty/lambdaMontreal.pjpeg" type="image/jpeg"> <source srcset="/blog/images/dotty/lambdaMontreal.pjp" type="image/jpeg"> <source srcset="/blog/images/dotty/lambdaMontreal.gif" type="image/gif"> <source srcset="/blog/images/dotty/lambdaMontreal.tif" type="image/tiff"> <source srcset="/blog/images/dotty/lambdaMontreal.tiff" type="image/tiff"> <source srcset="/blog/images/dotty/lambdaMontreal.bmp" type="image/bmp"> <source srcset="/blog/images/dotty/lambdaMontreal.ico" type="image/x-icon"> <source srcset="/blog/images/dotty/lambdaMontreal.cur" type="image/x-icon"> <img alt='Mike Slinn presents' class="imgImg rounded shadow" src="/blog/images/dotty/lambdaMontreal.png" style='width: 100%; ' title='Mike Slinn presents' /> </picture> </div> A Hybrid Machine Learning / Personality Simulation Platform 2019-10-24T00:00:00-04:00 https://mslinn.github.io/blog/2019/10/24/hybrid-ml-simulation <div class='imgWrapper imgFlex center' style=' '> <a href='https://www.meetup.com/MTL-Machine-Learning/events/265039754/' target='_blank' rel='nofollow' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/empathyWorks/mtlMLconference.svg" type="image/svg"> <!---<source srcset="/blog/images/empathyWorks/mtlMLconference.avif" type="image/avif">--> <source srcset="/blog/images/empathyWorks/mtlMLconference.webp" type="image/webp"> <source srcset="/blog/images/empathyWorks/mtlMLconference.apng" type="image/apng"> <source srcset="/blog/images/empathyWorks/mtlMLconference.png" type="image/png"> <source srcset="/blog/images/empathyWorks/mtlMLconference.jpg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/mtlMLconference.jpeg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/mtlMLconference.jfif" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/mtlMLconference.pjpeg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/mtlMLconference.pjp" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/mtlMLconference.gif" type="image/gif"> <source srcset="/blog/images/empathyWorks/mtlMLconference.tif" type="image/tiff"> <source srcset="/blog/images/empathyWorks/mtlMLconference.tiff" type="image/tiff"> <source srcset="/blog/images/empathyWorks/mtlMLconference.bmp" type="image/bmp"> <source srcset="/blog/images/empathyWorks/mtlMLconference.ico" type="image/x-icon"> <source srcset="/blog/images/empathyWorks/mtlMLconference.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/empathyWorks/mtlMLconference.png" style='width: 100%; ' /> </picture> </a> </div> <p> Yesterday I presented <a href='https://www.meetup.com/MTL-Machine-Learning/events/265039754/' target='_blank' rel="nofollow">&ldquo;EmpathyWorks: A Hybrid Machine Learning / Personality Simulation Platform&rdquo;</a> to the <a href='https://www.meetup.com/MTL-Machine-Learning/events/265039754/' target='_blank' rel="nofollow">Fall 2019 Montreal Machine Learning Mini-Conference</a>. </p> <p> The slides are <a href='https://www.slideshare.net/mslinn/empathyworks-towards-an-eventbased-simulationml-hybrid-platform' target='_blank' rel="nofollow">here</a>. The video recording is <a href='https://youtu.be/PiDsiyJIMmo' target='_blank' rel="nofollow">here</a>. </p> <div style="text-align: center"> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/assets/images/robotCircle207x207.svg" type="image/svg"> <!---<source srcset="/assets/images/robotCircle207x207.avif" type="image/avif">--> <source srcset="/assets/images/robotCircle207x207.webp" type="image/webp"> <source srcset="/assets/images/robotCircle207x207.apng" type="image/apng"> <source srcset="/assets/images/robotCircle207x207.png" type="image/png"> <source srcset="/assets/images/robotCircle207x207.jpg" type="image/jpeg"> <source srcset="/assets/images/robotCircle207x207.jpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle207x207.jfif" type="image/jpeg"> <source srcset="/assets/images/robotCircle207x207.pjpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle207x207.pjp" type="image/jpeg"> <source srcset="/assets/images/robotCircle207x207.gif" type="image/gif"> <source srcset="/assets/images/robotCircle207x207.tif" type="image/tiff"> <source srcset="/assets/images/robotCircle207x207.tiff" type="image/tiff"> <source srcset="/assets/images/robotCircle207x207.bmp" type="image/bmp"> <source srcset="/assets/images/robotCircle207x207.ico" type="image/x-icon"> <source srcset="/assets/images/robotCircle207x207.cur" type="image/x-icon"> <img alt='Mike Slinn presents' class="imgImg liImg" src="/assets/images/robotCircle207x207.png" style='width: 100%; ' title='Mike Slinn presents' /> </picture> </div> <div class='imgWrapper imgFlex inline quartersize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.svg" type="image/svg"> <!---<source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.avif" type="image/avif">--> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.webp" type="image/webp"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.apng" type="image/apng"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.png" type="image/png"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.jpg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.jpeg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.jfif" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.pjpeg" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.pjp" type="image/jpeg"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.gif" type="image/gif"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.tif" type="image/tiff"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.tiff" type="image/tiff"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.bmp" type="image/bmp"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.ico" type="image/x-icon"> <source srcset="/blog/images/empathyWorks/montrealMachineLearningMeetup.cur" type="image/x-icon"> <img alt='Montreal Machine Learning Meetup' class="imgImg rounded shadow" src="/blog/images/empathyWorks/montrealMachineLearningMeetup.png" style='width: 100%; margin-left: 3em; margin-top: 3em;' title='Montreal Machine Learning Meetup' /> </picture> </div> </div> Decentralized Ponytails 2018-09-13T00:00:00-04:00 https://mslinn.github.io/blog/2018/09/13/decentralized-ponytails <p> I&rsquo;d like to point out the similarity of the early days of the open-source movement with today&rsquo;s decentralized blockchain movement. </p> <p> Open-source software was brought to mainstream attention during the last technology bubble at the end of the last millennium. The open-source software movement had a loyal cadre of zealots who believed that their cause would overcome any need for a business case. Sun Microsystems was the hardware company whose servers powered the Internet, and their software included the Java programming language, plus many other important networking-related products. Sun's slogan was &ldquo;The network is the computer&rdquo;. </p> <div class='imgWrapper imgFlex center' style='width: 40%; '> <picture class='imgPicture'> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.svg" type="image/svg"> <!---<source srcset="/blog/images/ponytails/Sun-Logo_225x99.avif" type="image/avif">--> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.webp" type="image/webp"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.apng" type="image/apng"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.png" type="image/png"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.jpg" type="image/jpeg"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.jpeg" type="image/jpeg"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.jfif" type="image/jpeg"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.pjp" type="image/jpeg"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.gif" type="image/gif"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.tif" type="image/tiff"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.tiff" type="image/tiff"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.bmp" type="image/bmp"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.ico" type="image/x-icon"> <source srcset="/blog/images/ponytails/Sun-Logo_225x99.cur" type="image/x-icon"> <img alt='Sun Microsystems logo' class="imgImg rounded shadow" src="/blog/images/ponytails/Sun-Logo_225x99.png" style='width: 100%; padding: 1em' title='Sun Microsystems logo' /> </picture> </div> <p> Jonathan Schwartz, the CEO of Sun Microsystems was one of the open-source zealots. He was famous for his ponytail. Unfortunately, zealotry and dogma is bad for business, and as a result Sun Microsystems is no longer with us. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/ponytails/jonathanSchwartz.svg" type="image/svg"> <!---<source srcset="/blog/images/ponytails/jonathanSchwartz.avif" type="image/avif">--> <source srcset="/blog/images/ponytails/jonathanSchwartz.webp" type="image/webp"> <source srcset="/blog/images/ponytails/jonathanSchwartz.apng" type="image/apng"> <source srcset="/blog/images/ponytails/jonathanSchwartz.png" type="image/png"> <source srcset="/blog/images/ponytails/jonathanSchwartz.jpg" type="image/jpeg"> <source srcset="/blog/images/ponytails/jonathanSchwartz.jpeg" type="image/jpeg"> <source srcset="/blog/images/ponytails/jonathanSchwartz.jfif" type="image/jpeg"> <source srcset="/blog/images/ponytails/jonathanSchwartz.pjpeg" type="image/jpeg"> <source srcset="/blog/images/ponytails/jonathanSchwartz.pjp" type="image/jpeg"> <source srcset="/blog/images/ponytails/jonathanSchwartz.gif" type="image/gif"> <source srcset="/blog/images/ponytails/jonathanSchwartz.tif" type="image/tiff"> <source srcset="/blog/images/ponytails/jonathanSchwartz.tiff" type="image/tiff"> <source srcset="/blog/images/ponytails/jonathanSchwartz.bmp" type="image/bmp"> <source srcset="/blog/images/ponytails/jonathanSchwartz.ico" type="image/x-icon"> <source srcset="/blog/images/ponytails/jonathanSchwartz.cur" type="image/x-icon"> <img alt='Jonathan Schwartz and his ponytail' class="imgImg rounded shadow" src="/blog/images/ponytails/jonathanSchwartz.png" style='width: 100%; ' title='Jonathan Schwartz and his ponytail' /> </picture> <figcaption class='imgFigCaption halfsize'> Jonathan Schwartz and his ponytail </figcaption> </figure> </div> <p> Eventually, companies like <a href='https://redhat.com' target='_blank' rel="nofollow">Red Hat</a> developed solid business models for open-source software, but that took years to develop. Today we see many companies attempting using decentralized blockchain technology to create cryptocurrencies, other token-based economies, and evangelizing decentralized dogma without a solid business case. Most of these ventures will die a horrible death, and the investors will get nothing. It will take years for solid business models based on decentralization to be proven. </p> <p> Mr. Schwartz's ponytail was the fashion statement that fueled the YouTube parody below. For background, <a href='https://en.wikipedia.org/wiki/Scott_McNealy' target='_blank' rel="nofollow">Scott McNealy</a> was the previous CEO at Sun Microsystems. </p> <iframe allow="autoplay; encrypted-media" allowfullscreen class="rounded shadow liImg" frameborder="0" height="388" src="https://www.youtube.com/embed/5r3JSciJf5M" width="690" ></iframe> <p> Full disclosure: I also had a ponytail in 2008, and for a few years I had my Sun Spark 2 workstation at home. </p> <div class='imgWrapper imgBlock center' style='width: 300px; '> <figure> <picture class='imgPicture'> <source srcset="/images/mike/mikeclose3.svg" type="image/svg"> <!---<source srcset="/images/mike/mikeclose3.avif" type="image/avif">--> <source srcset="/images/mike/mikeclose3.webp" type="image/webp"> <source srcset="/images/mike/mikeclose3.apng" type="image/apng"> <source srcset="/images/mike/mikeclose3.png" type="image/png"> <source srcset="/images/mike/mikeclose3.jpg" type="image/jpeg"> <source srcset="/images/mike/mikeclose3.jpeg" type="image/jpeg"> <source srcset="/images/mike/mikeclose3.jfif" type="image/jpeg"> <source srcset="/images/mike/mikeclose3.pjpeg" type="image/jpeg"> <source srcset="/images/mike/mikeclose3.pjp" type="image/jpeg"> <source srcset="/images/mike/mikeclose3.gif" type="image/gif"> <source srcset="/images/mike/mikeclose3.tif" type="image/tiff"> <source srcset="/images/mike/mikeclose3.tiff" type="image/tiff"> <source srcset="/images/mike/mikeclose3.bmp" type="image/bmp"> <source srcset="/images/mike/mikeclose3.ico" type="image/x-icon"> <source srcset="/images/mike/mikeclose3.cur" type="image/x-icon"> <img alt='Mike Slinn and his ponytail back in 2008' class="imgImg rounded shadow" src="/images/mike/mikeclose3.png" style='width: 100%; ' title='Mike Slinn and his ponytail back in 2008' /> </picture> <figcaption class='imgFigCaption '> Mike Slinn and his ponytail back in 2008 </figcaption> </figure> </div> <p> Here is a transcription of the video, which I paraphrased for clarity:</p> </p> <div class="quote"> <p><b>Steve Gilmore:</b> Hi, this is Steve Gilmore and this is a video special edition of the Gilmore gang. I'm here with Jonathan Schwartz. It's a great pleasure &ndash; it's been a long, long time coming &ndash; I haven't seen Jonathan for quite a while. Jonathan Schwartz, who is the president and CEO of Sun Microsystems, agreed to sit down for the first time in three or four years and talk about what's going on with Sun. I wanna start, Jonathan, by thanking you for joining us. </p> <p><b>Jonathan Schwartz:</b> Thank you for having me, Steve. It has been a long time, nice to see you. </p> <p><b>Steve Gilmore:</b> So, you know there's been a lot of turmoil on Wall Street as I know you know. </p> <p><b>Jonathan Schwartz:</b> Yes. </p> <p><b>Steve Gilmore:</b> What's your take on that? </p> <p><b>Jonathan Schwartz:</b> Well, I think that it's a cyclical thing as you know Sun was been very prepared for this because we took <a href='https://www.thestreet.com/story/10334514/1/pipe-deal-shines-up-sun.html' target='_blank' rel="nofollow">three quarters of a billion dollars off of KKR</a> a few years ago so that's in our war chest and I think that we're in a very good position moving forward, Steve. </p> <p><b>Steve Gilmore:</b> Ahh, and specifically what are you doing? </p> <p><b>Jonathan Schwartz:</b> Well, what we're doing is as you know Sun has always been very proactive in the open-source movement. I know you're very familiar with that. What we want to do to help our customers during this very difficult time is keep up with that trend of open-source. So I'm actually very pleased to announce, Steve, that Sun is starting a new open-source initiative. We're going to be releasing the source code to my ponytail as open-source, Steve. </p> <p><b>Steve Gilmore:</b> How's that going to help the situation? </p> <p><b>Jonathan Schwartz:</b> It's open-source, Steve. </p> <p><b>Steve Gilmore:</b> Yeah. </p> <p><b>Jonathan Schwartz:</b> It's my ponytail, Steve. </p> <p><b>Steve Gilmore:</b> You know, we've had this conversation in the past. </p> <p><b>Jonathan Schwartz:</b> Yes. </p> <p><b>Steve Gilmore:</b> Something is open-source, fine, and you get a lot of adoption, you get a lot of exposure in the in the marketing arena... </p> <p><b>Jonathan Schwartz:</b> Yes. </p> <p><b>Steve Gilmore:</b> ... but how do you make money on your ponytail? </p> <p><b>Jonathan Schwartz:</b> Well, what we're going to be doing is releasing my ponytail as open-source. So what we're hoping is that our developers take my ponytail and develop some kind of revenue stream with my ponytail. Did I mention this is open-source, Steve? </p> <p><b>Steve Gilmore:</b> Yeah, so how do you open-source a ponytail? What does that mean? </p> <p><b>Jonathan Schwartz:</b> Well, basically what we do is, we will have some of our best and brightest engineers here at Sun go through my ponytail and find out the unique attributes about what makes my ponytail so successful in the valley. And what we've done Steve, we've open-sourced my ponytail, Steve. </p> <p><b>Steve Gilmore:</b> Jonathan, you're just repeating this over and over again; it doesn't necessarily arrive at a business model. </p> <p><b>Jonathan Schwartz:</b> Umm, Steve? </p> <p><b>Steve Gilmore:</b> Yeah. </p> <p><b>Jonathan Schwartz:</b> We're gonna take my ponytail right and make it open-source. Now I know that this is a big concept for you but I really think it's a game changer, Steve. </p> <p><b>Steve Gilmore:</b> So, who do you see as your competition in the open-source ponytail arena? </p> <p><b>Jonathan Schwartz:</b> I think that we pretty much have it locked up. I don't see anybody who can compete with Sun Microsystems, when it comes to the open-source ponytail market. I think that we're in very good shape, Steve. How much do you miss Scott McNealy? </p> <p><b>Steve Gilmore:</b> Right now, a lot. </p> <p><b>Jonathan Schwartz:</b> Not nearly as much as I do, Steve. </p> <p><b>Steve Gilmore:</b> Okay, so what are you gonna do about, uh, you've laid off a lot of people in the last few months. </p> <p><b>Jonathan Schwartz:</b> Yes, it's going well in fact we have another round of layoffs coming. Once the ponytail is released into the wild, we'll be releasing the team that open-sourced my ponytail, Steve. </p> <p><b>Steve Gilmore:</b> Well you know I have to say that I was hoping for something a little bit more visionary from you Jonathan. </p> <p><b>Jonathan Schwartz:</b> Well I think that this is quite visionary. I don't think that IBM will be releasing a ponytail, and if they did it certainly wouldn't be open-source, Steve. We are very, very excited about our open-source ponytail program if you want you can go to <code>sunmicrosystems.com/ponytail</code>. Any other questions, Steve? </p> <p><b>Steve Gilmore:</b> Yeah I got one that I hope will be a stumper for you, which is a do you see the relationship between your open-source ponytail strategy and micro-messaging as popularized by Twitter? </p> <p><b>Jonathan Schwartz:</b> I'd like to open the pipe to the ponytail. Except as you know, Twitter is not exactly handling XMPP correctly at this time. I don't see the correlation between an open-source ponytail, and a closed-off micro-blogging system. Steve, I think that the ponytail is much bigger than Twitter. </p> <p><b>Steve Gilmore:</b> And the business model again? </p> <p><b>Jonathan Schwartz:</b> Let me see this real slowly and clearly: OPEN. SOURCE. PONYTAIL. </p> <p><b>Steve Gilmore:</b> This has been Jonathan Schwartz along with me, Steve Gilmore. Good luck, Jon. </p> <p><b>Jonathan Schwartz:</b> Thank you Steve. God, I miss McNealy. Think it's going to work? </p> <p><b>Steve Gilmore:</b> No. </p> </div> Evaluating Technology Companies 2018-09-12T00:00:00-04:00 https://mslinn.github.io/blog/2018/09/12/evaluating-tech-companies <!-- #region intro --> <p> I have performed evaluations for investors since 1985, and for legal cases since 1998. I typically assess the people, the processes they follow, and the technology they work with. Although the goals of assessments performed for investors are different from the goals of the legal team that retain me as an expert, the work that I do for both types of engagements is in many ways quite similar. </p> <!-- endregion --> <!-- #region Common Aspects --> <h2 id="common">Aspects Common to Investment and Ligigation Engagements</h2> <p> The major steps that I follow for both types of engagements are: </p> <ol> <li>Examine evidence and interview the people involved</li> <li>Perform analysis</li> <li>Write a report</li> <li>Present the report, and defend or discuss it</li> </ol> <p> Strategic risks typically involve technical and business considerations. I assess the strengths and weaknesses of the technical team, the technology they use, and the processes that they follow. I am also called upon to opine on product/market fit; is this a solution that addresses a burning unmet need in a potentially lucrative market in a given time period? </p> <p> Some engagements require me to develop an opinion on whether significant intellectual property was developed and applied effectively. I develop an opinion on whether the company in question is or was able to generate and profitably maintain and evolve a product or service that meets compelling unmet customer needs in various market segments. </p> <p> I generally inspect the following primary sources of data for assessments: </p> <ol> <li> My notes from personally using the product or service, stress-testing it, and performing failure mode analysis </li> <li> One-on-one interviews of employees and customers (when possible) </li> <li> Examining correspondence and working documents (Jira logs, git logs, internal product requirements and customer-facing documentation) </li> </ol> <p> While assessments for legal cases have many similarities with assessments for investors, the approach is usually quite different. In addition, investors are often interested in sensitivity analysis, while legal cases generally do not require that. </p> <!-- endregion --> <!-- #region Investments --> <h2 id="invest">Investments</h2> <p> Investors are primarily interested in minimizing risk and maximizing reward. Investors often request assessments of technology companies to determine if a company might be a good investment, to optimize the performance of an investment, to identify risk and opportunity. </p> <!-- endregion --> <!-- #region Legal Cases --> <h2 id="legal">Legal Cases</h2> <p> Legal cases often involve assigning blame, quantifying loss or missed opportunity. </p> <p> Some cases engagements require an opinion on whether significant intellectual property was developed and applied effectively. Legal cases often require an assessment of how the suitability of a company’s product or service existed at a point in time, or how it changed over time. As an expert, I am often called upon to discuss the relative strengths of the arguments of the parties in a dispute from a technology perspective. </p> <!-- endregion --> <!-- #region 1:1 Interviews --> <h2 id="onetoone">1:1 Interviews</h2> <p> I try to individually interviewing everyone in the company. To the extent possible, I take each person’s photograph, gather job descriptions, resumes and their work history. The information obtained from the lowest, most recently hired person is valuable and important. The most influential people cannot help but apply spin to their story. I look for evidence to support every piece of information that is presented to me. </p> <p> Open-ended questions yield much more interesting and insightful responses than specific questions. Multiple-choice questions have limited value. When interviewing someone, I recognize that all experiences are individual; I never attempt to equate my experience with those that I am interviewing. These conversations are not self-promotional opportunities. </p> <p> Each interview takes 45 minutes, followed by 5 minutes of note taking. I prepare for each interview by reading the resume, job description, and any other information before meeting with them. I can process about 40 people a week this way. </p> <p> If there are more people than I can interview in the time available, I interview everyone from the front-line managers on up, plus a random selection of the others. I do not allow the company to select the lower-level people because I want to know of problems. Generally I interview the top level twice: a half-hour interview at the beginning, then a 90-minute second interview at the end. </p> <!-- endregion --> <!-- #region Overspecialization vs. Resilience --> <h2 id="resil">Overspecialization vs. Resilience</h2> <p> The more highly specialized and optimized an animal or an organization is, the more fragile it is in an evolutionary sense. Attaining product/market fit is a continuous process, not a one-time event. Markets evolve, products mature, and organizations are social constructs subject to politics and an ever-changing historical interpretation. </p> <p> I try to evaluate the interdependence, resilience and resourcefulness of the individuals in an organization. Cross training compensates for highly specialized jobs; highly trained people like PhDs generally know more and more about less and less. Extremely specialized people are generally unaware of the big picture; instead, they view events through their personal microscope. </p> <!-- endregion --> <!-- #region Resilience Superposition --> <h2 id="super">Resilience Superposition</h2> <p> Resilience can only be measured by testing it, and testing an organization’s resilience changes it. This is similar to the story in the field quantum mechanics, which asks if Schrodinger’s cat is alive or dead within a box. </p> <!-- endregion --> <!-- #region Change Is Traumatic --> <h2 id="trauma">Change Is Traumatic</h2> <p> I generally try to anticipate the results of two sources of change: </p> <ol> <li>A large investment or a change in ownership</li> <li>Market changes</li> </ol> <p> A large investment or a change in ownership can cause a fundamental behavioral change in the existing company's leadership. Replacing the leadership or changing their behavior is usually a traumatic event for the organization. </p> <p> Because markets change, an organization that lacks resilience may crumble in the face of dramatic market shifts. I try to evaluate an organization’s ability to continually anticipate and respond to changes in product/market fit. It is all mental, a social construct, stories that the people who work at a company have internalized. </p> <!-- endregion --> <!-- #region Anarchists Are The Best Assessors --> <h2 id="anarchy">Anarchists Are The Best Assessors</h2> <p> One of the primary tasks of an assessor is to question the boundaries of the target market and the product or service. Rarely do those boundaries remain unchanged through the lifespan of the product or service, or the company that produces them. </p> <p> Anarchists perform the most useful assessments. An assessor cannot be effective if they restrict themselves to coloring within the given outlines; by this I mean respecting the status quo cannot yield an objective or useful assessment. </p> <!-- endregion --> IBM Personality Insights 2018-08-29T00:00:00-04:00 https://mslinn.github.io/blog/2018/08/29/personality-assessment <style> .comment { color: magenta; font-style: italic; } .diff { color: green; } </style> <p> EmpathyWorks&trade; is my name for the original research I've done on modeling personality and behavior of individuals and groups since 2007. For me, the work has been both therapeutic and insightful, and I now have a better basis for understanding myself and others as a result. </p> <p> Recently, an IBM employee pointed me to <a href='https://console.bluemix.net/docs/services/personality-insights' target='_blank' rel="nofollow">IBM Personality Insights</a>, and I eagerly visited the site. The <a href='https://console.bluemix.net/docs/services/personality-insights/science.html#science' target='_blank' rel="nofollow">scientific basis</a> for the results is interesting. I am greatly interested in the analysis that IBM Personality Insights provided of the articleing I wrote on my birthday last year. I also submitted a <a href='/blog/2008/04/28/cult-of-software-god.html'>short humorous posting</a> I wrote ten years ago for analysis. </p> <h2 id="salt">A Modern Horoscope?</h2><p> I think the results from IBM Personality Insights are about as accurate as a horoscope. <a href='https://www.quora.com/How-accurate-is-IBMs-Watson-Personality-Insights-application/answer/Abhishek-Srivastava-198' target='_blank' rel="nofollow">Abhishek Srivastava&rsquo;s Quora posting</a> of November 18, 2016, expresses this well. </p> <div class="quote"> I think most answers here have missed the point of IBM Watson’s personality insight service. It is a NLP based approach to find scores of individuals on some well-established scales in psychology like Big 5, Basic Humans Values and Needs. So, when those numbers are arrived using those standard questionnaires or through text analysis done by Watson, they are pretty close statistically. In that respect Watson is definitely very accurate. As far as interpretation of those results are concerned, that’s beyond the purview of current scope of Watson and rather is a question for psychologist. If I was a team member at IBM Watson, I would have actually not given the interpretation and would rather just give the scores and let people interpret them. </div> <p> Furthermore, until a peer review of IBM Personality Insights concludes that the results are well-founded, I would not be comfortable using the technology for decision-making. </p> <p> To be fair, <a href='https://www.news.vcu.edu/article/An_untested_foundation_A_VCU_study_finds_that_many_published' target='_blank' rel="nofollow">a recent examination</a> of nearly 350 published psychological experiments found that 42% failed to show that they were based on a valid foundation of empirical evidence, suggesting that a wide swath of psychological science is based on an untested foundation. With that in mind, let's see what this modern horoscope serves up! </p> <h2 id="highLevel">High Level Results</h2> <p> Results were fairly consistent between the two articleings. When I concatenated the two posts, the longer article dominated. <span class="diff">Differences between the two personality assessments are shown this way.</span> <span class="comment">My comments are shown this way.</span> </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify" width="100%"> <tr> <th width="50%">Birthday Posting Summary</th> <th width="50%"><a href='/blog/2008/04/28/cult-of-software-god.html'>Humorous Posting</a> Summary</th> </tr> <tr> <td> The results in JSON format are <a href='/blog/ibmPersonality/ibmPersonality.json'>here</a>. </td> <td> The results in JSON format are <a href='/blog/ibmPersonality/ibmPersonality2.json'>here</a>. </td> </tr> <tr> <td> You are shrewd and <span class="diff">skeptical</span>. </td> <td> You are shrewd<span class="diff">, inner-directed and guarded</span>. </td> </tr> <tr> <td> You are philosophical: you are open to and intrigued by new ideas and love to explore them. You are independent: you have a strong desire to have time to yourself. <span class="diff">And you are authority-challenging: you prefer to challenge authority and traditional values to help bring about positive changes.</span> <td> You are philosophical: you are open to and intrigued by new ideas and love to explore them. You are independent: you have a strong desire to have time to yourself. <span class="diff">And you are solemn: you are generally serious and do not joke much.</span> <span class="comment">I guess IBM did not like my humor!</span> </td> </tr> <tr> <td> Your choices are driven by a desire for <span class="diff">discovery</span> <span class='comment'>often true</span>. </td> <td> Your choices are driven by a desire for <span class="diff">organization</span> <span class='comment'>often true</span>. </td> </tr> <tr> <td> You are relatively unconcerned with both <span class="diff">tradition</span> and taking pleasure in life. <span class="diff">You care more about making your own path than following what others have done. And you prefer activities with a purpose greater than just personal enjoyment.</span> </td> <td> You are relatively unconcerned with both <span class="diff">achieving success</span> and taking pleasure in life. <span class="diff">You prefer activities with a purpose greater than just personal enjoyment. And you make decisions with little regard for how they show off your talents.</span> </td> </tr> </table> <p> An old friend, who I've known since university, is a clinical psychologist with a PhD and works as a doctor in a mental hospital. His comments on the results were:</p> <div class="quote"> <p> I think that some aspects of the profile are pretty accurate; however, other aspects such as calling you shrewd and skeptical are a bit evaluate: I would instead say intelligent and not naïve. The openness results would seem pretty accurate. </p> <p> I think you're probably a bit more extraverted than this analysis suggests. </p> <p> The ‘emotional range’ factor of the Big 5 is typically labeled Neuroticism. I kind of think of Agreeableness as a compliance/ conformity dimension, and I if I am not mistaken, it is not unusual for (male) entrepreneurs to score low on that dimension. </p> </div> <h2 id="more">But Wait, There's More!</h2> <p> IBM's Personality Insights provides additional information that explains the above in more detail: </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify"> <tr> <th width="50%">Birthday Posting Summary</th> <th width="50%"><a href='/blog/2008/04/28/cult-of-software-god.html'>Humorous Posting</a> Summary</th> </tr> <tr> <td> You are <b>likely</b> to: <ul> <li> <span class="diff">like musical movies</span><br /> <span class="comment">Oops, that was way off!</span> </li> <li> be sensitive to ownership cost when buying automobiles </li> <li> have experience playing music <br /> <span class="comment">True: I am a multi-instrumentalist</span> </li> </ul> </td> <td> You are <b>likely</b> to: <ul> <li> <span class="diff">like historical movies</span> <br /> <span class="comment">True!</span></li> <li>be sensitive to ownership cost when buying automobiles </li> <li> have experience playing music</li> </ul> </td> </tr> <tr> <td> You are <b>unlikely</b> to: <ul> <li> be influenced by social media during product purchases <span class="comment">The truth is more complex</span> </li> <li> prefer style when buying clothes <span class="comment">True, and to compensate I try to shop for clothes with carefully selected friends</span> </li> <li><span class="diff">like country music</span> <span class="comment">Spot on!</span></li> </ul> </td> <td> <p style="bold"> You are <b>unlikely</b> to: </p> <ul> <li> be influenced by social media during product purchases <br> <br> </li> <li> prefer style when buying clothes <br> <br> <br> </li> <li> <span class="diff">be influenced by brand name when making product purchases</span> </li> </ul> </td> </tr> </table> <p> I think that the above was a pretty good assessment of me, and the detailed breakdowns which follow are interesting. Before you look at that, however, you should know that the <a href='https://www.verywellmind.com/the-big-five-personality-dimensions-2795422' target='_blank' rel="nofollow">Big 5 Personality Model</a> defines the 5 traits using words with meanings that might seem different from the meanings you might expect. The 5 traits are: <a href='https://console.bluemix.net/docs/services/personality-insights/openness.html' target='_blank' rel="nofollow"><i>openness</i></a>, <a href='https://console.bluemix.net/docs/services/personality-insights/conscientiousness.html' target='_blank' rel="nofollow"><i>conscientiousness</i></a>, <a href='https://console.bluemix.net/docs/services/personality-insights/extroversion.html' target='_blank' rel="nofollow"><i>extroversion</i></a>, <a href='https://console.bluemix.net/docs/services/personality-insights/agreeableness.html' target='_blank' rel="nofollow"><i>agreeableness</i></a>, and <a href='https://console.bluemix.net/docs/services/personality-insights/emotional-range.html' target='_blank' rel="nofollow"><i>emotional range</i></a>. </p> <h2 id="pnv">Personality, Needs and Values</h2> <p> Each of the 5 traits are broken down into various aspects. For example, openness consists of <i>adventurousness</i>, <i>artistic interests</i>, <i>emotionality</i>, <i>imagination</i>, <i>intellect</i>, and <i>authority-challenging</i>. Again, these terms are <a href='https://console.bluemix.net/docs/services/personality-insights/openness.html#dimensions' target='_blank' rel="nofollow">defined in specific ways</a> that might be different from the definitions that you might expect. </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify"> <tr> <th width="50%">Birthday Posting Summary</th> <th width="50%"><a href='/blog/2008/04/28/cult-of-software-god.html'>Humorous Posting</a> Summary</th> </tr> <tr> <td> <p><br /><br /> <i>1724 words; decent analysis.</i> </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.svg" type="image/svg"> <!---<source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.avif" type="image/avif">--> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.webp" type="image/webp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.apng" type="image/apng"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.png" type="image/png"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.jpg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.jpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.jfif" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.pjpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.pjp" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.gif" type="image/gif"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.tif" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.tiff" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.bmp" type="image/bmp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.ico" type="image/x-icon"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.cur" type="image/x-icon"> <img alt='Birthday posting summary by IBM Personality Insights' class="imgImg rounded shadow zoom" src="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders.png" style='width: 100%; ' title='Birthday posting summary by IBM Personality Insights' /> </picture> </div> </td> <td> <p> <i>516 words; we need a minimum of 600, preferably 1,200 or more, to compute statistically significant estimates.</i> </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.svg" type="image/svg"> <!---<source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.avif" type="image/avif">--> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.webp" type="image/webp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.apng" type="image/apng"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.png" type="image/png"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.jpg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.jpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.jfif" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.pjpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.pjp" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.gif" type="image/gif"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.tif" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.tiff" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.bmp" type="image/bmp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.ico" type="image/x-icon"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.cur" type="image/x-icon"> <img alt='Humorous Posting summary by IBM Personality Insights' class="imgImg rounded shadow zoom" src="/blog/ibmPersonality/ibmPersonalityInsightMslinnSliders2.png" style='width: 100%; ' title='Humorous Posting summary by IBM Personality Insights' /> </picture> </div> </td> </tr> </table> <h2 id="detail">Detailed Breakdown</h2> <p> Here is my detailed breakdown, shown as sunburst charts: </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify" style="width: 100%;"> <tr> <th width="50%"> Birthday Posting Summary </th> <th width="50%"> <a href='/blog/2008/04/28/cult-of-software-god.html'>Humorous Posting</a> Summary </th> </tr> <tr> <td> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.svg" type="image/svg"> <!---<source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.avif" type="image/avif">--> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.webp" type="image/webp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.apng" type="image/apng"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.png" type="image/png"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.jpg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.jpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.jfif" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.pjpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.pjp" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.gif" type="image/gif"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.tif" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.tiff" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.bmp" type="image/bmp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.ico" type="image/x-icon"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.cur" type="image/x-icon"> <img alt='Birthday posting summary by IBM Personality Insights' class="imgImg rounded shadow zoom" src="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst.png" style='width: 100%; ' title='Birthday posting summary by IBM Personality Insights' /> </picture> </div> </td> <td> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.svg" type="image/svg"> <!---<source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.avif" type="image/avif">--> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.webp" type="image/webp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.apng" type="image/apng"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.png" type="image/png"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.jpg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.jpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.jfif" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.pjpeg" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.pjp" type="image/jpeg"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.gif" type="image/gif"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.tif" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.tiff" type="image/tiff"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.bmp" type="image/bmp"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.ico" type="image/x-icon"> <source srcset="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.cur" type="image/x-icon"> <img alt='Humorous posting summary by IBM Personality Insights' class="imgImg rounded shadow zoom" src="/blog/ibmPersonality/ibmPersonalityInsightMslinnSunburst2.png" style='width: 100%; ' title='Humorous posting summary by IBM Personality Insights' /> </picture> </div> </td> </tr> </table> <p> The sunburst charts paint me as an unusual person. If this personality assessment is accurate, I am rather complex. However, further reading suggests that people with very high openness scores defy most structured evaluations, and I score in the 99<sup>th</sup> percentile for openness. </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify" style="width: 100%"> <tr> <th width="50%"> Birthday Posting Summary </th> <th width="50%"> <a href='/blog/2008/04/28/cult-of-software-god.html'>Humorous Posting</a> Summary </th> </tr> <tr> <td> <b>Openness to experience</b>: high (99<sup>th</sup> percentile). This means I have an unusually high fluid intelligence (I am able to learn complex concepts and tasks very quickly), and I am likely to be eccentric. <br /> <span class="comment">I believe this to be true</span>. </td> <td> <b>Openness to experience</b>: high (99<sup>th</sup> percentile) &ndash; <span class="comment">Same</span> </td> </tr> <tr> <td> <b>Extraversion</b>: Low (15th percentile) &ndash; Cold, withdrawn, unfriendly. <span class="comment">That feels harsh.</span> </td> <td> <b>Extraversion</b>: Even lower (6<sup>th</sup> percentile). <span class="comment">Yikes!</span> </td> </tr> <tr> <td> <b>Agreeableness</b>: Low (5<sup>th</sup> percentile) &ndash; Independent, tough, dominant, possibly manipulative. <span class="comment">Yes, I make up my mind and I follow what I believe to be the appropriate course, regardless of what others might say or do.</span> <br /> Additionally, I have very strong sympathy (97<sup>th</sup> percentile) and I am rather uncompromising (82nd percentile), strongly cooperative (82<sup>nd</sup> percentile) with strong altruism (87<sup>th</sup> percentile). <br /> <span class="comment">Perhaps that means that I am a crusty individual with a warm heart.</span> <br /> According to the breakdown, I am quite trusting (82<sup>nd</sup> percentile), yet I am also very cautious (90<sup>th</sup> percentile), so for me I go with &ldquo;trust but verify&rdquo;. </td> <td> <b><a href='https://cloud.ibm.com/docs/services/personality-insights?topic=personality-insights-agreeableness' target='_blank' rel="nofollow">Agreeableness</a></b>: Even lower (0<sup>th</sup> percentile). <span class="comment">Yikes!</span> </td> </tr> <tr> <td> <b>Emotional range</b>: average (> 59<sup>th</sup> percentile). <span class="comment"><a href='https://cloud.ibm.com/docs/services/personality-insights?topic=personality-insights-emotionalRange' target='_blank' rel="nofollow">IBM defines this</a> as &ldquo;the extent to which a person's emotions are sensitive to the individual's environment&rdquo;</span> </td> <td> <b>Emotional range</b>: very high (> 91<sup>st</sup> percentile). When coupled with low agreeableness, IBM predicts: temperamental, irritable, quarrelsome, impatient, grumpy. When coupled with low conscientiousness, IBM predicts: compulsive, nosy, self-indulgent, forgetful, impulsive. When coupled with low extroversion, IBM predicts: guarded, fretful, insecure, pessimistic, secretive. When coupled with high openness, IBM predicts: excitable, passionate, sensual. <span class="comment">Hmm, quarrelsome, impatient, compulsive, self-indulgent, forgetful, insecure, pessimistic and yet passionate and sensual. What a combination!</span> </td> </tr> <tr> <td> <b>Low Conservation (1<sup>st</sup> percentile)</b> &ndash; <span class="comment">This is called Hedonism in the other graph.</span> </td> <td> <b>Hedonism</b>: very low (1<sup>st</sup> percentile) &ndash; <span class="comment">This is called Conservation in the other graph.</span> Within this category I scored low self-enhancement, low Hedonism, low Openness to change and low conservation. </td> </tr> <tr> <td> <b>Low Harmony (5<sup>th</sup> percentile)</b> &ndash; <span class="comment">This is called Closeness in the other graph.</span> </td> <td> <b>Closeness:</b> low (1<sup>th</sup> percentile) &ndash; <span class="comment">This is called Harmony in the other graph.</span> Within this category I scored low Excitement, Harmony, Ideal, Liberty, Love, Self-expression and Stability. I also scored high Curiosity (78<sub>th</sub> percentile) and high Structure (88<sup>th</sup> percentile). <span class="comment">Sounds like I'm pretty much a curious robot.</span> </td> </tr> </table> <p> These traits, if true, would make me a good expert witness, and a good evaluator for technical due diligence. This might also explain my propensity for constantly inventing new things. </p> <p> According to these results, I also registered some extreme <a href='https://console.bluemix.net/docs/services/personality-insights/needs.html#needs' target='_blank' rel="nofollow">needs</a> and <a href='https://console.bluemix.net/docs/services/personality-insights/values.html#values' target='_blank' rel="nofollow">values</a>. Because the results of analyzing both documents were almost the same, I show them together. </p> <p> First, let's look at the results that I identify with: </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify" style="width: 100%"> <tr> <th> Both articles </th> </tr> <tr> <td> An extremely liberal mindset (<b>conservation</b>: 1st percentile) </td> </tr> <tr> <td> Virtually no interest in comfort (<b>hedonism</b>: 2nd percentile) </td> </tr> <tr> <td> Almost no interest in social power, authority, wealth, success, capability, etc. (<a href='https://www.researchgate.net/publication/322627891_The_five_pillars_of_self-enhancement_and_self-protection' target='_blank' rel="nofollow"><b>self-enhancement</b></a>: 4th percentile) </td> </tr> <tr> <td> Almost no interest in <b>harmony</b> (5th percentile) </td> </tr> <tr> <td> Strong <b>curiosity</b> (87th percentile) </td> </tr> <tr> <td> Low connection to family and setting up a home (<b>closeness</b>: 9%) </td> </tr> <tr> <td> Very little respect, commitment, and acceptance of the customs and ideas that culture and/or religion provides. </td> </tr> </table> <p> I think the following needs assessments are way off the mark. Perhaps I protest too much? </p> <table class="table table_striped table_cell_top table_cell_vspace table_cell_justify" style="width: 100%"> <tr> <td> Very little interest in just having fun for its own sake (<b>excitement</b>: 7th percentile). </td> </tr> <tr> <td> Low desire for perfection or a sense of community <span class="comment">The truth is complicated: I have spent decades building professional and musical communities, yet I am isolated in many ways</span> </td> </tr> <tr> <td> Low interest in discovering and asserting their identities (<b>self-expression</b>: 9%) <span class="comment">This seems way off the mark, for example consider the motivation for my spending thousands of hours on EmpathyWorks.</span> </td> </tr> </table> <h2 id="another">Another Assessment</h2> <p> The more clocks you have, the less certain you are of the correct time. Some of the following is true, but my personality expresses itself differently according to circumstance, so no one single description could be realistic. </p> <p> <a href='https://We3app.com' target='_blank' rel="nofollow">We3app.com</a> rated my personality type as: </p> <p> <b>The Good-Timer</b><br> Calm, messy, sociable, curious, egoistic<br> 3.3% of women | 5.3% of men<br><br> You’re independent, imaginative, and intelligent, and you want to make fun where you can. Life is great, and you’ve got a good idea of how to live it—it’d take a lot to make you change your mind.<br><br> Your positive vibe and strong opinions mean that people tend to give you what you need, and are happy to follow your lead. You’ll listen to what they have to say, but you usually end up being right anyway. Besides, you’re the only one who knows how to get away with cutting corners: nobody can beat the system like you.<br><br> You’re never sure exactly how much is in your bank account, you regularly have to improvise when you run out of underwear, and you are disgusted at the concept of a cleaning schedule. You might agree to washing the car, but only because it will inevitably turn into a water fight.<br><br> You’re not one to say no to temptation, and there are certain habits you know it would be better to kick, but self-discipline is for monks and bodybuilders. </p> Evaluating Blockchain Companies 2018-08-29T00:00:00-04:00 https://mslinn.github.io/blog/2018/08/29/evaluating-blockchain-companies <div> <p> I have performed <a href='/evaluation/index.html'>technical due diligence</a> for investors since the mid-1980s. In this 40-minute presentation I explain how I evaluate blockchain-related technology companies. </p> <p> This talk was presented at the 6th Annual Global Big Data Conference in Santa Clara, California on August 29, 2018. The promotional video recording is <a href='https://www.youtube.com/watch?v=o_9q2USRzzI' target='_blank' rel="nofollow">here</a>. </p> <p> <a href='https://www.infoq.com/presentations/evaluate-blockchain-companies/' target='_blank' rel="nofollow">InfoQ produced the presentation</a> in their unique format. </p> <p> The slides are <a href='https://www.slideshare.net/mslinn/evaluating-blockchain-companies/mslinn/evaluating-blockchain-companies' target='_blank' rel="nofollow">here</a>. </p> <p> The video recording is <a href='https://big.mslinn.com/video/18-aug-evaluatingbccompanies.mp4' target='_blank' rel="nofollow">here</a>. </p> </div> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/6thGlobalBlockchain.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.webp" type="image/webp"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.apng" type="image/apng"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.png" type="image/png"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.gif" type="image/gif"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/6thGlobalBlockchain.cur" type="image/x-icon"> <img alt='Mike Slinn presents' class="imgImg rounded shadow" src="/blog/images/blockchain/6thGlobalBlockchain.png" style='width: 100%; ' title='Mike Slinn presents' /> </picture> </div> Bob Summerwill 2018-08-28T00:00:00-04:00 https://mslinn.github.io/blog/2018/08/28/summerwill <p> This is a great photo of Bob Summerwill and me in San Francisco August 2018! </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/meBobSummerwill_690x518.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.webp" type="image/webp"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.apng" type="image/apng"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.png" type="image/png"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.gif" type="image/gif"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/meBobSummerwill_690x518.cur" type="image/x-icon"> <img alt='Mike Slinn and Bob Summerwill in San Francisco August 2018' class="imgImg rounded shadow" src="/blog/images/blockchain/meBobSummerwill_690x518.png" style='width: 100%; ' title='Mike Slinn and Bob Summerwill in San Francisco August 2018' /> </picture> </div> Keynote Panel Discussion - The Future of Blockchain 2018-08-23T00:00:00-04:00 https://mslinn.github.io/blog/2018/08/23/blockchain-conference-2018-08-28 <p> I will participate in a keynote panel discussion on the Future of Blockchain on August 23 from 4:50 PM to 6:00 PM at the <a href='https://sanjose.eventful.com/events/blockchain-new-infrastructure-ai-/E0-001-112130527-9' target='_blank' rel="nofollow">6th Annual Global Big Data Conference</a> in Santa Clara, California. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/mikeBigData_500x431.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/mikeBigData_500x431.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/mikeBigData_500x431.webp" type="image/webp"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.apng" type="image/apng"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.png" type="image/png"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.gif" type="image/gif"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/mikeBigData_500x431.cur" type="image/x-icon"> <img alt='Global Big Data Conference featuring me as a speaker' class="imgImg rounded shadow" src="/blog/images/blockchain/mikeBigData_500x431.png" style='width: 100%; ' title='Global Big Data Conference featuring me as a speaker' /> </picture> <figcaption class='imgFigCaption fullsize'> Global Big Data Conference featuring me as a speaker </figcaption> </figure> </div> <p> Following are some ideas I hope to discuss with the other members of the panel. First, I would like to remind the reader that blockchain data is passive; without a program to access it blockchain data is inert. Now I'd like to give the definition that I will use for this discussion. </p> <p> The word <i>blockchain</i> is defined by <a href='https://en.wikipedia.org/wiki/Blockchain' target='_blank' rel="nofollow">Wikipedia</a> as: &ldquo;a growing list of records, called blocks, which are linked using cryptography.&rdquo; The main theme I&rsquo;d like to personally bring forward during this discussion is that Blockchain does not imply or require decentralization, or even distributed systems. Instead, blockchain is a useful technology in its own right, even if it is not distributed / decentralized. I have nothing against decentralization, when used appropriately; the point I want to make is that decentralization is not necessary for blockchain to provide value. </p> <p> Here are the use cases I want to talk about: </p> <h2 id="dbs">#1: Secure Mobile and Embedded Databases</h2> <p> For me, <i>blockchain</i> is a file format that yields immutable data, highly resistant to modification. It is not a database, instead, it is a generic type of storage technology. Yes, an API could be provided that provides a structured interface to blockchain data, but there is no universally accepted standard in use for this in any blockchain implementation that I am aware of. Such a standard should exist, and I would be interested in learning about any standards work in this area. </p> <p> A database built using blockchain would not support the full <a href='https://en.wikipedia.org/wiki/Create,_read,_update_and_delete' target='_blank' rel="nofollow">CRUD</a> API (create, read, update, and delete) functionality traditionally supported by SQL. Instead, only create and read functionality would be supported. </p> <p> This seems especially useful for mobile and embedded data recorders, for example flight recorders used in airplanes. I have not yet attempted to compute how feasible it might be to use blockchain to store video or audio streams from devices such as <a href='https://www.amazon.com/slp/police-body-camera/4r72vj5jucggbyv' target='_blank' rel="nofollow">police body cameras</a>. </p> <h2 id="data">#2: Secure Data Distribution</h2> <p> Blockchain is a data storage technology that is known to be highly resistant to corruption or modification. Because data can only be created or read, but not modified or deleted, if you have a file of blockchain data that passes inspection you can be confident that you have all the data that was available at the time the file was last updated. </p> <p> Blockchain data could be stored in RFID tags. This is possible because RFID tags exist which can <a href='https://www.rfidjournal.com/faq/show?66' target='_blank' rel="nofollow">store up to 66 KB of data</a>, and read/write RFID tags are available. Read-write RFID tags usually have a serial number that can&rsquo;t be written over. Additional blocks of data can be used to store additional information about the items the tag is attached to (these can usually be locked to prevent overwriting of data). </p> <p> This would allow blockchain to be used for <a href='https://www.theglobalist.com/smart-passports-making-travel-safer/' target='_blank' rel="nofollow">smart passports</a> and smart ID cards. Secure, smart passports would provide extra security for passport/ID cardholders and would make counterfeiting extremely difficult, and would provide a digital record of countries visited, instead of relying on passport stamps, which are easy to counterfeit. </p> <p> Similarly, smart ID cards could hold mutable yet secure data. </p> <h2 id="builders">#3: Secure Software Build Systems</h2> <p> There have been many instances of corporate spies infiltrating software companies or their repositories. The perpetrators altered the source code, accompanying data or modified other build dependencies. When the next version of the software is published, all the customers who install updates will potentially be compromised. </p> <p> <a href='https://en.wikipedia.org/wiki/Git' target='_blank' rel="nofollow"><code>git</code></a> uses similar cryptography techniques as does blockchain. A blockchain-based build system would use a git tag to kick off build, and would need to also store and compare hashes for all dependencies to ensure that they had not changed. Because the software build toolchain is also a dependency, hashes for all the software used in the toolchain would need to be validated at build time. </p> <h2 id="dist">#4: Secure Software Distribution</h2> <p> Computer viruses have been a problem for decades. There have been many instances where programs have viruses embedded after they are published. This is generally true for any software downloaded from a torrent site. If software was packaged using blockchain technology, it could be retrieved for installation with confidence. Software installers that used blockchain in this way could be trusted. </p> <hr /> <h2 id="panel">The Panelists</h2> <p> After the panel, we panelists were photographed together. From left to right: Hayden Kirkpatrick, VP of Strategy at Esurance (moderator); Karen Hsu, CRO at BlockXypher; Steve Beauregard, CRO at Bloq; Mike Slinn, CTO at Micronautics Research (holding the microphone); Mark Javier, Account Executive at Ambisafe; and Camille Sanandaji, CEO at Foodstems. </p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.webp" type="image/webp"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.apng" type="image/apng"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.png" type="image/png"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.gif" type="image/gif"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.cur" type="image/x-icon"> <img alt='Hayden Kirkpatrick (moderator), Karen Hsu, Steve Beauregard, Mike Slinn, and Camille Sanandaji' class="imgImg rounded shadow" src="/blog/images/blockchain/futureOfBlockchainPanelCrop2_690x192.png" style='width: 100%; ' title='Hayden Kirkpatrick (moderator), Karen Hsu, Steve Beauregard, Mike Slinn, and Camille Sanandaji' /> </picture> <figcaption class='imgFigCaption '> Hayden Kirkpatrick (moderator), Karen Hsu, Steve Beauregard, Mike Slinn, and Camille Sanandaji </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.webp" type="image/webp"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.apng" type="image/apng"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.png" type="image/png"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.gif" type="image/gif"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/blockchainFuturePanel_690x690.cur" type="image/x-icon"> <img alt='Mark Javier, Mike Slinn, Hayden Kirkpatrick (moderator), Steve Beauregard, Karen Hsu, and Camille Sanandaji' class="imgImg rounded shadow" src="/blog/images/blockchain/blockchainFuturePanel_690x690.png" style='width: 100%; ' title='Mark Javier, Mike Slinn, Hayden Kirkpatrick (moderator), Steve Beauregard, Karen Hsu, and Camille Sanandaji' /> </picture> <figcaption class='imgFigCaption '> Mark Javier, Mike Slinn, Hayden Kirkpatrick (moderator), Steve Beauregard, Karen Hsu, and Camille Sanandaji </figcaption> </figure> </div> Windows Subsystem for Linux Revisited 2018-08-20T00:00:00-04:00 https://mslinn.github.io/blog/2018/08/20/wsl-revisited <!-- #region intro --> <p> I&rsquo;ve been using <a href='https://docs.microsoft.com/en-us/windows/wsl/install-win10' target='_blank' rel="nofollow">Windows Subsystem for Linux</a> (WSL) headless since it was first released with Windows 10 version 1607 in August 2016. The <a href='https://blogs.msdn.microsoft.com/commandline/2018/03/07/windows10v1803/' target='_blank' rel="nofollow">April 2018 release</a> of Windows 10 (version 1803) significantly improved WSL. </p> <p> It is possible to work with Ubuntu graphically on a vanilla Windows machine. No special drivers are required. No special Linux or Ubuntu support is required from the computer vendor. </p> <p> This is my setup for running an X client like <a href='https://www.x.org/releases/X11R7.5/doc/man/man1/xeyes.1.html' target='_blank' rel="nofollow"><code>xeyes</code></a> or <a href='https://www.jetbrains.com/idea/' target='_blank' rel="nofollow">IntelliJ IDEA</a> from WSL or WSL2, accessed via a Windows X server like <a href='https://sourceforge.net/projects/vcxsrv' target='_blank' rel="nofollow">VcXsrv</a>. These scripts assume <a href='https://www.microsoft.com/en-us/p/ubuntu-1804/9n9tngvndl3q' target='_blank' rel="nofollow">Ubuntu 18.04 was installed</a> under WSL. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/wsl.svg" type="image/svg"> <!---<source srcset="/blog/images/wsl.avif" type="image/avif">--> <source srcset="/blog/images/wsl.webp" type="image/webp"> <source srcset="/blog/images/wsl.apng" type="image/apng"> <source srcset="/blog/images/wsl.png" type="image/png"> <source srcset="/blog/images/wsl.jpg" type="image/jpeg"> <source srcset="/blog/images/wsl.jpeg" type="image/jpeg"> <source srcset="/blog/images/wsl.jfif" type="image/jpeg"> <source srcset="/blog/images/wsl.pjpeg" type="image/jpeg"> <source srcset="/blog/images/wsl.pjp" type="image/jpeg"> <source srcset="/blog/images/wsl.gif" type="image/gif"> <source srcset="/blog/images/wsl.tif" type="image/tiff"> <source srcset="/blog/images/wsl.tiff" type="image/tiff"> <source srcset="/blog/images/wsl.bmp" type="image/bmp"> <source srcset="/blog/images/wsl.ico" type="image/x-icon"> <source srcset="/blog/images/wsl.cur" type="image/x-icon"> <img alt='Desktop showing Windows Subsystem for Linux (WSL)' class="imgImg rounded shadow" src="/blog/images/wsl.png" style='width: 100%; ' title='Desktop showing Windows Subsystem for Linux (WSL)' /> </picture> </div> <!-- endregion --> <!-- #region essential scripts --> <h2 id="essential">Essential Scripts</h2> <p>Here are bash scripts to install everything:</p> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_init"> </script> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_setup"> </script> <h2 id="optional">Optional Scripts</h2> <p>The remaining scripts are all optional.</p> <h3 id="vnc">VNC</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_vnc"> </script> <p>These bash scripts allow VNC to connect to a remote machine or to WSL on the local machine.</p> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=vncRun"> </script> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl"> </script> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=vncSample"> </script> <h2 id="packages">Packages and Programming Environments</h2> <p>Installation of various packages and programming environments follow.</p> <h3 id="git">Git</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_git"> </script> <h3 id="python">Python</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_python"> </script> <h3 id="nodejs">NodeJS</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_node"> </script> <h3 id="jvm">Java Virtual Machine</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_jvm"> </script> <h3 id="misc">Miscellaneous</h3> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/7be077ebeb9c172572e211e354a8c5f8.js?file=wsl_misc"> </script> <h3 id="wine">Wine</h3> <p> The advent of WSL means that Wine is no longer required! </p> <!-- endregion --> <!-- #region Configuration --> <h2 id="config">Configuration</h2> <p> This is my WSL configuration file, in my Windows home directory (<code>%systemdrive%%homepath%\.wslconfig</code>). See <a href='https://github.com/MicrosoftDocs/WSL/blob/a18d455b23d51d0bddf4f38a48adc498dc6ce243/WSL/wsl-config.md' target='_blank' rel="nofollow">Advanced settings configuration in WSL</a> for more information. </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>&percnt;systemdrive&percnt;&percnt;homepath&percnt;\.wslconfig</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2f1cfb7237c1'><button class='copyBtn' data-clipboard-target='#id2f1cfb7237c1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># See https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig # See https://github.com/microsoft/WSL/issues/4166 [wsl2] memory=8GB swap=60GB #processor=4 localhostForwarding=true [experimental] autoMemoryReclaim=drop sparseVhd=true</pre> </div> <!-- endregion --> <div class="alert rounded shadow"> <h2 id="update2.1.3">2024-02-20 Update</h2> The <a href='https://github.com/microsoft/WSL/releases/tag/2.1.3' target='_blank' rel="nofollow">WSL v2.1.3</a> prerelease sets the the default memory reclamation mode to dropcache. The following is now a default setting:<br> <div class="jekyll_pre" style='margin-top: 1em; margin-bottom: 1em;'> <div class='codeLabel unselectable' data-lt-active='false'>&percnt;systemdrive&percnt;&percnt;homepath&percnt;\.wslconfig</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id748947d16773'><button class='copyBtn' data-clipboard-target='#id748947d16773' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>wsl2.autoMemoryReclaim=drop</pre> </div> The memory will be freed using dropcache after 10 minutes of inactivity. To upgrade WSL using bash, cmd or powershell: <!-- #region --> <div class="jekyll_pre" style='margin-top: 1em; margin-bottom: 1em;'> <div class='codeLabel unselectable' data-lt-active='false'>bash, cmd or powershell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc2006bd525b2'><button class='copyBtn' data-clipboard-target='#idc2006bd525b2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>wsl.exe --update --pre-release</pre> </div> Wait a few minutes before restarting a terminal session. It is not necessary to reboot after upgrading. <!-- endregion --> </div> <!-- endregion --> <!-- #region upgrading from Ubuntu 19.11 to Ubuntu 20.04 --> <h2 id="update">Upgrading from Ubuntu 19.11 to Ubuntu 20.04</h2> <p> <b>Update Aug 17, 2020</b>: Here are my notes on upgrading WSL and WSL2 from Ubuntu 19.11 to Ubuntu 20.04. </p> <noscript><pre>400: Invalid request</pre></noscript><script src="https://gist.github.com/50a3a8d26cbab51c634ed8e2edf129ae.js"> </script> <!-- endregion --> <!-- #region Run Associated Windows Program --> <h2 id="assoc">Run Associated Windows Program</h2> <p> To run the Windows program associated with a file, preface the file with <code>cmd.exe /C start</code>. </p> <p> For example, to run the default Windows video viewer for <code>.mkv</code> files, type: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0b031ebf7d47'><button class='copyBtn' data-clipboard-target='#id0b031ebf7d47' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cmd.exe /C start my_file.mkv</pre> </div> <!-- endregion --> <p> I defined a bash alias to make this easier: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.bash_aliases</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id491b57a0bd39'><button class='copyBtn' data-clipboard-target='#id491b57a0bd39' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>alias run='cmd.exe /C start'</pre> </div> <!-- endregion --> <p> Reload the bash aliases after editing <code>~/.bash_aliases</code>: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7af9814e885c'><button class='copyBtn' data-clipboard-target='#id7af9814e885c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>source ~/.bash_aliases</pre> </div> <!-- endregion --> <p> Now use the <code>run</code> alias to run the default Windows video viewer for <code>.mkv</code> files like this: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida2119705c2c5'><button class='copyBtn' data-clipboard-target='#ida2119705c2c5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>run my_file.mkv</pre> </div> <!-- endregion --> <!-- endregion --> Ethereum Source Code Walkthrough 2018-06-13T00:00:00-04:00 https://mslinn.github.io/blog/2018/06/13/evm-source-walkthrough <div class="alert rounded shadow" id="about"> <h2 id="about" style="margin-top: 0;">April 27, 2020 Update</h2> <p> This was a sample work in progress as part of a proposal to the Ethereum Foundation Grants committee. They declined to fund this activity, but gave no reason and no feedback as to what might be acceptable. The preparation of my proposal took weeks, and the preliminary feedback that I had received from many knowledgeable people was that it had great value. </p> <p> I felt that the evaluation process was broken, in fact the entire organization was broken, and there was little hope that Ethereum would ever become a professional organization. Two years later, I believe history has proved me right. This was the last blockchain-related initiative I participated in. </p> <p> I no longer maintain <a href='/blog/2017/11/29/web3j-scala.html'><code>web3j-scala</code></a>, an Ethereum-related project for Scala programmers. I created that open source project and worked on it for free for 3 years. It has been forked and I&rsquo;ve been told it is or was used in production. However, I see no reason to continue working for free on it. Others can carry the project forward if they want. </p> </div> <p> This is a brief walkthrough of some of the core source files for smart contracts in the official <a href='https://golang.org/' target='_blank' rel="nofollow">Go language</a> Ethereum implementation, which includes the <a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/core/vm' target='_blank' rel="nofollow"><code>geth</code></a> command-line Ethereum client program, along with many other programs. Ethereum clients include an implementation of the Ethereum Virtual Machine (EVM), which are able to parse and verify the Ethereum blockchain, including smart contracts, and provides interfaces to create transactions and mine blocks. </p> <p> I&rsquo;ve added some suggestions for how the source code might be improved. If there is general agreement that these suggestions make sense (tell me in the comments!) then I&rsquo;ll create a pull request. </p> <h2 id="license">License</h2> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/lgplv3-147x51.svg" type="image/svg"> <!---<source srcset="/blog/images/lgplv3-147x51.avif" type="image/avif">--> <source srcset="/blog/images/lgplv3-147x51.webp" type="image/webp"> <source srcset="/blog/images/lgplv3-147x51.apng" type="image/apng"> <source srcset="/blog/images/lgplv3-147x51.png" type="image/png"> <source srcset="/blog/images/lgplv3-147x51.jpg" type="image/jpeg"> <source srcset="/blog/images/lgplv3-147x51.jpeg" type="image/jpeg"> <source srcset="/blog/images/lgplv3-147x51.jfif" type="image/jpeg"> <source srcset="/blog/images/lgplv3-147x51.pjpeg" type="image/jpeg"> <source srcset="/blog/images/lgplv3-147x51.pjp" type="image/jpeg"> <source srcset="/blog/images/lgplv3-147x51.gif" type="image/gif"> <source srcset="/blog/images/lgplv3-147x51.tif" type="image/tiff"> <source srcset="/blog/images/lgplv3-147x51.tiff" type="image/tiff"> <source srcset="/blog/images/lgplv3-147x51.bmp" type="image/bmp"> <source srcset="/blog/images/lgplv3-147x51.ico" type="image/x-icon"> <source srcset="/blog/images/lgplv3-147x51.cur" type="image/x-icon"> <img alt='LGPL logo' class="imgImg " src="/blog/images/lgplv3-147x51.png" style='width: 100%; ' title='LGPL logo' /> </picture> </div> <p> This Ethereum client project was released under the <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html' target='_blank' rel="nofollow">GNU Lesser General Public License, version 3</a> or later, which permits use of the code as a library in proprietary programs. </p> <h2 id="src">Source Files</h2> <p> The <a href='https://github.com/hhatto/gocloc' target='_blank' rel="nofollow"><code>gocloc</code></a> program counted the following source files and lines: </p> <table class="table table_striped table_right"> <tr> <th>Language</th> <th>Files</th> <th>Blank Lines</th> <th>Comment Lines</th> <th>Code Lines</th> </tr> <tr> <th>Go</th> <td>1824</td> <td>58,134</td> <td>81,861</td> <td>639,435</td> </tr> <tr> <th>C</th> <td>55</td> <td>17,257</td> <td>30,909</td> <td>84,719</td> </tr> <tr> <th>C Header</th> <td>97</td> <td>2,559</td> <td>6,318</td> <td>15,083</td> </tr> <tr> <th>Markdown</th> <td>88</td> <td>3,152</td> <td>0</td> <td>9,175</td> </tr> <tr> <th>JavaScript</th> <td>13</td> <td>1,845</td> <td>4,495</td> <td>7,986</td> </tr> <tr> <th>Assembly</th> <td>39</td> <td>557</td> <td>957</td> <td>3,783</td> </tr> <tr> <th>JSON</th> <td>17</td> <td>0</td> <td>0</td> <td>2,065</td> </tr> <tr> <th>Protocol Buffers</th> <td>2</td> <td>113</td> <td>40</td> <td>1,030</td> </tr> <tr> <th>Plain Text</th> <td>11</td> <td>217</td> <td>0</td> <td>954</td> </tr> <tr> <th>C++</th> <td>4</td> <td>132</td> <td>102</td> <td>937</td> </tr> <tr> <th>BASH</th> <td>10</td> <td>178</td> <td>315</td> <td>931</td> </tr> <tr> <th>Perl</th> <td>10</td> <td>268</td> <td>1,289</td> <td>879</td> </tr> <tr> <th>JSX</th> <td>11</td> <td>119</td> <td>245</td> <td>722</td> </tr> <tr> <th>XML</th> <td>9</td> <td>0</td> <td>0</td> <td>651</td> </tr> <tr> <th>M4</th> <td>4</td> <td>79</td> <td>99</td> <td>649</td> </tr> <tr> <th>YAML</th> <td>20</td> <td>77</td> <td>42</td> <td>581</td> </tr> <tr> <th>NSIS</th> <td>5</td> <td>86</td> <td>154</td> <td>446</td> </tr> <tr> <th>Java</th> <td>4</td> <td>143</td> <td>187</td> <td>438</td> </tr> <tr> <th>Makefile</th> <td>11</td> <td>101</td> <td>84</td> <td>381</td> </tr> <tr> <th>Python</th> <td>6</td> <td>154</td> <td>250</td> <td>339</td> </tr> <tr> <th>HTML</th> <td>3</td> <td>15</td> <td>9</td> <td>245</td> </tr> <tr> <th>Solidity</th> <td>7</td> <td>56</td> <td>171</td> <td>213</td> </tr> <tr> <th>Bourne Shell</th> <td>6</td> <td>23</td> <td>25</td> <td>119</td> </tr> <tr> <th>CMake</th> <td>1</td> <td>9</td> <td>0</td> <td>35</td> </tr> <tr> <th>Awk</th> <td>1</td> <td>4</td> <td>4</td> <td>17</td> </tr> <tr> <th>TOML</th> <td>1</td> <td>0</td> <td>0</td> <td>3</td> </tr> <tr> <th>Total</th> <th class="table_right">2,260</th> <th class="table_right">85,278</th> <th class="table_right">127,556</th> <th class="table_right">771,825</th> </tr> </table> <h2 id="packages">Packages</h2> <p> I used the following incantation to discover that <code>geth</code> defines 244 packages: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3f962acea052'><button class='copyBtn' data-clipboard-target='#id3f962acea052' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>grep -rh "^package" | grep -v "not installed" | \ tr -d ';' | sed 's^//.*^^' | awk '{$1=$1};1' | \ sort | uniq | wc -l</pre> </div> <p> I won't list them all. The <a href='https://godoc.org/github.com/ethereum/go-ethereum#pkg-subdirectories' target='_blank' rel="nofollow"><code>godoc</code></a> for the project contains much of the <a href='https://godoc.org/github.com/ethereum/go-ethereum#pkg-subdirectories' target='_blank' rel="nofollow">following documentation</a> for the top-level packages. I provided the rest of the information from disparate sources, including reading the source code: </p> <table class="table table_striped"> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/accounts' target='_blank' rel="nofollow">accounts</a></th> <td>implements high-level Ethereum account management.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/blob/master/trie/trie.go' target='_blank' rel="nofollow"><code>trie</code></a></th> <td>provides a binary Merkle tree implementation.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd' target='_blank' rel="nofollow">cmd</a></th> <td>Contains the following command-line tools. Most tools support the <code>--help</code> option. <table class="table table_striped" style="margin-top: 0.5em"> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/abigen' target='_blank' rel="nofollow">abigen</a></th> <td>source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages. It operates on plain <a href='https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI' target='_blank' rel="nofollow">Ethereum contract ABIs</a> with expanded functionality if the contract bytecode is also available. However it also accepts Solidity source files, making development much more streamlined. Please see the <a href='https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts' target='_blank' rel="nofollow">Native DApps wiki page</a> for details. </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8//cmd/bootnode' target='_blank' rel="nofollow">bootnode</a></th> <td>runs a bootstrap node for the Ethereum Discovery Protocol. This is a stripped-down version of <code>geth</code> that only takes part in the network node discovery protocol, and does not run any of the higher level application protocols. It can be used as a lightweight bootstrap node to aid in finding peers in private networks.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/clef' target='_blank' rel="nofollow">clef</a></th> <td>a standalone signer that manages keys across multiple Ethereum-aware apps such as Geth, Metamask, and cpp-ethereum. <i>Alpha quality, not released yet.</i></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/ethkey' target='_blank' rel="nofollow">ethkey</a></th> <td>a key/wallet management tool for Ethereum keys. Allows user to add, remove and change their keys, and supports cold wallet device-friendly transaction inspection and signing. <a href='https://github.com/ethereum/guide/blob/master/ethkey.md' target='_blank' rel="nofollow">This documentation</a> was written for the C++ Ethereum client implementation, but it is probably suitable for the Go implementation as well. </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8//cmd/evm' target='_blank' rel="nofollow">evm</a></th> <td>a version of the EVM (Ethereum Virtual Machine) for running bytecode snippets within a configurable environment and execution mode. Allows isolated, fine-grained debugging of EVM opcodes. Example usage: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2be38d066985'><button class='copyBtn' data-clipboard-target='#id2be38d066985' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>evm --code 60ff60ff --debug</pre> </div> </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/faucet' target='_blank' rel="nofollow">faucet</a></th> <td>an Ether faucet backed by a light client.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/geth' target='_blank' rel="nofollow">geth</a></th> <td>official command-line client for Ethereum. It provides the entry point into the Ethereum network (main-, test- or private net), capable of running as a full node (default) archive node (retaining all historical state) or a light node (retrieving data live). It can be used by other processes as a gateway into the Ethereum network via JSON RPC endpoints exposed on top of HTTP, WebSocket and/or IPC transports. For more information see the <a href='https://github.com/ethereum/go-ethereum/wiki/Command-Line-Options' target='_blank' rel="nofollow">CLI Wiki page</a>.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8//cmd/p2psim' target='_blank' rel="nofollow">p2psim</a></th> <td>a simulation HTTP API. <a href='https://godoc.org/github.com/ethereum/go-ethereum/cmd/p2psim' target='_blank' rel="nofollow">Docs are here</a>.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/puppeth' target='_blank' rel="nofollow">puppeth</a></th> <td>assembles and maintains private networks.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/rlpdump' target='_blank' rel="nofollow">rlpdump</a></th> <td>a pretty-printer for RLP data. RLP (Recursive Length Prefix) is the data encoding used by the Ethereum protocol. Sample usage: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd6542dd4cb03'><button class='copyBtn' data-clipboard-target='#idd6542dd4cb03' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>rlpdump --hex CE0183FFFFFFC4C304050583616263</pre> </div> </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/blob/master/swarm/README.md' target='_blank' rel="nofollow">swarm</a></th> <td>provides the <code>bzzhash</code> command, which computes a swarm tree hash, and implements the swarm daemon and tools. See <a href='https://swarm-guide.readthedocs.io' target='_blank' rel="nofollow">the swarm documentation</a> for more information.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/cmd/wnode' target='_blank' rel="nofollow">wnode</a></th> <td>simple Whisper node. It could be used as a stand-alone bootstrap node. Also could be used for different test and diagnostics purposes.</td> </tr> </table> </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/common' target='_blank' rel="nofollow">common</a></th> <td>contains various helper functions worth checking out</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/consensus' target='_blank' rel="nofollow">consensus</a></th> <td>implements different Ethereum consensus engines (which must conform to the <a href='https://godoc.org/github.com/ethereum/go-ethereum/consensus#Engine' target='_blank' rel="nofollow"><code>Engine</code> interface</a>): <a href='https://godoc.org/github.com/ethereum/go-ethereum/consensus/clique' target='_blank' rel="nofollow"><code>clique</code></a> implements proof-of-authority consensus, and <a href='https://godoc.org/github.com/ethereum/go-ethereum/consensus/ethash' target='_blank' rel="nofollow">ethash</a> implements proof-of-work consensus.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/console' target='_blank' rel="nofollow">console</a></th> <td> Ethereum implements a JavaScript runtime environment (JSRE) that can be used in either interactive (console) or non-interactive (script) mode. Ethereum's JavaScript console exposes the full web3 JavaScript Dapp API and the admin API. <a href='https://github.com/ethereum/go-ethereum/wiki/JavaScript-Console' target='_blank' rel="nofollow">More documentation is here.</a> This package implements JSRE for the <code>geth console</code> and <code>geth console</code> subcommands. </td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/containers' target='_blank' rel="nofollow">containers</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/contracts' target='_blank' rel="nofollow">contracts</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/core' target='_blank' rel="nofollow">core</a></th> <td>implements the Ethereum consensus protocol, implements the Ethereum Virtual Machine, and other miscellaneous important bits</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/crypto' target='_blank' rel="nofollow">crypto</a></th> <td>cryptographic implementations</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/dashboard' target='_blank' rel="nofollow">dashboard</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/eth' target='_blank' rel="nofollow">eth</a></th> <td>implements the Ethereum protocol</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/ethclient' target='_blank' rel="nofollow">ethclient</a></th> <td>provides a client for the Ethereum RPC API</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/ethdb' target='_blank' rel="nofollow">ethdb</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8//ethstats' target='_blank' rel="nofollow">ethstats</a></th> <td>implements the network stats reporting service</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8//event' target='_blank' rel="nofollow">event</a></th> <td>deals with subscriptions to real-time events</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/internal' target='_blank' rel="nofollow">internal</a></th> <td>Debugging support, JavaScript dependencies, testing support</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/les' target='_blank' rel="nofollow">les</a></th> <td>implements the Light Ethereum Subprotocol</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/light' target='_blank' rel="nofollow">light</a></th> <td>implements on-demand retrieval capable state and chain objects for the Ethereum Light Client</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/log' target='_blank' rel="nofollow">log</a></th> <td>provides an opinionated, simple toolkit for best-practice logging that is both human and machine readable</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/metrics' target='_blank' rel="nofollow">metrics</a></th> <td>port of Coda Hale's Metrics library. Unclear why this was not implemented as a separate library, like <a href='https://github.com/rcrowley/go-metrics' target='_blank' rel="nofollow">this one</a>.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/miner' target='_blank' rel="nofollow">miner</a></th> <td>implements Ethereum block creation and mining</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/mobile' target='_blank' rel="nofollow">mobile</a></th> <td>contains the simplified mobile APIs to go-ethereum</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/node' target='_blank' rel="nofollow">node</a></th> <td>sets up multi-protocol Ethereum nodes</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/p2p' target='_blank' rel="nofollow">p2p</a></th> <td>implements the Ethereum p2p network protocols: Node Discovery Protocol, RLPx v5 Topic Discovery Protocol, Ethereum Node Records as defined in EIP-778, common network port mapping protocols, and p2p network simulation.</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/params' target='_blank' rel="nofollow">params</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/rlp' target='_blank' rel="nofollow">rlp</a></th> <td>implements the RLP serialization format</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/rpc' target='_blank' rel="nofollow">rpc</a></th> <td>provides access to the exported methods of an object across a network or other I/O connection</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/signer' target='_blank' rel="nofollow">signer</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/swarm' target='_blank' rel="nofollow">swarm</a></th> <td></td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/tests' target='_blank' rel="nofollow">tests</a></th> <td>implements execution of Ethereum JSON tests</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/trie' target='_blank' rel="nofollow">trie</a></th> <td>implements Merkle Patricia Tries</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/vendor' target='_blank' rel="nofollow">vendor</a></th> <td>contains a minimal framework for creating and organizing command line Go applications, and a rich testing extension for Go's testing package</td> </tr> <tr> <th class="code table_left"><a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/whisper' target='_blank' rel="nofollow">whisper</a></th> <td>implements the Whisper protocol</td> </tr> </table> <p> <i>I used the following incantation to list the package names:</i> </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id93963e1dee18'><button class='copyBtn' data-clipboard-target='#id93963e1dee18' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>find . -maxdepth 1 -type d | sed 's^\./^^' | sed '/\..*/d'</pre> </div> <p> The <a href='https://github.com/ethereum/go-ethereum/tree/release/1.8/build' target='_blank' rel="nofollow"><code>build/</code></a> directory does not contain a Go source package; instead, it contains scripts and configurations for building the package in various environments. </p> <h2 id="smartSrc">Smart Contract Source Code</h2> <p> The <code>core/vm</code> directory contains the files that implement the EVM. These files are part of the <code>vm</code> package. Let's look at two of them: </p> <ul> <li> <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go' target='_blank' rel="nofollow"><code>contract.go</code></a>, which defines smart contract behavior. </li> <li> <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contracts.go' target='_blank' rel="nofollow"><code>contracts.go</code></a>, responsible for executing smart contracts on the EVM. </li> </ul> <h3 id="refs">Referenced Types</h3> <p> Two of the types used in the source files that we would like to understand are defined in <a href='https://github.com/ethereum/go-ethereum/blob/master/common/types.go' target='_blank' rel="nofollow"><code>common/types.go</code></a>. Let's look at them first. </p> <p> <a href='https://github.com/ethereum/go-ethereum/blob/master/common/types.go#L137-L138' target='_blank' rel="nofollow"><code>Address</code></a> is defined as an array of 20 <code>byte</code>s: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id640730fee5ba'><button class='copyBtn' data-clipboard-target='#id640730fee5ba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>const ( HashLength = 32 AddressLength = 20 ) // Address represents the 20 byte address of an Ethereum account. type Address [AddressLength]byte</pre> </div> <p> <a href='https://github.com/ethereum/go-ethereum/blob/master/common/types.go#L43' target='_blank' rel="nofollow"><code>Hash</code></a> is defined as an array of 32 <code>byte</code>s: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide9fcace93a4e'><button class='copyBtn' data-clipboard-target='#ide9fcace93a4e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// Hash represents the 32 byte Keccak256 hash of arbitrary data. type Hash [HashLength]byte</pre> </div> <p> The opcodes for each version of the EVM are defined in <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/jump_table.go' target='_blank' rel="nofollow"><code>jump_table.go</code></a>. The <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/jump_table.go#L35-L51' target='_blank' rel="nofollow"><code>operation</code></a> <code>struct</code> defines the properties: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id199d55189f56'><button class='copyBtn' data-clipboard-target='#id199d55189f56' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>type operation struct { // execute is the operation function execute executionFunc // gasCost is the gas function and returns the gas required for execution gasCost gasFunc // validateStack validates the stack (size) for the operation validateStack stackValidationFunc // memorySize returns the memory size required for the operation memorySize memorySizeFunc halts bool // indicates whether the operation should halt further execution jumps bool // indicates whether the program counter should not increment writes bool // determines whether this a state modifying operation valid bool // indication whether the retrieved operation is valid and known reverts bool // determines whether the operation reverts state (implicitly halts) returns bool // determines whether the operations sets the return data content }</pre> </div> <p> Notice the <code>jumps</code> property, a Boolean, which if set indicates that the program counter should not increment after executing any form of jump opcode. </p> <p> The <code>destinations</code> type maps the hash of a smart contract to a bit vector for each the smart contract's entry points. If a bit is set, that indicates the EMV's program counter should increment after executing the entry point. <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/analysis.go#L25-L28' target='_blank' rel="nofollow"><code>analysis.go</code></a> defines the <code>destinations</code> type like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id29586b050fd0'><button class='copyBtn' data-clipboard-target='#id29586b050fd0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// destinations stores one map per contract (keyed by hash of code). // The maps contain an entry for each location of a JUMPDEST instruction. type destinations map[common.Hash]bitvec</pre> </div> <h3 class="code">contract.go</h3> <p> This file defines smart contract behavior. </p> <h4 id="imports">Imports</h4> <p> This comment applies to all of the Go source files in the entire project. I think the following absolute import would have been better specified as a relative import: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6dc414e27151'><button class='copyBtn' data-clipboard-target='#id6dc414e27151' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"github.com/ethereum/go-ethereum/common"</pre> </div> <p> The relative import would look like this instead: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddaff9d170832'><button class='copyBtn' data-clipboard-target='#iddaff9d170832' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"../../common"</pre> </div> <p> If relative imports were used instead of absolute imports that point to the github repo, local changes to the project made by a developer would automatically be picked up. As currently written, absolute imports cause local changes to be ignored, in favor of the version on github. It might take a software developer a while to realize that the reason why their changes are ignored by most of the code base is because absoluate imports were used. It would then be painful to for the developer to modify the affected source files throughout the project such that they used relative imports. </p> <h4 id="types">Types</h4> <p> The publicly visible <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go#L30-L40' target='_blank' rel="nofollow"><code>AccountRef</code></a> type is defined as: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id547ae6ade945'><button class='copyBtn' data-clipboard-target='#id547ae6ade945' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// Account references are used during EVM initialisation and // it's primary use is to fetch addresses. Removing this object // proves difficult because of the cached jump destinations which // are fetched from the parent contract (i.e. the caller), which // is a ContractRef. type AccountRef common.Address</pre> </div> <p> The same file defines a type cast from <code>AccountRef</code> to <code>Address</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0511e2bfb237'><button class='copyBtn' data-clipboard-target='#id0511e2bfb237' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// Address casts AccountRef to a Address func (ar AccountRef) Address() common.Address { return (common.Address)(ar) }</pre> </div> <p> The <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go#L25-L28' target='_blank' rel="nofollow"><code>ContractRef</code></a> interface is used by the <code>Contract</code> <code>struct</code>, which we'll see in a moment. This <code>ContractRef</code> interface just consists of an <code>Address</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idee497ce37594'><button class='copyBtn' data-clipboard-target='#idee497ce37594' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>// ContractRef is a reference to the contract's backing object type ContractRef interface { Address() common.Address }</pre> </div> <p> The <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go#L42-L65' target='_blank' rel="nofollow"><code>Contract</code></a> struct defines the behavior of Ethereum smart contracts, and is central to the topic, so here it is in all its glory: </p><div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9d425782c337'><button class='copyBtn' data-clipboard-target='#id9d425782c337' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>type Contract struct { CallerAddress common.Address caller ContractRef self ContractRef jumpdests destinations // result of JUMPDEST analysis. Code []byte CodeHash common.Hash CodeAddr *common.Address Input []byte Gas uint64 value *big.Int Args []byte DelegateCall bool }</pre> </div> <p> <code>CallerAddress</code> is a publicly visible <code>Address</code> of the caller. <code>caller</code> and <code>self</code> are private <code>ContractRef</code>s, which as we know are really just <code>Address</code>es. </p> <p> <code>jumpdests</code>, a private field, has type <code>destinations</code>, which as we've already discussed defines if the entry point in the smart contract that need the program counter to be incremented after executing. </p> <p> <code>Code</code> is a a publicly visible <code>byte</code> slice. We don't yet know if this is the smart contract source code, compiled code, or something else. </p> <p> <code>CodeHash</code> is the publicly visible hash of the <code>Code</code>, while <code>CodeAddr</code> is a publicly visible pointer to the <code>Address</code> (of the code, presumably). </p> <p> <code>Gas</code> is the publicly visible amount of Ethereum gas allocated by the user for executing this smart contract, stored as an unsigned 64-bit integer. </p> <p> <code>Value</code> is a private pointer to a big integer. Possibly this might be the result of executing the contract? </p> <p> <code>Args</code> is a publicly visible <code>byte</code> slice, not sure what it is for. </p> <p> <code>DelegateCall</code> is a publicly visible Boolean value, unclear if this means the smart contract was invoked using <a href='https://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html#delegatecall-callcode-and-libraries' target='_blank' rel="nofollow"><code>delegatecall</code></a>. From the documentation: "This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address. This makes it possible to implement the “library” feature in Solidity: Reusable library code that can be applied to a contract’s storage, e.g. in order to implement a complex data structure." </p> <h3 class="code">contracts.go</h3> <p> This file is responsible for executing smart contracts on the EVM. </p> <h4 id="imps">Imports</h4> <p> The following imports are used: </p> <ul> <li> <a href='https://golang.org/pkg/crypto/sha256/' target='_blank' rel="nofollow">Package <code>sha256</code></a> from the <code>crypto</code> project implements the SHA224 and SHA256 hash algorithms as defined in FIPS 180-4. </li> <li> <a href='https://godoc.org/github.com/pkg/errors' target='_blank' rel="nofollow"><code>errors</code></a>, the Go language simple error handling primitives, such as <code>error</code>. </li> <li> <a href='https://golang.org/pkg/math/big/' target='_blank' rel="nofollow"><code>math/big</code></a> implements arbitrary-precision arithmetic (big numbers). </li> <li> Other packages in this project (<code>go-ethereum</code>): <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id37ef16405a84'><button class='copyBtn' data-clipboard-target='#id37ef16405a84' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/bn256" "github.com/ethereum/go-ethereum/params"</pre> </div> <p> Again, I think the above imports would have been better specified as relative imports: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id88ee438958b1'><button class='copyBtn' data-clipboard-target='#id88ee438958b1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"../../common" "../../common/math" "../../crypto" "../../crypto/bn256" "../../params"</pre> </div> </li> <li> <a href='https://godoc.org/golang.org/x/crypto/ripemd160' target='_blank' rel="nofollow"><code>ripemd160</code></a> implements the <a href='https://homes.esat.kuleuven.be/~bosselae/ripemd160.html' target='_blank' rel="nofollow">RIPEMD-160 hash algorithm</a>, a secure replacement for the MD4 and MD5 hash functions. These hashes are also termed RIPE message digests. </li> </ul> <h4 id="pres">Type <span class="code">PrecompiledContract</span></h4> <p><a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contracts.go#L32-L38' target='_blank' rel="nofollow"><code>PrecompiledContract</code></a> is the interface for native Go smart contracts. This interface is used by precompiled contracts, as we will see next. <code>Contract</code> is a <code>struct</code> defined in <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go' target='_blank' rel="nofollow"><code>contract.go</code></a>. </p> <h4 id="maps">Pre-Compiled Contract Maps</h4> <p> These maps specify various types of cryptographic hashes and utility functions, accessed via their address. </p> <p> <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contracts.go#L40-L47' target='_blank' rel="nofollow"><code>PrecompiledContractsHomestead</code></a> contains the default set of pre-compiled contract addresses used in the Frontier and Homestead releases of Ethereum: <code>ecrecover</code>, <code>sha256hash</code>, <code>ripemd160hash</code> and <code>dataCopy</code>.</p> <p><a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contracts.go#L49-L60' target='_blank' rel="nofollow"><code>PrecompiledContractsByzantium</code></a> contains the default set of pre-compiled contract addresses used in the Byzantium Ethereum release. All of the previously defined pre-compiled contract addresses are provided in Byzantium, plus: <code>bigModExp</code>, <code>bn256Add</code>, <code>bn256ScalarMul</code> and <code>bn256Pairing</code>.</p> <p> I&rsquo;m not happy about the code duplication, whereby the contents of <code>PrecompiledContractsHomestead</code> are incorporated into <code>PrecompiledContractsByzantium</code> by listing the values again; this would be better expressed by referencing the values of <code>PrecompiledContractsHomestead</code> instead of duplicating them. </p> <h4 id="eval">Contract Evaluator Function</h4> <p> The <code>RunPrecompiledContract</code> function runs and evaluates the output of a precompiled contract. It accepts three parameters: </p> <ul> <li> A <code>PrecompiledContract</code> instance. </li> <li> A byte array of input data. </li> <li> A reference to a <code>Contract</code>, defined in <a href='https://github.com/ethereum/go-ethereum/blob/master/core/vm/contract.go#L44-L65' target='_blank' rel="nofollow"><code>contract.go</code></a>, discussed above. </li> </ul> <p> The function returns: </p> <ul> <li> A byte array containing the output of the contract. </li> <li> An <code>error</code> value, which could be <code>nil</code>. </li> </ul> <h4 id="other">Other Functions</h4> <ul> <li> <code>RunPrecompiledContract</code> &ndash; runs and evaluates the output of a precompiled contract; returns the output as a byte array and an <code>error</code>. </li> <li> <code>RequiredGas</code> (overloaded) &ndash; Computes the gas required for input data, specified as a byte array and returns a <code>uint64</code>. </li> <li> <code>Run</code> (overloaded) &ndash; Computes the smart contract for input data, specified as a byte array and returns the result as a left-padded byte array and an <code>error</code>. </li> <li> <code>newCurvePoint</code> &ndash; Unmarshals a binary blob into a bn256 elliptic curve point. BN-curves are an elliptic curves suitable for cryptographic pairings that provide high security and efficiency cryptographic schemes. See the IETF paper on <a href='https://tools.ietf.org/id/draft-kasamatsu-bncurves-01.html' target='_blank' rel="nofollow">Barreto-Naehrig Curves</a> for more information. </li> </ul> Smart Contracts That Learn 2018-04-03T00:00:00-04:00 https://mslinn.github.io/blog/2018/04/03/smart-contracts-that-learn <p> It seems natural that machine learning and smart contracts will commonly be integrated over the next few years. </p> <iframe width="853" height="480" src="https://www.youtube.com/embed/a9PjjkMXsi0?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen class="rounded shadow liImg" style="width: 100%"></iframe> <p> I presented <b>Smart Contracts That Learn</b> at the Global Blockchain Conference on April 3, 2018, in Santa Clara. This was a 40-minute technical presentation. Slides are <a href='https://www.slideshare.net/mslinn/smart-contracts-that-learn' target='_blank' rel="nofollow">here</a>. </p> <p> <a href='https://www.infoq.com/presentations/smart-contracts-learn' target='_blank' rel="nofollow">InfoQ.com</a> published the presentation in their unique format, featuring synchronized video and transcripts. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/gbc2018MikeCrop.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.webp" type="image/webp"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.apng" type="image/apng"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.png" type="image/png"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.gif" type="image/gif"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/gbc2018MikeCrop.cur" type="image/x-icon"> <img alt='Mike Slinn presenting Smart Contracts That Learn' class="imgImg rounded shadow" src="/blog/images/blockchain/gbc2018MikeCrop.png" style='width: 100%; ' title='Mike Slinn presenting Smart Contracts That Learn' /> </picture> </div> <p> A full-length (70 minute) version of this presentation was presented at IBM Ottawa on May 28, 2018. <a href='https://www.slideshare.net/mslinn/fullsize-smart-contracts-that-learn' target='_blank' rel="nofollow">Here are the slides for the full-length version</a>. </p> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/ibmOttawa.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/ibmOttawa.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/ibmOttawa.webp" type="image/webp"> <source srcset="/blog/images/blockchain/ibmOttawa.apng" type="image/apng"> <source srcset="/blog/images/blockchain/ibmOttawa.png" type="image/png"> <source srcset="/blog/images/blockchain/ibmOttawa.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/ibmOttawa.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/ibmOttawa.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/ibmOttawa.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/ibmOttawa.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/ibmOttawa.gif" type="image/gif"> <source srcset="/blog/images/blockchain/ibmOttawa.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/ibmOttawa.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/ibmOttawa.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/ibmOttawa.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/ibmOttawa.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/blockchain/ibmOttawa.png" style='width: 100%; ' /> </picture> </div> SVIEF Blockchain Conference at Stanford University 2018-03-02T00:00:00-05:00 https://mslinn.github.io/blog/2018/03/02/svief <p> Images from the <a href='https://twitter.com/SVIEF1/status/978771845796134912' target='_blank' rel="nofollow">SVIEF Blockchain Conference</a> at Stanford University March 24, 2018. My presentation was entitled &ldquo;Smart Contracts that Learn&rdquo;. </p> <div class='imgWrapper imgFlex inline' style=' '> <a href='https://us5.campaign-archive.com/?e=&id=29967a597f&u=21313a52472a2961f2cb58a34' target='_blank' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/blockchain/bottos_svief_690x388.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/bottos_svief_690x388.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/bottos_svief_690x388.webp" type="image/webp"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.apng" type="image/apng"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.png" type="image/png"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.gif" type="image/gif"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/bottos_svief_690x388.cur" type="image/x-icon"> <img alt='Chainovation with Michael Slinn and Sima Yazdani' class="imgImg rounded shadow" src="/blog/images/blockchain/bottos_svief_690x388.png" style='width: 100%; ' title='Chainovation with Michael Slinn and Sima Yazdani' /> </picture> </a> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/mike_690x460.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/mike_690x460.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/mike_690x460.webp" type="image/webp"> <source srcset="/blog/images/blockchain/mike_690x460.apng" type="image/apng"> <source srcset="/blog/images/blockchain/mike_690x460.png" type="image/png"> <source srcset="/blog/images/blockchain/mike_690x460.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike_690x460.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike_690x460.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike_690x460.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike_690x460.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike_690x460.gif" type="image/gif"> <source srcset="/blog/images/blockchain/mike_690x460.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/mike_690x460.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/mike_690x460.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/mike_690x460.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/mike_690x460.cur" type="image/x-icon"> <img alt='Mike Slinn presenting at SVIEF Blockchain Conference' class="imgImg rounded shadow" src="/blog/images/blockchain/mike_690x460.png" style='width: 100%; ' title='Mike Slinn presenting at SVIEF Blockchain Conference' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/mike2_690x460.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/mike2_690x460.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/mike2_690x460.webp" type="image/webp"> <source srcset="/blog/images/blockchain/mike2_690x460.apng" type="image/apng"> <source srcset="/blog/images/blockchain/mike2_690x460.png" type="image/png"> <source srcset="/blog/images/blockchain/mike2_690x460.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike2_690x460.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike2_690x460.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike2_690x460.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike2_690x460.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike2_690x460.gif" type="image/gif"> <source srcset="/blog/images/blockchain/mike2_690x460.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/mike2_690x460.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/mike2_690x460.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/mike2_690x460.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/mike2_690x460.cur" type="image/x-icon"> <img alt='Mike Slinn talking about fradulent events' class="imgImg rounded shadow" src="/blog/images/blockchain/mike2_690x460.png" style='width: 100%; ' title='Mike Slinn talking about fradulent events' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/mike5_690x460.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/mike5_690x460.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/mike5_690x460.webp" type="image/webp"> <source srcset="/blog/images/blockchain/mike5_690x460.apng" type="image/apng"> <source srcset="/blog/images/blockchain/mike5_690x460.png" type="image/png"> <source srcset="/blog/images/blockchain/mike5_690x460.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike5_690x460.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike5_690x460.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike5_690x460.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike5_690x460.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike5_690x460.gif" type="image/gif"> <source srcset="/blog/images/blockchain/mike5_690x460.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/mike5_690x460.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/mike5_690x460.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/mike5_690x460.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/mike5_690x460.cur" type="image/x-icon"> <img alt='Mike Slinn talking about self-optimizing contracts' class="imgImg rounded shadow" src="/blog/images/blockchain/mike5_690x460.png" style='width: 100%; ' title='Mike Slinn talking about self-optimizing contracts' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/mike6_690x460.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/mike6_690x460.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/mike6_690x460.webp" type="image/webp"> <source srcset="/blog/images/blockchain/mike6_690x460.apng" type="image/apng"> <source srcset="/blog/images/blockchain/mike6_690x460.png" type="image/png"> <source srcset="/blog/images/blockchain/mike6_690x460.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike6_690x460.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike6_690x460.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike6_690x460.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike6_690x460.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/mike6_690x460.gif" type="image/gif"> <source srcset="/blog/images/blockchain/mike6_690x460.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/mike6_690x460.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/mike6_690x460.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/mike6_690x460.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/mike6_690x460.cur" type="image/x-icon"> <img alt='Mike Slinn talking about the security / complexity tradeoff' class="imgImg rounded shadow" src="/blog/images/blockchain/mike6_690x460.png" style='width: 100%; ' title='Mike Slinn talking about the security / complexity tradeoff' /> </picture> </div> Smart Contracts for Enterprises 2018-01-18T00:00:00-05:00 https://mslinn.github.io/blog/2018/01/18/smart-contracts-for-enterprises <p> &lsquo;Polyglot&rsquo; means &ldquo;of many languages&rdquo;, and I believe that enterprises that integrate smart contracts into their infrastructure will require solutions that incorporate many simultaneous software languages, libraries and runtime environments, all working as one in a distributed environment. </p> <p> I anticipate significant need for system integration. I am interested in distributed consensus / blockchain / cryptocurrency architecture that support enterprise needs by providing useful features that are easy to use effectively. </p> <p text-align="left"> I presented &ldquo;Polyglot Ethereum Smart Contracts for the Enterprise&rdquo; at the <a href='https://wcef.co/' target='_blank' rel="nofollow">World Crypto Economic Forum</a> in San Francisco, January 16, 2018. <a href='https://www.slideshare.net/mslinn/polyglot-ethereum-smart-contracts-for-the-enterprise' target='_blank' rel="nofollow">Here is my slide deck.</a> </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.webp" type="image/webp"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.apng" type="image/apng"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.png" type="image/png"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.gif" type="image/gif"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/WorldCryptoEconForum_690x439.cur" type="image/x-icon"> <img alt='Mike Slinn presents at the World Crypto Economic Forum' class="imgImg rounded shadow" src="/blog/images/blockchain/WorldCryptoEconForum_690x439.png" style='width: 100%; width:100%' title='Mike Slinn presents at the World Crypto Economic Forum' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/blockchain/WCEF2_690x414.svg" type="image/svg"> <!---<source srcset="/blog/images/blockchain/WCEF2_690x414.avif" type="image/avif">--> <source srcset="/blog/images/blockchain/WCEF2_690x414.webp" type="image/webp"> <source srcset="/blog/images/blockchain/WCEF2_690x414.apng" type="image/apng"> <source srcset="/blog/images/blockchain/WCEF2_690x414.png" type="image/png"> <source srcset="/blog/images/blockchain/WCEF2_690x414.jpg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WCEF2_690x414.jpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WCEF2_690x414.jfif" type="image/jpeg"> <source srcset="/blog/images/blockchain/WCEF2_690x414.pjpeg" type="image/jpeg"> <source srcset="/blog/images/blockchain/WCEF2_690x414.pjp" type="image/jpeg"> <source srcset="/blog/images/blockchain/WCEF2_690x414.gif" type="image/gif"> <source srcset="/blog/images/blockchain/WCEF2_690x414.tif" type="image/tiff"> <source srcset="/blog/images/blockchain/WCEF2_690x414.tiff" type="image/tiff"> <source srcset="/blog/images/blockchain/WCEF2_690x414.bmp" type="image/bmp"> <source srcset="/blog/images/blockchain/WCEF2_690x414.ico" type="image/x-icon"> <source srcset="/blog/images/blockchain/WCEF2_690x414.cur" type="image/x-icon"> <img alt='Mike Slinn presents at the World Crypto Economic Forum' class="imgImg rounded shadow" src="/blog/images/blockchain/WCEF2_690x414.png" style='width: 100%; width:100%' title='Mike Slinn presents at the World Crypto Economic Forum' /> </picture> </div> Tweet Stream Manager 2018-01-03T00:00:00-05:00 https://mslinn.github.io/blog/2018/01/03/tweet-stream-manager <p> I publish multiple tweet streams, so I wrote a Node.js web application using Express and Pug to help me manage them. This is what the user interface looks like. This is a silent video. Hopefully, it makes sense to everyone. </p> <iframe width="690px" height="388px" src="https://www.youtube.com/embed/r-47Jaa9Zek?rel=0" frameborder="1" class="liImg shadow" gesture="media" allow="encrypted-media" allowfullscreen></iframe> <p> For example, <a href='https://www.ScalaCourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a> currently has two tweet streams, each with their schedule (times shown are <code>HH:MM</code> for a 24-hour clock): </p> <ul> <li><b>ScalaCourses</b> &ndash; tweets are published at 6:45 every day</li> <li><b>SaleMore33</b> &ndash; This is a promotional campaign, starting on a certain day/time and ending on another day/time. This stream tweets at 3:24, 9:24, 13:24, 18:24, and 23:24.</li> </ul> <p><code>crontab</code>, running on a local XUbuntu machine, runs a Node.js command-line app that publishes the tweet streams:</p> <pre class="modest" style="font-size: small; padding: 0; margin-left: 0; margin-right:-20px"> NODE=/usr/bin/node TWEETER=/var/work/training/projects/tweeter 45 6 * * * $NODE $TWEETER/index.js $TWEETER/ScalaCourses 24 3,9,13,18,23 * * * $NODE $TWEETER/index.js $TWEETER/SaleMore33 2018-01-03 2018-01-15</pre> <p> I have been thinking about re-implementing this command-line program as an <a href='https://aws.amazon.com/lambda/' target='_blank' rel="nofollow">AWS Lambda function</a> one day. Instead of using CSV files for persistence, I'll probably go with Dynamo. </p> The web3j-scala Ethereum Library 2017-11-29T00:00:00-05:00 https://mslinn.github.io/blog/2017/11/29/web3j-scala <p> This video shows <a href='https://github.com/mslinn/web3j-scala' target='_blank'><code>web3j-scala</code></a>, an idiomatic Scala wrapper I wrote around <a href='https://web3j.io/' target='_blank' rel="nofollow"><code>web3j</code></a>, which is a Java 8 version of <a href='https://github.com/ethereum/web3.js' target='_blank' rel="nofollow"><code>web3.js</code></a>. These 3 libraries all leverage the <a href='https://github.com/ethereum/wiki/wiki/JSON-RPC' target='_blank' rel="nofollow"><code>json-rpc</code></a> protocol that all Ethereum clients support. <code>Web3j</code> is a lightweight, reactive, somewhat type-safe library for Java and Android that integrates with nodes on Ethereum blockchains. <code>Web3j-scala</code> provides type safety and enhanced scalability over its Java and JavaScript cousins, as well as the pleasure of writing solutions in Scala. </p> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/8jZ7kICi4SE" frameborder="0" allowfullscreen></iframe></div> <h2 id="transcript">Transcript</h2> <p> Much of the material for this presentation was taken from the <a href='https://github.com/mslinn/web3j-scala' target='_blank'><code>web3j-scala</code> GitHub page</a>. </p> Kafka Streams vs. Akka 2017-07-28T00:00:00-04:00 https://mslinn.github.io/blog/2017/07/28/kafka-vs-akka <div style="text-align: center; vertical-align: middle;"> <img src='/blog/images/akkaKafka/kafka.webp' alt="Kafka logo" class="liImg" style="height: 150px" title="Kafka logo" /> <img src='/blog/images/akkaKafka/akka_full_color.svg' alt="Akka logo" class="liImg" style="margin-left: 2em; height: 120px;" title="Akka logo" /> </div> <p> Context switches are expensive for CPUs to perform, and each incoming message in an <a href='https://akka.io/' target='_blank' rel="nofollow">Akka</a> system requires a context switch when fairness (minimal latency deviation) is important <sup style='font-size: smaller'><a href='#1'>[1]</a></sup>. In contrast, <a href='https://kafka.apache.org/documentation/streams/' target='_blank' rel="nofollow">Apache Kafka Streams</a> offers consistent processing times without requiring context switches, so Kafka Streams produces significantly higher throughput than Akka can when contending with a high volume of small computations that must be applied fairly. </p> <h2 id="fairness">Fairness</h2> <p> &ldquo;Fairness&rdquo; in a streaming system is a measure of how evenly computing resources are applied to each incoming message. A fair system is characterized by consistent and predictable latency for the processing of each message. An emergent effect of fair systems is that messages are journaled, processed, and transformed for downstream computation in approximately the order received. The output of a fair system roughly matches input in real time; a perfectly fair system would provide a perfect correspondence between input messages and output messages. </p> <p> &ldquo;Fair enough&rdquo; systems have a some acceptable amount of jitter in the ordering of the input vs. output messages. </p> <p> You might expect that streaming systems are generally fair, but systems based on Akka rarely are because of the implementation details of Akka's multithreaded architecture. Instead, Akka-based systems enqueue incoming messages for each actor, and the Akka scheduler periodically initiates the processing of each actor's input message queue. This is actually a type of batch processing. The reason Akka does this is so the high cost of CPU context switches can be amortized over several messages, to keep system throughput at an acceptable rate. </p> <h2 id="contextSwitching">Context Switching</h2> <p> It is desirable for the primary type of context switching in Akka systems to be between tasks on a thread because other types of context switching are more expensive. However, <a href='https://stackoverflow.com/questions/20288379/analysis-performance-of-forkjoinpool' target='_blank' rel="nofollow">switching tasks on a thread</a> costs a surprisingly large amount of CPU cycles. To put this into context, the accompanying table shows various latencies compared to the amount of time necessary for a context switch on an <a href='https://ark.intel.com/products/27218/Intel-Xeon-Processor-5150-4M-Cache-2_66-GHz-1333-MHz-FSB' target='_blank' rel="nofollow">Intel Xeon 5150</a> CPU <a href='#2' style='font-size:smaller>[2]</sup>'><sup</a>. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/akkaKafka/latency.svg" type="image/svg"> <!---<source srcset="/blog/images/akkaKafka/latency.avif" type="image/avif">--> <source srcset="/blog/images/akkaKafka/latency.webp" type="image/webp"> <source srcset="/blog/images/akkaKafka/latency.apng" type="image/apng"> <source srcset="/blog/images/akkaKafka/latency.png" type="image/png"> <source srcset="/blog/images/akkaKafka/latency.jpg" type="image/jpeg"> <source srcset="/blog/images/akkaKafka/latency.jpeg" type="image/jpeg"> <source srcset="/blog/images/akkaKafka/latency.jfif" type="image/jpeg"> <source srcset="/blog/images/akkaKafka/latency.pjpeg" type="image/jpeg"> <source srcset="/blog/images/akkaKafka/latency.pjp" type="image/jpeg"> <source srcset="/blog/images/akkaKafka/latency.gif" type="image/gif"> <source srcset="/blog/images/akkaKafka/latency.tif" type="image/tiff"> <source srcset="/blog/images/akkaKafka/latency.tiff" type="image/tiff"> <source srcset="/blog/images/akkaKafka/latency.bmp" type="image/bmp"> <source srcset="/blog/images/akkaKafka/latency.ico" type="image/x-icon"> <source srcset="/blog/images/akkaKafka/latency.cur" type="image/x-icon"> <img alt='Latency numbers every programmer should know' class="imgImg rounded shadow" src="/blog/images/akkaKafka/latency.png" style='width: 100%; ' title='Latency numbers every programmer should know' /> </picture> </div> <p> As you can see, a lot of computing can be done during the time that a CPU performs a context switch. My <a href='https://scalacourses.com/showCourse/45' target='_blank' rel="nofollow">Intermediate Scala</a> course has several lectures that explore multithreading in great detail, with lots of code examples. </p> <h2 id="distributed">Building Distributed OSes is Expensive and Hard</h2> <p> Akka is rather low-level, and the actor model it is built around was originally conceived 45 years ago. Applications built using Akka require a lot of custom plumbing. Akka applications are rather like custom-built distributed operating systems spanning multiple <a href='https://en.wikipedia.org/wiki/OSI_model' target='_blank' rel="nofollow">layers of the OSI model</a>, where application-layer logic is intertwined with transport-layer, session-layer and presentation-layer logic. </p> <p> Debugging and tuning a distributed system is inherently difficult. Building an operating system that spans multiple network nodes and continues to operate properly in the face of network partitions is even harder. Because each Akka application is unique, customers find distributed systems built with Akka to be expensive to maintain. </p> <h2 id="summary">Kafka vs. Akka</h2> <p> For most software engineering projects, it is better to use vendor-supplied or open-source libraries for building distributed systems because the library maintainers can amortize the maintenance cost over many systems. This allows users to focus on their problem, instead of having to develop and maintain the complex and difficult-to-understand plumbing for their distributed application. </p> <p> Akka is a poor choice for apps that need to do small amounts of computation on high volumes of messages where latency must be minimal and roughly equal for all messages. <a href='https://docs.confluent.io/current/streams/index.html' target='_blank' rel="nofollow">Kafka Streams</a> is a better choice for those types of applications because programmers can focus on the problem at hand, without having to build and maintain a custom distributed operating system. <a href='https://docs.confluent.io/current/streams/concepts.html#ktable' target='_blank' rel="nofollow">KTable</a> is also a nice abstraction to work with. </p> <!--p> Kafka also provides a higher-level mechanism for partitioning work among many servers than <a href='https://doc.akka.io/docs/akka/current/scala/common/cluster.html' target='_blank' rel="nofollow">Akka clusters</a>, and it is no less efficient or fair. </p--> <h2 id="motivation">Motivation</h2> <div class='imgWrapper imgFlex right quartersize' style=' '> <picture class='imgPicture'> <source srcset="/assets/images/robotCircle400.svg" type="image/svg"> <!---<source srcset="/assets/images/robotCircle400.avif" type="image/avif">--> <source srcset="/assets/images/robotCircle400.webp" type="image/webp"> <source srcset="/assets/images/robotCircle400.apng" type="image/apng"> <source srcset="/assets/images/robotCircle400.png" type="image/png"> <source srcset="/assets/images/robotCircle400.jpg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.jpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.jfif" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.pjpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.pjp" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.gif" type="image/gif"> <source srcset="/assets/images/robotCircle400.tif" type="image/tiff"> <source srcset="/assets/images/robotCircle400.tiff" type="image/tiff"> <source srcset="/assets/images/robotCircle400.bmp" type="image/bmp"> <source srcset="/assets/images/robotCircle400.ico" type="image/x-icon"> <source srcset="/assets/images/robotCircle400.cur" type="image/x-icon"> <img alt='EmpathyWorks™ &mdash; Artificial Personality For AI Systems' class="imgImg rounded shadow" src="/assets/images/robotCircle400.png" style='width: 100%; ' title='EmpathyWorks™ &mdash; Artificial Personality For AI Systems' /> </picture> </div> <p> EmpathyWorks&trade; is a platform for modeling human emotions and network effects amongst groups of people as events impinge upon them. Each incoming event has the potential to cause a cascade of second-order events, which are spread throughout the network via people's relationships with one another. The actual processing required for each event is rather small, but given that the goal is to model everyone on earth (all 7.5 billion people), this is a huge computation to continuously maintain. For EmpathyWorks to function properly, incoming messages must be processed somewhat fairly. </p> <h2 id="ktables">KTables and Compacted Topics</h2> <p> Kafka Streams makes it easy to transform one or more immutable streams into another immutable stream. A KTable is an abstraction of a changelog stream, where each record represents an update. Instead of a using actors to track the state of multiple entities, a KTable provides an analogy to database tables, where the rows in a table contains the current state for each of the entities. Kafka is responsible for dealing with network partitions and data collisions, so the application layer does not become polluted with lower-layer concerns. </p> <p> Kafka&rsquo;s <a href='https://stackoverflow.com/questions/53390170/how-to-create-kafka-compacted-topic' target='_blank' rel="nofollow">compacted topics</a> are a unique type of stream that only maintains the most recent message for each key. This produces something like a <i>materialized</i> or table-like view of a stream, with up-to-date values (subject to eventual consistency) for all key/value pairs in the log. </p> <hr class="separator"> <p id="1"> [1] From the <a href='https://letitcrash.com/article/40755146949/tuning-dispatchers-in-akka-applications' target='_blank' rel="nofollow">Akka blog</a>: &ldquo;... pay close attention to your &lsquo;throughput&rsquo; setting on your dispatcher. This defines thread distribution &lsquo;fairness&rsquo; in your dispatcher &ndash; telling the actors how many messages to handle in their mailboxes before relinquishing the thread so that other actors do not starve. However, a context switch in CPU caches is likely each time actors are assigned threads, and warmed caches are one of your biggest friends for high performance. It may behoove you to be less fair so that you can handle quite a few messages consecutively before releasing it.&rdquo; </p> <p id="2">[2] Taken from <a href='https://gist.github.com/nelsnelson/3955759' target='_blank' rel="nofollow">Latency numbers every programmer should know</a>. The Intel Xeon 5150 was released in 2006; for a more up-to-date information about CPU context switch time, please see <a href='https://www.quora.com/Linux-Kernel-How-much-processor-time-does-a-process-switching-cost-to-the-process-scheduler#pphKmT' target='_blank' rel="nofollow">this Quora answer</a>. Although CPUs have gotten slightly faster since 2006, networks and SSDs have gotten much faster, so the relative latency penalty has actually increased over time. </p> Resume-Driven Development 2017-05-30T00:00:00-04:00 https://mslinn.github.io/blog/2017/05/30/resume-driven-development <div class='imgWrapper imgFlex right quartersize' style=' '> <a href='https://spark.apache.org/' target='_blank' rel='nofollow' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/assets/images/rdd/spark.svg" type="image/svg"> <!---<source srcset="/assets/images/rdd/spark.avif" type="image/avif">--> <source srcset="/assets/images/rdd/spark.webp" type="image/webp"> <source srcset="/assets/images/rdd/spark.apng" type="image/apng"> <source srcset="/assets/images/rdd/spark.png" type="image/png"> <source srcset="/assets/images/rdd/spark.jpg" type="image/jpeg"> <source srcset="/assets/images/rdd/spark.jpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/spark.jfif" type="image/jpeg"> <source srcset="/assets/images/rdd/spark.pjpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/spark.pjp" type="image/jpeg"> <source srcset="/assets/images/rdd/spark.gif" type="image/gif"> <source srcset="/assets/images/rdd/spark.tif" type="image/tiff"> <source srcset="/assets/images/rdd/spark.tiff" type="image/tiff"> <source srcset="/assets/images/rdd/spark.bmp" type="image/bmp"> <source srcset="/assets/images/rdd/spark.ico" type="image/x-icon"> <source srcset="/assets/images/rdd/spark.cur" type="image/x-icon"> <img alt='Apache Spark logo' class="imgImg rounded shadow" src="/assets/images/rdd/spark.png" style='width: 100%; padding: 1em;' title='Apache Spark logo' /> </picture> </a> </div> <p> I had occasion to speak with a technical manager at a Very Large Corporation at length recently. He wanted advice on where to find Spark programmers. After my usual 20 questions, I realized that his project did not have any requirement for the usual Spark capabilities: </p> <ul> <li>No need of machine learning or pattern recognition.</li> <li>He emphatically did not want to process streaming data.</li> <li>He had a rather small volume of data.</li> <li>He had no need of interactive analysis.</li> <li>There was no need for ETL.</li> <li>&ldquo;Data enrichment&rdquo; in his case was no more difficult than joining two SQL tables.</li> <li>There was no requirement for trigger event detection.</li> <li>... and there was no requirement for session analysis.</li> </ul> <p>This man just wanted to put &ldquo;I managed the development of an Apache Spark application&rdquo; on his resume.</p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.svg" type="image/svg"> <!---<source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.avif" type="image/avif">--> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.webp" type="image/webp"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.apng" type="image/apng"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.png" type="image/png"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.jpg" type="image/jpeg"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.jpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.jfif" type="image/jpeg"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.pjpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.pjp" type="image/jpeg"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.gif" type="image/gif"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.tif" type="image/tiff"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.tiff" type="image/tiff"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.bmp" type="image/bmp"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.ico" type="image/x-icon"> <source srcset="/assets/images/rdd/cart_before_the_horse1_450x313.cur" type="image/x-icon"> <img alt='The cart is before the horse' class="imgImg rounded shadow" src="/assets/images/rdd/cart_before_the_horse1_450x313.png" style='width: 100%; ' title='The cart is before the horse' /> </picture> </div> <h2 id="turd">Shine Up That Turd</h2> <p> Buffing up a resume with a nonsensical misapplication of the latest software fashion is nothing new. Few people involved with such a project have a positive experience, however. When complex technologies are misapplied they often fail to deliver value, or the project just fails, which contributes to the <a href='https://www.gartner.com/technology/research/methodologies/hype-cycle.jsp' target='_blank' rel="nofollow">trough of disillusionment</a> in the Gartner Hype Cycle. </p> <div class='imgWrapper imgFlex center' style='width: 250px; '> <picture class='imgPicture'> <source srcset="/assets/images/rdd/WCFields_450x576.svg" type="image/svg"> <!---<source srcset="/assets/images/rdd/WCFields_450x576.avif" type="image/avif">--> <source srcset="/assets/images/rdd/WCFields_450x576.webp" type="image/webp"> <source srcset="/assets/images/rdd/WCFields_450x576.apng" type="image/apng"> <source srcset="/assets/images/rdd/WCFields_450x576.png" type="image/png"> <source srcset="/assets/images/rdd/WCFields_450x576.jpg" type="image/jpeg"> <source srcset="/assets/images/rdd/WCFields_450x576.jpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/WCFields_450x576.jfif" type="image/jpeg"> <source srcset="/assets/images/rdd/WCFields_450x576.pjpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/WCFields_450x576.pjp" type="image/jpeg"> <source srcset="/assets/images/rdd/WCFields_450x576.gif" type="image/gif"> <source srcset="/assets/images/rdd/WCFields_450x576.tif" type="image/tiff"> <source srcset="/assets/images/rdd/WCFields_450x576.tiff" type="image/tiff"> <source srcset="/assets/images/rdd/WCFields_450x576.bmp" type="image/bmp"> <source srcset="/assets/images/rdd/WCFields_450x576.ico" type="image/x-icon"> <source srcset="/assets/images/rdd/WCFields_450x576.cur" type="image/x-icon"> <img alt='W. C. Fields' class="imgImg rounded shadow" src="/assets/images/rdd/WCFields_450x576.png" style='width: 100%; ' title='W. C. Fields' /> </picture> </div> <p> Consider the cynical and selfish world view that such a person must have to engage in this sort of activity. Software professionals are rewarded for being fashionable, and part of the mystique is to say and do things that others don&rsquo;t fully comprehend. If one keeps moving fast enough, and learns some key phrases to use as put downs that sound impressive but have no meaning when taken out of context (&ldquo;your approach would likely cause a data race condition&rdquo;), others are kept off balance long enough for the perpetrator to achieve advantage. </p> <p> As <a href='https://en.wikipedia.org/wiki/W._C._Fields' target='_blank' rel="nofollow">W.C. Fields</a> said 100 years ago: &ldquo;If you can&rsquo;t dazzle them with brilliance, baffle them with bullshit.&rdquo; He also made a movie entitled <a href='https://www.youtube.com/watch?v=-AdXkJbW1Tg' target='_blank' rel="nofollow">Never Give a Sucker an Even Break</a>... but I digress. </p> <h2 id="fashion">Fashion is the Enemy of Delivering Valuable Results on Time</h2> <div class='imgWrapper imgFlex center' style='width: 250px; '> <picture class='imgPicture'> <source srcset="/assets/images/rdd/fashion_450x675.svg" type="image/svg"> <!---<source srcset="/assets/images/rdd/fashion_450x675.avif" type="image/avif">--> <source srcset="/assets/images/rdd/fashion_450x675.webp" type="image/webp"> <source srcset="/assets/images/rdd/fashion_450x675.apng" type="image/apng"> <source srcset="/assets/images/rdd/fashion_450x675.png" type="image/png"> <source srcset="/assets/images/rdd/fashion_450x675.jpg" type="image/jpeg"> <source srcset="/assets/images/rdd/fashion_450x675.jpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/fashion_450x675.jfif" type="image/jpeg"> <source srcset="/assets/images/rdd/fashion_450x675.pjpeg" type="image/jpeg"> <source srcset="/assets/images/rdd/fashion_450x675.pjp" type="image/jpeg"> <source srcset="/assets/images/rdd/fashion_450x675.gif" type="image/gif"> <source srcset="/assets/images/rdd/fashion_450x675.tif" type="image/tiff"> <source srcset="/assets/images/rdd/fashion_450x675.tiff" type="image/tiff"> <source srcset="/assets/images/rdd/fashion_450x675.bmp" type="image/bmp"> <source srcset="/assets/images/rdd/fashion_450x675.ico" type="image/x-icon"> <source srcset="/assets/images/rdd/fashion_450x675.cur" type="image/x-icon"> <img alt='Fashion model' class="imgImg rounded shadow" src="/assets/images/rdd/fashion_450x675.png" style='width: 100%; ' title='Fashion model' /> </picture> </div> <p> I often remark that the software business is just like the fashion business. The cycle goes like this: </p> <ol> <li>The latest thing is preannounced. No documentation is available. Big claims are made.</li> <li>Early code is provided, but it is slow, very buggy, there is no documentation, and the wild claims continue.</li> <li>Seminars pump up the hype. Arm-waving abounds. &lsquo;Experts&rsquo; associated with vendors are interviewed.</li> <li>A whole new vocabulary is invented to describe the features. However, since those terms are never defined, few people realize that the message is actually &ldquo;same old stuff with a new coat of paint.&rdquo; The cool kids learn the words and can parrot them back they way they heard them, but they don't actually know what they are saying. Even if you knew what the phrase &ldquo;a monad is a monoid in the category of endofunctors&rdquo; means, it would not help you add value to a business process... and for most programmers who work in the business world, that is what actually pays their bills.</li> <li>Gartner, ThoughtWorks and O&rsquo;Reilly Radar say nice things about the new tech.</li> <li>Marketing and sales activity shifts into high gear. Everyone&rsquo;s laptop has stickers on it that show the owner is hip.</li> <li>Some incomplete and mostly useless docs trickle out. Release candidates are eventually made available but it is not obvious how they provide value without significant magic being applied.</li> <li>Version 1 finally arrives, completely useless to everyone except those few customers who paid huge dollars to the vendor, so they could get the bragging rights for what is essentially a custom build.</li> <li>No useful benchmarks are available. Bugs crawl out from every crevasse. Documentation reads like it was badly translated from Chinese into Bantu, then into Navajo and then English.</li> <li>Version 1.1 comes out, to address stop-ship problems that somehow did not prevent version 1 from shipping.</li> <li>Version 2 is announced, completely incompatible with Version 1.x, and it is touted as revolutionary.</li> <li>The digital lemmings decide that this is the cliff they hold dearest in their hearts, and proceed to jump from it <i>en masse</i>.</li> <li>Gartner, ThoughtWorks and O&rsquo;Reilly Radar downgrade the new tech to &ldquo;meh&rdquo;</li> <li>... and the churn continues.</li> </ol> <p> Documentation is not written to be understood, they are written to encourage payments to the digital elite &ndash; who have no interest in providing value; they just want to make money from foolish lemmings. This encourages others to aspire to be elitists. The hype cycle only works because our society accepts bullshit, and software has become a cultural phenomenon. We have the software quality and utility that we deserve as a society. If we want better software, we as a society will have to modify our purchasing behavior accordingly. </p> <p> The software business has often demonstrated an insatiable need to be cool, at the expense of providing little or no value because evidencing value is either too hard for the many posers in the software business, or too boring for those who are capable but jaded. </p> Better Syntactic Sugar for Scala Futures 2017-05-25T00:00:00-04:00 https://mslinn.github.io/blog/2017/05/25/better-syntactic-sugar-for-scala-futures <p> Ever since Scala&rsquo;s <code>Future</code>s were initially provided as part of Akka 2.0, programmers have been confused by the non-intuitive syntax. That is why <a href='https://www.ScalaCourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a> dedicates an entire lecture to <a href='https://www.scalacourses.com/student/showLecture/172' target='_blank' rel="nofollow">For-comprehensions With Futures</a>, as part of an 8 lecture series on Scala <code>Future</code>s within the <a href='https://www.scalacourses.com/showCourse/45' target='_blank' rel="nofollow">Intermediate Scala</a> course. </p> <p> As Viktor Klang <a href='https://viktorklang.com/blog/Futures-in-Scala-protips-2.html' target='_blank' rel="nofollow">points out</a> in his blog, the following code does not run 3 <code>Future</code>s in parallel: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id49ee6ba4d9d9'><button class='copyBtn' data-clipboard-target='#id49ee6ba4d9d9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { v1 &lt;- Future(someCalculation()) v2 &lt;- Future(someOtherCalculation()) v3 &lt;- Future(someDifferentCalculation()) } yield doSomethingWith(v1, v2, v3)</pre> </div> <p> The compiler has no way of ascertaining the programmer&rsquo;s intent &ndash; perhaps it is desirable for some reason to run the 3 <code>Future</code>s one after the other. </p> <p>Viktor suggests this syntax to make futures run in parallel:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf9ce7a27d314'><button class='copyBtn' data-clipboard-target='#idf9ce7a27d314' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { f1 = Future(someCalculation()) f2 = Future(someOtherCalculation()) f3 = Future(someDifferentCalculation()) v1 &lt;- f1 v2 &lt;- f2 v3 &lt;- f3 } yield doSomethingWith(v1, v2, v3)</pre> </div> <p> While Viktor&rsquo;s solution works, it is verbose. Worse, it silently fails to run the futures in parallel if the programmer accidentally writes even one of the expressions out of order: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id670578742e3d'><button class='copyBtn' data-clipboard-target='#id670578742e3d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { f1 = Future(someCalculation()) v1 &lt;- f1 f2 = Future(someOtherCalculation()) v2 &lt;- f2 f3 = Future(someDifferentCalculation()) v3 &lt;- f3 } yield doSomethingWith(v1, v2, v3)</pre> </div> <p> Again, the compiler has no way of ascertaining the programmer's intent, so it should not generate an error or warning message. </p> <h2 id="macro">We Need A Macro</h2> <p> A new right-associative operator, implemented as a Scala macro, would make the programmer's intent clear. Let&rsquo;s call this operator <code>&lt;=:</code> (parallel generator). The above code could be rewritten using the parallel generation operator like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9ba4921bfe1e'><button class='copyBtn' data-clipboard-target='#id9ba4921bfe1e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { v1 &lt;=: Future(someCalculation()) v2 &lt;=: Future(someOtherCalculation()) v3 &lt;=: Future(someDifferentCalculation()) } yield doSomethingWith(v1, v2, v3)</pre> </div> <p>There is no longer any doubt that the programmer intended for all 3 futures to run in parallel.</p> <p> The macro would examine all the for-expression&rsquo;s generators and expand consecutive expressions that use the <code>&lt;=:</code> operator to a series of variable declarations using the <code>=</code> operator followed by a series of assignments using the <code>&lt;-</code> operator, exactly as we saw earlier: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idef652ed4ae34'><button class='copyBtn' data-clipboard-target='#idef652ed4ae34' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { f1 = Future(someCalculation()) f2 = Future(someOtherCalculation()) f3 = Future(someDifferentCalculation()) v1 &lt;- f1 v2 &lt;- f2 v3 &lt;- f3 } yield doSomethingWith(v1, v2, v3)</pre> </div> <p> Because the compiler &lsquo;knows&rsquo; that the programmer&rsquo;s intention was to run the <code>Future</code>s in parallel, this sort of error could cause an error or warning message to be generated: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd6c9690de942'><button class='copyBtn' data-clipboard-target='#idd6c9690de942' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def doSomething(someParameter: SomeType) (implicit ec: ExecutionContext): Future[Something] = for { v1 &lt;=: Future(someCalculation()) x <- List(1, 2, 3) v2 &lt;=: Future(someOtherCalculation()) y <- List("a", "b") v3 &lt;=: Future(someDifferentCalculation()) } yield doSomethingWith(v1, v2, v3)</pre> </div> <p>... or the macro might reorder the generators and issue a warning that it did so.</p> <h2 id="macro">Include the Macro in Scala 2.12.x</h2> <p> This macro should become part of the Scala language so long as <code>Future</code>s are part of the standard runtime. If and when <code>Future</code>s are hived out of the standard runtime, the macro should be packaged with <code>Future</code>s. </p> <hr /> <p> PS: <a href='https://twitter.com/flaviusbraz' target='_blank' rel="nofollow">@flaviusbraz</a> tweeted on May 25, 2017: <a href='https://twitter.com/flaviusbraz/status/867868408267685888' target='_blank' rel="nofollow">&ldquo;Easily doable with a macro transformation. In fact, I&rsquo;ve implemented this transformation at Twitter, but it&rsquo;s not open source.&rdquo;</a> </p> Exploratory Conversation With AIs 2017-04-29T00:00:00-04:00 https://mslinn.github.io/blog/2017/04/29/exploratory-conversations-with-ais <p> This is a high-level proposal, not documentation for any specific system that exists today. I would be happy to discuss possible implementation details with interested parties, including the creation of a prototype. </p> <h2 id="overview">Intents and Subgoals</h2> <p> Some of today&rsquo;s interactive voice response systems, such as AWS&rsquo;s <a href='https://aws.amazon.com/lex/' target='_blank' rel="nofollow">Lex</a>, Amazon&rsquo;s <a href='https://aws.amazon.com/alexa/' target='_blank' rel="nofollow">Alexa</a>, and Google&rsquo;s <a href='https://cloud.google.com/dialogflow/docs/' target='_blank' rel="nofollow">Dialogflow</a>, use <i>intent</i>s as a means of gathering the required parameters to fulfill a voice command. The next big step in voice interfaces to computation would be the ability to have an exploratory conversation, where one or more subgoals may or may not emerge. </p> <p> Subgoals might be fulfilled by intents. Continuous tracking of viable subgoals could be accomplished by a <a href='https://en.wikipedia.org/wiki/Constraint_programming' target='_blank' rel="nofollow">constraint-based solver</a> that uses rules to participate in an exploratory conversation by recognizing and prioritizing potential subgoals, thereby activating and deactivating intents during the conversation. Once a user confirms that a potential subgoal is desirable, the system might consider that goal to be the only goal worth pursuing, or it might continuously elicit more information from the user regarding other potential subgoals. User-supplied information might be applied to a model associated with a subgoal/intent, and/or it might be retained as a user preference. </p> <div class='imgWrapper imgFlex center' style='width: 500px; '> <picture class='imgPicture'> <source srcset="/blog/images/aiConversation/anthropomorph.svg" type="image/svg"> <!---<source srcset="/blog/images/aiConversation/anthropomorph.avif" type="image/avif">--> <source srcset="/blog/images/aiConversation/anthropomorph.webp" type="image/webp"> <source srcset="/blog/images/aiConversation/anthropomorph.apng" type="image/apng"> <source srcset="/blog/images/aiConversation/anthropomorph.png" type="image/png"> <source srcset="/blog/images/aiConversation/anthropomorph.jpg" type="image/jpeg"> <source srcset="/blog/images/aiConversation/anthropomorph.jpeg" type="image/jpeg"> <source srcset="/blog/images/aiConversation/anthropomorph.jfif" type="image/jpeg"> <source srcset="/blog/images/aiConversation/anthropomorph.pjpeg" type="image/jpeg"> <source srcset="/blog/images/aiConversation/anthropomorph.pjp" type="image/jpeg"> <source srcset="/blog/images/aiConversation/anthropomorph.gif" type="image/gif"> <source srcset="/blog/images/aiConversation/anthropomorph.tif" type="image/tiff"> <source srcset="/blog/images/aiConversation/anthropomorph.tiff" type="image/tiff"> <source srcset="/blog/images/aiConversation/anthropomorph.bmp" type="image/bmp"> <source srcset="/blog/images/aiConversation/anthropomorph.ico" type="image/x-icon"> <source srcset="/blog/images/aiConversation/anthropomorph.cur" type="image/x-icon"> <img alt='Do not anthropomorphize computers. They hate that!' class="imgImg rounded shadow" src="/blog/images/aiConversation/anthropomorph.png" style='width: 100%; ' title='Do not anthropomorphize computers. They hate that!' /> </picture> </div> <h2 id="anthropomorphism">Anthropomorphism</h2> <p>If an AI system is a good conversationalist, even if it has no physical presence, people will <a href='https://en.wikipedia.org/wiki/Anthropomorphism#In_computing' target='_blank' rel="nofollow">anthropomorphically</a> attribute the system with human traits, emotions, and intentions. This should not be discouraged. Rather, AI systems should aspirationally model our higher selves. The movie <a href='https://en.wikipedia.org/wiki/Her_(film)' target='_blank' rel="nofollow">Her</a> explored this in a charming way. </p> <h2 id="breakdown">Breaking It Down</h2> <p>An interactive voice response system recognizes an <i>intent</i> by a key word or phrase uttered by the user. For example, if the user says: &ldquo;Computer, order dinner&rdquo;, and the system was previously &lsquo;taught&rsquo; to understand the word &ldquo;order&rdquo; as a key word for launching the <code>OrderDinner</code> intent, the system would then elicit the kind of food the user wanted, how many people would be eating, etc, and then order the desired dinner.</p> <p style="font-style: italic">Anthropomorphically: if intents are the only programming technique employed, they present interactive voice systems as slaves.</p> <p>Intents are useful for recognizing commands and gathering enough associated data to carry out the command. Intents are not useful for open-ended conversations where there is no explicit goal, or potential subgoals emerge as the result of a dialog. Once a subgoal is identified and confirmed, however, processing an intent is an efficient mechanism for fulfilling the emergent subgoal. </p> <p>Designers of chatbots and video games are familiar with goal-seeking behavior. <i>Exploratory conversation</i> is how people interact with each other IRL (in real life). Each individual uses a variety of strategies for initiating exploratory conversation, and participating in it. For example, small talk at a gathering is a useful skill for getting to know other people, and a socially adept practitioner of small talk can innocuously gather a lot of information in this way. Another strategy is to make a controversial statement, and use the resulting banter to learn about the conversationalists and the possible subgoals that they might request. A wide variety of such strategies exist, and could be utilized by conversational systems. </p> <p style="font-style: italic">Anthropomorphically: with exploratory conversation capability, interactive voice systems can be presented socially as co-operative entities.</p> <h2 id="agenda">Agenda and Strategy</h2> <p>Unlike people, computers can only do a finite number of tasks, and voice recognition systems are programmed with a finite number of intents. I define the potential <i>agenda</i> for a chatbot or video game to be the entire scope of its pre-programmed intents. Agendas may be fully disclosed, or they might be obvious, or they might be unveiled over time. Ethical considerations might apply to the design and implementation of conversational AI systems. </p> <p>Users should be apprised of the agenda of every autonomous computer entity they encounter. To mitigate potential problems, an industrial code of conduct should be established. The Europe Union will likely be the first government to require published standards and possibly a certification process. </p> <p>I define <i>strategy</i> to mean the autonomous modification to goal-seeking behavior when a criterion is met. For example, an insurance chatbot might begin to solicit sensitive information only after it has reason to believe that a modicum of trust has been established with the prospect. How a strategy is executed is quite significant; by this I mean the degree of <i>diplomacy</i>. Some circumstances might sacrifice diplomacy to save lives, while under normal circumstances the AI entity should treat everyone with respect and kindness. Again, the Europe Union is likely to be the first government to require published standards and possibly a certification process. </p> <h2 id="2017-05-05">Update May 5, 2017</h2> <p>Today, a week after I first published this articleing, I received the following email from <a href='https://cloud.google.com/dialogflow/docs/' target='_blank' rel="nofollow"><code>api.ai</code></a>. Two points worth mentioning:</p> <ol> <li>I have no apps running on api.ai that use their Small Talk feature.</li> <li>The Small Talk feature, even after the changes mentioned below, does not approach the capability I proposed above.</li> </ol> <p>Nonetheless, it was great to get the information.</p> <div class="quote"> <p>Hi Michael,</p> <p> We noticed you are using the <a href='https://dialogflow.cloud.google.com/#/agent/smalltalk' target='_blank' rel="nofollow">Small Talk customization feature</a> in your agent, and that's why we are getting in touch. </p> <p>The Small Talk customization has been updated.</p> <p> The new version will be even faster and more accurate. It also includes some additional intents that you can customize. The update also includes changes in actions and parameters returned with the responses. </p> <p> If you use them in your business logic, please review the changes <a href='https://cloud.google.com/dialogflow/docs/' target='_blank' rel="nofollow">here</a>. If you’re ready, you can apply change right now here. Otherwise, the changes will be applied automatically on May 29, 2017. </p> <p>If you’d like a more flexible way to implement small talk, then get your <a href='https://console.api.ai/api-client/#/agent/prebuiltAgents/Small-Talk' target='_blank' rel="nofollow">source agent</a> and implement the use cases you care about in your timeline. </p> <p>Regards,</p> <p><a href='https://cloud.google.com/dialogflow' target='_blank' rel="nofollow"><code>API.AI</code></a> Team</p> </div> <h2 id="author">About the Author</h2> <p> Mike Slinn has been pursuing EmpathyWorks&trade; original AI research (augmenting machine learning with simulations and event-driven architecture) since 2007. </p> UI Considerations for the Visually Impaired 2017-04-04T00:00:00-04:00 https://mslinn.github.io/blog/2017/04/04/ui-considerations-for-the-visually-impaired <p> If you look at a computer screen all day, and the screen has a lot of glare, the resulting eyestrain can temporarily degrade your vision to the same degree as a visually impaired person. </p> <div class="info"> <h2>2022-05-18 Update</h2> <p> An excellent article was just published by Andrew Somers about <a href='https://tangledweb.xyz/please-stop-using-grey-text-3d3e71acfca8' target='_blank' rel="nofollow">WCAG 2.0 (Web Content Accessibility Guidelines)</a>. The article discusses contrast, dark mode, minimum font size, line spacing, and the limitations of current scientific support for readability. Mr. Somers is a W3 AGWG invited expert and a readability and color science researcher. </p> </div> <h2 id="background">Background</h2> <p> According to the Farlex Free Medical Dictionary, <a href='https://medical-dictionary.thefreedictionary.com/Visual+Impairment' target='_blank' rel="nofollow">visual impairment</a>, or low vision, is a severe reduction in vision that cannot be corrected with standard glasses or contact lenses and reduces a person's ability to function at certain or all tasks. The World Health Organization <a href='https://www.who.int/mediacentre/factsheets/fs282/en/' target='_blank' rel="nofollow">estimates</a> that 285 million people are visually impaired worldwide, of which 246 million have low vision and 39 million are blind. People's vision generally deteriorates with age. </p> <p> In the US, cataracts are the most common form of visual impairment, according to the <a href='https://www.nei.nih.gov/learn-about-eye-health/resources-for-health-educators/eye-health-data-and-statistics' target='_blank' rel="nofollow">National Eye Institute</a>. The NEI estimates that more than 24 million Americans over the age of 40 have cataracts. &ldquo;The risk of cataracts increases with each decade of life starting around age 40. By age 75, half of white Americans have cataracts. By age 80, 70 percent of whites have cataracts, compared with 53 percent of blacks and 61 percent of Hispanic Americans.&rdquo; Males are twice as likely as females to get cataracts. </p> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.svg" type="image/svg"> <!---<source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.avif" type="image/avif">--> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.webp" type="image/webp"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.apng" type="image/apng"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.png" type="image/png"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.jpg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.jpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.jfif" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.pjpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.pjp" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.gif" type="image/gif"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.tif" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.tiff" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.bmp" type="image/bmp"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.ico" type="image/x-icon"> <source srcset="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.cur" type="image/x-icon"> <img alt='2010 US prevalence rates for cataracts by age and race' class="imgImg rounded shadow" src="/blog/images/visuallyImpaired/1_2010_Prevalence_Rates_Age_Race_Cataract_v4.png" style='width: 100%; ' title='2010 US prevalence rates for cataracts by age and race' /> </picture> </div> <p> Cataracts start slowly. If you wear glasses, at first you think you can&lsquo;t quite clean them properly. Then you notice your sunglasses are also always dirty. Then you notice that white screens with black text are hard to read because of the glare from the white areas. </p> <p> The medical treatment is to replace the cloudy and .webp lens in the eye that has the cataract with a plastic lens. In <a href='https://ec.europa.eu/eurostat/statistics-explained/index.php/Surgical_operations_and_procedures_statistics' target='_blank' rel="nofollow">many European countries</a>, cataract surgery is the most common operation performed in that country. Usually, people delay the surgery until their quality of life and independence his diminished markedly. </p> <h2 id="coping">Coping With Visual Impairment</h2> <h3 id="saturation">Saturated Color and White Backgrounds</h3> <p> Dark screens with a minimum of saturated color helps. Increasing font size helps, and sanserif fonts, which have fewer spindly features, are easier to read. </p> <p> Some OSes offer a dark theme. You can enable the Windows 10 dark theme from <b>Settings</b> / <b>Personalization</b> / <b>Colors</b>. Scroll down and select &lsquo;Dark&rsquo; under &lsquo;Choose your app mode&rsquo;. The Universal Windows Platform applications will immediately darken. However, if a developer did not make the effort to support the dark theme, their program will continue with their original color theme. Unfortunately, Windows File Explorer is one of those applications. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.svg" type="image/svg"> <!---<source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.avif" type="image/avif">--> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.webp" type="image/webp"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.apng" type="image/apng"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.png" type="image/png"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.jpg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.jpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.jfif" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.pjpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.pjp" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.gif" type="image/gif"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.tif" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.tiff" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.bmp" type="image/bmp"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.ico" type="image/x-icon"> <source srcset="/blog/images/visuallyImpaired/img_57a8f5e080b37.cur" type="image/x-icon"> <img alt='Enabling the Windows 10 dark theme' class="imgImg rounded shadow" src="/blog/images/visuallyImpaired/img_57a8f5e080b37.png" style='width: 100%; ' title='Enabling the Windows 10 dark theme' /> </picture> <figcaption class='imgFigCaption fullsize'> Enabling the Windows 10 dark theme </figcaption> </figure> </div> <p>Some software offers a nice dark theme that does not require any fussing, for example IntelliJ IDEA's Darkula, Adobe Premiere Pro and Adobe Audition.</p> <p> The Firefox web browser&rsquo;s <a href='https://addons.mozilla.org/en-US/firefox/addon/dark-background-light-text/' target='_blank' rel="nofollow">Dark Background and Light Text</a> plugin is very helpful. By default, the plugin overrides the CSS styles for all web pages, so the background is black, and text is white. The plugin can be trained to leave certain pages untouched, for those pages that become unusable with the modified default stylesheet, for example, <a href='https://calendar.google.com' target='_blank' rel="nofollow">Google Calendar</a>. Other sites already offer a nice dark theme, so they do not need the modified theme thrust upon them, for example <a href='https://tweetdeck.twitter.com/' target='_blank' rel="nofollow"><code>tweetdeck.twitter.com</code></a>. </p> <p> Some software offers a dark theme which requires further modification before a good result is a achieved. For example, the Thunderbird email client with the <a href='https://addons.thunderbird.net/en-US/thunderbird/addon/tt-deepdark/' target='_blank' rel="nofollow">TT DeepDark</a> addon gets you partway there, then <b>Tools</b> / <b>Options</b> / <b>Display</b> / <b>Fonts & Colors</b> / <b>Colors</b> shows the following dialog, where you can set the colors as shown: </p> <div class='imgWrapper imgBlock center' style='width: 75%; '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.svg" type="image/svg"> <!---<source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.avif" type="image/avif">--> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.webp" type="image/webp"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.apng" type="image/apng"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.png" type="image/png"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.jpg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.jpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.jfif" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.pjpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.pjp" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.gif" type="image/gif"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.tif" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.tiff" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.bmp" type="image/bmp"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.ico" type="image/x-icon"> <source srcset="/blog/images/visuallyImpaired/ttDeepDarkColors.cur" type="image/x-icon"> <img alt='Display Fonts & Colors' class="imgImg rounded shadow" src="/blog/images/visuallyImpaired/ttDeepDarkColors.png" style='width: 100%; ' title='Display Fonts & Colors' /> </picture> <figcaption class='imgFigCaption '> Display Fonts & Colors </figcaption> </figure> </div> <p> As another example, Adobe Reader has a dark theme, which can be enabled via <b>Edit</b> / <b>Preferences</b> / <b>Accessibility</b>; enable <b>Replace Document Colors</b> and click on <b>Custom Color</b>; change the <b>Page Background</b> to black and <b>Document Text</b> to light gray. Disable <b>Only change the color of black text or line art</b>, and ensure that <b>Change the color of line art as well as text</b> is enabled. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.svg" type="image/svg"> <!---<source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.avif" type="image/avif">--> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.webp" type="image/webp"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.apng" type="image/apng"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.png" type="image/png"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.jpg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.jpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.jfif" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.pjpeg" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.pjp" type="image/jpeg"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.gif" type="image/gif"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.tif" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.tiff" type="image/tiff"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.bmp" type="image/bmp"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.ico" type="image/x-icon"> <source srcset="/blog/images/visuallyImpaired/adobeReaderColors_690x525.cur" type="image/x-icon"> <img alt='Enabling the Adobe Reader dark theme' class="imgImg rounded shadow" src="/blog/images/visuallyImpaired/adobeReaderColors_690x525.png" style='width: 100%; ' title='Enabling the Adobe Reader dark theme' /> </picture> <figcaption class='imgFigCaption fullsize'> Enabling the Adobe Reader dark theme </figcaption> </figure> </div> <h3 id="hard">Small Text and Low Contrast Colors</h3> <p> Users who know CSS can enforce a custom stylesheet on a per-site or a per-page basis for hard-to-read web pages. The <a href='https://chrome.google.com/webstore/detail/my-style/ljdhjpmbnkbengahefamnhmegbdifhlb?hl=en' target='_blank' rel="nofollow">My Style</a> plugin for the Chrome browser activates with Ctrl-M, and users can enter CSS to suit. As any web developer knows, <kbd>CTRL</kbd>-<kbd>Shift</kbd>-<kbd>C</kbd> starts HTML inspection, so the HTML in question can be viewed, and the CSS experimented with before immortalizing the changes with My Style. </p> <h2 id="design">Design Considerations</h2> <p> If your web page has images in it, try to use transparency instead of a white background. It would help visually impaired readers to only inflict strong primary colors, or a white background, when necessary. For example, this entry in a stylesheet would only show a light gray background when the user mouses over the image, otherwise, the background would remain transparent: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida1370bfd7740'><button class='copyBtn' data-clipboard-target='#ida1370bfd7740' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>img:hover { background-color: #666; }</pre> </div> Are My Hands-Free Devices Always Listening? 2017-01-15T00:00:00-05:00 https://mslinn.github.io/blog/2017/01/15/are-my-handsfree-devices-always-listening <div class="alert rounded shadow"> <h2>2022-04-27 Update</h2> <p> Amazon is <a href='https://www.theregister.com/2022/04/27/amazon_audio_data/' target='_blank' rel="nofollow">monetizing your conversations</a>, according to a study published by 10 researchers at the University of Washington, the University of California-Davis, the University of California-Irvine, and Northeastern University. The study is titled <a href='https://arxiv.org/pdf/2204.10920.pdf' target='_blank' rel="nofollow">&ldquo;Your Echos are Heard: Tracking, Profiling, and Ad Targeting in the Amazon Smart Speaker Ecosystem&rdquo;</a>. </p> <h3>Abstract</h3> <i> <p> Smart speakers collect voice input that can be used to infer sensitive information about users. Given a number of egregious privacy breaches, there is a clear unmet need for greater transparency and control over data collection, sharing, and use by smart speaker platforms as well as third party skills supported on them. To bridge the gap, we build an auditing framework that leverages online advertising to measure data collection, its usage, and its sharing by the smart speaker platforms. </p> <p> We evaluate our framework on the Amazon smart speaker ecosystem. Our results show that Amazon and third parties (including advertising and tracking services) collect smart speaker interaction data; where Amazon shares it with as many as 41 advertising partners. We find that Amazon processes voice data to infer user interests and uses it to serve targeted ads on-platform (Echo devices) as well as off-platform (web). Smart speaker interaction leads to as much as 30x higher ad bids from advertisers. Finally, we find that Amazon’s and skills’ (<i>apps’</i>) operational practices are often inconsistent with their privacy policies. </p> </i> <p> Some of the references cited by the paper are disturbing. This is the reference that concerned me the most: </p> <ul> <li><a href='https://www.washingtonpost.com/technology/2019/05/06/alexa-has-been-eavesdropping-you-this-whole-time/' target='_blank' rel="nofollow">Alexa has been eavesdropping on you this whole time.</a><br> &ndash; Washington Post, 2019-05-06.</li> </ul> </div> <p> The short answer: probably not. </p> <p> A longer answer: Depends on what you mean by &lsquo;listening&rsquo;. If you mean &ldquo;is Alexa storing or sending every sound or voice utterance to a server&rdquo;, the answer is again &ldquo;probably not&rdquo;. </p> <p> A more complete answer: unless all the source code for a device is made available for scrutiny, and the build process is similarly examined, the only way to be sure that device does not have operating modes that are contrary to expectations would be to connect the device to a network monitor that only allows communication with specific servers at designated times. Normal computer security concerns also pose risks; for example, viruses and other malware could be injected into a device could cause it to behave differently. </p> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/handsfree/echoDevices.svg" type="image/svg"> <!---<source srcset="/blog/images/handsfree/echoDevices.avif" type="image/avif">--> <source srcset="/blog/images/handsfree/echoDevices.webp" type="image/webp"> <source srcset="/blog/images/handsfree/echoDevices.apng" type="image/apng"> <source srcset="/blog/images/handsfree/echoDevices.png" type="image/png"> <source srcset="/blog/images/handsfree/echoDevices.jpg" type="image/jpeg"> <source srcset="/blog/images/handsfree/echoDevices.jpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/echoDevices.jfif" type="image/jpeg"> <source srcset="/blog/images/handsfree/echoDevices.pjpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/echoDevices.pjp" type="image/jpeg"> <source srcset="/blog/images/handsfree/echoDevices.gif" type="image/gif"> <source srcset="/blog/images/handsfree/echoDevices.tif" type="image/tiff"> <source srcset="/blog/images/handsfree/echoDevices.tiff" type="image/tiff"> <source srcset="/blog/images/handsfree/echoDevices.bmp" type="image/bmp"> <source srcset="/blog/images/handsfree/echoDevices.ico" type="image/x-icon"> <source srcset="/blog/images/handsfree/echoDevices.cur" type="image/x-icon"> <img alt='Amazon Echo device locations in homes' class="imgImg rounded shadow" src="/blog/images/handsfree/echoDevices.png" style='width: 100%; ' title='Amazon Echo device locations in homes' /> </picture> </div> <h2 id="update2021-01-12">At Least Echo&rsquo;s Mute Button Works</h2> <p> <b>Update 2021-01-12</b> &ndash; Amazon Echo&rsquo;s mute button was <a href='https://electronupdate.blogspot.com/2021/01/amazon-echo-flex-microphone-mute-real.html' target='_blank' rel="nofollow">reverse engineered</a>. </p> <p class='quote'> The mute button appears to be very real and functional. When the button glows red the power is removed from the microphones. </p> <h2 id="paranoia" style='clear: both'>Some Paranoia is Healthy</h2> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/handsfree/aristotle_300x344.svg" type="image/svg"> <!---<source srcset="/blog/images/handsfree/aristotle_300x344.avif" type="image/avif">--> <source srcset="/blog/images/handsfree/aristotle_300x344.webp" type="image/webp"> <source srcset="/blog/images/handsfree/aristotle_300x344.apng" type="image/apng"> <source srcset="/blog/images/handsfree/aristotle_300x344.png" type="image/png"> <source srcset="/blog/images/handsfree/aristotle_300x344.jpg" type="image/jpeg"> <source srcset="/blog/images/handsfree/aristotle_300x344.jpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/aristotle_300x344.jfif" type="image/jpeg"> <source srcset="/blog/images/handsfree/aristotle_300x344.pjpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/aristotle_300x344.pjp" type="image/jpeg"> <source srcset="/blog/images/handsfree/aristotle_300x344.gif" type="image/gif"> <source srcset="/blog/images/handsfree/aristotle_300x344.tif" type="image/tiff"> <source srcset="/blog/images/handsfree/aristotle_300x344.tiff" type="image/tiff"> <source srcset="/blog/images/handsfree/aristotle_300x344.bmp" type="image/bmp"> <source srcset="/blog/images/handsfree/aristotle_300x344.ico" type="image/x-icon"> <source srcset="/blog/images/handsfree/aristotle_300x344.cur" type="image/x-icon"> <img alt='Mattel Aristotle' class="imgImg liImg" src="/blog/images/handsfree/aristotle_300x344.png" style='width: 100%; width: 300px' title='Mattel Aristotle' /> </picture> </div> <p> <a href='https://www.engadget.com/2017/01/03/mattel-aristotle-echo-speaker-kids/' target='_blank' rel="nofollow">Mattel&rsquo;s Aristotle</a> is targeted at children. It uses Microsoft Bing for searching, Microsoft Cortana for voice processing and streams video to the cloud. It can read bedtime stories and play soothing sounds if your child wakes up at the night. It can recognize your children&rsquo;s imperfect speech, and apparently adapts as they get older and become curious about the world. Aristotle is an AI to help raise your child. Did they do a terrific job or a terrible job? It depends on the factors you take into consideration. </p> <p> Aristotle can respond to adults differently than to children. Its logging capability is profound. It tracks things like wet diapers and feedings, and can order supplies when asked by an adult. </p> <p> Aristotle can use object recognition to identify flashcards, or co-opt a toy without electronics and thereby enhance it with sound effects or even another personality. </p> <p> Mattel has about 500 partners, and they have been invited to build connected toys and apps. The hardware uses 256-bit encryption for all transmissions to Aristotle&rsquo;s servers, and data is handled internally in compliance with COPAA and HIPAA. </p> <p class='pullQuote'>It is not difficult to write code that detects when a child is alone</p> <p> How hard would it be to write code that detects when the child is alone? If a malicious entity wanted to, they could embed their program in the device, use IP geolocation and other characteristics to identify specific families, and cause the device to behave differently and/or tell the child things when no adults were paying attention. Who knows what malicious software might do? </p> <h2 id="open">Open Source To The Rescue</h2> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/handsfree/louis.svg" type="image/svg"> <!---<source srcset="/blog/images/handsfree/louis.avif" type="image/avif">--> <source srcset="/blog/images/handsfree/louis.webp" type="image/webp"> <source srcset="/blog/images/handsfree/louis.apng" type="image/apng"> <source srcset="/blog/images/handsfree/louis.png" type="image/png"> <source srcset="/blog/images/handsfree/louis.jpg" type="image/jpeg"> <source srcset="/blog/images/handsfree/louis.jpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/louis.jfif" type="image/jpeg"> <source srcset="/blog/images/handsfree/louis.pjpeg" type="image/jpeg"> <source srcset="/blog/images/handsfree/louis.pjp" type="image/jpeg"> <source srcset="/blog/images/handsfree/louis.gif" type="image/gif"> <source srcset="/blog/images/handsfree/louis.tif" type="image/tiff"> <source srcset="/blog/images/handsfree/louis.tiff" type="image/tiff"> <source srcset="/blog/images/handsfree/louis.bmp" type="image/bmp"> <source srcset="/blog/images/handsfree/louis.ico" type="image/x-icon"> <source srcset="/blog/images/handsfree/louis.cur" type="image/x-icon"> <img alt='Justice Louis Brandeis' class="imgImg rounded shadow" src="/blog/images/handsfree/louis.png" style='width: 100%; ' title='Justice Louis Brandeis' /> </picture> </div> <p> Closed source systems cannot be adequately vetted for public usage. It is conceivable that selected children might become secretly radicalized by their toy. In 1913 <a href='https://www.brandeis.edu/legacyfund/bio.html' target='_blank' rel="nofollow">Justice Louis Brandeis</a> said &ldquo;Sunlight is the best disinfectant&rdquo;. Open-source applications, with updates that can be vetted by any interested party, are the only way to ensure these devices are truly safe. </p> Hands-Free Voice as a User Interface 2017-01-10T00:00:00-05:00 https://mslinn.github.io/blog/2017/01/10/voice-activated-apps <p> Amazon&rsquo;s Alexa is a runaway success. Already, 5% of US households have a hands-free voice operated device. 50% of Echo owners keep one in the kitchen. <a href='https://www.gartner.com/doc/3021226/market-trends-voice-ui-consumer' target='_blank' rel="nofollow">Gartner predicts</a> that by 2018, 30% of our interactions with technology will be via voice conversations. The near future will have many hands-free, voice-driven, Internet-aware applications, and some will also provide a web interface in order to satisfy use cases that require a text or graphic display. </p> <h2 id="near">The Near Future</h2> <p> The near future will have many hands-free, voice-driven, Internet-aware applications, and some will also provide a web interface to satisfy use cases that require a text or graphic display. Potential usages include hands-free voice control of applications running on computers, tablets and phones, hands-free voice control of home devices and vehicles, dictaphones, and games that provide anthropomorphic characters with the ability to generate and understand speech. </p> <div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/google-home_384x500.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/google-home_384x500.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/google-home_384x500.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/google-home_384x500.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/google-home_384x500.png" type="image/png"> <source srcset="/blog/images/voiceActivated/google-home_384x500.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/google-home_384x500.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/google-home_384x500.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/google-home_384x500.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/google-home_384x500.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/google-home_384x500.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/google-home_384x500.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/google-home_384x500.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/google-home_384x500.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/google-home_384x500.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/google-home_384x500.cur" type="image/x-icon"> <img alt='Google Home smart speaker' class="imgImg liImg right rounded shadow" src="/blog/images/voiceActivated/google-home_384x500.png" style='width: 100%; height: 500px;' title='Google Home smart speaker' /> </picture> </div> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/echo_205x500.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/echo_205x500.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/echo_205x500.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/echo_205x500.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/echo_205x500.png" type="image/png"> <source srcset="/blog/images/voiceActivated/echo_205x500.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echo_205x500.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echo_205x500.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echo_205x500.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echo_205x500.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echo_205x500.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/echo_205x500.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echo_205x500.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echo_205x500.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/echo_205x500.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/echo_205x500.cur" type="image/x-icon"> <img alt='Amazon Echo smart speaker' class="imgImg liImg left" src="/blog/images/voiceActivated/echo_205x500.png" style='width: 100%; height: 500px;' title='Amazon Echo smart speaker' /> </picture> </div> </div> <p style="clear: both"> Services that developers and integrators can use for hands-free, voice-operated applications include <a href='https://www.amazon.com/gp/help/customer/display.html?nodeId=201602040' target='_blank' rel="nofollow">Amazon&rsquo;s Alexa</a>, <a href='https://assistant.google.com/' target='_blank' rel="nofollow">Google Assistant</a>, <a href='https://voice.google.com/' target='_blank' rel="nofollow">Google Voice</a>, <a href='https://www.apple.com/ca/siri/' target='_blank' rel="nofollow">Apple&rsquo;s Siri</a>, and <a href='https://www.microsoft.com/en-us/cortana' target='_blank' rel="nofollow">Microsoft Cortana</a>. </p> <h2 id="present">The Present</h2> <p> Only recently has it become feasible to use hands-free voice as a user interface. The best hands-free, voice applications are innately distributed &ndash; that is, they perform some computation on a local device, and they also require a connection to a server to do the heavy computation necessary for a high-quality experience. Most companies developing applications that use hands-free voice as a user interface require and will continue to require services provided by third parties. However, from the point of view of the application developer's company, the data shared with these third parties can represent a significant security risk. From the user's point of view, this data can represent a potentially significant privacy breach. I'm going to discuss why this is so in this article, and what can be done about it. </p> <p> Privacy and security concerns for synthesizing speech are minimal because quality speech can be synthesized without context specific to an individual. This means that user data need not be associated with the words or phrases being synthesized, so anonymous phrases can be and should be sent to the remote service that generates the audio files produced by the speech synthesis. </p> <p> The best value for high-quality voice generation is achieved with a distributed solution. In this scenario, voice generation is simple to initiate: a text string is sent to a remote service, and an audio clip containing synthesized speech is returned. Most voice generators support embedded markup to control inflection. For example, <a href='https://docs.aws.amazon.com/polly/latest/dg/ssml.html' target='_blank' rel="nofollow">Amazon Polly</a> and <a href='https://api.ai' target='_blank' rel="nofollow">Google Assistant</a> (formerly <code>api.ai</code>) both use <a href='https://developers.google.com/actions/reference/ssml' target='_blank' rel="nofollow">SSML</a>; Apple uses a <a href='https://eclecticlight.co/2015/12/09/opening-access-text-speech/' target='_blank' rel="nofollow">variety of techniques</a> across its products, and Microsoft uses <a href='https://en.wikipedia.org/wiki/Microsoft_Speech_API' target='_blank' rel="nofollow">SAPI</a>. </p> <p> In contrast, the heavy lifting necessary to recognize unconstrained vocabulary requires lots of compute resource, and raises privacy and ethical issues. The main issues are: </p> <ol> <li><a href='#trigger'>Recognizing</a> a trigger word or phrase</li> <li><a href='#training'>Training</a> a voice recognition engine</li> <li>Determining the appropriate <a href='#privacy'>privacy/effectiveness</a> trade-off for your application</li> <li><a href='#integrating'>Integrating</a> with third-party or proprietary services</li> </ol> <h2 id="components">What Are These Things Made From?</h2> <p> Hands-free voice operated devices have most of the same components as a tablet, but often do not have a screen. An ARM A8 to A11 processor is typically embedded in a chip die that also contains a powerful digital signal processor. In other words, they can do a lot of computing; they are more powerful than any mobile phone, and they are more powerful than most tablets. </p> <div class="centered"> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/echoParts.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/echoParts.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/echoParts.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/echoParts.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/echoParts.png" type="image/png"> <source srcset="/blog/images/voiceActivated/echoParts.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoParts.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoParts.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoParts.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoParts.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoParts.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/echoParts.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echoParts.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echoParts.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/echoParts.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/echoParts.cur" type="image/x-icon"> <img alt='Amazon Echo parts' class="imgImg rounded shadow" src="/blog/images/voiceActivated/echoParts.png" style='width: 100%; max-height: 45%' title='Amazon Echo parts' /> </picture> <figcaption class='imgFigCaption fullsize'> Amazon Echo parts </figcaption> </figure> </div> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/echoBoard.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/echoBoard.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/echoBoard.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/echoBoard.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/echoBoard.png" type="image/png"> <source srcset="/blog/images/voiceActivated/echoBoard.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoBoard.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoBoard.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoBoard.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoBoard.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/echoBoard.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/echoBoard.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echoBoard.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/echoBoard.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/echoBoard.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/echoBoard.cur" type="image/x-icon"> <img alt='Amazon Echo PC board' class="imgImg rounded shadow" src="/blog/images/voiceActivated/echoBoard.png" style='width: 100%; max-height: 45%' title='Amazon Echo PC board' /> </picture> <figcaption class='imgFigCaption fullsize'> Amazon Echo PC board </figcaption> </figure> </div> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/tiDM37x.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/tiDM37x.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/tiDM37x.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/tiDM37x.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/tiDM37x.png" type="image/png"> <source srcset="/blog/images/voiceActivated/tiDM37x.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/tiDM37x.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/tiDM37x.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/tiDM37x.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/tiDM37x.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/tiDM37x.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/tiDM37x.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/tiDM37x.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/tiDM37x.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/tiDM37x.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/tiDM37x.cur" type="image/x-icon"> <img alt='TI DM37x processors' class="imgImg rounded shadow" src="/blog/images/voiceActivated/tiDM37x.png" style='width: 100%; width: 500px; max-height: 45%' title='TI DM37x processors' /> </picture> <figcaption class='imgFigCaption '> TI DM37x processors </figcaption> </figure> </div> </div> <p class="clear"> The open source ecosystem that has grown up around these DSP/CPU chips includes a variety of operating systems, including real-time and various Linux distributions, and many applications. The OSes are: Android, DSP/BIOS, Neutrino, Integrity, Windows Embedded CE, Linux, and VxWorks. It is quick and easy to design a complete device similar to Alexa and bring it to market. </p> <h2 id="trigger" class="clear">Recognizing a trigger word or phrase</h2> <p> One of the first major challenges one encounters when designing a device that responds to voice, is how to ignore silence, or recognizing noise that should be ignored. This requires some level of signal processing. In contrast, requiring the user to push a button makes for a much simpler user interface, but this requirement would greatly restrict the types of applications possible. </p> <div class='imgWrapper imgBlock center halfsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.png" type="image/png"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.cur" type="image/x-icon"> <img alt='Icom 706 Mark II g<br>mobile transceiver' class="imgImg rounded shadow" src="/blog/images/voiceActivated/Icom-IC-706MKIIG_300x180.png" style='width: 100%; padding: 1em;' title='Icom 706 Mark II g<br>mobile transceiver' /> </picture> <figcaption class='imgFigCaption halfsize'> Icom 706 Mark II g<br>mobile transceiver </figcaption> </figure> </div> <p> I am a ham radio operator, and I use the well-known protocol for addressing a specific individual when using a broadcast medium. If I want to talk to another ham operator, I address them by their call sign three times and await their acknowledgement before giving my message. &ldquo;KG6LBG, KG6LBG, KG6LBG, this is KG6LDE, do you read me?&rdquo; If that person hears their call sign, they reply &ldquo;KG6LDE, this is KG6LBG, go ahead.&rdquo; &ldquo;KG6LBG, I just called to say hello, over.&rdquo; &ldquo;KG6LDE, Hello yourself, over and out.&rdquo; </p> <p> Hands-free voice applications also need to recognize a trigger word or phrase (also known as a hotword) that prefaces an audio stream which will be processed for voice recognition. Without such a trigger, either the user would need to press a button to start recording their speech, or a continuous audio stream would have to be processed. I'm interested in hands-free voice recognition. Since the voice recognition processing must be done on a remote service, a lot of bandwidth and CPU power would be wasted processing silence or irrelevant sound. Because it is undesirable to a continuous audio stream to a server to accurately recognize trigger words, the methods used to recognize them are less accurate. </p> <h3 id="alternatives">Proprietary Alternatives</h3> <p> Both of these products can be configured to perform the trigger word recognition without requiring any bandwidth between the CPU running the recognition program and a server. Neither of them provide a JavaScript implementation, which means that unless the trigger word recognition program in installed on the local machine, it must be installed on a connected server and bandwidth will be used at all times. </p> <ul> <li> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/THF-VC-72dpi.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.png" type="image/png"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/THF-VC-72dpi.cur" type="image/x-icon"> <img alt='Truly Handsfree voice control' class="imgImg " src="/blog/images/voiceActivated/THF-VC-72dpi.png" style='width: 100%; width: 250px' title='Truly Handsfree voice control' /> </picture> </div> Sensory, Inc's <a href='https://www.sensory.com/products/technologies/trulyhandsfree/' target='_blank' rel="nofollow">TrulyHandsfree library</a> (<a href='https://github.com/Sensory/alexa-rpi/blob/master/LICENSE.txt' target='_blank' rel="nofollow">license</a>), based in Santa Clara, CA. &ldquo;We do not have any low cost or free license or library. We are unable to support any student or individual.&rdquo; </li> <li style='clear: both'> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.png" type="image/png"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/snowboyLogo_250x56.cur" type="image/x-icon"> <img alt='Snowboy Logo' class="imgImg " src="/blog/images/voiceActivated/snowboyLogo_250x56.png" style='width: 100%; width: 250px; margin-top: 1em' title='Snowboy Logo' /> </picture> </div> kitt.ai's <a href='https://github.com/Kitt-AI/snowboy' target='_blank' rel="nofollow">Snowboy</a>, based in Seattle, WA and <a href='https://www.geekwire.com/2016/backed-amazon-paul-allen-kitt-ai-launches-first-hotword-detection-software-toolkit/' target='_blank' rel="nofollow">partially funded by Amazon</a>. </li> </ul> <h3 id="sphinx" style='clear: both'>CMU Sphinx</h3> <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.svg" type="image/svg"> <!---<source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.avif" type="image/avif">--> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.webp" type="image/webp"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.apng" type="image/apng"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.png" type="image/png"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.jpg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.jpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.jfif" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.pjpeg" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.pjp" type="image/jpeg"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.gif" type="image/gif"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.tif" type="image/tiff"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.tiff" type="image/tiff"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.bmp" type="image/bmp"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.ico" type="image/x-icon"> <source srcset="/blog/images/voiceActivated/CMUSphinx_300x72.cur" type="image/x-icon"> <img alt='CMU Sphinx Logo' class="imgImg " src="/blog/images/voiceActivated/CMUSphinx_300x72.png" style='width: 100%; width: 300px' title='CMU Sphinx Logo' /> </picture> </div> <p> Designed for low-resource platforms, implementations of <a href='http://cmusphinx.sourceforge.net/' target='_blank' rel="nofollow">CMU&rsquo;s Sphinx</a> exist for C (which supports Python) and Java. <a href='http://cmusphinx.sourceforge.net/wiki/faq#qhow_to_implement_hot_word_listening' target='_blank' rel="nofollow">Hotword</a> spotting is supported. <a href='https://en.wikipedia.org/wiki/CMU_Sphinx' target='_blank' rel="nofollow">Several versions</a> of Sphinx exist, with varying free and commercial licenses. Reports suggest that Sphinx works reasonably well but I have not tested yet. Sphinx powers <a href='https://jasperproject.github.io/documentation/faq/' target='_blank' rel="nofollow">Jasper</a>. </p> <h3 id="js">JavaScript alternatives</h3> <ul> <li> <a href='https://github.com/TalAter/annyang' target='_blank' rel="nofollow">Annyang</a> works well, but requires Google's web browsers and servers, so it is probably not reasonable to use it just for hotword detection. </li> <li> <a href='https://github.com/jimmybyrum/voice-commands.js' target='_blank' rel="nofollow"><code>Voice-commands.js</code></a> also requires Google's web browsers and servers. </li> <li> <a href='https://github.com/evancohen/sonus' target='_blank' rel="nofollow">Sonus</a> is a Node framework which will be able to be configured to use a variety of back ends one day. It does hotword detection by using Snowboy; the authors obviously ignored Snowboy's license terms. </li> <li> <a href='https://github.com/zzmp/juliusjs' target='_blank' rel="nofollow">JuliusJS</a> is a JavaScript port of the &ldquo;Large Vocabulary Continuous Speech Recognition Engine Julius&rdquo;. It does not call any servers and runs in most browsers. Recognition is weak and it requires a lot of CPU. </li> <li <a href='https://justbuildsomething.com/cross-browser-voice-recognition-with-pocketsphinx-js/' target='_blank' rel="nofollow">PocketSphinx.js</a> is a free browser-based alternative, unfortunately it is horrible. </li> </ul> <h2 id="training">Training a voice recognition engine</h2> <p>Voice recognition engines need to be trained on a large dataset for the desired languages. Recognition effectiveness is less than linearly proportional to the size of the dataset, and high-quality datasets are important. Truly large amounts of data are required. Amazon, Apple, Google and Microsoft have commercial products that were trained using enormous proprietary datasets. This is a substantial investment, so only well-capitalized organizations will be able to offer their own voice recognition engines for unconstrained vocabularies. </p> <h2 id="privacy">Determining Your Application's Privacy / Effectiveness Tradeoff</h2> <p> A voice recognition's effectiveness increases for specific users if their voice streams are recorded and stored, then used for further training. However, this means that <a href='https://www.reddit.com/r/technology/comments/2wzmmr/everything_youve_ever_said_to_siricortana_has/' target='_blank' rel="nofollow">privacy</a> and security are traded off for effectiveness. This service provider's tradeoff might not be optimal for your use case. Apple's Siri only associates the stored voice recordings with you for <a href='https://www.wired.com/2013/04/siri-two-years/' target='_blank' rel="nofollow">6 months</a>. Google's Assist and Voice, and <a href='https://www.amazon.com/gp/help/customer/display.html?nodeId=201602040' target='_blank' rel="nofollow">Amazon Alexa</a> store all your voice recordings forever, unless you explicitly delete them. I could not discover how long Microsoft's Cortana and Skype store voice recordings, or how to delete them. <p> <h2 id="integrating">Integrating With Third-party or Proprietary Services</h2> <p> Because voice recognition returns a structured document like JSON or XML, and voice generation is simple to initiate, integration is well understood and many options exist. </p> I Updated the Apache Spark Reference Applications 2016-11-15T00:00:00-05:00 https://mslinn.github.io/blog/2016/11/15/lessons-from-updating-the-twitter-classifier-apache-spark-reference-application <h2 id="intro">Overview</h2> <p> The Apache Spark committers just accepted my pull request that updated the official <a href='https://github.com/databricks/reference-apps/tree/master/twitter_classifier' target='_blank' rel="nofollow">Twitter Classifier Reference Application</a> from Spark 1.4 / Scala 2.10 to Spark 2 / Scala 2.11. This article discusses some things I did in the pull request from the point of view of a Scala programmer. A primary goal was to rewrite the reference application using idiomatic and functional-style Scala. This article briefly discusses two unique aspects that I addressed: command-line parsing and DRYing up the code by importing scopes. I did several other things to improve the reference application, such as modularizing the code and providing run scripts, but this article does not address them because those techniques are generally well understood. </p> <p> I did not upgrade the reference application to Scala 2.12, which was released a couple of weeks ago because Spark does not yet support Scala 2.12. Josh Rosen of Databricks wrote me and said: </p> <div class="quote"> &ldquo;Some of Spark&rsquo;s dependencies and by Scala-version-specific code changes necessary to work around method overloads became ambiguous in 2.12. The umbrella ticket tracking 2.12 support can be found at <a href='https://issues.apache.org/jira/browse/SPARK-14220' target='_blank' rel="nofollow"><code>issues.apache.org/jira/browse/SPARK-14220</code></a>. One of the hardest pieces will be <a href='https://issues.apache.org/jira/browse/SPARK-14643' target='_blank' rel="nofollow"><code>issues.apache.org/jira/browse/SPARK-14643</code></a> (see the linked design document on that issue). Lack of 2.12 support for Breeze and its dependencies is likely to be another serious blocker, but that might be avoidable by only publishing a subset of the projects with 2.12 to begin with (e.g. only Spark core / SQL at first).&rdquo; </div> <h2 id="cli">Command Line Parsing</h2> <p>I modified the reference applications&rsquo; command line parsing to use a Scala library that supported idiomatic Scala (<a href='https://github.com/acrisci/commander-scala' target='_blank' rel="nofollow">Commander Scala</a>), instead of <a href='https://commons.apache.org/proper/commons-cli' target='_blank' rel="nofollow">Apache Commons CLI</a>, which is the Java library that was previously used. The result was simple, clean and very terse code that is intuitive to understand and easy to maintain. Commander Scala automatically generates the help message. Take a look at the <a href='https://github.com/databricks/reference-apps/blob/1793e3dc2335696e98a335130673b58b35086c26/twitter_classifier/scala/src/main/scala/com/databricks/apps/twitterClassifier/CollectOptions.scala' target='_blank'><code>collect</code> command&rsquo;s parsing</a>. You&rsquo;ll notice that it uses some common code for parsing Twitter authentication parameters. This code is much shorter than the previous code, easier to understand and modify, and is more flexible.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaae8f9e256ea'><button class='copyBtn' data-clipboard-target='#idaae8f9e256ea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>import com.github.acrisci.commander.Program import java.io.File abstract sealed case class CollectOptions( twitterOptions: TwitterOptions, overWrite: Boolean = false, tweetDirectory: File = new File(System.getProperty("user.home"), "/sparkTwitter/tweets/"), numTweetsToCollect: Int = 100, intervalInSecs: Int = 1, partitionsEachInterval: Int = 1 ) object CollectOptions extends TwitterOptionParser { override val _program = super._program .option(flags="-w, --overWrite", description="Overwrite all data files from a previous run") .usage("Collect [options] &lt;tweetDirectory> &lt;numTweetsToCollect> &lt;intervalInSeconds> &lt;partitionsEachInterval>") def parse(args: Array[String]): CollectOptions = { val program: Program = _program.parse(args) if (program.args.length!=program.usage.split(" ").length-2) program.help new CollectOptions( twitterOptions = super.apply(args), overWrite = program.overWrite, tweetDirectory = new File(program.args.head.replaceAll("^~", System.getProperty("user.home"))), numTweetsToCollect = program.args(1).toInt, intervalInSecs = program.args(2).toInt, partitionsEachInterval = program.args(3).toInt ){} } }</pre> </div> <p> Here is how to sidestep the Spark help message and display the help message for the collect entry point: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id230548e5019a'><button class='copyBtn' data-clipboard-target='#id230548e5019a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>spark-shell \ -class com.databricks.apps.twitterClassifier.Collect \ -jars target/scala-2.11/spark-twitter-lang-classifier-assembly-2.0.0.jar \ -- -help <span class='unselectable'>Usage: Collect [options] &lt;tweetDirectory> &lt;numTweetsToCollect> &lt;intervalInSeconds> &lt;partitionsEachInterval> Options: -h, — help output usage information -V, — version output the version number -w, — overWrite Overwrite all data files from a previous run -v, — accessTokenSecret [type] Twitter OAuth Access Token Secret -t, — accessToken [type] Twitter OAuth Access Token -s, — consumerSecret [type] Twitter OAuth Consumer Secret -c, — consumerKey [type] Twitter OAuth Consumer Key </span></pre> </div> <h2 id="import">Importing Inner Scope Into Another Object</h2> <p> Apache Spark is unusual in that you cannot encapsulate a Spark streaming context in a type instance. A memory overflow occurs when you try to instantiate a Scala trait or class that creates a Spark context. The solution is to use a unique Scala feature: the ability to import inner scope from an object into another scope. This meant that the code was made DRY (common code was not repeated), without using classes or traits. </p> <p> Here is how I took advantage of this little-known Scala technique: first I defined the <a href='https://github.com/databricks/reference-apps/blob/1793e3dc2335696e98a335130673b58b35086c26/twitter_classifier/scala/src/main/scala/com/databricks/apps/twitterClassifier/package.scala#L7-L19' target='_blank' rel="nofollow"><code>SparkObject</code> object</a> within a package object so it was easily found: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idddb031853a98'><button class='copyBtn' data-clipboard-target='#idddb031853a98' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>object SparkSetup { val spark = SparkSession .builder .appName(getClass.getSimpleName.replace("$", "")) .getOrCreate() val sqlContext = spark.sqlContext val sc: SparkContext = spark.sparkContext sc.setLogLevel("ERROR") }</pre> </div> <p> Next I imported all the variables defined in <code>SparkSetup</code> into the <code>Collect</code> object&rsquo;s scope, including <code>sc</code>, which was used twice, <a href='https://github.com/databricks/reference-apps/blob/1793e3dc2335696e98a335130673b58b35086c26/twitter_classifier/scala/src/main/scala/com/databricks/apps/twitterClassifier/Collect.scala' target='_blank' rel="nofollow">like this</a>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id82cb8c3328b3'><button class='copyBtn' data-clipboard-target='#id82cb8c3328b3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>object Collect extends App { val options = CollectOptions.parse(args) import SparkSetup._ val ssc = new StreamingContext(sc, Seconds(options.intervalInSecs)) Collector.doIt(options, sc, ssc) }</pre> </div> <p> Want to learn more practical Scala techniques? Head over to <a href='https://www.ScalaCourses.com' target='_blank' rel="nofollow"><code>ScalaCourses.com</code></a> and enroll! The combination of the Introduction to Scala and Intermediate Scala courses will teach you everything you need to know to start your journey with Apache Spark. </p> <p> Mike Slinn is the lead Scala instructor at <a href='https://www.ScalaCourses.com' target='_blank' rel="nofollow">ScalaCourses.com</a>. </p> Publishing Maven Artifacts to AWS S3 2013-07-07T00:00:00-04:00 https://mslinn.github.io/blog/2013/07/07/publishing-maven-artifacts-to-aws-s3 <!-- #region intro --> <p> I wanted a simple, flexible and cheap way of publishing proprietary Maven artifacts created by <code>sbt</code> projects such that they could be securely retrieved by authorized individuals. I liked the idea of versioned artifacts, but did not want to use GitHub or BitBucket to host the artifacts because of the hassle of maintaining ever-larger git repos. Instead, I opted for S3's optional versioning mechanism. </p> <p> The technique described here relies on the fact that publishing to a local file (using <code>sbt publish</code>) generates all the necessary artifacts, which merely need to be copied to the right directory on the Artifactory server. The server need not be anything special: a normal web server works fine, as does <code>webdav</code>. A variety of other protocols are also supported by <code>sbt</code>. </p> <p> I created two test projects on GitHub: <a href='https://github.com/mslinn/testPublishLib' target='_blank'><code>testPublishLib</code></a> and <a href='https://github.com/mslinn/testPublishApp' target='_blank'><code>testPublishApp</code></a>. You could clone them if you would like to try this. Each of them has a <code>README</code> that explains what they do.</p> I used <code>s3cmd</code> to manage the AWS S3 buckets that hold the repository. You can <a href='https://s3tools.org/s3cmd' target='_blank' rel="nofollow">obtain <code>s3cmd</code></a> for most OSes. I found I needed to install <code>python-magic</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5c4f1cbf31cd'><button class='copyBtn' data-clipboard-target='#id5c4f1cbf31cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo pip install python-magic</pre> </div> <ol> <li>Let <code>s3cmd</code> know your s3 keys: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1fefcf2b1d12'><button class='copyBtn' data-clipboard-target='#id1fefcf2b1d12' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd --configure</pre> </div> </li> <li>Create the S3 bucket, which must be unique. If you want to repository to be publicly visible, be sure that the bucket name starts with <code>www.</code> If you already have the S3 bucket then just omit this step. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id46eb99204ead'><button class='copyBtn' data-clipboard-target='#id46eb99204ead' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd mb s3://www.mymavenrepo</pre> </div> </li> <li>If you want to repository to be publicly visible, you need to enable the S3 bucket web site option: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide80db77fbffa'><button class='copyBtn' data-clipboard-target='#ide80db77fbffa' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd ws-create s3://www.mymavenrepo</pre> </div> </li> <li>Publish your library to a local repository. <a href='https://github.com/mslinn/testPublishLib' target='_blank'><code>testPublishLib</code></a> is an example of how to set up a project properly. <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6f40e4ef5fda'><button class='copyBtn' data-clipboard-target='#id6f40e4ef5fda' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sbt publish</pre> </div> </li> <li>Copy your locally published artifacts to S3. You can either type it out longhand: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5d4409a60c06'><button class='copyBtn' data-clipboard-target='#id5d4409a60c06' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd -P sync ~/.ivy2/local/com/micronautics/test_publish_lib \ s3://www.mymavenrepo/snapshots/com/micronautics/test_publish_lib</pre> </div> ... (note that the <code>-P</code> option makes the files publicly visible), or you can use <code>s3publish</code>: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id97ec48da41f9'><button class='copyBtn' data-clipboard-target='#id97ec48da41f9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3publish com/micronautics/test_publish_lib</pre> </div> </li> <li>If your repository is not public, you will need to provide authentication in a file which I called <tt>~/.sbt/awsCreds.sbt</tt> for convenience: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>~/.sbt/awsCreds.sbt</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4bd189f00057'><button class='copyBtn' data-clipboard-target='#id4bd189f00057' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>credentials += Credentials("AWS Realm", "www.mavenrepo.s3.amazonaws.com", "myUserId", "myPassword")</pre> </div> </li> <li>Use the published artifact from your sbt project by including a resolver of the form: <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id86aff9662c11'><button class='copyBtn' data-clipboard-target='#id86aff9662c11' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"AWS Snapshots" at "https://www.mavenrepo.s3.amazonaws.com/snapshots"</pre> </div> The <code>TestPublishApp</code> project is a working example of how to do that.</li> </ol> <p> Following is the script I wrote to upload to S3, which I called <code>s3publish</code>. It assumes you are always publishing a snapshot; and that the files should be public. I leave it to you to extend this script to handle releases and private content if you have the need. </p> <noscript><pre>#!/bin/bash if [ $# -eq 0 ]; then echo &quot;Usage: `basename $0` pubpath/name [repo]&quot; echo &quot; Where pubpath might be something like com/micronautics&quot; echo &quot; name is name of artifact to publish&quot; echo &quot; repo is the optional name of the bucket to publish to&quot; echo &quot; Example: `basename $0` com/micronautics/test_publish_lib&quot; exit 1 fi REPO=www.mymavenrepo OPTIONS=-P if [ $# -eq 2 ]; REPO=$2; fi s3cmd $OPTIONS sync ~/.ivy2/local/$1 s3://$REPO/snapshots/$1</pre></noscript><script src="https://gist.github.com/mslinn/5945115.js"> </script> <!-- endregion --> Load Testing ScalaCourses.com 2013-06-01T00:00:00-04:00 https://mslinn.github.io/blog/2013/06/01/load-testing-scalacoursescom <p> <a href='https://scalacourses.com/' target='_blank'>ScalaCourses.com</a>, which will be announced next week, is built using the entire Typesafe stack: Scala 2.10, Play 2.1, Slick 1.0 and Akka 2.1. It runs on Heroku. </p> <p> I ran a load test on the app running on only one Heroku dyno. I configured JMeter to use 300 threads, hammering at full speed (no pauses between hits). Testing was done from my desktop. The test generated 16.5Mb/s inbound and 4.1Mb/s outbound according to <code>iptraf</code>. The test did not download page assets because they are served directly from AWS S3. </p> <p> 50% of all responses were received in under 110ms, and 95% were received in under 165ms. The distance from my workstation in Half Moon Bay, CA, USA to the Heroku app server, running from an AWS server in Ashburn, Virginia, USA is about 3000 miles or 4,828 km away. Considering that average ping time is ~100ms, that is amazingly good! Ping measures round-trim time for a test packet, and <code>mtr</code> showed the average ping time as ~95ms with a standard deviation of 18. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.svg" type="image/svg"> <!---<source srcset="/blog/images/cadenzaLoad/jmeter_690x448.avif" type="image/avif">--> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.webp" type="image/webp"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.apng" type="image/apng"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.png" type="image/png"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.jpg" type="image/jpeg"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.jpeg" type="image/jpeg"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.jfif" type="image/jpeg"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.pjpeg" type="image/jpeg"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.pjp" type="image/jpeg"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.gif" type="image/gif"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.tif" type="image/tiff"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.tiff" type="image/tiff"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.bmp" type="image/bmp"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.ico" type="image/x-icon"> <source srcset="/blog/images/cadenzaLoad/jmeter_690x448.cur" type="image/x-icon"> <img alt='50% of all responses were received in under 110ms, and 95% were received in under 165ms' class="imgImg rounded shadow" src="/blog/images/cadenzaLoad/jmeter_690x448.png" style='width: 100%; ' title='50% of all responses were received in under 110ms, and 95% were received in under 165ms' /> </picture> </div> <p> Yes, I did put a lot of care into the design of the app so that it would scale well, but I had not expected such fantastic results. Kudos to each of the Typesafe product teams, and to Heroku! </p> Cleaning the Heroku Cache 2013-03-18T00:00:00-04:00 https://mslinn.github.io/blog/2013/03/18/cleaning-heroku-cache <p> In an <a href='/blog/2013/02/27/command-line-sbt-on-heroku-dyno.html'>earlier article</a>, I talked about using a bash console to experiment with a Heroku app. I mentioned that you should not run <code>sbt</code>. Of course, that is exactly what I did, and I discovered the hard way that the cache can't be cleared from the Heroku bash shell. The build cache is held outside the dyno and cannot be accessed from inside a running dyno. </p> <p> Cleaning the cache is accomplished with a special cache cleaner buildpack. Set the buildpack in your app like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2947538b416c'><button class='copyBtn' data-clipboard-target='#id2947538b416c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>heroku config:add BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-scala.git#cleancache --app myapp</pre> </div> <p>Push your code:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4974abe5cff9'><button class='copyBtn' data-clipboard-target='#id4974abe5cff9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>git push heroku master</pre> </div> <p>Remove the cache cleaner buildpack:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id829164216e77'><button class='copyBtn' data-clipboard-target='#id829164216e77' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>heroku config:remove BUILDPACK_URL --app myapp</pre> </div> <p>Change a file, commit and push again:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4e8d9ee830eb'><button class='copyBtn' data-clipboard-target='#id4e8d9ee830eb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>git push heroku master</pre> </div> <p>All better!</p> Using Scala’s String Interpolation to Access a Map 2013-03-15T00:00:00-04:00 https://mslinn.github.io/blog/2013/03/15/using-scala-210s-string-interpolation <p> Given a map, would it not be nice to have a shorthand way of looking up a value from a key, and to provide a default value? I&rsquo;ve wanted to be able to do this for a long time. Scala 2.10 makes this really easy! </p> <p> The <code>MapLookup</code> class below contains a <code>Map[String, Int]</code> that can be looked up by using a dollar sign ($) from code that contains a reference to the implicit class. If the lookup key is not defined by the map, a zero is returned. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida624c4402ec5'><button class='copyBtn' data-clipboard-target='#ida624c4402ec5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>implicit class MapLookup(val sc: StringContext) { val map = Map(("a", 1), ("b", 2), ("c", 3)).withDefaultValue(0)<br /> def $(args: Any*): Int = { val orig = sc.s (args : _*) map.get(orig) } }</pre> </div> <p> Assuming that the above is stored in a file called <code>strInterp.scala</code>, here are some examples of usage: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd4dccab8840d'><button class='copyBtn' data-clipboard-target='#idd4dccab8840d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>scala -i strInterp.scala <span class='unselectable'>Loading strInterp.scala... defined class MapLookup<br /> Welcome to Scala version 2.10.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_17). Type in expressions to have them evaluated. Type :help for more information.<br /> scala&gt; </span>$"a" <span class='unselectable'>res2: Int = 1<br /> scala&gt; </span>$"z" <span class='unselectable'>res3: Int = 0 </span></pre> </div> <p>Short and sweet!</p> Listing of all AWS Elastic Transcoder Presets 2013-03-15T00:00:00-04:00 https://mslinn.github.io/blog/2013/03/15/listing-of-all-aws-elastic-transcoder <p> Here is a listing of all <a href='https://console.aws.amazon.com/elastictranscoder/home?region=us-east-1#presets:' target='_blank' rel="nofollow">AWS Elastic Transcoder presets</a>, provided &lsquo;out of the box&rsquo; as system-wide presets. </p> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000001</code><br> <b>Name:</b> <code>System preset: Generic 1080p</code><br> <b>Description:</b> <code>System preset generic 1080p</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=4},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 5400,FrameRate: 29.97,AspectRatio: 16:9,MaxWidth: 1920,MaxHeight: 1080,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000010</code><br> <b>Name:</b> <code>System preset: Generic 720p</code><br> <b>Description:</b> <code>System preset generic 720p</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 2400,FrameRate: 29.97,MaxWidth: 1280,MaxHeight: 720,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000020</code><br> <b>Name:</b> <code>System preset: Generic 480p 16:9</code><br> <b>Description:</b> <code>System preset generic 480p 16:9</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 1200,FrameRate: 29.97,MaxWidth: 854,MaxHeight: 480,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000030</code><br> <b>Name:</b> <code>System preset: Generic 480p 4:3</code><br> <b>Description:</b> <code>System preset generic 480p 4:3</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 900,FrameRate: 29.97,MaxWidth: 640,MaxHeight: 480,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000040</code><br> <b>Name:</b> <code>System preset: Generic 360p 16:9</code><br> <b>Description:</b> <code>System preset generic 360p 16:9</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 720,FrameRate: 29.97,MaxWidth: 640,MaxHeight: 360,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000050</code><br> <b>Name:</b> <code>System preset: Generic 360p 4:3</code><br> <b>Description:</b> <code>System preset generic 360p 4:3</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 600,FrameRate: 29.97,MaxWidth: 480,MaxHeight: 360,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-000060</code><br> <b>Name:</b> <code>System preset: Generic 320x240</code><br> <b>Description:</b> <code>System preset generic 320x240</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 22050,BitRate: 64,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=1.3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 300,FrameRate: 15,MaxWidth: 320,MaxHeight: 240,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100010</code><br> <b>Name:</b> <code>System preset: iPhone4</code><br> <b>Description:</b> <code>System preset: iPod touch 5G, 4G, iPad 1G, 2G</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 2200,FrameRate: 30,MaxWidth: 1280,MaxHeight: 720,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100020</code><br> <b>Name:</b> <code>System preset: iPhone4S</code><br> <b>Description:</b> <code>System preset: iPhone 5, iPad 3G, 4G, iPad mini, Samsung Galaxy S2/S3/Tab 2</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=high, Level=4.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 5000,FrameRate: 30,MaxWidth: 1920,MaxHeight: 1080,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100030</code><br> <b>Name:</b> <code>System preset: iPhone3GS</code><br> <b>Description:</b> <code>System preset: iPhone 3GS</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 600,FrameRate: 30,MaxWidth: 640,MaxHeight: 480,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100040</code><br> <b>Name:</b> <code>System preset: iPod Touch</code><br> <b>Description:</b> <code>System preset: iPhone 1, 3, iPod classic</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 1500,FrameRate: 30,MaxWidth: 640,MaxHeight: 480,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100050</code><br> <b>Name:</b> <code>System preset: Apple TV 2G</code><br> <b>Description:</b> <code>System preset: Apple TV 2G</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 5000,FrameRate: 30,MaxWidth: 1280,MaxHeight: 720,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100060</code><br> <b>Name:</b> <code>System preset: Apple TV 3G</code><br> <b>Description:</b> <code>System preset: Apple TV 3G, Roku HD/2 XD</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=high, Level=4},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 5000,FrameRate: 30,MaxWidth: 1920,MaxHeight: 1080,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100070</code><br> <b>Name:</b> <code>System preset: Web</code><br> <b>Description:</b> <code>System preset: Facebook, SmugMug, Vimeo, YouTube</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 2200,FrameRate: 30,MaxWidth: 1280,MaxHeight: 720,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100080</code><br> <b>Name:</b> <code>System preset: KindleFireHD</code><br> <b>Description:</b> <code>System preset: Kindle Fire HD</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=4},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 2200,FrameRate: 30,MaxWidth: 1280,MaxHeight: 720,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100090</code><br> <b>Name:</b> <code>System preset: KindleFireHD8.9</code><br> <b>Description:</b> <code>System preset: Kindle Fire HD 8.9</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=4},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 5400,FrameRate: 30,MaxWidth: 1920,MaxHeight: 1080,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-100100</code><br> <b>Name:</b> <code>System preset: KindleFire</code><br> <b>Description:</b> <code>System preset: Kindle Fire</code><br> <b>Container:</b> <code>mp4</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 48000,BitRate: 160,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: false,BitRate: 1600,FrameRate: 30,MaxWidth: 1024,MaxHeight: 576,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-200010</code><br> <b>Name:</b> <code>System preset: HLS 2M</code><br> <b>Description:</b> <code>System preset: HLS 2M</code><br> <b>Container:</b> <code>ts</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: true,BitRate: 1872,FrameRate: auto,MaxWidth: 1024,MaxHeight: 768,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-200020</code><br> <b>Name:</b> <code>System preset: HLS 1.5M</code><br> <b>Description:</b> <code>System preset: HLS 1.5M</code><br> <b>Container:</b> <code>ts</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: true,BitRate: 1372,FrameRate: auto,MaxWidth: 960,MaxHeight: 640,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-200030</code><br> <b>Name:</b> <code>System preset: HLS 1M</code><br> <b>Description:</b> <code>System preset: HLS 1M</code><br> <b>Container:</b> <code>ts</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=main, Level=3.1},KeyframesMaxDist: 90,FixedGOP: true,BitRate: 872,FrameRate: auto,MaxWidth: 640,MaxHeight: 432,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-200040</code><br> <b>Name:</b> <code>System preset: HLS 600k</code><br> <b>Description:</b> <code>System preset: HLS 600k</code><br> <b>Container:</b> <code>ts</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=3, Profile=baseline, Level=3.0},KeyframesMaxDist: 90,FixedGOP: true,BitRate: 472,FrameRate: auto,MaxWidth: 480,MaxHeight: 320,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> <div style="margin-bottom: 8pt;"> <b>Id:</b> <code>1351620000001-200050</code><br> <b>Name:</b> <code>System preset: HLS 400k</code><br> <b>Description:</b> <code>System preset: HLS 400k</code><br> <b>Container:</b> <code>ts</code><br> <b>Audio:</b> <code>{Codec: AAC,SampleRate: 44100,BitRate: 128,Channels: 2}</code><br> <b>Video:</b> <code>{Codec: H.264,CodecOptions: {MaxReferenceFrames=1, Profile=baseline, Level=3.0},KeyframesMaxDist: 90,FixedGOP: true,BitRate: 272,FrameRate: auto,MaxWidth: 400,MaxHeight: 288,DisplayAspectRatio: auto,SizingPolicy: ShrinkToFit,PaddingPolicy: NoPad}</code> </div> Bash shell on a Heroku Dyno 2013-02-27T00:00:00-05:00 https://mslinn.github.io/blog/2013/02/27/command-line-sbt-on-heroku-dyno <p> Up to now most of my work with Heroku has been via the <code>heroku</code> command line client, and pushing builds to app instances to have them compiled and run. Recently, I've been messing around with the remote <code>bash</code> shell of my Heroku apps. To enter a remote shell, just type the following from the root directory of a git project that declares your Heroku app as a remote repository. This command uses the <code>heroku</code> client provided by the Heroku toolbelt: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id455a9b84a58b'><button class='copyBtn' data-clipboard-target='#id455a9b84a58b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>heroku run bash</pre> </div> <p> Upon login, you are placed into the root directory of your deployed app. Unfortunately, you do not have access to the table of mounted file systems and you cannot run <code>sudo</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcd1858a5a324'><button class='copyBtn' data-clipboard-target='#idcd1858a5a324' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>~ $ </span>pwd <span class='unselectable'>/app </span><br> <span class='unselectable'>~ $ </span>ls -alF <span class='unselectable'>total 64 drwx------ 10 u37570 37570 4096 Dec&nbsp; 1 01:07 ./ drwxr-xr-x 15 root&nbsp;&nbsp; root&nbsp; 4096 Oct 31&nbsp; 2011 ../ -rwx------&nbsp; 1 u37570 37570&nbsp; 183 Dec&nbsp; 1 01:05 .gitignore* drwx------&nbsp; 3 u37570 37570 4096 Dec&nbsp; 1 01:05 .ivy2/ drwxrwxr-x&nbsp; 6 u37570 37570 4096 Dec&nbsp; 1 01:05 .jdk/ drwx------&nbsp; 4 u37570 37570 4096 Dec&nbsp; 1 01:08 .sbt_home/ -rw-------&nbsp; 1 u37570 37570&nbsp;&nbsp; 98 Dec&nbsp; 1 01:05 Procfile -rw-------&nbsp; 1 u37570 37570 3833 Dec&nbsp; 1 01:05 README.md drwx------&nbsp; 5 u37570 37570 4096 Dec&nbsp; 1 01:05 app/ drwx------&nbsp; 2 u37570 37570 4096 Dec&nbsp; 1 01:05 conf/ drwx------&nbsp; 5 u37570 37570 4096 Dec&nbsp; 1 01:06 project/ drwx------&nbsp; 5 u37570 37570 4096 Dec&nbsp; 1 01:05 public/ -rwx------&nbsp; 1 u37570 37570&nbsp;&nbsp; 27 Dec&nbsp; 1 01:05 system.properties* drwx------&nbsp; 6 u37570 37570 4096 Dec&nbsp; 1 01:08 target/ </span></pre> </div> <p>Discover total used file space (my slug uses 1.7 GB):</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf408d2f8b2dd'><button class='copyBtn' data-clipboard-target='#idf408d2f8b2dd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>~ $ </span>du -sh --exclude='/proc/*' / <span class='unselectable'>du: cannot read directory `/lost+found': Permission denied du: cannot read directory `/etc/ssl/private': Permission denied 1.7G / </span></pre> </div> <p>All your environment variables are available:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5f8aaee424d1'><button class='copyBtn' data-clipboard-target='#id5f8aaee424d1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>~ $ </span>set <span class='unselectable'><i>... miles of output ...</i> </span></pre> </div> <p><code>ifconfig</code> is not available, but the IP address of your dyno can be discovered this way:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3ee446da04b0'><button class='copyBtn' data-clipboard-target='#id3ee446da04b0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>~ $ </span>tail -n 1 /etc/hosts <span class='unselectable'>10.92.81.76 e3d315e6-4392-4f3a-b7ae-75438c469697 </span></pre> </div> <p> You can run <code>sbt</code>, but this is a bad idea because the Ivy cache is held outside the dyno, and there is no way for you to clean it without using some voodoo. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1214b9728714'><button class='copyBtn' data-clipboard-target='#id1214b9728714' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>~ $ </span>sbt update <span class='unselectable'>[info] Loading global plugins from /app/.sbt_home/.sbt/plugins [info] Loading project definition from /app/project [info] Set current project to blahblah (in build file:/app/) [info] Updating {file:/app/}blahblah... <i>... miles of output ...</i> [info] Done updating. [success] Total time: 6 s, completed Feb 28, 2013 4:40:21 AM </span></pre> </div> AWS S3 websites and Naked HTTP Redirects 2012-11-14T00:00:00-05:00 https://mslinn.github.io/blog/2012/11/14/aws-s3-web-sites-and-naked-http <p> Sites hosted directly off AWS S3 only respond to the <code>www</code> subdomain. In other words, if you tried to navigate to <code>https://mysite.com</code> for a site that was hosted on AWS S3, a 404 status would result, however <code>https://www.mysite.com</code> would work. Some registrars, like Namecheap and GoDaddy have URL Redirect, while others, like GKG.net, do not. </p> <p> Here is how to set up Namecheap to redirect requests like <code>https://mysite.com/blah</code> to <code>https://www.mysite.com/blah</code> so AWS S3 will respond. I also show how to set up the DNS for email with Rackspace. </p> <ol> <li>Originally AWS buckets could only be used for serving websites if they started with <code>www</code>, for example: <code>www.artforhealingenvironments</code>. This restriction no longer exists. </li> <li>Go to the <a href='https://www.namecheap.com/products/freedns.aspx' target='_blank' rel="nofollow">Namecheap FreeDNS service</a>. This will set up your domain for a smooth transfer to Namecheap, for uninterrupted web presence during and after the transfer. </li> <li>Enter your domain name, for example <code>artforhealingenvironments.com</code>, and click on <b>Get DNS</b></li> <li>On the next page, click on <b>Add DNS Service For the Selected Domains</b></li> <li>Fill in the Namecheap <a href='https://manage.www.namecheap.com/myaccount/hosteddomainslist.aspx' target='_blank' rel="nofollow">Hosted Domains page</a>. It will look something like this when you are done, for the <code>artforhealingenvironments.com</code> domain. </li> </ol> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/awsS3/dns.svg" type="image/svg"> <!---<source srcset="/blog/images/awsS3/dns.avif" type="image/avif">--> <source srcset="/blog/images/awsS3/dns.webp" type="image/webp"> <source srcset="/blog/images/awsS3/dns.apng" type="image/apng"> <source srcset="/blog/images/awsS3/dns.png" type="image/png"> <source srcset="/blog/images/awsS3/dns.jpg" type="image/jpeg"> <source srcset="/blog/images/awsS3/dns.jpeg" type="image/jpeg"> <source srcset="/blog/images/awsS3/dns.jfif" type="image/jpeg"> <source srcset="/blog/images/awsS3/dns.pjpeg" type="image/jpeg"> <source srcset="/blog/images/awsS3/dns.pjp" type="image/jpeg"> <source srcset="/blog/images/awsS3/dns.gif" type="image/gif"> <source srcset="/blog/images/awsS3/dns.tif" type="image/tiff"> <source srcset="/blog/images/awsS3/dns.tiff" type="image/tiff"> <source srcset="/blog/images/awsS3/dns.bmp" type="image/bmp"> <source srcset="/blog/images/awsS3/dns.ico" type="image/x-icon"> <source srcset="/blog/images/awsS3/dns.cur" type="image/x-icon"> <img alt='Namecheap Hosted Domains page' class="imgImg rounded shadow" src="/blog/images/awsS3/dns.png" style='width: 100%; ' title='Namecheap Hosted Domains page' /> </picture> <figcaption class='imgFigCaption fullsize'> Namecheap Hosted Domains page </figcaption> </figure> </div> <ol> <ol> <li>In the first IP ADDRESS/URL, put the fully resolved HTTP URL, with a trailing slash; set the record type to <b>URL Redirect</b>: <code>https://www.artforhealingenvironments.com/</code> </li> <li>In the second IP ADDRESS/URL, put the AWS bucket name, followed by the S3 site's domain; Namecheap will automatically add a period after, so you do not have to. Set the record type to <b>CNAME</b>:<code>www.artforhealingenvironments.com.s3-website-us-east-1.amazonaws.com</code> </li> <li>Namecheap's minimum TTL is 60 minutes.</li> <li>Namecheap's website automatically adds a period after each domain name.</li> </ol> </ol> <ol> <li>The MX records for email are shown at the bottom; they won't appear until you press the Save Changes button once (not shown). The MAILSERVER HOST NAME values are <code>mx1.emailsrvr.com</code> and <code>mx2.emailsrvr.com</code>. </li> <li>After the transfer completes, the FreeDNS entry will automatically be removed, and you will manage the domain on the Namecheap <a href='https://manage.www.namecheap.com/myaccount/domain-list.asp' target='_blank' rel="nofollow">Manage Domains</a> page. </li> </ol> Debugging JVM Programs on Heroku 2012-09-28T00:00:00-04:00 https://mslinn.github.io/blog/2012/09/28/debugging-jvm-programs-on-heroku <h2 id="java">Debugging Java Applications</h2> <p> Sometimes there is no substitute for debugging a remote application. The Java virtual machine provides the <a href='https://docs.oracle.com/javase/6/docs/technotes/guides/jpda/index.html' target='_blank' rel="nofollow">JPDA</a> facility for this. JPDA is flexible, and can be configured in a variety of ways. Two attachment mechanisms are supported for debugging remote applications: inbound connections, whereby a debugging process on your machine <i>attaches</i> to a remote process via designated port at a specific IP address, and outbound connections, whereby a debugging process on your local machine <i>listens</i> to a designated port for a remote process to attach to it. JPDA can be used by all JVM-based languages, such as Java, Scala, Groovy and Clojure. </p> <p> Heroku only allows one incoming port, so because an incoming port is used by Heroku to connect to the hosted app, debug connections originating from your IDE will not succeed. Instead, you must set up your Heroku application to initiate an outbound connection for debugging. This cannot be done if your app uses more than one dyno, so you must scale your Heroku back to one dyno before you can remotely debug it, like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id407b8613a258'><button class='copyBtn' data-clipboard-target='#id407b8613a258' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>heroku scale web=1</pre> </div> <p> A Heroku app can initiate an outbound connection for debugging with or without a proxy server. The examples below use the Java debugger (<code>jdb</code>) and IntelliJ IDEA, but you could equally well set up a debug configuration for Eclipse in a similar manner. The settings below were used with a Play 2 application with Java and Scala controller classes. </p> <p> <b>Tip</b>: <code>heroku restart</code> will restart your Heroku app using your existing slug. This accomplishes the same thing as: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaf40076e6822'><button class='copyBtn' data-clipboard-target='#idaf40076e6822' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>heroku scale web=0 <span class='unselectable'>$ </span>heroku scale web=1</pre> </div> <p> The other way you can restart your app is by building a new slug, which then automatically starts. You can do this by checking in a bogus file: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idad21bede3375'><button class='copyBtn' data-clipboard-target='#idad21bede3375' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>date &gt; ignoreme.txt; git add ignoreme.txt; git push heroku</pre> </div> <p> <b>Warning</b>: Your Heroku app will crash if there is no listening process. Use the <code>heroku logs</code> command to check for a crash. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id988e8a3a309c'><button class='copyBtn' data-clipboard-target='#id988e8a3a309c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>heroku logs <span class='unselectable'>2012-09-29T18:20:53+00:00 heroku[web.1]: Starting process with command `target/start -Dhttp.port=${PORT} ${JAVA_OPTS}` 2012-09-29T18:20:55+00:00 app[web.1]: ERROR: transport error 202: connect failed: Connection refused 2012-09-29T18:20:55+00:00 app[web.1]: ERROR: JDWP Transport dt_socket failed to initialize, TRANSPORT_INIT(510) 2012-09-29T18:20:55+00:00 app[web.1]: JDWP exit error AGENT_ERROR_TRANSPORT_INIT(197): No transports initialized [../../../src/share/back/debugInit.c:741] 2012-09-29T18:20:55+00:00 app[web.1]: FATAL ERROR in native method: JDWP No transports initialized, jvmtiError=AGENT_ERROR_TRANSPORT_INIT(197) 2012-09-29T18:20:56+00:00 heroku[web.1]: Process exited with status 134 2012-09-29T18:20:56+00:00 heroku[web.1]: State changed from starting to crashed </span></pre> </div> <h2 id="remote">Remote Debugging Without a Proxy Server</h2> <p> If your local machine is accessible from the Internet at an IP address (a domain such as <code>blah.no-ip.info</code> would work equally well): </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id10a5c94d65da'><button class='copyBtn' data-clipboard-target='#id10a5c94d65da' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jdb -listen 9999&amp; # Do not type this line if you are using an IDE # If using an IDE, start debugging now <span class='unselectable'>$ </span># Assumes that eth0 accesses the Internet; only works if you are not behind NAT <span class='unselectable'>$ </span>IPADDR=`ifconfig eth0|grep "inet addr"|awk -F: '{print $2}'|awk '{print $1}'` <span class='unselectable'>$ </span># If you are behind NAT, you will need to use a dynamic DNS and set IPADDR to the machine name instead: <span class='unselectable'>$ </span>IPADDR=mycomputer.no-ip.info # modify to suit <span class='unselectable'>$ </span># This is a really long line. I wrapped it, but you should not: <span class='unselectable'>$ </span>heroku config:add JAVA_OPTS='-Xdebug -Xrunjdwp:transport=dt_socket,address=$IPADDR:9999 -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -XX:+UseCompressedOops' <span class='unselectable'>$ </span>heroku restart</pre> </div> <p> Here is what the run configuration for IntelliJ IDEA looks like. Note that debugger mode is set to <code>listen</code>, which implies <code>server="n"</code>. You need to launch this run configuration before restarting your Heroku app. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/heroku/ideaListen.svg" type="image/svg"> <!---<source srcset="/blog/images/heroku/ideaListen.avif" type="image/avif">--> <source srcset="/blog/images/heroku/ideaListen.webp" type="image/webp"> <source srcset="/blog/images/heroku/ideaListen.apng" type="image/apng"> <source srcset="/blog/images/heroku/ideaListen.png" type="image/png"> <source srcset="/blog/images/heroku/ideaListen.jpg" type="image/jpeg"> <source srcset="/blog/images/heroku/ideaListen.jpeg" type="image/jpeg"> <source srcset="/blog/images/heroku/ideaListen.jfif" type="image/jpeg"> <source srcset="/blog/images/heroku/ideaListen.pjpeg" type="image/jpeg"> <source srcset="/blog/images/heroku/ideaListen.pjp" type="image/jpeg"> <source srcset="/blog/images/heroku/ideaListen.gif" type="image/gif"> <source srcset="/blog/images/heroku/ideaListen.tif" type="image/tiff"> <source srcset="/blog/images/heroku/ideaListen.tiff" type="image/tiff"> <source srcset="/blog/images/heroku/ideaListen.bmp" type="image/bmp"> <source srcset="/blog/images/heroku/ideaListen.ico" type="image/x-icon"> <source srcset="/blog/images/heroku/ideaListen.cur" type="image/x-icon"> <img alt='IntelliJ IDEA listening as a client' class="imgImg rounded shadow" src="/blog/images/heroku/ideaListen.png" style='width: 100%; width: 100%' title='IntelliJ IDEA listening as a client' /> </picture> <figcaption class='imgFigCaption fullsize'> IntelliJ IDEA listening as a client </figcaption> </figure> </div> <h2 id="impl">Bash Script Implementation</h2> <p>I put the bash scripts in a gist:</p> <noscript><pre>#!/bin/bash set -e export JAVA_OPTS=&quot;&quot; export JAVA_OPTS=&quot;$JAVA_OPTS -Xmx2048m -Xss512m -Xverify:none&quot; # Java 8 no longer supports MaxPermSize: # export JAVA_OPTS=&quot;$JAVA_OPTS -XX:MaxPermSize=384m&quot; </pre></noscript><script src="https://gist.github.com/mslinn/7937391.js"> </script> <h2 id="proxy">Remote Debugging With a Proxy Server</h2> <p> If your local machine is hidden from the Internet by a proxy server at <code>blah.domain.com</code>, open a tunnel to it from your local machine, and listen to it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id15005982f8c6'><button class='copyBtn' data-clipboard-target='#id15005982f8c6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span># Assumes that eth0 accesses the Internet; only works if you are not behind NAT <span class='unselectable'>$ </span>IPADDR=`ifconfig eth0|grep "inet addr"|awk -F: '{print $2}'|awk '{print $1}'` <span class='unselectable'>$ </span># If you are behind NAT, you will need to use a dynamic DNS and set IPADDR to the machine name instead: <span class='unselectable'>$ </span>IPADDR=mycomputer.no-ip.info # modify to suit <span class='unselectable'>$ </span>ssh -NR *:9999:localhost:9999 $IPADDR&amp; <span class='unselectable'>$ </span>jdb -listen 9999&amp; # Do not type this line if you are using an IDE</pre> </div> <p>If using an IDE, start debugging now.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc7c6b6c500be'><button class='copyBtn' data-clipboard-target='#idc7c6b6c500be' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span># This is a really long line. I wrapped it, but you should not: <span class='unselectable'>$ </span>heroku config:add JAVA_OPTS='-Xdebug -Xrunjdwp:transport=dt_socket,address=$IPADDR:9999 -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -XX:+UseCompressedOops' <span class='unselectable'>$ </span>heroku restart</pre> </div> <h2 id="disablingRemote">Disabling Remote Debugging</h2> <p> The <code>JAVA_OPTS</code> value set earlier will remain in effect across multiple restarts of your Heroku app until you change it. To disable remote debugging, redefine the environment variable without the <code>-Xdebug</code> and <code>-Xrunjdwp</code> options: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0523d08a3149'><button class='copyBtn' data-clipboard-target='#id0523d08a3149' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span># This is a really long line. I wrapped it, but you should not: <span class='unselectable'>$ </span>heroku config:add JAVA_OPTS='-Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -XX:+UseCompressedOops' <span class='unselectable'>$ </span>heroku restart</pre> </div> <h2 id="saving">Saving the Run Configuration</h2> <p> If you enable the <b>Share</b> checkbox in the IntelliJ IDEA run configuration dialog box, the definition will be written to <code>.idea/runConfiguration/</code>. Normally you would not check in the contents of <code>.idea/</code> to your source code repository, but this subdirectory is an exception, and because this run configuration has no local dependencies it can be shared without modification amongst all of your developer team. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3dcaf61a0301'><button class='copyBtn' data-clipboard-target='#id3dcaf61a0301' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cat .idea/runConfigurations/Heroku_remote.xml <span class='unselectable'>&lt;component name="ProjectRunConfigurationManager"&gt; &lt;configuration default="false" name="Heroku remote" type="Remote" factoryName="Remote"&gt; &lt;option name="USE_SOCKET_TRANSPORT" value="true" /&gt; &lt;option name="SERVER_MODE" value="true" /&gt; &lt;option name="SHMEM_ADDRESS" value="javadebug" /&gt; &lt;option name="HOST" value="localhost" /&gt; &lt;option name="PORT" value="9999" /&gt; &lt;method /&gt; &lt;/configuration&gt; &lt;/component&gt; </span></pre> </div> Composable Futures with Akka 2.0 2012-08-09T00:00:00-04:00 https://mslinn.github.io/blog/2012/08/09/composable-futures-with-akka-20 <div class='imgWrapper imgFlex right' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/futuresCover226x296.svg" type="image/svg"> <!---<source srcset="/blog/images/futuresCover226x296.avif" type="image/avif">--> <source srcset="/blog/images/futuresCover226x296.webp" type="image/webp"> <source srcset="/blog/images/futuresCover226x296.apng" type="image/apng"> <source srcset="/blog/images/futuresCover226x296.png" type="image/png"> <source srcset="/blog/images/futuresCover226x296.jpg" type="image/jpeg"> <source srcset="/blog/images/futuresCover226x296.jpeg" type="image/jpeg"> <source srcset="/blog/images/futuresCover226x296.jfif" type="image/jpeg"> <source srcset="/blog/images/futuresCover226x296.pjpeg" type="image/jpeg"> <source srcset="/blog/images/futuresCover226x296.pjp" type="image/jpeg"> <source srcset="/blog/images/futuresCover226x296.gif" type="image/gif"> <source srcset="/blog/images/futuresCover226x296.tif" type="image/tiff"> <source srcset="/blog/images/futuresCover226x296.tiff" type="image/tiff"> <source srcset="/blog/images/futuresCover226x296.bmp" type="image/bmp"> <source srcset="/blog/images/futuresCover226x296.ico" type="image/x-icon"> <source srcset="/blog/images/futuresCover226x296.cur" type="image/x-icon"> <img alt='Composable Futures with Akka 2.0 cover' class="imgImg shadow rounded" src="/blog/images/futuresCover226x296.png" style='width: 100%; ' title='Composable Futures with Akka 2.0 cover' /> </picture> </div> <p> My latest book is finally complete! <a href='https://www.slinnbooks.com/books/futures/index.html' target='_blank' rel="nofollow">Composable Futures with Akka 2.0</a> features Java and Scala code examples. The book is available in PDF format. </p> <p> Akka is hot, and Akka futures are important for creating responsive applications that can scale. This book is intended for &lsquo;the rest of us&rsquo; &ndash; Java and Scala programmers who would like to quickly learn how design and integrate applications using composable futures. <p> <p> I <a href='https://www.meetup.com/sv-jug/events/50402942/' target='_blank' rel="nofollow">presented a preview of the book</a> at Googleplex April 18, 2012 to the Silicon Valley Java User Group. </p> Proposal – Mandatory Countervailing Tip 2012-08-07T00:00:00-04:00 https://mslinn.github.io/blog/2012/08/07/proposal-mandatory-countervailing-tip <p> Inequity between countries and ethnicities exploits individuals who are the primary producers and harms the individuals and societies that contain the primary consumers; it only benefits international traders. Over 99% of the world is exploited to serve less than the top 1%. This imbalance threatens world stability, as well as national, regional and local stability. </p> <p> This proposal is designed to create jobs in every country where the trade balance is negative, and is intended to reverse the exploitation of individuals in producing countries. I propose a Mandatory Countervailing Tip (MCT). It’s like a tip that you might offer someone who provides service, and whose wages are not sufficient to justify their employment. Waiters, waitresses, bellhops and musicians are paid tips for this reason. This proposal is unlike a countervailing tax because the consuming country does not keep the surcharge; instead, it would provide money directly to individuals in producing regions. The exact mechanism(s) for disbursing funds locally is not addressed in this proposal. </p> <p> The MCT would be levied at the point of purchase, like a sales tax, and would be disbursed directly to residents at the point(s) of origin of the goods who created the product. This would serve to improve the lives of all parties involved in the transaction, except the international trader. </p> <p> The MCT would be calculated such that exploited individuals themselves in the producing nation would directly receive enough money to raise their standard of living to within 25% of the standard of living for the middle 80% of the consuming region in the producing country. The money would be collected by the same government departments that collect sales tax and would be remitted every 30 days to a new agency in the UN. The entire staff of the UN agency would be required to be replaced every 2 years, and working committees and task forces in the agency would be comprised of residents of primary producers and consumers, as well as enough individuals from neutral countries to ensure impartiality and honesty. The states collecting the sales tax would be entitled to 1% of the funds collected for administrative overhead. The UN agency would be entitled to an additional 2%. The UN would be responsible for computing the MCT for every category of product sold, for every producing / consuming country. </p> <p> The sums involved would be enormous, so the potential for fraud is also enormous. To counteract this, unprecedented transparency must be implemented; this is possible because of modern computer and communications technology. Access to the data should be made public to the world at large. No sign-in should be required to download data concerning anonymized transactions and aggregated statistics. No data about individuals in producers or consumers must be available without security clearance. </p> <p> Transactions and queries requiring authorization must be audited, and auditors must be selected from presumably hostile countries and regions. For example, given that India and China are fierce competitors, they should provide the personnel to audit each other’s transactions. Those auditors would be paid from 0.002% of the total receipts. Auditors must also be reassigned every year, and may not ever be reassigned to the same region. </p> <p> Implementation need not require world-wide agreement; only one country need to decide that it wants to go ahead, and rough estimates of economic disparity would suffice to start. The first country to implement will be the first country to enjoy the benefits of a local economic resurgence. This can be done before the UN is ready, by setting up a temporary agency. </p> <p> This proposal is intended to greatly reduce the disparity between rich and poor throughout the world. It is fair because it is based on consumption, and the degree of equalization applied is proportional to the disparity between the standards of living between producer and consumer. This should greatly decrease the suicide rate of the indentured slaves who build iPads in China, while providing an opportunity for manufacturing to resurge in the US. Think of it as a regulated fair trade for world stability, prosperity and peace. </p> <p> Colonialism is based on exploitation, but capitalism does not require it. One might argue that a certain amount of exploitation is healthy because without some measure of economic disparity, undeveloped regions would not have a cost advantage and therefore never develop. This is probably true, so the question then becomes: how much economic disparity is healthy? I don’t know the answer, but a legislated policy on whatever that figure needs to be is preferable to uncontrolled exploitation. I doubt that the figure would need to be exceed 50%, and probably should be less. Current figures are in the range of 1% to 5%. </p> <p> The MCT is an example of how local, regional, national and world government can provide for the greater good. The only parties who have reason to oppose this type of proposal are those who benefit in some way from exploitation. Think of the goodwill that the recipients will feel towards their end users, and the countries that they live in. </p> Scala Existential Types and Salat 2012-08-06T00:00:00-04:00 https://mslinn.github.io/blog/2012/08/06/scala-existential-types <p> For Scala programmers, the term 'existential types' does not refer to <a href='https://en.wikipedia.org/wiki/Existentialism' target='_blank' rel="nofollow">philosophers</a>, or even authentic people. Ironically, most of the discussions of Scala's existential types that I found are too abstract to be useful to me. I learn by doing; rarely do I learn from reading an abstract treatise. I guess this means that I could be labelled an existentialist. </p> <p> Section 31.3 of <a href='https://www.artima.com/shop/programming_in_scala_2ed' target='_blank' rel="nofollow">Programming in Scala</a> has some good information on Scala's existential types. Existential types is an abstraction of Java types, and abstraction is the antithesis of existentialism... aaaanyway, here is a sentence I stole from the book:</p> <p class="rounded shadow liImg" style="padding: 1em;"><code>Iterator[_]</code> means the same thing as <code>Iterator[T] forSome { type T }</code></p> <p> <code>forSome</code> is the keyword which tells the compiler that an existential type is being defined. This becomes useful if upper and/or lower bounds are used to define the type. For example, you can use a lower bound to specify that the type must be a subclass of <code>PubSubAction</code> with the following existential type: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1036a2998e7c'><button class='copyBtn' data-clipboard-target='#id1036a2998e7c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>T forSome { type T &lt;: PubSubAction }</pre> </div> <p>That is a lot of characters, so let's give this type a name:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8621d8996739'><button class='copyBtn' data-clipboard-target='#id8621d8996739' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>type PubSubActionSubclass = T forSome { type T &lt;: PubSubAction }</pre> </div> <p>We can also define a type to help us with <code>SalatDAO</code>:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9ae6addaad37'><button class='copyBtn' data-clipboard-target='#id9ae6addaad37' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>type SalatObject = T forSome { type T &lt;: AnyRef }</pre> </div> <p>Now lets use <code>SalatObject</code> as the parametric type for an invocation of <code>Manifest.classType()</code>:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd570bc5f6c89'><button class='copyBtn' data-clipboard-target='#idd570bc5f6c89' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>val mot = Manifest.classType[SalatObject](msg.getClass)</pre> </div> <p> This is useful because the manifest can be passed to a <a href='https://github.com/novus/salat/wiki/SalatDAO' target='_blank' rel="nofollow"><code>SalatDAO</code></a> constructor, which will create a DAO object for whatever type is supplied. In the following code, <code>PubSubActionClass</code> is just used to guarantee that the <code>msg</code> parameter is of the correct type; <code>SalatDAO</code>'s constructor is defined to accept all subclasses of <code>AnyRef</code>. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4d325586ada1'><button class='copyBtn' data-clipboard-target='#id4d325586ada1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>import com.novus.salat.global.ctx abstract class PubSubAction object PubSubAction { def makeDAO[PubSubActionSubclass](msg: PubSubActionSubclass) (implicit coll: MongoCollection) = { val mot = Manifest.classType[SalatObject](msg.getClass) val mid = Manifest.classType[Int](classOf[Int]) new SalatDAO(coll)(mot, mid, ctx){} } }</pre> </div> <p> Now you know that you do not have to hard-code the creation of a DAO for every <code>PubSubAction</code> subclass. You can examine the <a href='https://github.com/novus/salat/blob/master/salat-core/src/main/scala/com/novus/salat/dao/SalatDAO.scala' target='_blank' rel="nofollow">source code for <code>SalatDAO</code></a> if you would like to learn more. </p> <p> Assuming that <code>psaMsg</code> is an instance of a <code>PubSubActionSubclass</code>, you could call <code>insert()</code> as follows to do a generic insert. Because Salat has multiple methods called <code>insert()</code>, there is not enough type information for Scala to disambiguate the method reference unless the returned value from the single insert is stored in a variable, and that variable's type is provided: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5c158d0f3973'><button class='copyBtn' data-clipboard-target='#id5c158d0f3973' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>val ignoredIndex: Option[Int] = makeDAO.insert(psaMsg)</pre> </div> <p> There is only one variant of <code>insert()</code> which accepts a collection, so the reference is not ambiguous and the return value can be ignored: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd194c8300d10'><button class='copyBtn' data-clipboard-target='#idd194c8300d10' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>dao.insert(Seq(psaMsg, psaMsg2, psaMsg3), WriteConcern.Normal)</pre> </div> Pushing Notifications to Nagios from Java and Scala 2012-08-04T00:00:00-04:00 https://mslinn.github.io/blog/2012/08/04/pushing-notifications-to-nagios-from <!-- #region intro --> <p> I was investigating how to push notifications from JVM-based programs when I found the <a href='https://sourceforge.net/projects/nagiosappender' target='_blank' rel="nofollow">NagiosAppender</a> project. Push notifications are termed &lsquo;passive checks&rsquo; because Nagios does not poll for results. For the curious, see the Nagios Plugins &ndash; Passive Service Check section of <a href='https://www.novell.com/communities/node/4131/application-monitoring-made-easy-java-applications-using-nagios' target='_blank' rel="nofollow">Application Monitoring Made Easy for Java Applications Using Nagios</a>. </p> <p> NagiosAppender integrates Log4j or Logback with Nagios&rsquo; optional NSCA server. The only &lsquo;programming&rsquo; required is setting up configuration files for Nagios client and Nagios server, adding a new dependency, and writing appropriate log messages for forwarding to Nagios by the plugin. Unfortunately, NagiosAppender is not compatible with Akka because <a href='https://logback.qos.ch/manual/mdc.html' target='_blank' rel="nofollow">it uses MDC</a>, which uses <a href='https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html' target='_blank' rel="nofollow"><code>ThreadLocal</code></a> variables, which should not be used with Akka. I took the NagiosAppender project, slimmed it down, removed the Log4j interface and the MDC code, and created the <a href='https://github.com/mslinn/PushToNagios' target='_blank'>PushToNagios</a> project. </p> <p> NB: The document mentions MDC without defining it. From the <a href='https://logging.apache.org/log4j/1.2/' target='_blank' rel="nofollow">Apache log4j</a> docs: &ldquo;A Mapped Diagnostic Context, or MDC, is an instrument for distinguishing interleaved log output from different sources. Log output is typically interleaved when a server handles multiple clients near-simultaneously. The MDC is managed on a per-thread basis. A child thread automatically inherits a copy of the mapped diagnostic context of its parent.&rdquo; The Logback documentation has a <a href='https://logback.qos.ch/manual/mdc.html' target='_blank' rel="nofollow">whole chapter on MDC</a>. </p> <!-- endregion --> <!-- #region NSCA --> <h2 id="nsca">NSCA</h2> <!-- #region implicit --> <p> NSCA is a Nagios add-on that allows you to send <a href='http://nagios.sourceforge.net/docs/nagioscore/3/en/passivechecks.html' target='_blank' rel="nofollow">passive check</a> results from remote hosts to the Nagios daemon running on the monitoring server. This is very useful in <a href='http://nagios.sourceforge.net/docs/nagioscore/3/en/distributed.html' target='_blank' rel="nofollow">distributed</a> and <a href='http://nagios.sourceforge.net/docs/nagioscore/3/en/redundancy.html' target='_blank' rel="nofollow">redundant/failover</a> monitoring setups. The NSCA addon can be found on <a href='http://exchange.nagios.org/directory/Addons/Passive-Checks/NSCA--2D-Nagios-Service-Check-Acceptor/details' target='_blank' rel="nofollow">Nagios Exchange</a>. For more information, see <a href='http://exchange.nagios.org/directory/Addons/Passive-Checks/NSCA--2D-Nagios-Service-Check-Acceptor/details' target='_blank' rel="nofollow">Addon &ndash; Nagios Passive Checks with NSCA</a>. </p> <div class='imgWrapper imgFlex center' style='width: 75%; '> <picture class='imgPicture'> <source srcset="/blog/images/nagios/nsca.svg" type="image/svg"> <!---<source srcset="/blog/images/nagios/nsca.avif" type="image/avif">--> <source srcset="/blog/images/nagios/nsca.webp" type="image/webp"> <source srcset="/blog/images/nagios/nsca.apng" type="image/apng"> <source srcset="/blog/images/nagios/nsca.png" type="image/png"> <source srcset="/blog/images/nagios/nsca.jpg" type="image/jpeg"> <source srcset="/blog/images/nagios/nsca.jpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nsca.jfif" type="image/jpeg"> <source srcset="/blog/images/nagios/nsca.pjpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nsca.pjp" type="image/jpeg"> <source srcset="/blog/images/nagios/nsca.gif" type="image/gif"> <source srcset="/blog/images/nagios/nsca.tif" type="image/tiff"> <source srcset="/blog/images/nagios/nsca.tiff" type="image/tiff"> <source srcset="/blog/images/nagios/nsca.bmp" type="image/bmp"> <source srcset="/blog/images/nagios/nsca.ico" type="image/x-icon"> <source srcset="/blog/images/nagios/nsca.cur" type="image/x-icon"> <img alt='Nagios addon: Passive Checks with NSCA' class="imgImg rounded shadow" src="/blog/images/nagios/nsca.png" style='width: 100%; ' title='Nagios addon: Passive Checks with NSCA' /> </picture> </div> <!-- endregion --> <!-- #region Installation --> <h3 id="installation">Installation</h3> <p>For Ubuntu:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id21f3f640a240'><button class='copyBtn' data-clipboard-target='#id21f3f640a240' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt-get install nagios3 nsca</pre> </div> <p> Installs Nagios Core 3.2.3, which is outdated but compatible, and <code>nsca 2.7.2+nmu2</code>, which is current. The current version of Nagios Core is 3.4.1, released on 2012-05-14. Nagios starts automatically after installation, but NSCA needs to be started manually (don't do that yet, keep reading). Navigate your web browser to <a href='http://http://localhost/nagios3' target='_blank' rel="nofollow"><code>http://localhost/nagios3</code></a> and specify userid <code>nagiosadmin</code>. </p> <p>FYI, <code>/etc/init.d/nagios3</code> contains:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/init.d/nagios3</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida1a46640a9cf'><button class='copyBtn' data-clipboard-target='#ida1a46640a9cf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>DAEMON=/usr/sbin/nagios3 NAGIOSCFG="/etc/nagios3/nagios.cfg" CGICFG="/etc/nagios3/cgi.cfg"</pre> </div> <p><code>/etc/nagios3/nagios.cfg</code> contains:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nagios3/nagios.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idadb5e71e4713'><button class='copyBtn' data-clipboard-target='#idadb5e71e4713' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>log_file=/var/log/nagios3/nagios.log cfg_file=/etc/nagios3/commands.cfg cfg_dir=/etc/nagios-plugins/config</pre> </div> <p><code>/etc/nagios3/resource.cfg</code> contains:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nagios3/resource.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide9aa19c6058a'><button class='copyBtn' data-clipboard-target='#ide9aa19c6058a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Sets $USER1$ to be the path to the plugins $USER1$=/usr/lib/nagios/plugins</pre> </div> <p><code>/etc/init.d/nsca</code> contains:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/init.d/nsca</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaa96cc41808f'><button class='copyBtn' data-clipboard-target='#idaa96cc41808f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>DAEMON=/usr/sbin/nsca CONF=/etc/nsca.cfg OPTS="--daemon -c $CONF" PIDFILE="/var/run/nsca.pid"</pre> </div> <p> We saw above that plugins are in <code>/usr/lib/nagios/plugins/</code>. I added one called <code>check_domain_bus</code> with permissions set to 755, and owned by <code>nagios:nagios</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>check_domain_bus</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide249f6fbed6e'><button class='copyBtn' data-clipboard-target='#ide249f6fbed6e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/sh echo "All OK: $1" exit 0 Configuration</pre> </div> <p>Edit <code>/etc/nagios3/nagios.cfg</code> and enable external commands on line 145 so the entry looks like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nagios3/nagios.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd1d9e8f2e363'><button class='copyBtn' data-clipboard-target='#idd1d9e8f2e363' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>check_external_commands=1</pre> </div> <p>Edit the last line of <code>/etc/nsca.cfg</code> to disable encryption:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nsca.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7ad8afb964ea'><button class='copyBtn' data-clipboard-target='#id7ad8afb964ea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>decryption_method=0</pre> </div> <p>Define a new Nagios command called <code>check_domain_bus</code> in <code>/etc/nagios3/commands.cfg</code> by adding the following anywhere in that file:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nagios3/commands.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id061b35371e67'><button class='copyBtn' data-clipboard-target='#id061b35371e67' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>define command { command_name check_domain_bus command_line $USER1$/check_domain_bus $ARG1$ }</pre> </div> <p> Define a template for passive services, and an instance of a passive service called <code>domainBus</code> that responds to the <code>check_domain_bus</code> command by adding the following to <code>/etc/nagios3/conf.d/services_nagios2.cfg</code>: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>/etc/nagios3/conf.d/services_nagios2.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb8a83fa94748'><button class='copyBtn' data-clipboard-target='#idb8a83fa94748' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>define service { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; passive-service &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; use&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; generic-service &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; check_freshness&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; passive_checks_enabled&nbsp; 1 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; active_checks_enabled&nbsp;&nbsp; 0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; is_volatile&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; flap_detection_enabled&nbsp; 0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; notification_options&nbsp;&nbsp;&nbsp; w,u,c,s &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; freshness_threshold&nbsp;&nbsp;&nbsp;&nbsp; 57600&nbsp;&nbsp;&nbsp;&nbsp; ;12hr } define service { &nbsp;&nbsp;&nbsp; use&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; passive-service&nbsp; &nbsp;&nbsp;&nbsp; host_name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; localhost &nbsp;&nbsp;&nbsp; service_description&nbsp;&nbsp;&nbsp;&nbsp; domainBus &nbsp;&nbsp;&nbsp; check_command&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; check_domain_bus!0 }</pre> </div> <!-- endregion --> <!-- #region Usage --> <h3 id="usage">Usage</h3> <p>Start NSCA server, then restart Nagios:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id37819669945e'><button class='copyBtn' data-clipboard-target='#id37819669945e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo service nsca start <span class='unselectable'>$ </span>sudo service nagios3 restart</pre> </div> <p> The custom service, called <code>domainBus</code>, should be viewable as a Nagios service, shown in the red rectangle below: </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/nagios/nagiosOK.svg" type="image/svg"> <!---<source srcset="/blog/images/nagios/nagiosOK.avif" type="image/avif">--> <source srcset="/blog/images/nagios/nagiosOK.webp" type="image/webp"> <source srcset="/blog/images/nagios/nagiosOK.apng" type="image/apng"> <source srcset="/blog/images/nagios/nagiosOK.png" type="image/png"> <source srcset="/blog/images/nagios/nagiosOK.jpg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.jpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.jfif" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.pjpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.pjp" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.gif" type="image/gif"> <source srcset="/blog/images/nagios/nagiosOK.tif" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosOK.tiff" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosOK.bmp" type="image/bmp"> <source srcset="/blog/images/nagios/nagiosOK.ico" type="image/x-icon"> <source srcset="/blog/images/nagios/nagiosOK.cur" type="image/x-icon"> <img alt='Nagios domainBus service' class="imgImg " src="/blog/images/nagios/nagiosOK.png" style='width: 100%; width: 100%' title='Nagios domainBus service' /> </picture> <figcaption class='imgFigCaption fullsize'> Nagios domainBus service </figcaption> </figure> </div> <p> Nagios will need to be restarted each time a service definition is modified. New services are shown as <code>PENDING</code> until they receive their first result. Passive services have no scheduled updates. </p> <!-- endregion --> <!-- #region Testing with the PushToNagios Java Client --> <h3 id="testing">Testing with the PushToNagios Java Client</h3> <p>See the <a href='https://github.com/mslinn/PushToNagios' target='_blank'>PushToNagios</a> documentation.</p> <!-- endregion --> <!-- #region Testing With the Compiled C NSCA Client --> <h3 id="test2">Testing With the Compiled C NSCA Client</h3> <p> Let&rsquo;s send a message and have the result displayed on the web interface. <code>send_nsca</code> is a <a href='https://sourceforge.net/projects/nagios/files/nsca-2.x/nsca-2.7.2/nsca-2.7.2.tar.gz/download' target='_blank' rel="nofollow">compiled C nsca client</a> that can be used to send a test message. Unpack <code>nsca-2.7.2.tar.gz</code> into a directory, and compile it: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id249ce8a1c42f'><button class='copyBtn' data-clipboard-target='#id249ce8a1c42f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./configure <span class='unselectable'>$ </span>make install</pre> </div> <p>Again, edit the last line of <code>sample-config/send_nsca.cfg</code> and change it to read:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>sample-config/send_nsca.cfg</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide9f064f78af7'><button class='copyBtn' data-clipboard-target='#ide9f064f78af7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>decryption_method=0</pre> </div> <p>Create a test message in the root of the unpacked NSCA project. The format for a service check packet using NSCA contains tab characters and ends in a newline, like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id41f3ea51be73'><button class='copyBtn' data-clipboard-target='#id41f3ea51be73' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;hostname&gt;[tab]&lt;svc_description&gt;[tab]&lt;return_code&gt;[tab]&lt;plugin_output&gt;</pre> </div> <p>I am unsure if <code>&lt;hostname&gt;</code> refers to the Nagios host or the sending host. The allowable values for <code>&lt;return_code&gt;</code> are:</p> <pre>0 - OK state<br> 1 - Warning state<br> 2 - Error state<br> 3 - Unknown state</pre> <p><code>&lt;plugin_output&gt;</code> can be up to 512 bytes long.</p> <p>Create a text message called <code>testCritical</code>, with embedded tabs, that NSCA uses as a field delimiter.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id965173e13db1'><button class='copyBtn' data-clipboard-target='#id965173e13db1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>localhost&nbsp;&nbsp; domainBus&nbsp;&nbsp;&nbsp; 2&nbsp;&nbsp; This is a Test Error</pre> </div> <p>Watch the Nagios log and syslog in one console:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4c5c60424d96'><button class='copyBtn' data-clipboard-target='#id4c5c60424d96' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tail -f /var/log/nagios3/nagios.log /var/log/syslog</pre> </div> <p>Send the test message like this in another console; let's call this the command console:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc837ae2e6b26'><button class='copyBtn' data-clipboard-target='#idc837ae2e6b26' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>src/send_nsca localhost -c sample-config/send_nsca.cfg &lt; testCritical</pre> </div> <p>Notice the log output in the console with the log output:</p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7a100698514a'><button class='copyBtn' data-clipboard-target='#id7a100698514a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>==&gt; /var/log/nagios3/nagios.log &lt;== [1343251589] EXTERNAL COMMAND: PROCESS_SERVICE_CHECK_RESULT;localhost;domainBus;2;This is a Test Error<br> ==&gt; /var/log/syslog &lt;== Jul 25 14:26:29 natty nagios3: EXTERNAL COMMAND: PROCESS_SERVICE_CHECK_RESULT;localhost;domainBus;2;This is a Test Error<br> ==&gt; /var/log/nagios3/nagios.log &lt;== [1343251590] PASSIVE SERVICE CHECK: localhost;domainBus;2;This is a Test Error<br> ==&gt; /var/log/syslog &lt;== Jul 25 14:26:30 natty nagios3: PASSIVE SERVICE CHECK: localhost;domainBus;2;This is a Test Error<br> ==&gt; /var/log/nagios3/nagios.log &lt;== [1343251590] SERVICE ALERT: localhost;domainBus;CRITICAL;SOFT;1;This is a Test Error<br> ==&gt; /var/log/syslog &lt;== Jul 25 14:26:30 natty nagios3: SERVICE ALERT: localhost;domainBus;CRITICAL;SOFT;1;This is a Test Error</pre> </div> <!-- endregion --> <p> In the web browser, click on <b>Services</b> again and notice that the status of the domainBus service is now <code>CRITICAL</code>, and <b>Status Information</b> now reads: </p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id84e7b5f859df'><button class='copyBtn' data-clipboard-target='#id84e7b5f859df' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>This is a Test Error</pre> </div> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/nagios/nagiosCritical.svg" type="image/svg"> <!---<source srcset="/blog/images/nagios/nagiosCritical.avif" type="image/avif">--> <source srcset="/blog/images/nagios/nagiosCritical.webp" type="image/webp"> <source srcset="/blog/images/nagios/nagiosCritical.apng" type="image/apng"> <source srcset="/blog/images/nagios/nagiosCritical.png" type="image/png"> <source srcset="/blog/images/nagios/nagiosCritical.jpg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosCritical.jpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosCritical.jfif" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosCritical.pjpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosCritical.pjp" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosCritical.gif" type="image/gif"> <source srcset="/blog/images/nagios/nagiosCritical.tif" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosCritical.tiff" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosCritical.bmp" type="image/bmp"> <source srcset="/blog/images/nagios/nagiosCritical.ico" type="image/x-icon"> <source srcset="/blog/images/nagios/nagiosCritical.cur" type="image/x-icon"> <img alt='Status of the domainBus service is now 'CRITICAL'' class="imgImg " src="/blog/images/nagios/nagiosCritical.png" style='width: 100%; width: 100%' title='Status of the domainBus service is now 'CRITICAL'' /> </picture> <figcaption class='imgFigCaption fullsize'> Status of the domainBus service is now 'CRITICAL' </figcaption> </figure> </div> <!-- endregion --> <p>Create a text message called <code>testClear</code>, and do not forget the embedded tabs:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>testClear</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id463972074147'><button class='copyBtn' data-clipboard-target='#id463972074147' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>localhost&nbsp;&nbsp; domainBus&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp; Mischief Managed</pre> </div> <p>Send this new test message in the command console:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcfac0e3efc53'><button class='copyBtn' data-clipboard-target='#idcfac0e3efc53' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>src/send_nsca localhost -c sample-config/send_nsca.cfg &lt; testClear</pre> </div> <p>The log output console should show something like this:</p> <!-- #region --> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1dc3e9ed0d5e'><button class='copyBtn' data-clipboard-target='#id1dc3e9ed0d5e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>==&gt; /var/log/nagios3/nagios.log &lt;== [1343252049] EXTERNAL COMMAND: PROCESS_SERVICE_CHECK_RESULT;localhost;domainBus;0;Mischief Managed<br> ==&gt; /var/log/syslog &lt;== Jul 25 14:34:09 natty nagios3: EXTERNAL COMMAND: PROCESS_SERVICE_CHECK_RESULT;localhost;domainBus;0;Mischief Managed<br> ==&gt; /var/log/nagios3/nagios.log &lt;== [1343252050] PASSIVE SERVICE CHECK: localhost;domainBus;0;Mischief Managed [1343252050] SERVICE ALERT: localhost;domainBus;OK;SOFT;2;Mischief Managed<br> ==&gt; /var/log/syslog &lt;== Jul 25 14:34:10 natty nagios3: PASSIVE SERVICE CHECK: localhost;domainBus;0;Mischief Managed</pre> </div> <!-- endregion --> <p> In the web browser, the <b>Services</b> information should automatically update after a pause of up to 90 seconds (by default), or you can click on Services to immediately see the new status. Notice that the status of the <code>domainBus</code> service is now <code>OK</code>, and <b>Status Information</b> now reads <code>Mischief Managed</code>. </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/nagios/nagiosOK.svg" type="image/svg"> <!---<source srcset="/blog/images/nagios/nagiosOK.avif" type="image/avif">--> <source srcset="/blog/images/nagios/nagiosOK.webp" type="image/webp"> <source srcset="/blog/images/nagios/nagiosOK.apng" type="image/apng"> <source srcset="/blog/images/nagios/nagiosOK.png" type="image/png"> <source srcset="/blog/images/nagios/nagiosOK.jpg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.jpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.jfif" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.pjpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.pjp" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosOK.gif" type="image/gif"> <source srcset="/blog/images/nagios/nagiosOK.tif" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosOK.tiff" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosOK.bmp" type="image/bmp"> <source srcset="/blog/images/nagios/nagiosOK.ico" type="image/x-icon"> <source srcset="/blog/images/nagios/nagiosOK.cur" type="image/x-icon"> <img alt='Status Information now reads 'Mischief Managed'' class="imgImg " src="/blog/images/nagios/nagiosOK.png" style='width: 100%; ' title='Status Information now reads 'Mischief Managed'' /> </picture> <figcaption class='imgFigCaption fullsize'> Status Information now reads 'Mischief Managed' </figcaption> </figure> </div> <p> The third possible message status is <code>warning</code>. Create a text message called <code>testWarning</code> and do not forget the embedded tabs: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>testWarning</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide9ddbd20d192'><button class='copyBtn' data-clipboard-target='#ide9ddbd20d192' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>localhost&nbsp;&nbsp; domainBus&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp; Do you know where your chocolate is?</pre> </div> <p>Send the test message like this in the command console:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbdd32d374768'><button class='copyBtn' data-clipboard-target='#idbdd32d374768' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>src/send_nsca localhost -c sample-config/send_nsca.cfg &lt; testWarning</pre> </div> <p> In the web browser, click on <b>Services</b> to immediately see the new status. Notice that the status of the <code>domainBus</code> service is now <code>OK</code>, and <b>Status Information</b> now reads <code>Do you know where your chocolate is?</code> </p> <div class='imgWrapper imgBlock inline fullsize' style=' '> <figure> <picture class='imgPicture'> <source srcset="/blog/images/nagios/nagiosWarning.svg" type="image/svg"> <!---<source srcset="/blog/images/nagios/nagiosWarning.avif" type="image/avif">--> <source srcset="/blog/images/nagios/nagiosWarning.webp" type="image/webp"> <source srcset="/blog/images/nagios/nagiosWarning.apng" type="image/apng"> <source srcset="/blog/images/nagios/nagiosWarning.png" type="image/png"> <source srcset="/blog/images/nagios/nagiosWarning.jpg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosWarning.jpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosWarning.jfif" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosWarning.pjpeg" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosWarning.pjp" type="image/jpeg"> <source srcset="/blog/images/nagios/nagiosWarning.gif" type="image/gif"> <source srcset="/blog/images/nagios/nagiosWarning.tif" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosWarning.tiff" type="image/tiff"> <source srcset="/blog/images/nagios/nagiosWarning.bmp" type="image/bmp"> <source srcset="/blog/images/nagios/nagiosWarning.ico" type="image/x-icon"> <source srcset="/blog/images/nagios/nagiosWarning.cur" type="image/x-icon"> <img alt='Status Information now reads &lsquo;Do you know where your chocolate is?&rsquo;' class="imgImg " src="/blog/images/nagios/nagiosWarning.png" style='width: 100%; width: 100%' title='Status Information now reads &lsquo;Do you know where your chocolate is?&rsquo;' /> </picture> <figcaption class='imgFigCaption fullsize'> Status Information now reads &lsquo;Do you know where your chocolate is?&rsquo; </figcaption> </figure> </div> <!-- endregion --> <!-- endregion --> Scala Type Parameters, Implicit Manifests and Salat 2012-08-02T00:00:00-04:00 https://mslinn.github.io/blog/2012/08/02/scala-type-parameters-implicit <p>Type parameters provide types for constructing any arguments that are implicit manifests.</p> <p>This helps when working with <a href='https://github.com/novus/salat/' target='_blank' rel="nofollow">Salat</a>. In the following code, <code>dbo</code> is a <code>com.mongodb.DBObject</code>, perhaps retrieved as the result of a <code>find()</code>. Salat uses <code>_typeHint</code> to store the fully qualified name of the persisted class; this property is returned as one of the properties of <code>dbo</code>. The call to Salat's <code>grater()</code> method converts <code>dbo</code> into the desired type. The type is constrained using a lower bound to be a subclass of <code>PubSubAction</code>.</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf5fa42c21a39'><button class='copyBtn' data-clipboard-target='#idf5fa42c21a39' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>def findWithSeqNum[T &lt;: PubSubAction](seqNum: Long) (implicit manifest: Manifest[T]): Option[T] = { val klass = manifest.erasure.asInstanceOf[Class[T]] val item = myCollection.findOne( MongoDBObject("seqNum" -&gt; seqNum, "_typeHint" -&gt; klass.getName)) item match { case Some(dbo: DBObject) =&gt; Some(grater(ctx, Manifest.classType(klass)).asObject(dbo))<br /> case None =&gt; None } }</pre> </div> <p>When called as follows, the type parameter need not be provided because there is an explicit manifest argument:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8543c60397c'><button class='copyBtn' data-clipboard-target='#idc8543c60397c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>findWithSeqNum(11)(Manifest.classType(classOf("com.micronautics.Blah"))</pre> </div> <p>When called as follows, the type parameter provides the value for constructing the implicit manifest argument:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2d61ec6fc10f'><button class='copyBtn' data-clipboard-target='#id2d61ec6fc10f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>findWithSeqNum[Blah](11)</pre> </div> <p>Of course, you can also explicitly define an implicit manifest at any scope that you desire:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf9e764028bf6'><button class='copyBtn' data-clipboard-target='#idf9e764028bf6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>implicit val manifest: ClassManifest[Blah] findWithSeqNum(11)</pre> </div> <p>If the type of the manifest is dynamic (only known at runtime), you can define manifest this way:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idede236c987d8'><button class='copyBtn' data-clipboard-target='#idede236c987d8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>val mtype = "com.micronautics.Blah" implicit val manifest = Manifest.classType(Class.forName(mtype)) findWithSeqNum(11)</pre> </div> Pigs Can Fly 2011-09-30T00:00:00-04:00 https://mslinn.github.io/blog/2011/09/30/pigs-can-fly <p> Details matter. If all you read are headlines and subtitles, then it is likely that you are horribly misinformed. Let&rsquo;s imagine that the text of an article was something like this: </p> <div class="informalNotice shadow rounded liImg"> <h2 id="pigsFly" style="margin-top: 0;">Pigs Can Fly</h2> <p> Outfitted with 3D goggles and haptic feedback mechanisms customized for a pig&rsquo;s extremities, porcine subjects showed that they quickly adapted to visual stimulus that suggested to them that their movements caused them to fly. With training, pigs demonstrated that they could control the flight of simulated cargo planes, stunt triplanes, jet fighters and paper planes. </p> </div> <p> So, does the passage say that pigs can actually fly unassisted? Does it say that pigs <i>might think</i> they can fly? Does it discuss the possibility that pigs might be able to be trained as pilots? You would have to read the passage carefully to be able to answer those questions. If you just skim headlines, then you would have no idea what the passage was about, or even if it was just humor. </p> <p> Details matter a lot when you are working with technologists. If you are easily put off by even the slightest techno-speak, then you have no business managing technologists. Specialized vocabulary summarizes complex thoughts succinctly; learn it, don&rsquo;t ask for the baby-talk version. </p> <ul> <li>You can&rsquo;t</li> <li>Summarize complex interactions</li> <li>Into a few bullet points</li> <li>Without getting it wrong</li> </ul> <p> This is true for reading and writing. If the best that a manager can grunt via a keyboard is &ldquo;you know what I mean&rdquo; then it is probable that the recipient of the email has at best only a vague idea of what the lazy, inarticulate writer meant. In truth, the writer has probably not thought through their ideas properly, and they are not clear what they meant either. </p> <p>Pick your information sources and sinks with discretion, and strive to communicate in complete thoughts.</p> <p> Here is some <a href='https://www.newscientist.com/article/dn24541-monkey-controls-two-virtual-arms-with-thoughts-alone/' target='_blank' rel="nofollow">related science</a>. </p> AMF over HTTP in another Multiverse 2011-09-21T00:00:00-04:00 https://mslinn.github.io/blog/2011/09/21/amf-over-http-in-another-multiverse <p> This silly story is taken from my book <a href='https://www.slinnbooks.com/books/serverSide/index.html' target='_blank' rel="nofollow">Flex Data Services, Hibernate and Eclipse</a>. No other part of that book is deliberately silly. I thought it would be fun to post this here. In the story, produce and food are allegories for data and a cellar is an allegory for a database. As for Klay and Adobe, well, you get the idea. </p> <p> Once upon a time, in an alternate multiverse, there might or might not have been a world called Klay. Klay was a world very like Earth, except that the peaceful inhabitants were obsessed with food and food technology. This world had perfected the art and science of growing and cooking food. Each region had its specialty, however because transportation was not mechanized. Only seasoned travelers had ever experienced the wondrous tastes and textures of the exquisite culinary creations in far-off lands. Because growing conditions varied widely, food could only be made from local produce, and thus could not be replicated outside where the food was grown. Instead, the pudgy townsfolk would gather around wandering minstrels as they sang of the nutritional marvels that they had experienced in far-off lands. </p> <p> Food was grown, prepared, cooked and eaten nearby the same fields that it was grown. The Klaylings valued freshness and texture very highly, but they found that some produce actually improved when it was aged. This produce was stored in cellars next to the fields where the produce was grown. When a chef prepared a meal, he/she would gather the ingredients and cook it to order. </p> <p> A group of rich merchants in a prosperous region realized that there was profit to be made by selling prepared food to neighboring regions. Their first attempt was to have a chef prepare a dish that resembled a souffl&eacute;, and have it delivered by horseback. The rider, holding the dish in front of him, ended up with egg all over himself. Clearly, a means of packaging the food was required. </p> <p> The merchants also had another problem. Each country had different customs regarding the shape of the containers from which they ate. It was inconceivable that a person accustomed to eating from a square bowl might eat instead from a round or oval bowl. This complication was considered serious because as any chef will tell you, presentation is very important. The food had to be prepared for the specific containers that it was served in, and once prepared could not be transferred to another container without ruining the presentation, thereby severely reducing the selling price. </p> <p> The merchants called upon the local wise woman, who meditated and went into a trance. An acolyte wrote down the muttered sentences that she uttered while communing with the Fat God. Without knowing what the words meant, she wrote down the words and presented them to the merchants after the wise woman fell asleep. The words were: </p> <div class="quote">Awesome Mouthwatering Food over Highspeed Trusty Transport Provider.</div> <p> Another acolyte, skilled in numerology, tried turning the words into sacred acronyms: AMF over HTTP. Try as he might, he could not understand any significance in those words. </p> <p>When the wise woman awoke, she drew this diagram:</p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/amfOverHttp/allegory.svg" type="image/svg"> <!---<source srcset="/blog/images/amfOverHttp/allegory.avif" type="image/avif">--> <source srcset="/blog/images/amfOverHttp/allegory.webp" type="image/webp"> <source srcset="/blog/images/amfOverHttp/allegory.apng" type="image/apng"> <source srcset="/blog/images/amfOverHttp/allegory.png" type="image/png"> <source srcset="/blog/images/amfOverHttp/allegory.jpg" type="image/jpeg"> <source srcset="/blog/images/amfOverHttp/allegory.jpeg" type="image/jpeg"> <source srcset="/blog/images/amfOverHttp/allegory.jfif" type="image/jpeg"> <source srcset="/blog/images/amfOverHttp/allegory.pjpeg" type="image/jpeg"> <source srcset="/blog/images/amfOverHttp/allegory.pjp" type="image/jpeg"> <source srcset="/blog/images/amfOverHttp/allegory.gif" type="image/gif"> <source srcset="/blog/images/amfOverHttp/allegory.tif" type="image/tiff"> <source srcset="/blog/images/amfOverHttp/allegory.tiff" type="image/tiff"> <source srcset="/blog/images/amfOverHttp/allegory.bmp" type="image/bmp"> <source srcset="/blog/images/amfOverHttp/allegory.ico" type="image/x-icon"> <source srcset="/blog/images/amfOverHttp/allegory.cur" type="image/x-icon"> <img alt='Wise Woman’s Food Delivery Service diagram' class="imgImg rounded shadow" src="/blog/images/amfOverHttp/allegory.png" style='width: 100%; padding: 1em' title='Wise Woman’s Food Delivery Service diagram' /> </picture> </div> <p> The merchants gathered around the wise woman as she explained the diagram. &ldquo;The food from the fields and the cellars are brought into the kitchen as required. The meal is cooked to order, then deconstituted and packaged. A magic carpet uses AMF over HTTP to deliver the deconstituted food to the client’s kitchen, where it is reconstituted by one of the local chefs. The food is served into the appropriate containers for the patrons, and the carpet returns with the chef and payment. </p> <p> &ldquo;The key is AMF over HTTP. This magical process, yet to be invented, would consist of a tesseract, larger on the inside than it is on the outside. Within the tesseract, AMF would absorb the raw essence of the prepared food. The magic carpet would transport the tesseract almost instantaneously to the far-off land where the client lives. The magical AMF process would then reconstitute the food into the client’s desired containers.&rdquo; </p> <p> The merchants were incredulous. &ldquo;But we have never heard of AMF over HTTP. Where can we find this magical process, and the magic carpet?&rdquo; </p> <p> The old woman replied that she did not know. &ldquo;It was only a dream,&rdquo; she replied despondently. </p> <p> Fortunately, in this multiverse, AMF over HTTP does exist, and it transports data between client and servers just as the wise old woman described &ndash; and a lot more. Under the covers, AMF is used internally by Flex for many purposes. The next few chapters discuss how that can be done, and more. The Internet is our magic carpet. </p> Tracking Down a Mismatched JAR 2011-08-04T00:00:00-04:00 https://mslinn.github.io/blog/2011/08/04/tracking-down-mismatched-jar <p> For debugging mismatched jars, first build a list of the jars in question, then run <a href='https://github.com/rh1247/jwhich/blob/master/releases/jwhich-1.01.zip' target='_blank' rel="nofollow"><code>jwhich</code></a> to discover the jar that provides a specific Java class. Put the jar and the script provided in the download in a directory on your <code>PATH</code> such as <tt>/usr/local/bin</tt>. </p> <p>For *nix and Mac, build the list of jars under the current directory like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id310525f28be2'><button class='copyBtn' data-clipboard-target='#id310525f28be2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>export CLASSPATH=$(find . -name \*.jar -printf %h/%p:)</pre> </div> <p>For Cygwin, build the list of jars under the current directory like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0cd172e3221a'><button class='copyBtn' data-clipboard-target='#id0cd172e3221a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>export CLASSPATH=$(find . -name \*.jar -printf '%h/%p;')</pre> </div> <p>Run the script like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id118caeae47f6'><button class='copyBtn' data-clipboard-target='#id118caeae47f6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jwhich classNameToFindHere</pre> </div> Debugging Spring's SEVERE Error ListenerStart 2011-08-04T00:00:00-04:00 https://mslinn.github.io/blog/2011/08/04/debugging-severe-error-listenerstart <p> This Spring startup error can be mystifying if you don&rdquo;t know how to tackle it. <code data-lt-active='false'>listenerStart()</code> configures and invokes application event listeners for a <code>Context</code>. Most Spring applications have several listeners, and they should be invoked in the following order. Each of them causes debug output, so if you do not see any, the first listener died: </p> <ol data-lt-active='false'> <li><code>org.springframework.web.context.ContextLoaderListener</code></li> <li><code>org.springframework.web.context.request.RequestContextListener</code></li> <li><code>flex.messaging.HttpFlexSession</code> (for Spring/Flex integration)</li> </ol> <p> If the error message appeared, one of the above did not initialize correctly. Put breakpoints in each of the listener classes&rsquo; <code>contextInitialized()</code> or <code>requestInitialized()</code> methods. </p> <p> To <a href='https://dev-answers.blogspot.com/2010/03/enable-debugtrace-level-logging-for.html' target='_blank' rel="nofollow">view debug log messages</a> for <code>StandardContext</code>, add: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id900c3e2ad288'><button class='copyBtn' data-clipboard-target='#id900c3e2ad288' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>catalina.org.apache.juli.FileHandler.bufferSize = -1</pre> </div> <p> <code>SEVERE: Error listenerStart</code> messages can be debugged by setting a breakpoint at <code data-lt-active='false'>org.springframework.web.context.ContextLoaderListener</code>, line 47. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4d09e0d3bb52'><button class='copyBtn' data-clipboard-target='#id4d09e0d3bb52' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>contextLoader.initWebApplicationContext(event.getServletContext());</pre> </div> <p> Step return once and wait for the container to load everything. If there is an error you will now be at <code data-lt-active='false'>standardcontext.listenerstart</code> and you will see the error in the variable window under the <code data-lt-active='false'>t</code> variable. </p> <p>For debug output, add this to <code>web.xml</code>:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id935f12e6842a'><button class='copyBtn' data-clipboard-target='#id935f12e6842a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;listener&gt; &lt;listener-class&gt; org.springframework.web.util.Log4jConfigListener &lt;/listener-class&gt; &lt;/listener&gt;</pre> </div> Assessing Sample Code from Job Applicants 2011-05-24T00:00:00-04:00 https://mslinn.github.io/blog/2011/05/24/assessing-sample-code-from-job <p> Once again I am reviewing programmer resumes for a client. This client needs to hire an entire team, including Flex and Java programmers. Because programmers write software, I always request sample code from candidates. I do not ask stupid hypothetical questions or play mind games &ndash; I just want to assess the quality of their work. </p> <p> I request the code along with a resume. Code is truth. I do not interview candidates who do not provide sample code that passes inspection. The requirement for sample code is published in the job description. Surprisingly, most applicants do not provide sample code. I do not respond to those applications because clearly those people do not follow simple instructions. </p> <p> This is worth repeating: <b>If a job applicant wants to be considered as a candidate, they need to provide sample code along with their resume.</b> </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/assessingCode/samples_690x518.svg" type="image/svg"> <!---<source srcset="/blog/images/assessingCode/samples_690x518.avif" type="image/avif">--> <source srcset="/blog/images/assessingCode/samples_690x518.webp" type="image/webp"> <source srcset="/blog/images/assessingCode/samples_690x518.apng" type="image/apng"> <source srcset="/blog/images/assessingCode/samples_690x518.png" type="image/png"> <source srcset="/blog/images/assessingCode/samples_690x518.jpg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/samples_690x518.jpeg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/samples_690x518.jfif" type="image/jpeg"> <source srcset="/blog/images/assessingCode/samples_690x518.pjpeg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/samples_690x518.pjp" type="image/jpeg"> <source srcset="/blog/images/assessingCode/samples_690x518.gif" type="image/gif"> <source srcset="/blog/images/assessingCode/samples_690x518.tif" type="image/tiff"> <source srcset="/blog/images/assessingCode/samples_690x518.tiff" type="image/tiff"> <source srcset="/blog/images/assessingCode/samples_690x518.bmp" type="image/bmp"> <source srcset="/blog/images/assessingCode/samples_690x518.ico" type="image/x-icon"> <source srcset="/blog/images/assessingCode/samples_690x518.cur" type="image/x-icon"> <img alt='Samples of produce' class="imgImg rounded shadow" src="/blog/images/assessingCode/samples_690x518.png" style='width: 100%; ' title='Samples of produce' /> </picture> </div> <p> I assume that the programs that candidates show me are examples of their best work, or at least work that they are proud of. Most of my clients need programmers to develop software products or in-house applications, so efficiency and maintainability are important. </p> <p> I ask for sample code in at least two languages. It is up to the candidate to decide how much to show me, and what they show me. The more sample code they show me, the more sure I can be about my assessment. </p> <p>Here are some issues that I look for in submitted code:</p> <ol> <li>Is it difficult or expensive to maintain?</li> <li>Was the code written with the next programmer on this project in mind?</li> <li>Is logic used instead of data structures?</li> <li>Is the code neat, and does it follow a consistent pattern? I don’t care if particular formatting conventions are followed, I am looking for an ordered mind and a conscientious worker.</li> <li>Is the code testable? Were unit tests and/or integration tests provided?</li> <li>Does the code contain magic values instead of declared constants?</li> <li>Is the code inefficient (are there lots of conversions between string and int, for example)?</li> <li>Does the programmer use idioms specific to the language that it is written in?</li> <li>Are there appropriate comments, especially Javadoc / asdoc style comments?</li> <li>Are default actions or values present in the code? Defaults are tacitly understood by competent programmers, and providing them just adds noise. Code and documentation should be written with the assumption that readers will be technically competent.</li> <li>Is encapsulation violated because most methods are public?</li> <li>Are there syntax errors?</li> </ol> <p>For Adobe Flex programmers I also look for:</p> <ol> <li>Is there proper usage of the Flex component life cycle?</li> <li>Is there a heavy dependence on binding?</li> <li>Are events used properly?</li> <li>Are heavyweight Halo containers used, when Spark Groups would suffice?</li> <li>Is all logic in a controller, or is it shoved into views (or worse, item renderers)?</li> <li>Are style sheets used, or is style information explicitly coded?</li> </ol> <p> I also provide a reading and comprehension test, and ask them to critique the code I provide. Some code I provide is good, some bad. </p> <p> Many enticing resumes are simply not supported by good sample code. Would you hire a chef without tasting a sample of their food first? </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/assessingCode/chef.svg" type="image/svg"> <!---<source srcset="/blog/images/assessingCode/chef.avif" type="image/avif">--> <source srcset="/blog/images/assessingCode/chef.webp" type="image/webp"> <source srcset="/blog/images/assessingCode/chef.apng" type="image/apng"> <source srcset="/blog/images/assessingCode/chef.png" type="image/png"> <source srcset="/blog/images/assessingCode/chef.jpg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/chef.jpeg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/chef.jfif" type="image/jpeg"> <source srcset="/blog/images/assessingCode/chef.pjpeg" type="image/jpeg"> <source srcset="/blog/images/assessingCode/chef.pjp" type="image/jpeg"> <source srcset="/blog/images/assessingCode/chef.gif" type="image/gif"> <source srcset="/blog/images/assessingCode/chef.tif" type="image/tiff"> <source srcset="/blog/images/assessingCode/chef.tiff" type="image/tiff"> <source srcset="/blog/images/assessingCode/chef.bmp" type="image/bmp"> <source srcset="/blog/images/assessingCode/chef.ico" type="image/x-icon"> <source srcset="/blog/images/assessingCode/chef.cur" type="image/x-icon"> <img alt='Happy chef offering a sample of her food' class="imgImg rounded shadow" src="/blog/images/assessingCode/chef.png" style='width: 100%; ' title='Happy chef offering a sample of her food' /> </picture> </div> Mobile and Desktop Technology Trends and Issues 2011-05-13T00:00:00-04:00 https://mslinn.github.io/blog/2011/05/13/mobile-and-desktop-technology-trends <p> As principal of Micronautics Research I develop or oversee development of desktop and mobile client/server applications. We use whatever technology is appropriate for a client project. </p> <p> I make a distinction between applications (apps) and web pages (HTML and HTML5). Over time this distinction will blur. This article deals with current reality for apps that need to be developed and deployed using today’s technology. </p><p> Mobile and desktop apps can be described in terms of usability, performance, development cost, and maintainability; the latter is an indicator of the time, cost and risk of modifying an existing application. Whereas desktop environments are quite stable, new mobile devices and operating system versions now appear daily. Mobile apps are therefore have a much more difficult time staying relevant than do desktop apps. Mobile app environments also vary a lot more than desktop app environments. </p> <p> There is no perfect one-size-fits-all technology for every app, and this is especially true for mobile apps. Some mobile apps must integrate tightly with the operating system; for mobile apps this currently means writing in Java for Android or Objective C for Apple iOS. The majority of mobile apps, however, must be deployed to any device that the user might happen to own; the time and cost required to target every possible device explodes unless a cross-platform technology is used. </p> <p> I’d like to share a lesson learned from migrating several desktop apps to mobile apps. To reuse the original logic and data structures from the desktop app in various mobile apps, the logic and data structures must be cleanly separated from presentation to the greatest extent possible. We have yet to see a project where rework was not required in this regard before we could begin the task of making mobile versions. </p> <p> Presentation logic for adaptable views is not much of an issue for desktop apps, but it is important when targeting devices whose resolutions and pixel densities vary greatly. Implementing the logic for adaptable views is not difficult, but designers today are generally unfamiliar with the issues of how to create <a href='https://mobile-patterns.com/' target='_blank' rel="nofollow">adaptable designs</a> for such a wide variety of platforms. We find that our interaction with designers has deepened when working on mobile projects. </p> <p> Processing power varies greatly between mobile devices. Tablets have quite a bit more power than phones, and desktops have yet more power. However, mobile devices have specialized hardware not present in most desktops, such as GPS and various forms of telephony communications; many Android devices also have a built-in bar code scanner. When adapting a desktop app to run on mobile apps, we strongly suggest using built-in hardware to the greatest extent possible. Mobile apps should obtain data from hardware or remote services instead of asking the user to type, click or swipe. </p> <p> We also enhance data structures as much as possible so less processing is required; although this is generally a good practice, today’s desktops are so powerful that careless programming often goes unnoticed. When an app is ported from a desktop environment to a mobile device, the reduced CPU and memory resources cause the app to run unacceptably slowly. </p> <p> For applications that must continue to work on desktops after they are ported to mobile devices, we recommend identifying and optimizing the application core such that it is present without changes for all environments. Sometimes clients are surprised by the results of this partitioning process because their assumptions of what is core do not align with actual commonalities between target environments. </p> Mounting compressed folders / Looping back zip files 2010-02-19T00:00:00-05:00 https://mslinn.github.io/blog/2010/02/19/mounting-compressed-folders-looping <p> Big file systems make computers run more slowly. This problem is most noticeable for laptops. On a project that I am currently working on, I am using multiple versions of three frameworks: The Flex SDK (source and ASDoc), the MicroStrategy Flex SDK and the Blaze DS source tree. The result is thousands of files that takes hours to copy and slows down disk access even when I’m not programming. </p> <h2 id="readonly">Read-only Files</h2> <p> Because these files are only read and never written, the solution is to not unzip them. Instead, the files should only be accessed from within the zip files that contains them. This capability is called a &ldquo;local loopback filesystem&rdquo; on Linux, and &ldquo;accessing compressed folders&rdquo; under Windows. </p><p> Results are dramatic. Usage is simple. What’s not to love? </p> <h2 id="windows">Windows</h2> <p> On Windows, I use the free <a href='https://www.pismotechnic.com/pfm/ap/' target='_blank' rel="nofollow">Pismo File Mount Audit Package</a>. It provides the ability for zip files to be mounted as virtual drives. </p> <h2 id="linux">Linux</h2> <p> On Linux, use <a href='https://github.com/refi64/fuse-zip' target='_blank' rel="nofollow"><code>fuse-zip</code></a>. It provides the ability for zip files to be mounted on any directory mount point. </p> Shutting down Zamples.com 2009-11-23T00:00:00-05:00 https://mslinn.github.io/blog/2009/11/23/shutting-down-zamplescom <p> I&rsquo;ve decided that it is long past the time that zamples.com should be shut off. The site got steady traffic, but it generated almost no revenue and demanded a lot of my time. Now that the server needs to be replaced, it is time to say goodbye. </p> <p> I first created the predecessor to Zamples in 1994 for The Internet Factory, manufacturers of the first programmable web server. Eleven years later, it is abundantly clear that no-one really cares about live online code examples, at least not enough to spend money for the privilege of using the facility. </p> <p> <a href='https://www.w3schools.com/charsets/tryit.asp?deci=128521' target='_blank' rel="nofollow">w3schools</a> built their own client-side only version recently. </p> <p> It’s rather sad to turn the switch off, but I won’t miss spending any more time and money on Zamples. </p> 2009 Open Source Community Leadership Summit 2009-07-20T00:00:00-04:00 https://mslinn.github.io/blog/2009/07/20/community-leadership-summit-09 <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/communityLeadership/community-leadership-summit.svg" type="image/svg"> <!---<source srcset="/blog/images/communityLeadership/community-leadership-summit.avif" type="image/avif">--> <source srcset="/blog/images/communityLeadership/community-leadership-summit.webp" type="image/webp"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.apng" type="image/apng"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.png" type="image/png"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.jpg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.jpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.jfif" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.pjpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.pjp" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.gif" type="image/gif"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.tif" type="image/tiff"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.tiff" type="image/tiff"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.bmp" type="image/bmp"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.ico" type="image/x-icon"> <source srcset="/blog/images/communityLeadership/community-leadership-summit.cur" type="image/x-icon"> <img alt='Open Source Community Leadership Summit' class="imgImg rounded shadow" src="/blog/images/communityLeadership/community-leadership-summit.png" style='width: 100%; ' title='Open Source Community Leadership Summit' /> </picture> </div> <p> The first <a href='https://www.communityleadershipsummit.com' target='_blank' rel="nofollow">Open Source Community Leadership Summit</a> happened this weekend, and I was lucky enough to be able to attend. Jono Bacon, Ubuntu Community Manager, and the other volunteers did a terrific job of putting this together. </p> <p> I had a chance to get to know and renew acquaintances with some fascinating people from Canonical, Cisco, Collab.net, the Linux Foundation, the Linux Fund, Novell, the Open Voting Consortium, O&rsquo;Reilly Media, Red Hat, the Software Freedom Law Center, Sun and two of the the &lsquo;old men&rsquo; of the open source world, Bruce Perens and Larry Rosen. </p> <div class='imgWrapper imgBlock right quartersize' style=' '> <figure> <a href='https://www.rosenlaw.com/rosen.htm' target='_blank' rel='nofollow' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/communityLeadership/larryRosen.svg" type="image/svg"> <!---<source srcset="/blog/images/communityLeadership/larryRosen.avif" type="image/avif">--> <source srcset="/blog/images/communityLeadership/larryRosen.webp" type="image/webp"> <source srcset="/blog/images/communityLeadership/larryRosen.apng" type="image/apng"> <source srcset="/blog/images/communityLeadership/larryRosen.png" type="image/png"> <source srcset="/blog/images/communityLeadership/larryRosen.jpg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/larryRosen.jpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/larryRosen.jfif" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/larryRosen.pjpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/larryRosen.pjp" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/larryRosen.gif" type="image/gif"> <source srcset="/blog/images/communityLeadership/larryRosen.tif" type="image/tiff"> <source srcset="/blog/images/communityLeadership/larryRosen.tiff" type="image/tiff"> <source srcset="/blog/images/communityLeadership/larryRosen.bmp" type="image/bmp"> <source srcset="/blog/images/communityLeadership/larryRosen.ico" type="image/x-icon"> <source srcset="/blog/images/communityLeadership/larryRosen.cur" type="image/x-icon"> <img alt='Larry Rosen' class="imgImg rounded shadow" src="/blog/images/communityLeadership/larryRosen.png" style='width: 100%; ' title='Larry Rosen' /> </picture> </a> <figcaption class='imgFigCaption quartersize'> <a href="https://www.rosenlaw.com/rosen.htm" target='_blank' rel='nofollow'> Larry Rosen </a> </figcaption> </figure> </div> <div class='imgWrapper imgBlock left quartersize' style=' '> <figure> <a href='https://perens.com' target='_blank' rel='nofollow' class='imgImgUrl'><picture class='imgPicture'> <source srcset="/blog/images/communityLeadership/brucePerens.svg" type="image/svg"> <!---<source srcset="/blog/images/communityLeadership/brucePerens.avif" type="image/avif">--> <source srcset="/blog/images/communityLeadership/brucePerens.webp" type="image/webp"> <source srcset="/blog/images/communityLeadership/brucePerens.apng" type="image/apng"> <source srcset="/blog/images/communityLeadership/brucePerens.png" type="image/png"> <source srcset="/blog/images/communityLeadership/brucePerens.jpg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/brucePerens.jpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/brucePerens.jfif" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/brucePerens.pjpeg" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/brucePerens.pjp" type="image/jpeg"> <source srcset="/blog/images/communityLeadership/brucePerens.gif" type="image/gif"> <source srcset="/blog/images/communityLeadership/brucePerens.tif" type="image/tiff"> <source srcset="/blog/images/communityLeadership/brucePerens.tiff" type="image/tiff"> <source srcset="/blog/images/communityLeadership/brucePerens.bmp" type="image/bmp"> <source srcset="/blog/images/communityLeadership/brucePerens.ico" type="image/x-icon"> <source srcset="/blog/images/communityLeadership/brucePerens.cur" type="image/x-icon"> <img alt='Bruce Perens' class="imgImg rounded shadow" src="/blog/images/communityLeadership/brucePerens.png" style='width: 100%; ' title='Bruce Perens' /> </picture> </a> <figcaption class='imgFigCaption quartersize'> <a href="https://perens.com" target='_blank' rel='nofollow'> Bruce Perens </a> </figcaption> </figure> </div> Commonwealth Club Performance 2009-06-27T00:00:00-04:00 https://mslinn.github.io/blog/2009/06/27/commonwealth-club-performance <p> Cindie Steinmetz and I performed at the San Francisco Commonwealth Club two evenings ago. We had been invited to preview a jingle I wrote entitled &ldquo;I Love Chocolate&rdquo; for the &ldquo;Women and Chocolate&rdquo; event. I played the rest of the evening, while enjoying excellent wine and chocolate. </p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/images/mike/commonwealthChocolate_5_690x518.svg" type="image/svg"> <!---<source srcset="/images/mike/commonwealthChocolate_5_690x518.avif" type="image/avif">--> <source srcset="/images/mike/commonwealthChocolate_5_690x518.webp" type="image/webp"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.apng" type="image/apng"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.png" type="image/png"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.jpg" type="image/jpeg"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.jpeg" type="image/jpeg"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.jfif" type="image/jpeg"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.pjpeg" type="image/jpeg"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.pjp" type="image/jpeg"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.gif" type="image/gif"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.tif" type="image/tiff"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.tiff" type="image/tiff"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.bmp" type="image/bmp"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.ico" type="image/x-icon"> <source srcset="/images/mike/commonwealthChocolate_5_690x518.cur" type="image/x-icon"> <img alt='Mike Slinn and Cindie Steinmetz performing at the San Francisco Commonwealth Club' class="imgImg rounded shadow" src="/images/mike/commonwealthChocolate_5_690x518.png" style='width: 100%; width: 100%' title='Mike Slinn and Cindie Steinmetz performing at the San Francisco Commonwealth Club' /> </picture> </div> Presenting EmpathyWorks at a Private Equity Roundtable 2009-02-22T00:00:00-05:00 https://mslinn.github.io/blog/2009/02/22/presenting-empathyworks-at-private <p> The private equity round table vSIG is focused on applying virtual worlds to solve pressing business issues. The group meets once a month on Sandhill Road in Palo Alto, CA. Members include software entrepreneurs, VCs, academia and researchers. </p> <p class="rounded shadow formalNotice"> 2020 Update: the vSIG has morphed into <a href='https://www.vcr-sv.com/' target='_blank' rel="nofollow">Venture Capital Roundtable Silicon Valley</a>. </p> <p> This will be part two of two. During the last meeting I introduced the topics of modeling relationships and behavior and presented background concepts. </p> <p>The main points I made were:</p> <ul> <li>Relationships are far more than just directory entries, they are the foundation of expression and provide context for interpreting behavior</li> <li>The ability to recognize another individual is necessary in order to form a relationship with them</li> <li>Personality is perceived, not an absolute or fixed quality</li> <li>Behavior is defined by responses and is contextually dependent</li> <li>Today’s artificial beings suffer from Alzheimer’s</li> <li>It is more useful to model an entire community than a single individual</li> <li>Personas represent an individual in various contexts</li> </ul> <p> I then introduced EmpathyWorks&trade;, a generalized relationship and behavior modeler that I have been working on for a few years. </p> Good Code is Beautiful 2008-10-15T00:00:00-04:00 https://mslinn.github.io/blog/2008/10/15/good-code-is-beautiful <p> First, I&rsquo;d like to point out a few practical reasons why orderliness is important. Can you spot the bug in the following Flex code? This custom component is not as wide as expected: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id55940ef124b2'><button class='copyBtn' data-clipboard-target='#id55940ef124b2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;mx:TextArea width="100" height="100%" contextMenu="{cm}" xmlns:components="components.*" creationComplete="init()" width="100%" xmlns:mx="https://www.adobe.com/2006/mxml"&gt; ... &lt;/mx:TextArea&gt;</pre> </div> <p> The bug occurs because two width attributes were provided, one with the value 100 pixels and one with the value 100%. Jamming a long list of attributes into an MXML tag in no particular order is an invitation to chaos. No error message is generated if an attribute is specified twice. </p> <p> Long ago I adopted the convention of ordering all attributes in alphabetical order, one per line. I would write the above like this:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id37c31e5ea76c'><button class='copyBtn' data-clipboard-target='#id37c31e5ea76c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>&lt;mx:TextArea contextMenu="{cm}" creationComplete="init()" height="100%" width="100%" xmlns:components="components.*" xmlns:mx="https://www.adobe.com/2006/mxml"&gt; ... &lt;/mx:TextArea&gt;</pre> </div> <p> Other people write certain attributes first, like id, width and height. Whatever convention you adopt, stick to it. </p> <p> Here are the <a href='https://web.archive.org/web/20111006191913/https://opensource.adobe.com/wiki/display/flexsdk/Coding+Conventions' target='_blank' rel="nofollow">official ActionScript coding conventions</a>, as published by Adobe back in 2011, prior to canceling the Flex product. I take some of it the way the characters of &ldquo;Pirates of the Caribbean&rdquo; consider the Pirate&rsquo;s Code: &ldquo;Argh, it be more like a set of guidelines.&rdquo; </p> <div class='imgWrapper imgFlex inline fullsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/pirateCodex_690x460.svg" type="image/svg"> <!---<source srcset="/blog/images/pirateCodex_690x460.avif" type="image/avif">--> <source srcset="/blog/images/pirateCodex_690x460.webp" type="image/webp"> <source srcset="/blog/images/pirateCodex_690x460.apng" type="image/apng"> <source srcset="/blog/images/pirateCodex_690x460.png" type="image/png"> <source srcset="/blog/images/pirateCodex_690x460.jpg" type="image/jpeg"> <source srcset="/blog/images/pirateCodex_690x460.jpeg" type="image/jpeg"> <source srcset="/blog/images/pirateCodex_690x460.jfif" type="image/jpeg"> <source srcset="/blog/images/pirateCodex_690x460.pjpeg" type="image/jpeg"> <source srcset="/blog/images/pirateCodex_690x460.pjp" type="image/jpeg"> <source srcset="/blog/images/pirateCodex_690x460.gif" type="image/gif"> <source srcset="/blog/images/pirateCodex_690x460.tif" type="image/tiff"> <source srcset="/blog/images/pirateCodex_690x460.tiff" type="image/tiff"> <source srcset="/blog/images/pirateCodex_690x460.bmp" type="image/bmp"> <source srcset="/blog/images/pirateCodex_690x460.ico" type="image/x-icon"> <source srcset="/blog/images/pirateCodex_690x460.cur" type="image/x-icon"> <img alt='The Pirate&rsquo;s Codex, from the Pirates of the Caribbean movie' class="imgImg rounded shadow" src="/blog/images/pirateCodex_690x460.png" style='width: 100%; ' title='The Pirate&rsquo;s Codex, from the Pirates of the Caribbean movie' /> </picture> </div> <h3 id="compressed">ANSI/Unix vs. Vertically Compressed Styles</h3> <p>I realize that this topic might make me flame-bait, but my experience has shown that the ANSI/Unix style of vertically spacing parenthesis increases the maintenance costs of a program.</p> <p> Functions/methods should fit on a single screen; so should data structures. Because programs written in bygone days had to be editable on a green screen, 80 columns was the widest that a program could be written. In the &lsquo;bad old days&rsquo;, it was better to let a program run to more lines in order to avoid folding code. Here is an example: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99003b98effc'><button class='copyBtn' data-clipboard-target='#id99003b98effc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>private function GridContextEventHandler(event:GridContextEvent):void { if (event.menuItem=='Edit') { EditGridItem(event.selectedIndex); } else if (event.menuItem=='Remove') { RemoveGridItem(event.selectedIndex); } }</pre> </div> <p> A vertically compressed style is much more easily read. When the Python programming language was first introduced it was initially controversial in part because it used indentation to define control structure. Python programs do not need braces for scoping as a result. Why not write ActionScript in the same manner, so parenthesis are virtually redundant? The Sun/Java convention is a good example of this formatting convention. The above program fragment would be written like this using a vertically compressed style: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7ac1b9e30218'><button class='copyBtn' data-clipboard-target='#id7ac1b9e30218' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>private function GridContextEventHandler(event:GridContextEvent):void { if (event.menuItem=='Edit') { EditGridItem(event.selectedIndex); } else if (event.menuItem=='Remove') { RemoveGridItem(event.selectedIndex); } }</pre> </div> <h2 id="enforcing">Enforcing Style</h2> <p> Coding style conventions can be automatically applied by installing the Flex Pretty Printer plugin for Eclipse / Flash Builder. If you like, you can importing <a href='https://www.slinnbooks.com/books/FlexFormatter.properties' target='_blank' rel="nofollow">my formatting rules</a>, which enforce the vertically compressed style above. </p> Breathless Delirium 2008-09-24T00:00:00-04:00 https://mslinn.github.io/blog/2008/09/24/breathless-delerium <p> In his keynote presentation to the Agile 2008 conference in Toronto entitled &ldquo;<a href='https://medium.com/@MrAlanCooper/how-far-have-we-come-792a80625c94' target='_blank' rel="nofollow">The Wisdom of Experience</a>&rdquo;, Alan Cooper had a great sentence that described the &lsquo;first to market&rsquo; goal behind some products: </p> <div class="quote"> There is no large group of people out there waiting in a breathless delirium to purchase your lousy product sooner rather than later. </div> <p> Sure, it is terrific to be first. But the product still has to be good. </p> <p> Alan also makes some good points about design, engineering and construction. He took the long way around, because he doesn&rsquo;t get into his main topic, interactive design, until slide 75. Requirements are discussed starting at slide 87. In the slide entitled &ldquo;Requirements are not design&rdquo;, Alan says: </p> <ul class="quote"> <li>Giving people what they say they desire does not result in success.</li> <li>Your customers are not the same as your users.</li> <li>Neither your customers nor your users know what they want or even what they do.</li> <li>What people tell you has little bearing on the truth.</li> <li>Good user experience is not dependent on features</li> <li>Radically different products can have identical features.</li> <li>A list of features is not the same as the design of behavior.</li> <li>Expertise in a subject does not correlate to expertise in designing software behavior.</li> </ul> <p> Philosophy starts on slide 92. I like philosophy, it is one of the foundations of architecture. </p> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/geeklove/geeklove.svg" type="image/svg"> <!---<source srcset="/blog/images/geeklove/geeklove.avif" type="image/avif">--> <source srcset="/blog/images/geeklove/geeklove.webp" type="image/webp"> <source srcset="/blog/images/geeklove/geeklove.apng" type="image/apng"> <source srcset="/blog/images/geeklove/geeklove.png" type="image/png"> <source srcset="/blog/images/geeklove/geeklove.jpg" type="image/jpeg"> <source srcset="/blog/images/geeklove/geeklove.jpeg" type="image/jpeg"> <source srcset="/blog/images/geeklove/geeklove.jfif" type="image/jpeg"> <source srcset="/blog/images/geeklove/geeklove.pjpeg" type="image/jpeg"> <source srcset="/blog/images/geeklove/geeklove.pjp" type="image/jpeg"> <source srcset="/blog/images/geeklove/geeklove.gif" type="image/gif"> <source srcset="/blog/images/geeklove/geeklove.tif" type="image/tiff"> <source srcset="/blog/images/geeklove/geeklove.tiff" type="image/tiff"> <source srcset="/blog/images/geeklove/geeklove.bmp" type="image/bmp"> <source srcset="/blog/images/geeklove/geeklove.ico" type="image/x-icon"> <source srcset="/blog/images/geeklove/geeklove.cur" type="image/x-icon"> <img alt='Geek love' class="imgImg rounded shadow" src="/blog/images/geeklove/geeklove.png" style='width: 100%; ' title='Geek love' /> </picture> </div> Cult of the Software God 2008-04-28T00:00:00-04:00 https://mslinn.github.io/blog/2008/04/28/cult-of-software-god <p> I invented a &lsquo;religion&rsquo; in the mid-80s. Admittedly, it was done in jest, but I find that I make frequent reference to it and so it deserves a mention lest people I converse with find me just babbling. Have no fear, other whacked-out religions exist with high-profile devotees (you know some, I&rsquo;m sure) so I&rsquo;m in good company. </p> <p> The religion is called &ldquo;The Cult of the Software God&rdquo;, and this god oversees technical and financial success for software entrepreneurs and IT workers. Like many ancient religions of days gone by, the Software God requires sacrifices, which amounts to the destruction of software media. At one time he/she/it was content when I would hold a 5.25" floppy disk over a garbage can and cut it up, while intoning praises to &ldquo;the great Software God&rdquo; and praying that the bits stored on the media be returned to their maker, but this is a God of the Geeks, and he/she/it has kept up with advances in storage media. </p> <p> When 3.5" diskettes became available, the Software God soon lost interest in clunky old floppy disks and demanded sacrifices of the smaller rigid diskettes instead. I, as High Priest of the Cult, interpreted the wishes of the Software God and used a Sacred Hammer stored on a bench near our diskette duplicating station to smash up diskettes that had write errors during the duplication process. &ldquo;Oh great Software God&rdquo;, I would say. &ldquo;We humbly beseech you to accept this offering and pray that you bring abundance and a measurable quality improvement to our next release.&rdquo; </p> <p> Time moved on, I moved to other ventures, and yet I never abandoned my role as High Priest of the Cult of the Software God. Occasionally I would snap CDs in half in the name of the Software God when a write error occurred, and moved on to DVDs. I even used a blowtorch on a RLL-encoded 5.25&rdquo; hard drive and ground my heel on a flash drive, while singing praises to the Software God. </p> <p> You might think I had too much time on my hands. Certainly I am at least eccentric. Yet there is more! </p> <p> As High Priest of the Cult of the Software God, I became aware that he/she/it is also fed by <a href='https://askubuntu.com/a/12110/58760' target='_blank' rel="nofollow"><code>/dev/null</code></a>. Where do you think bits piped to <code>/dev/null</code> go, anyway? Why, they nourish the Software God, of course! You can turn this otherwise wasted gift to the Software God into a sacrificial offering simply by making a prayer as the bits are piped. Of course, since the Software God is a god for the geeks and by the geeks, automation counts. That is why I wrote a program in the late 90s (since lost) that would print out any prayer you desired on the screen, over and over... rather like how prayer wheels work. </p> <p> Next time you really need your computer to not crash while running a semi-stable program, or have a serious virus infestation, remember the Cult of the Software God. He/she/it is virtually there for you. &#x1F609; </p> AAAI Symposium 2008-03-26T00:00:00-04:00 https://mslinn.github.io/blog/2008/03/26/aaai-symposium-for-emotion-personality <p>This week I am attending the <b>AAAI Symposium for Emotion, Personality and Social Behavior</b> at Stanford. The symposium provides a forum for an interdisciplinary discussion of modeling affect and personality in social behavior. Attendees include researchers studying social computing, virtual reality, game design, robotics, believable agents, affective computing, psychotherapy and the arts. Panel discussions discussed successful interactions between artificial beings and humans.</p> <p>Some of the discussion topics that I am interested in include:</p> <ul> <li>How can compelling artificial characters be designed?</li> <li>How to facilitate social interaction between humans, and / or artificial beings?</li> <li>What are the components in the software toolbox?</li> <li>What are the emerging standards in affective artificial characters, robots and systems?</li> <li>What architectural designs work best?</li> <li>What knowledge base designs work best?</li> </ul> <h2 id="scheutz">Believability of Robot Affect</h2> <p> Matthias Scheutz, Associate Professor of Informatics at Indiana University, presented a paper entitled &ldquo;<b>Empirical Investigations into the Believability of Robot Affect</b>&rdquo;. During the discussion afterwards he suggested that people are more critical of <a href='https://en.wikipedia.org/wiki/Animatronics' target='_blank' rel="nofollow">animatronic</a> (physical) entities, compared to characters that are merely imaged. Seems that the bar for believable behavior for robots is higher than for characters which are merely rendered on a computer screen because people don't expect a rendered character to be real anyway. </p> <h2 id="tapus">Socially Assistive Robots</h2> <p>Adriana Tapus&rsquo; presentation, &ldquo;<b>Socially Assistive Robots: The Link between Personality, Empathy, Physiological Signals and Task Performance</b>&rdquo; provided me two pieces of interesting common-sense information:</p> <ul> <li>A robot that challenges the user during therapy rather than offering praise will be preferred by extroverts;</li> <li>A robot that offers nurturing praise rather than challenging-based motivation by introverts.</li> </ul> <p> In the discussion that followed Adriana&rsquo;s talk, mention was made of (uncited) research which found that extroverts are more sensitive to robot personalities because they seek outside stimulus; introverts are self-stimulated. It seems logical that when a robot first encounters a person about which nothing is known regarding their personality, the robot should display mildly extroverted behavior, and then adapt according to social cues. </p> <h2 id="olsen">Emotions for Strategic Real-time Systems</h2> <p><a href='http://www.cs.loyola.edu/~olsen/' target='_blank' rel="nofollow">Megan Olsen</a> discussed how adding an emotion framework to game engines improved performance in her talk entitled &ldquo;<b>Emotions for Strategic Real-time Systems</b>&rdquo;. The emotional framework models fear and frustration in the form of emotional maps, which are overlaid on a physical map of terrain. Emotions diffuse out and are picked up by co-operating agents nearby. Emotions have an intensity value which linearly decays over time. She showed an interesting video which illustrated the aggregated emotions of many agents over time. She also showed that emotions can cause groups of synthetic individuals to be more successful in combat. Different game engines responded differently to each emotion; some responded better to the addition of the ability to model fear, which others responded better to the addition of frustration, and <a href='https://globulation2.org/wiki/AINicowar' target='_blank' rel="nofollow">NicoWar</a> was able to benefit from the addition of both emotions. </p> <h2 id="hudlicka">Emotion Modeling 101</h2> <p> <a href='https://works.bepress.com/eva_hudlicka/' target='_blank' rel="nofollow">Dr. Eva Hudlicka</a> of Psychometric Associates spoke on &ldquo;<b>Emotion Modeling 101</b>&rdquo;. Emotion modeling incorporates emotion expression, recognition, generation, and the effect on agent behavior. One could model feelings, moods, emotions, affective states and personality traits; some people mean all of the above when discussing modeling emotion. As well, attitudes and preferences can be modeled. Interpersonal roles include: social coordination, rapid communication of intent; intrapsychic roles include: motivation, homeostatis, and adaptive behavior. Emotions are manifested across multiple interacting modalities: somatic / physiological, cognitive / interpretive, behavioral / motivational and experiential / subjective. Unfortunately, the literature on emotional models is inconsistent and terms are unclear. Eva suggested we view emotion models in terms of emotion generation and effects; that emotion modeling building blocks be identified. Emotion is generated from stimuli and appraisal by the individual; Eva briefly discussed both topics and the need to standardize the process of mapping stimulus to emotion. </p> <h2 id="camurri">Coffee With Dr. Antonio Camurri</h2> <p>During a coffee break, I chatted with <a href='http://www.infomus.org/people/person.php?name=acamurri' target='_blank' rel="nofollow">Dr. Antonio Camurri</a>, Associate Professor at the University of Genoa, Italy. Seems we both share a passion for boats. Dr. Camurri mentioned EyesWeb, an open software platform project that he leads, that enables the development of real-time multimodal distributed interactive applications. </p> <h2 id="wrapup">Wrapup</h2> <p>In the wrap-up session, I drew attention to one of the issues mentioned in the preface to the Technical Report that the Symposium was to intended to address: &ldquo;What are the emerging standards in affective artificial characters, robots and systems?&rdquo; One member of the audience (Rosamaria Barone?) mentioned some standards work for characters (unfortunately, I didn't write it down), then an awkward silence followed. I believe that standards will be key to artificial personality and emotion moving from the lab to mainstream society. </p> IKVM.NET – Java applications on .NET 2008-01-16T00:00:00-05:00 https://mslinn.github.io/blog/2008/01/16/ikvmnet-java-applications-on-net <p> I am building the first implementation of EmpathyWorks&trade; as a Java library packaged as a JAR. After looking around for a while to understand how EmpathyWorks might run on .NET, I found <a href='https://www.ikvm.net/' target='_blank' rel="nofollow">IKVM.NET</a>. </p> <h2 id="ikvm">IKVM Works Well</h2> <p> IKVM is an open source project that converts Java JARs into a .NET assembly. I was quite skeptical, but after downloading IKVM I was able to convert EmpathyWorks into an EXE that ran on Windows XP in about ten minutes. </p> <p> IKVM also has a mechanism for converting .NET stubs into JARs, important when developing .NET code in an IDE so that Intellisense tooltips appear for code completion. I am next going to try converting the Microsoft Robotics Studio runtime to a JAR so I can try writing a sample MSR application that co-operates with EmpathyWorks. </p> <p> There is a brief note in the IKVM documentation that says debugging from Java in this circumstance does not work. Hmm, what about the other way... can one debug the .NET application from Visual Studio, and reach right into the converted Java code? I suppose no source code would be available. </p> <hr /> <h2 id="update1">Update Jan 18/08</h2> <p>I built a DLL from Java using IKVM's <code>-target:library</code> option. Visual C# 2008's object browser happily showed all the public objects and methods in the DLL. Once I discovered the &ldquo;Add to References&rdquo; button in the object browser my test C# program was able to call into the DLL. Most cool!</p> <hr /> <h2 id="update2">The End of IKVM.NET </h2> <p> Jeroen Frijters, the principal author of IKVM.NET <a href='https://weblog.ikvm.net/CommentView.aspx?guid=33ea525f-a291-418a-bd6a-abdf22d0662b' target='_blank' rel="nofollow">posted the following</a> Apr 21/17. </p> <div class="shadow rounded liImg" style="padding: 1em;"> After almost fifteen years I have decided to quit working on IKVM.NET. The decision has been a long time coming. Those of you that saw yesterday’s Twitter spat, please don’t assume that was the cause. It rather shared an underlying cause. I’ve slowly been losing faith in .NET. Looking back, I guess this process started with the release of .NET 3.5. On the Java side things don’t look much better. The Java 9 module system reminds me too much of the generics erasure debacle. <br /><br /> I hope someone will fork IKVM.NET and continue working on it. Although, I’d appreciate it if they’d pick another name. I’ve gotten so much criticism for the name over the years, that I’d like to hang on to it 😊 <br /><br /> I’d like to thank the following people for helping me make this journey or making the journey so much fun: Brian Goetz, Chris Brumme, Chris Laffra, Dawid Weiss, Erik Meijer, Jb Evain, John Rose, Mads Torgersen, Mark Reinhold, Volker Berlin, Wayne Kovsky, The GNU Classpath Community, The Mono Community. <br /><br /> And I want to especially thank my friend Miguel de Icaza for his guidance, support, inspiration and tireless efforts to promote IKVM. <br /><br /> Thank you all and goodbye. </div> I'm a Dragon on a Business Plan Panel 2007-11-21T00:00:00-05:00 https://mslinn.github.io/blog/2007/11/21/im-dragon-on-business-plan-panel <p> A few months ago the Canadian Consulate in Silicon Valley asked me if I would be interested in working with Canadian entrepreneurs. I was happy to oblige. Since then the Canadian National Research Council has been using me to provide guidance to young entrepreneurs. </p> <p> I was asked by <a href='https://www.linkedin.com/in/eanjackson/' target='_blank' rel="nofollow">Ean Jackson</a>, an instructor at <a href='https://www.sfu.ca/' target='_blank' rel="nofollow">Simon Fraser University</a> in Burnaby, BC to sit on a <a href='https://bus477.com/class-videos/' target='_blank' rel="nofollow">&ldquo;Dragon Panel&rdquo;</a> to evaluate student projects. The 4th year undergraduate business students enrolled in the New Ventures Planning Program share the course with 4th year Engineering students at SFU. </p> <p> One of the &ldquo;Suits&rdquo; is teamed up with three &ldquo;Geeks&rdquo; enrolled in the Engineering program. The Geeks come up with a technology idea and the Suits and Geeks work together to create a business plan. That business is entered into the Ken Spencer Business plan competition at SFU where cash prizes are offered. </p> <p> Today (November 21, 2007) is the final class. The 14 Suits will pitch their business ideas to a panel of &ldquo;investors&rdquo;. I am acting in the role of a prospective investor; each of the &ldquo;investors&rdquo; will grade the pitch. Sounds like fun! I'm looking forward to hearing the presentations this afternoon. </p> Digital Mentat 2007-09-23T00:00:00-04:00 https://mslinn.github.io/blog/2007/09/23/digital-mentat <div class='quote'> <div class='quoteText clearfix'> It is by caffeine alone I set my mind in motion.<br /><br /> It is by the Beans of Java that thoughts acquire speed, the hands acquire shaking, the shaking becomes a warning.<br /><br /> It is by caffeine alone I set my mind in motion. </div><div class='quoteAttribution'> &nbsp;&ndash; From <a href='https://dune.fandom.com/wiki/Mentat#The_Mentat_Mantra_.28From_David_Lynch.27s_movie.2C_and_a_game_made_by_Westwood.29' rel='nofollow' target='_blank'>Brian Herbert, Mentat Mantra of Dune</a></div> </div> <p> It&rsquo;s just possible that I might have been spending too much time programming EmpathyWorks&trade; lately! </p> Singularity Summit 2007-09-09T00:00:00-04:00 https://mslinn.github.io/blog/2007/09/09/singularity-summit <p> I&rsquo;m at the Singularity Summit today, after spending two months on my sailboat in B.C. Quite a change in venue! </p> <p> As you can tell by reading this blog, I&rsquo;m quite interested in AI. The theme of &lsquo;friendly intelligence&rsquo; strikes a chord in me. I&rsquo;m currently working on an artificial personality simulator, EmpathyWorks&trade;, a project that I&rsquo;ve been noodling on for over thirty years. Hopefully I&rsquo;ll get a chance to chat with various people here at the conference about integrating artificial personality into a whole artificial organism. </p> 2nd Annual Silicon Valley Ruby Conference 2007-04-25T00:00:00-04:00 https://mslinn.github.io/blog/2007/04/25/2nd-annual-silicon-valley-ruby <p> Whew, it is a wrap! The Second Annual Silicon Valley Ruby Conference is done, and I&rsquo;m just about finished doing post-production on the videos of the talks. I&rsquo;ll hand a DVD to SDForum to make arrangements for attendees of the event to view the videos. </p> <p> Slides for most of the presentations are available at <a href='https://www.slideshare.net' target='_blank' rel="nofollow">slideshare.net</a>. </p> <p> 2007 seems to be shaping up as the Year of the Ruby/Rails IDE. Later this year commercial-quality IDEs from Sun and CodeGear will join Aptana&rsquo;s RadRails IDE. All three of these software publishers presented at this year&rsquo;s event. </p> <p> I ran a quick survey by a show of hands, and asked 24 questions. Here are a few results: </p> <table class="table"> <tbody> <tr> <td style="text-align: left; padding: 5px;">Which of you use Rails to make web sites but don&rsquo;t care about Ruby?</td> <td align="right" style="padding: 5px;">25%</td> </tr> <tr> <td style="text-align: left; padding: 5px;">Which of you use Dreamweaver, Illustrator or Photoshop?</td> <td align="right" style="padding: 5px;">17%</td> </tr> <tr> <td style="text-align: left; padding: 5px;">Which of you use a Mac?</td> <td align="right" style="padding: 5px;">45%</td> </tr> <tr> <td style="text-align: left; padding: 5px;">Which of you use Linux?</td> <td align="right" style="padding: 5px;">35%</td> </tr> <tr> <td style="text-align: left; padding: 5px;">Which of you use Windows?</td> <td align="right" style="padding: 5px;">50%</td> </tr> </tbody> </table> <p> It&rsquo;s been fun leading the organization of these first two years of this event, but this year will be my last. </p> Announcing Micronautics Research 2006-10-17T00:00:00-04:00 https://mslinn.github.io/blog/2006/10/17/micronautics-research-is-now-officially <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/assets/images/robotCircle400.svg" type="image/svg"> <!---<source srcset="/assets/images/robotCircle400.avif" type="image/avif">--> <source srcset="/assets/images/robotCircle400.webp" type="image/webp"> <source srcset="/assets/images/robotCircle400.apng" type="image/apng"> <source srcset="/assets/images/robotCircle400.png" type="image/png"> <source srcset="/assets/images/robotCircle400.jpg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.jpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.jfif" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.pjpeg" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.pjp" type="image/jpeg"> <source srcset="/assets/images/robotCircle400.gif" type="image/gif"> <source srcset="/assets/images/robotCircle400.tif" type="image/tiff"> <source srcset="/assets/images/robotCircle400.tiff" type="image/tiff"> <source srcset="/assets/images/robotCircle400.bmp" type="image/bmp"> <source srcset="/assets/images/robotCircle400.ico" type="image/x-icon"> <source srcset="/assets/images/robotCircle400.cur" type="image/x-icon"> <img alt='The EmpathyWorks Mascot' class="imgImg rounded shadow" src="/assets/images/robotCircle400.png" style='width: 100%; ' title='The EmpathyWorks Mascot' /> </picture> </div> Cool robot mascot, huh? I&rsquo;m going to use it for EmpathyWorks&trade;, which at this point is still just a twinkle in my eye. A physical metaphor for IT innovation 2006-06-23T00:00:00-04:00 https://mslinn.github.io/blog/2006/06/23/a-physical-metaphor-for-it-innovation <p>I got an email today from a friend, an innovative engineer:</p > <div class='liImg rounded shadow' style="padding: 1em"> <p>Hi Mike, <p>The other day, I had a reasonably good idea for a startup. And I ran it by a friend, who said "Why would the enterprise buy that?"</p> <p>I replied "Much better than existing solutions."</p> <p>He said "Unless it&rsquo;s a 10x, nope. And, while it is better, it&rsquo;s not a 10x." </p> <p>I thought "That&rsquo;s gotta be wrong."</p> <p>But then it occurred to me. In spite of having worked as a consultant to enterprises, and in spite of having run professional services for an enterprise software company, I still don&rsquo;t have a very good model of enterprise software adoption. To wit:</p> <ul> <li>What makes a new piece of software or IT infrastructure compelling for the enterprise?</li> <li>When do enterprises decide to upgrade or change infrastructure?</li> </ul> <p>Do you know of any references or models?</p> </div> <p>Here is what I told him:</p> <p> A 10x return on investment applies to investors (although 7x works too). If the sales pitch is predicated on cost reduction (optimizing operations), then 5-10x would apply and is most interesting when the client&rsquo;s business is relatively mature. </p> <p> Increasing client sales (optimizing business development) needs to be presented along the client&rsquo;s strategic direction, which might be a moving target. 2x or 3x return would suffice if the timing between a vendor&rsquo;s presentation and a client&rsquo;s view of their strategic direction aligned. This would be difficult for the vendor to guess without incredible intelligence on specific targets. </p> <p> CIOs are risk-averse, so solutions that do not require vetting new servers and client-side software are easier. When selling into a datacenter, a 10x return might not be enough if security issues are not mitigated. On the other hand, repurposing existing investments are much easier to sell: a 2x or 3x return might be sufficient. </p> <p> In other words: inertia is a dominant force of nature that also applies in IT. If you consider trends to be acceleration (positive or negative), then the net force driving the inertia is composed of external and internal factors. Those factors might be political, economic, technical, etc. Newton&rsquo;s <code>F = m * a</code> can be restated in entrepreneurial terms as: </p> <div style="padding-left: 3em; margin-bottom: 12pt;"> net force = investment times rate of adoption</div> <p>or, more to the point:</p> <div style="padding-left: 3em; margin-bottom: 12pt"> rate of adoption = net force / investment</div> <p> The investment (mass) a client has made in developing and deploying their intellectual property; training personnel; capital investment in infrastructure; and intimate knowledge of the current system. ("The devil you know".) </p> <p> For entrepreneurs that want to convince clients that a new technology that is worth adopting, continuing our physics metaphor may be illuminating. Let&rsquo;s equate profitability with speed, and technology platform with direction. Thus velocity is the cost-effectiveness of a given technical platform. </p> <p> It follows that in order to change a company&rsquo;s direction, and increase their profitability (speed), the client will need to experience a strong enough net force over a long enough period of time to overcome inertia and justify the risk and the investment. </p> <p> In addition, the prospect will only understand new information in their current context, which might not use the same dictionary that an entrepreneur uses, since they tend to live in the future. </p> Success! Silicon Valley Ruby Conference 2006-04-24T00:00:00-04:00 https://mslinn.github.io/blog/2006/04/24/success-silicon-valley-ruby-conference <p> After months of planning, the big weekend arrived and a great time was had by all. Thank you, presenters! </p> <p>I organized the event and chaired the second day of the conference.</p> <div class='imgWrapper imgFlex inline' style=' '> <figure> <picture class='imgPicture'> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.svg" type="image/svg"> <!---<source srcset="/images/mike/mikeSlinnRubyConf_690x524.avif" type="image/avif">--> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.webp" type="image/webp"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.apng" type="image/apng"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.png" type="image/png"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.jpg" type="image/jpeg"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.jpeg" type="image/jpeg"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.jfif" type="image/jpeg"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.pjpeg" type="image/jpeg"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.pjp" type="image/jpeg"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.gif" type="image/gif"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.tif" type="image/tiff"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.tiff" type="image/tiff"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.bmp" type="image/bmp"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.ico" type="image/x-icon"> <source srcset="/images/mike/mikeSlinnRubyConf_690x524.cur" type="image/x-icon"> <img alt='Yours truly, addressing the 2006 Silicon Valley Ruby Conference' class="imgImg rounded shadow" src="/images/mike/mikeSlinnRubyConf_690x524.png" style='width: 100%; ' title='Yours truly, addressing the 2006 Silicon Valley Ruby Conference' /> </picture> <figcaption class='imgFigCaption '> Yours truly, addressing the 2006 Silicon Valley Ruby Conference </figcaption> </figure> </div> <p>See you next year...</p> Silicon Valley Ruby Conference 2006-02-21T00:00:00-05:00 https://mslinn.github.io/blog/2006/02/21/silicon-valley-ruby-conference <p> SDForum just announced the <a href='https://archive.upcoming.org/event/silicon-valley-ruby-conference-59161' target='_blank' rel="nofollow">Silicon Valley Ruby Conference</a>. I am one of the two co-chairmen. </p> <p> We expect the event to sell out very quickly. Some great speakers have been lined up, it's quite inexpensive and only 210 people can attend. </p> <p>See you there!</p> <div class='imgWrapper imgFlex inline' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.svg" type="image/svg"> <!---<source srcset="/blog/images/svRuby/sdforum_ruby_690x565.avif" type="image/avif">--> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.webp" type="image/webp"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.apng" type="image/apng"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.png" type="image/png"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.jpg" type="image/jpeg"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.jpeg" type="image/jpeg"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.jfif" type="image/jpeg"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.pjpeg" type="image/jpeg"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.pjp" type="image/jpeg"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.gif" type="image/gif"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.tif" type="image/tiff"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.tiff" type="image/tiff"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.bmp" type="image/bmp"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.ico" type="image/x-icon"> <source srcset="/blog/images/svRuby/sdforum_ruby_690x565.cur" type="image/x-icon"> <img class="imgImg rounded shadow" src="/blog/images/svRuby/sdforum_ruby_690x565.png" style='width: 100%; ' /> </picture> </div> <p class="rounded shadow formalNotice"> Update: Andrew Burke wrote up some good notes for <a href='http://www.andrewburke.me/blogposts/15' target='_blank' rel="nofollow">day 1</a> and <a href='http://www.andrewburke.me/blogposts/16' target='_blank' rel="nofollow">day 2</a> of the conference. </p> Instantiating Java Inner Classes 2005-12-30T00:00:00-05:00 https://mslinn.github.io/blog/2005/12/30/instantiating-java-inner-classes <p> I've been writing Java code for years. Today I learned something new. I have been adding a new feature to Zamples that was best expressed as a doubly nested inner class. The hierarchy looks like this: </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb1951e92064f'><button class='copyBtn' data-clipboard-target='#idb1951e92064f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>public class CodeRange { public CodeRange(String str) { /* ... */ }<br /> public class RangeSpec { public RangeSpec(int i, int j) { /* ... */ } public RangeSpec(RangeItem start, RangeItem end) { /* ... */ }<br /> public class RangeItem { public RangeItem(String str, int i) { /* ... */ } } } }</pre> </div> <p>When it came time to write JUnit tests, I needed to instantiate the hierarchy. The syntax surprised me:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9c38ea027193'><button class='copyBtn' data-clipboard-target='#id9c38ea027193' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>CodeRange.RangeSpec.RangeItem range = new CodeRange("test").new RangeSpec(1, 1).new RangeItem("middle", 3);</pre> </div> <p>Not exactly intuitive, eh? It gets even more interesting when trying to write unit tests for the second RangeSpec constructor, the one that accepts two <code>RangeItems</code>. I found I had to create a protected no-args constructor for <code>RangeSpec</code> with an empty body, plus a method within <code>RangeSpec</code> to create <code>RangeItems</code> on demand:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id03ab62c833c3'><button class='copyBtn' data-clipboard-target='#id03ab62c833c3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>/** For JUnit only */ protected RangeSpec() {}<br /> /** For JUnit only */ protected RangeItem newRangeItem(String searchString, int offset) { return new RangeItem(searchString, offset); }</pre> </div> <p>Now I could write my test case setup code:</p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99b6dd18380b'><button class='copyBtn' data-clipboard-target='#id99b6dd18380b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>codeRange = new CodeRange(code, ""); CodeRange.RangeSpec.RangeItem rangeItemStart = codeRange.new RangeSpec().newRangeItem("middle", 3); CodeRange.RangeSpec.RangeItem rangeItemEnd = codeRange.new RangeSpec().newRangeItem("back", 0);<br /> CodeRange.RangeSpec rangeSpec = codeRange.new RangeSpec(rangeItemStart, rangeItemEnd); /* ... */</pre> </div> <p> Everything I read about doubly nested classes amounted to a warning to the effect that they should be avoided. I never had occasion to need this type of solution before, however the particular problem I am solving yields very nicely to this approach. It is simple, elegant and efficient. Don't believe everything you read (except this blog!) &#128521;. </p> Zamplized Ruby User’s Guide 2005-12-16T00:00:00-05:00 https://mslinn.github.io/blog/2005/12/16/zamplized-ruby-users-guide <p> Santa's elves have been cheering us on at Zamples! Just in time for the holidays, Zamples v2.6 is now live. </p> <p> We've &ldquo;Zamplized&rdquo; the Ruby User&rsquo;s Guide and made it available for free. Now you can curl up with a glass of eggnog and sit by the fire with your laptop, while learning Ruby, the hottest new programming language today. </p> <p> Zamples&rsquo; live code examples let you &ldquo;learn by doing&rdquo; and give you a hands-on and foolproof experience. There is no faster, easier way to gain experience with a language or programming interface than by interacting with a Zamplized document. </p> <p> Best of the season! </p> Programmatic IM Generation 2005-12-16T00:00:00-05:00 https://mslinn.github.io/blog/2005/12/16/programmatic-im-generation <p> I thought it would be fun to create a live code example using Zamples that would generate an IM (instant message). </p> <p> The following Java code example uses Jive Software&rsquo;s Smack library to send a message to a Jabber user with a Gaim client. You will need to put real values in for <tt>from</tt>, <tt>password</tt> and <tt>to</tt> before a message will be sent. BTW, if the userid and password are wrong, you&rsquo;ll get an odd error message. Since the program fragment automatically compiles and runs after you click on the <b>Try It!</b> button, the error message will appear on the right. </p> <p> Once you provide the <tt>to</tt> and <tt>from</tt> user ids and <tt>password</tt> and rerun the code fragment, a message will be sent. I tested this by sending myself a message and found that <tt>from</tt> and <tt>to</tt> were the same. </p> <table> <tbody> <tr> <td> <form action="" class="zform" method="post" target="TomcatMain"> <input name="cmd" type="hidden" value="XMPPConnection connection = new XMPPConnection(&quot;jabber.org&quot;); connection.login(&quot;from&quot;, &quot;password&quot;); connection.createChat(&quot;to@jabber.org/Gaim&quot;).sendMessage(&quot;Howdy!&quot;);"> <input name="imports" type="hidden" value="import org.jivesoftware.smack.*;"> <input name="applet" type="hidden" value=""> <input name="format" type="hidden" value="2"> <input name="pre" type="hidden" value="false"> <input name="/stylesheet" type="hidden" value="StyleSheet.css"> <input class="zform" type="submit" value="Try It!"></form> </td> <td> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb2404dea3cb4'><button class='copyBtn' data-clipboard-target='#idb2404dea3cb4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>XMPPConnection connection = new XMPPConnection("jabber.org"); connection.login("from", "password"); connection.createChat("to@jabber.org/Gaim").sendMessage("Howdy!");</pre> </div> </td> </tr> </tbody> </table> Zamples REST Interface 2005-12-15T00:00:00-05:00 https://mslinn.github.io/blog/2005/12/15/zamples-rest-interface <p> Wouldn&rsquo;t it be great to be able to build in a facility for live code examples into your favorite software tools? We thought so, and are proud to announce the Zamples REST Interface. Now you can use any programming language you choose to embed Zamples&rsquo; functionality as an IDE plugin or online learning environment. </p> “I Want to Build a Girl” 2005-07-28T00:00:00-04:00 https://mslinn.github.io/blog/2005/07/28/i-want-to-build-girl <!-- #region intro --> <p> I said that as an undergraduate engineering student in 1975. Now a Japanese researcher has made <a href='https://news.bbc.co.uk/1/hi/sci/tech/4714135.stm' target='_blank' rel="nofollow">real progress</a> towards a synthetic companion. The artificial skin doesn't seem to have a high density sensory grid that other researchers are experimenting with (<a href='https://news.bbc.co.uk/1/hi/sci/tech/4154366.stm' target='_blank' rel="nofollow">future artificial skins</a> might incorporate sensors not only for pressure and temperature, but also for light, humidity, strain or sound), but I&rsquo;m sure it won't be long before a similar android with tactile response and <a href='http://www.ai.mit.edu/projects/sociable/videos.html' target='_blank' rel="nofollow">expression ability</a> graduates from the lab. </p> <p> I still want to be involved in bringing that girl... and a boy... and a whole range of socially acceptable companions to market. Some day soon... </p> <p class="rounded shadow formalNotice"> Update: <a href='https://en.wikipedia.org/wiki/QRIO' target='_blank' rel="nofollow">Sony&rsquo;s Qrio</a>, a humanoid robot under development is exactly along the lines I&rsquo;ve been dreaming of! </p> <div class='imgWrapper imgFlex center halfsize' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.svg" type="image/svg"> <!---<source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.avif" type="image/avif">--> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.webp" type="image/webp"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.apng" type="image/apng"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.png" type="image/png"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.jpg" type="image/jpeg"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.jpeg" type="image/jpeg"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.jfif" type="image/jpeg"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.pjpeg" type="image/jpeg"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.pjp" type="image/jpeg"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.gif" type="image/gif"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.tif" type="image/tiff"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.tiff" type="image/tiff"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.bmp" type="image/bmp"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.ico" type="image/x-icon"> <source srcset="/blog/images/buildAGirl/sonyQrioRobot_225x300.cur" type="image/x-icon"> <img alt='Sony Qrio robot' class="imgImg rounded shadow" src="/blog/images/buildAGirl/sonyQrioRobot_225x300.png" style='width: 100%; ' title='Sony Qrio robot' /> </picture> </div> <p class="rounded shadow formalNotice"> Update: from PC Magazine, &ldquo;<a href='https://www.wearable.technology/' target='_blank' rel="nofollow">Eleksen</a>, a small UK-based firm is introducing electronic fabric, essentially carbon-embedded nylon sandwiched between layers of nylon mesh that, when a milliamps charge is passed through it, can recognize touch, pressure and even the direction and path of a stroke. This thin, flexible, and washable fabric connects to a small 8-bit processor, which then can be connected to a standard electronic device like an iPod. Eleksen company executives said the <a href='https://www.sciencedirect.com/topics/engineering/smart-fabric' target='_blank' rel="nofollow">washable fabric</a> can also withstand extreme pressure; they&rsquo;ve rolled a car over it without any ill effects.&rdquo; Seems we are moving towards artificial skin! </p> <p class="rounded shadow formalNotice"> Update: from The Economist, <a href='https://economist.com/world/asia/displaystory.cfm?story_id=5323427&amp;no_na_tran=1' target='_blank' rel="nofollow">Japan&rsquo;s humanoid robots &ndash; Better than people</a>:<br /> &ldquo;What seems to set Japan apart from other countries is that few Japanese are all that worried about the effects that hordes of robots might have on its citizens. Nobody seems prepared to ask awkward questions about how it might turn out. If this bold social experiment produces lots of isolated people, there will of course be an outlet for their loneliness: they can confide in their robot pets and partners. Only in Japan could this be thought less risky than having a compassionate Filipina drop by for a chat.&rdquo; </p> <!-- endregion --> Self-Fullfilling Prophecy 2005-05-15T00:00:00-04:00 https://mslinn.github.io/blog/2005/05/15/self-fullfilling-prophecy <h2 id="heisenberg">Heisenberg&rsquo;s Uncertainty Principle</h2> <p>The Greek philosopher Democritus believed that the natural world was immutable. In 1927 <a href='https://www.aip.org/history/heisenberg/p01.htm' target='_blank' rel="nofollow">Werner Heisenberg</a> published his &ldquo;Uncertainty Principle&rdquo; which implied, amongst other things, that observing a quantum phenomenon also significantly altered the phenomenon. For many people, such an idea was counter-intuitive. </p> <p> What is true in the physical realm is even more true in the realms of interpersonal relationships and business. Our beliefs and belief systems affect outcomes more than we perhaps realize. Consider the diagram below. </p> <div class='imgWrapper imgFlex center' style='width: 60%; '> <picture class='imgPicture'> <source srcset="/blog/images/prophecy/prophecy1.svg" type="image/svg"> <!---<source srcset="/blog/images/prophecy/prophecy1.avif" type="image/avif">--> <source srcset="/blog/images/prophecy/prophecy1.webp" type="image/webp"> <source srcset="/blog/images/prophecy/prophecy1.apng" type="image/apng"> <source srcset="/blog/images/prophecy/prophecy1.png" type="image/png"> <source srcset="/blog/images/prophecy/prophecy1.jpg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy1.jpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy1.jfif" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy1.pjpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy1.pjp" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy1.gif" type="image/gif"> <source srcset="/blog/images/prophecy/prophecy1.tif" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy1.tiff" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy1.bmp" type="image/bmp"> <source srcset="/blog/images/prophecy/prophecy1.ico" type="image/x-icon"> <source srcset="/blog/images/prophecy/prophecy1.cur" type="image/x-icon"> <img alt='First prophecy' class="imgImg rounded shadow" src="/blog/images/prophecy/prophecy1.png" style='width: 100%; ' title='First prophecy' /> </picture> </div> <h2 id="trends">Describing the Trend</h2> <p> Without knowing what the diagram represents, can you postulate the trend for the next cycle or two? The answer is, of course, no. You need to have an internal model of the behavior of the system and the entity&rsquo;s behavior within that system represented in the graph before you can make a prediction. Without any additional information, you might anticipate the trend like this: </p> <div class='imgWrapper imgFlex center' style='width: 60%; '> <picture class='imgPicture'> <source srcset="/blog/images/prophecy/prophecy2.svg" type="image/svg"> <!---<source srcset="/blog/images/prophecy/prophecy2.avif" type="image/avif">--> <source srcset="/blog/images/prophecy/prophecy2.webp" type="image/webp"> <source srcset="/blog/images/prophecy/prophecy2.apng" type="image/apng"> <source srcset="/blog/images/prophecy/prophecy2.png" type="image/png"> <source srcset="/blog/images/prophecy/prophecy2.jpg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy2.jpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy2.jfif" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy2.pjpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy2.pjp" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy2.gif" type="image/gif"> <source srcset="/blog/images/prophecy/prophecy2.tif" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy2.tiff" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy2.bmp" type="image/bmp"> <source srcset="/blog/images/prophecy/prophecy2.ico" type="image/x-icon"> <source srcset="/blog/images/prophecy/prophecy2.cur" type="image/x-icon"> <img alt='Second prophecy' class="imgImg rounded shadow" src="/blog/images/prophecy/prophecy2.png" style='width: 100%; ' title='Second prophecy' /> </picture> </div> <p> Your model is based on incomplete information, and is necessarily a simplification of the actual system. If you are told that the graph depicts rainfall, and if you believe in a rain god, then your natural reaction might be to find a virgin to sacrifice. If you instead had the belief that rainfall had diminished due to clear-cutting of a vast forest which no longer acted as a natural storage mechanism for rainwater, your reaction might be to start lobbying for an intensive tree planting effort. Both belief systems have at their core a central belief that action can affect the outcome to some degree, so your prediction as to how much rain will fall in subsequent years will rely on the vigor with which countermeasures are applied, and your belief in their efficacy. Probably your belief would be that rainfall levels will tend to vary around historical norms, like this: </p> <div class='imgWrapper imgFlex center' style='width: 60%; '> <picture class='imgPicture'> <source srcset="/blog/images/prophecy/prophecy3.svg" type="image/svg"> <!---<source srcset="/blog/images/prophecy/prophecy3.avif" type="image/avif">--> <source srcset="/blog/images/prophecy/prophecy3.webp" type="image/webp"> <source srcset="/blog/images/prophecy/prophecy3.apng" type="image/apng"> <source srcset="/blog/images/prophecy/prophecy3.png" type="image/png"> <source srcset="/blog/images/prophecy/prophecy3.jpg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy3.jpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy3.jfif" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy3.pjpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy3.pjp" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy3.gif" type="image/gif"> <source srcset="/blog/images/prophecy/prophecy3.tif" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy3.tiff" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy3.bmp" type="image/bmp"> <source srcset="/blog/images/prophecy/prophecy3.ico" type="image/x-icon"> <source srcset="/blog/images/prophecy/prophecy3.cur" type="image/x-icon"> <img alt='Third prophecy' class="imgImg rounded shadow" src="/blog/images/prophecy/prophecy3.png" style='width: 100%; ' title='Third prophecy' /> </picture> </div> <p> What if you were told instead that the first graph represented sales of a hybrid car model, and that &lsquo;experts&rsquo; believed that increasing SUV sales and the economy were believed to be responsible for declining sales? A marketing campaign that addressed the rising cost of gas and promoted decreasing the reliance of the US economy on foreign oil as being truly patriotic might gain traction. One might attribute the initial sales spike as being due to purchases by early adopters. The tapering off of sales might be the result of tax incentives that artificially promoted the sales of vehicles over 6,000 pounds. </p> <p> It is clear that China&rsquo;s consumption of gas will soon outpace even the US&rsquo;s rate of consumption, so gas prices will continue to rise, and remain at higher prices than any we have experienced to date. The US Department of Energy produced the following <a href='https://www.eia.doe.gov/emeu/cabs/china.html' target='_blank' rel="nofollow">projection</a> of Chinese gas consumption in June 2004. </p> <p> In 2003, BP showed <a href='http://www.gasandoil.com/goc/features/fex32825.htm' target='_blank' rel="nofollow">China&rsquo;s new influence on global oil supply</a>: &ldquo;Global oil demand, meanwhile, was broadly flat, increasing 290,000 bpd to 75.7 mm bpd from 75.5 mm bpd. All the increase is attributable to China where oil consumption increased 5.8 % or 332,000 bpd.&rdquo; As demand for a commodity increases, prices increase. As China continues to dramatically increase its demand for oil, prices can only continue higher. In a <a href='http://www.gasandoil.com/goc/features/fex51961.htm' target='_blank' rel="nofollow">feature article</a> on <a href='http://www.gasandoil.com/goc/features/' target='_blank' rel="nofollow">Alexander&rsquo;s Gas &amp; Oil Connections</a>, John Vidal writes &ldquo;China's oil consumption, which accounted for a third of extra global demand last year, grew 17% and is expected to double over 15 years to more than 10 mm bpd &ndash; half the US's present demand. India's consumption is expected to rise by nearly 30% in the next five years.&rdquo; Jim Meyer, an expert at London's Oil Depletion Analysis Centre was quoted in <a href='http://www.gasandoil.com/goc/features/fex51962.htm' target='_blank' rel="nofollow">an article last month</a> as saying &ldquo;After 2007, we can&rsquo;t see enough new supplies to meet almost any reasonable level of demand growth.&rdquo; Expect gas prices to permanently exceed $50/barrel, and $100+/barrel will be reached with startling speed. With that in mind, a long term sales trend for hybrid cars in the US might be projected to look like this: </p> <div class='imgWrapper imgFlex center' style='width: 60%; '> <picture class='imgPicture'> <source srcset="/blog/images/prophecy/prophecy4.svg" type="image/svg"> <!---<source srcset="/blog/images/prophecy/prophecy4.avif" type="image/avif">--> <source srcset="/blog/images/prophecy/prophecy4.webp" type="image/webp"> <source srcset="/blog/images/prophecy/prophecy4.apng" type="image/apng"> <source srcset="/blog/images/prophecy/prophecy4.png" type="image/png"> <source srcset="/blog/images/prophecy/prophecy4.jpg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy4.jpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy4.jfif" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy4.pjpeg" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy4.pjp" type="image/jpeg"> <source srcset="/blog/images/prophecy/prophecy4.gif" type="image/gif"> <source srcset="/blog/images/prophecy/prophecy4.tif" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy4.tiff" type="image/tiff"> <source srcset="/blog/images/prophecy/prophecy4.bmp" type="image/bmp"> <source srcset="/blog/images/prophecy/prophecy4.ico" type="image/x-icon"> <source srcset="/blog/images/prophecy/prophecy4.cur" type="image/x-icon"> <img alt='Forth prophecy' class="imgImg rounded shadow" src="/blog/images/prophecy/prophecy4.png" style='width: 100%; ' title='Forth prophecy' /> </picture> </div> <p> This projection assumes that historical sales figures are not a significant factor when gauging future sales of this hybrid car model, and that factors on the market that the product sells into have not yet been fully felt or internalized by consumers. </p> <h2 id="belief">Self-Fullfilling Prophecy</h2> <p> What if a hard-nosed automotive executive, looking at ROI on a quarter-by-quarter basis decided that the return was not immediate enough, and terminated the hybrid car based on sales history to date? That automotive manufacturer would soon miss the world&rsquo;s largest market, China, as the demand for fuel-efficient cars will soon be supported by a Chinese middle class newly able to afford more expensive hybrid cars. Unless the manufacturer was somehow able to introduce another technology, such as hydrogen powered vehicles, and successfully put together a national infrastructure network to support hydrogen fuel stations, they will have traded off short-term profitability for long-term viability. </p> <p> Returning to the original idea behind this articleing, one&rsquo;s expectations and beliefs do color what one might think to be reasonable. If you believe that most people are kind and generous, your behavior will probably reinforce this belief &ndash; with the result that even when you meet Scrooge incarnate, he might crack a smile despite himself. If you think that sales of product XYZ are going down, down, down and that nothing can be done about it then that product is as good as dead. If instead, you come up with an alternate world view, communicate higher expectations and provide sufficient resources over the appropriate time period, that product&rsquo;s best years may be yet to come. </p> Marshall Brain is a very smart man 2005-04-03T00:00:00-05:00 https://mslinn.github.io/blog/2005/04/03/marshall-brain-is-very-smart-man <p> Marshall Brain founded <a href='https://howstuffworks.com/' target='_blank' rel="nofollow">How Stuff Works</a>, and wrote many interesting and informative articles for the site. </p> <p> We share an interest in personal robotics. Like many US citizens, Mr. Brain's focus in Robotic Nation is on the potentially negative aspects of a integrating robots into society, such as mass unemployment. His writing reminds me of the endless fear-mongering that CNN's Lou Dobbs serves up daily on the evils of outsourcing and illegal immigration. I have a more optimistic point of view on personal robotics. </p> <p> The Japanese attitude for robotics is quite positive, and their enthusiasm for personal robots resembles their enthusiasm for advanced cell phones. iMode mobile phone systems have features beyond what is known in North America, and Japanese society has evolved to incorporate behavioral modes that depend on the advanced features of the iMode phone network. Beyond mobile phones, the Japanese culture celebrates electronic gadgetry, and they already accept robots, including personal robots, into their daily experience. </p> <p> Change is coming, and we can either embrace the future or bemoan the loss of the past. I think personal robotics will become a plaything of the rich in 2015; Mr. Brain predicts the mass market will explode in 2030. It's not like we didn't have any warning. </p> <p>I think I need to start learning Japanese.</p> Top 12 Klingon Programmer Sayings 2005-04-02T00:00:00-05:00 https://mslinn.github.io/blog/2005/04/02/top-12-klingon-programmer-sayings <style> ol { list-style-type:none; counter-reset:item 13; } ol > li { counter-increment:item -1; } ol > li:before { content:counter(item) ". "; } </style> <ol> <li>Specifications are for the weak and timid!</li> <li>This machine is a piece of GAGH! I need dual Opteron processors if I am to do battle with this code!</li> <li>You cannot really appreciate Dilbert unless you've read it in the original Klingon.</li> <li>Indentation?! &ndash; I will show you how to indent when I indent your skull!</li> <li>What is this talk of &lsquo;release?&rsquo; Klingons do not make software &lsquo;releases.&rsquo; Our software &lsquo;escapes,&rsquo; leaving a bloody trail of designers and quality assurance people in its wake.</li> <li>Klingon function calls do not have &lsquo;parameters&rsquo; &ndash; they have &lsquo;arguments&rsquo; &ndash; and they ALWAYS WIN THEM.</li> <li>Debugging? Klingons do not debug. Our software does not coddle the weak.</li> <li>I have challenged the entire quality assurance team to a Bat-Leth contest. They will not concern us again.</li> <li>A TRUE Klingon Warrior does not comment his code!</li> <li>By filing this PTR you have challenged the honor of my family. Prepare to die!</li> <li>You question the worthiness of my code? I should kill you where you stand!</li> <li>Our users will know fear and cower before our software! Ship it! Ship it and let them flee like the dogs they are!</li> </ol> <p> I lifted this from Monsters Island, now defunct. </p> Zample in a article 2005-01-05T00:00:00-05:00 https://mslinn.github.io/blog/2005/01/05/zample-in-blog <p> Erik Dasque, Product Manager for <a href='https://mono-project.com/' target='_blank' rel="nofollow">Mono</a> at <a href='https://www.novell.com/linux/ximian.html' target='_blank' rel="nofollow">Ximian</a> (now part of Novell) was the first person to include a Zample in a article. Way to go, Erik! May there be many more bloggers using Zamples to display live code in their blogs. </p> <p> I have had some trouble doing this &ndash; when I used SnipSnap it simply wouldn't accept raw HTML. I wrote a SnipSnap plugin to allow raw HTML, but then my SnipSnap installation b0rked and I gave up on it. WordPress really mangles the HTML for a Zamples button; here is my best effort (can't get the 'Try It!' button to remain vertically centered.) </p> <table> <tbody> <tr style="vertical-align: middle;" valign="middle"> <td><form action="" class="formInput" method="post" target="TomcatMain"> <input name="cmd" type="hidden" value="// Define a runnable C# class here. You must include a // &quot;main&quot; method. Command line arguments go in the text area above. public class Test { public static void Main() { System.Console.WriteLine(&quot;I love Zamples!&quot;); } }"> <input name="imports" type="hidden" value="# Command line arguments go here"> <input name="applet" type="hidden" value=""> <input name="format" type="hidden" value="4608"> <input name="pre" type="hidden" value="true"> <input name="stylesheet" type="hidden" value="/assets/css/style.css"> <input class="formInput/" type="submit" value="Try It!"></form> </td> </tr> </tbody> </table> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id062bb348f7e1'><button class='copyBtn' data-clipboard-target='#id062bb348f7e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>public class Test { public static void Main() { System.Console.WriteLine("I love Zamples!"); } }</pre> </div> <p>If you would like to include a live code example in your blog, first create it using the Zamplizer (easiest if you click on one of the selections under the Create Sample left-hand menu on the Zamples Solutions page &ndash; that will set up the Zamplizer with a short working program in the language you want, which you can then modify.)</p> <div class='imgWrapper imgFlex center' style=' '> <picture class='imgPicture'> <source srcset="/blog/images/zamples.svg" type="image/svg"> <!---<source srcset="/blog/images/zamples.avif" type="image/avif">--> <source srcset="/blog/images/zamples.webp" type="image/webp"> <source srcset="/blog/images/zamples.apng" type="image/apng"> <source srcset="/blog/images/zamples.png" type="image/png"> <source srcset="/blog/images/zamples.jpg" type="image/jpeg"> <source srcset="/blog/images/zamples.jpeg" type="image/jpeg"> <source srcset="/blog/images/zamples.jfif" type="image/jpeg"> <source srcset="/blog/images/zamples.pjpeg" type="image/jpeg"> <source srcset="/blog/images/zamples.pjp" type="image/jpeg"> <source srcset="/blog/images/zamples.gif" type="image/gif"> <source srcset="/blog/images/zamples.tif" type="image/tiff"> <source srcset="/blog/images/zamples.tiff" type="image/tiff"> <source srcset="/blog/images/zamples.bmp" type="image/bmp"> <source srcset="/blog/images/zamples.ico" type="image/x-icon"> <source srcset="/blog/images/zamples.cur" type="image/x-icon"> <img alt='Zamples logo' class="imgImg rounded shadow" src="/blog/images/zamples.png" style='width: 100%; ' title='Zamples logo' /> </picture> </div> Another Software Expert Assignment 2004-12-24T00:00:00-05:00 https://mslinn.github.io/blog/2004/12/24/another-software-litigation-assignment <p> I&lsquo;ve just picked up another software litigation case, this time doing research for a software vendor prior to going to court. My client is concerned that two former employees used their code in a product sold by a new company that the former employees started. </p> <p> I've worked on this type of case before. They are often settled out of court. It is interesting to see how human interactions affect technology and business decisions. </p> <p>I know what I'll be doing over the Christmas/New Year's holidays!</p> Nabu cast a long shadow 2004-12-18T00:00:00-05:00 https://mslinn.github.io/blog/2004/12/18/nabu-cast-long-shadow <p> Twenty years ago an Ottawa-based startup was formed by the merger of nine Canadian companies. Nabu sold a cable set-top box with a computer that booted off the cable for under $1000. I was one of the early employees. </p> <p> Nabu was first in many ways, but it never became the commercial success that the founders had hoped. Many years have since passed, and I forgot about the experience. In the last couple of years I have been contacted by various people regarding Nabu&rsquo;s products and services, mostly former customers. </p> <div class="quote"> Nabu was the first computer that I had when I was a kid. I remember playing games like Mummy&rsquo;s Tomb, Kiddy Park etc. Talk about being ahead of it's time! ... <br /><br /> I think maybe people that were influenced by NABU when they were young are probably at the point in their life where they can stop and take a look around... <br /><br /> Anyway, I thought I would drop you a message and say that this Ottawa boy (who now lives in Seattle, WA) still remembers the old Nabu days. <br /><br /> I&rsquo;m now a professional video game developer, which I&rsquo;m sure was influenced by playing the Nabu so long ago. Of course today&rsquo;s machines have a little more power! What you guys did was a pretty amazing accomplishment IMHO. </div> <div class="quote"> As someone who was fortunate enough to have access to the NABU network in my after school program at Regina Public School in Ottawa... I really enjoyed it as a kid. </div> <p> Recently I was contacted by a legal firm that has retained me to assist them in a patent litigation case. They are going to cite some of Nabu&rsquo;s products as prior art. I&rsquo;m having fun going back and reconnecting with people that I knew from that bygone era, who I lost track of. So far no-one that I knew has died of old age, which pleases me. </p> Live Code Examples for JDK 6 2004-12-09T00:00:00-05:00 https://mslinn.github.io/blog/2004/12/09/live-code-examples-for-j2se-6-mustang <p> No sooner did Sun push J2SE 5 (also known as JDK 1.5) out the door, then talk of the next version began. Java releases are generally 18 months apart, but for the first time Sun is letting the world see development builds as they are created. </p> <p> We at Zamples took advantage of this new openness and proudly bring you Live Code Examples for J2SE 6. Let us know if you discover any cool new features! </p> All Global 2000 Businesses Are Software Developers 2004-11-04T00:00:00-05:00 https://mslinn.github.io/blog/2004/11/04/web-services-means-all-global-2000 <p> With the advent of web services, any company that wishes to expose a programmable interface to their business processes also inherits the issues faced by software vendors. Connecting up a supply chain isn't quite as simple as one might want it to be. </p> <p> Software vendors have an urgent need for their channel partners and customers to adopt their technology quickly. The faster their products are understood, the more money they make. Shorter adoption times drive top line revenue. </p> <p> Similar dynamics affect companies that use web services to transact business. Businesses with custom interfaces need to somehow show system integrators how to program to their web services. Executives need to be able to wire their enterprises together quickly and easily in order to take advantage of business opportunities. Web services are supposed to make this easy... but nothing is ever quite as easy as it is supposed to be. </p> <p> The answer for both software vendors and businesses with custom web services interfaces is a comprehensive developer relations program. Not surprisingly, live online code examples are an important aspect of such a program. I have begun writing a follow-on to my article entitled <a href='https://productmanagement.buzz/index.php/2004/11/10/top_10_issues_f/' target='_blank' rel="nofollow">Top Ten Issues for Developer Relations Managers</a>. I would be happy to hear from you if you would like to review a draft before it is published. </p> Reading Java Properties Files from Bash 2004-11-03T00:00:00-05:00 https://mslinn.github.io/blog/2004/11/03/reading-java-properties-files-from-bash <p> Ever notice that Java properties files are fairly similar to bash scripts? Here is a short bash script that takes a valid Java properties file called <code>language.properties</code>, massages it into a format that is compatible with bash syntax, writes it to a temporary file and sources it. </p> <div class="jekyll_pre" > <div class='codeLabel unselectable' data-lt-active='false'>Shell</div> <pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id266fd939464e'><button class='copyBtn' data-clipboard-target='#id266fd939464e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash TEMPFILE=$(mktemp) cat language.properties | \ sed -re 's/"/"/'g | \ sed -re 's/=(.*)/="\1"/g' &gt; $TEMPFILE source $TEMPFILE rm $TEMPFILE</pre> </div> <p>After running this script, all of the properties in the Java properties file become available as environment variables.</p>