<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: wasteofserver</title>
    <description>The latest articles on DEV Community by wasteofserver (@wasteofserver).</description>
    <link>https://dev.to/wasteofserver</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1768741%2F52329734-ec82-4388-ab5e-1036b78202e1.png</url>
      <title>DEV Community: wasteofserver</title>
      <link>https://dev.to/wasteofserver</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wasteofserver"/>
    <language>en</language>
    <item>
      <title>Zoom M3 MicTrak file recovery</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Tue, 25 Feb 2025 04:18:31 +0000</pubDate>
      <link>https://dev.to/wasteofserver/zoom-m3-mictrak-file-recovery-4ba2</link>
      <guid>https://dev.to/wasteofserver/zoom-m3-mictrak-file-recovery-4ba2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/zoom-m3-mictrak-file-recovery/" rel="noopener noreferrer"&gt;https://wasteofserver.com/zoom-m3-mictrak-file-recovery/&lt;/a&gt;, you will find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrqqja2lh2id4j3n33co.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrqqja2lh2id4j3n33co.jpg" alt="Image description" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🎤 If you're in a hurry, head &lt;a href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover" rel="noopener noreferrer"&gt;directly to the Github repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While I'm fortunate enough to develop software for state-of-the-art companies, I've always been the "printer guy", a badge I wear with pride.&lt;/p&gt;

&lt;p&gt;And maybe that's the reason why, for more than a quarter of a century, people have turned up with storage medium to salvage. I never did it professionally, it’s just something I enjoy and, more often than not, I’m able to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The process
&lt;/h2&gt;

&lt;p&gt;My main approach is two-fold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;capture what you can from the dying medium&lt;/li&gt;
&lt;li&gt;try to recreate the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes, just by adjusting read tolerance, you're able to capture everything from a disk Windows or Mac reject, as the OS times out faster than the dying medium can respond. I've had a disk hooked to a box for 7 months; 100% recovery success!&lt;/p&gt;

&lt;p&gt;Sometimes you'll have chunks of data missing, but one of the File Allocation Table / Master File Table / Container superblock / root B-tree are still there and so most data is salvageable with both filenames and tree location.&lt;/p&gt;

&lt;p&gt;At the extreme, you’re left with nothing but some of the raw data. All you can do is carve it out - essentially reading every byte from the medium, identifying known headers like JPG (&lt;code&gt;FF D8&lt;/code&gt;) or MKV (&lt;code&gt;1A 45 DF A3&lt;/code&gt;), and proceed to capture all sequential data until the end of the file. If for any reason, the file is fragmented, carving will obviously fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The call from Pierre Zago
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Frankie, this hasn't happened before, I'm generally cautious... I don't know what I was thinking - I've accidentally formatted an SD card with audio for an entire session. Worse, I've saved some files onto it!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pierre is an incredibly talented comedian and an extraordinary actor. While his work is multifaceted, he became widely known for street sketches, some of them absolutely universal. Just check the one below where he simply says "excuse-me".&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/JC-yra6dtzw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This sketch’s simplicity and comedic charm create a lighthearted yet universally appealing work of art.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With that simple action, Pierre had joined the club. Everyone messes up. &lt;a href="https://screenrant.com/toy-story-2-movie-deleted-accident-recovered/" rel="noopener noreferrer"&gt;Even Pixar&lt;/a&gt;. I gladly took the card. Don't worry, I said, the overwritten contents are gone, but given that it's a microphone formatted card, even without FAT tables we should be able to carve out most of whatever you've recorded as data will probably be sequential.&lt;/p&gt;

&lt;p&gt;Little did I know, this would turn out to be one of the most interesting toy projects I’ve worked on in a while. The last time I had this much fun was when I had to rebuild a hardware RAID-0 that contained the masters for an album by '&lt;a href="https://www.youtube.com/watch?v=zw5l0x3QDnc" rel="noopener noreferrer"&gt;Os Azeitonas&lt;/a&gt;'.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Echo (echo, echo)...
&lt;/h2&gt;

&lt;p&gt;As expected, the only way to get data back was by carving it out of the image dump. There's a multitude of tools to choose from, &lt;a href="https://www.cgsecurity.org/wiki/photoRec" rel="noopener noreferrer"&gt;Photorec&lt;/a&gt; (open source), &lt;a href="https://www.ccleaner.com/recuva" rel="noopener noreferrer"&gt;Recuva&lt;/a&gt; (watch out for bundleware), &lt;a href="https://www.reclaime.com/" rel="noopener noreferrer"&gt;ReclaiMe&lt;/a&gt; (paid), etc... though I'm partial to &lt;a href="https://www.r-studio.com/data-recovery-software/" rel="noopener noreferrer"&gt;R-Studio&lt;/a&gt; (paid). Their results consistently outperform the competition.&lt;/p&gt;

&lt;p&gt;In this instance, nonetheless, things wouldn't be so simple. Every software I tried was able to extract the &lt;code&gt;wav&lt;/code&gt; files, but there was something rather wrong with the data as they all had this repetition, an echo of sorts.&lt;/p&gt;

&lt;p&gt;I'm a bit dull of hearing, so opened them in &lt;a href="https://www.audacityteam.org/" rel="noopener noreferrer"&gt;Audacity&lt;/a&gt; to check what was going on. You can clearly see a pattern here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F522bpo491o7s9zxlsgmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F522bpo491o7s9zxlsgmj.png" alt="Zoom M3 MicTrak file recovery" width="800" height="160"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;There's somewhat of a repetition every ~ 0.7 seconds&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While the chunks have a pretty similar waveform - there wasn't binary correlation. Besides, there was also something murky, every &lt;code&gt;wav&lt;/code&gt; file had what appeared to be 2 consecutive headers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwai0yf5g6t29yfop2i3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwai0yf5g6t29yfop2i3v.png" alt="Zoom M3 MicTrak file recovery" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mind you, at this point, my knowledge of &lt;code&gt;wav&lt;/code&gt; files was that the header is &lt;code&gt;52 49 46 46&lt;/code&gt;. Aside from using a mic, I didn't query Pierre on how he had actually recorded the data. However, when I saw "ZOOM M3" tag in the header, I called the authority on everything sound.&lt;/p&gt;
&lt;h2&gt;
  
  
  Perfect Pitch, a sort of wizardry
&lt;/h2&gt;

&lt;p&gt;Ed's been on speed dial for the better part of this lifetime. I'm lucky like that. Beyond being an unbelievable composer, just listen to the breathtaking &lt;a href="https://open.spotify.com/artist/3dFVFQhJ7koYGID3rnygYv?si=tjp6im06SrmKup4hISHIvw" rel="noopener noreferrer"&gt;Best Youth&lt;/a&gt;, he's also a pitch-perfect, living and breathing, encyclopedia on sound.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/ZGtOOPJKfYA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Zoom? Yes, yes. I have one. They record both wav and raw. Data is mangled? Ah. Sure. Recover? Twisted files? That's going to be an impossible task and, even if it's not, it'll be easier to just re-record.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And there he had me. It would definitely be easier to just re-record, even Pierre at one point suggested doing a voice-over, but where would the fun be in that?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn92lwie10z9lasmxjea1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn92lwie10z9lasmxjea1.jpg" alt="Zoom M3 MicTrak file recovery" width="800" height="286"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Zoom M3 MicTrak&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Magic Number
&lt;/h2&gt;

&lt;p&gt;I didn't even appreciate the concept of a &lt;code&gt;raw&lt;/code&gt; wave until Ed explained it. Now that I knew the mic saves two simultaneous files, everything fell into place.&lt;/p&gt;

&lt;p&gt;It's common for cameras to store both formats, but a microphone is a different beast. There's no way to determine upfront how long the user will record, which wouldn't be an issue if this were a single stream. As it's storing both tracks, there is quite a bit more play involved.&lt;/p&gt;

&lt;p&gt;You see, as soon as you hit record, the mic creates two files and continuously flushes the captured data from both onto the card. That, is done in chunks of data. One for the RAW, another for the WAV, repeat.&lt;/p&gt;

&lt;p&gt;Since we need to isolate those slices of data in order to untangle it, &lt;strong&gt;we must know their exact size&lt;/strong&gt;. And there lies &lt;strong&gt;our magic number&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;My first thought was that the chunks might be aligned with &lt;code&gt;exFAT&lt;/code&gt; allocation unit size. In this case, &lt;code&gt;128 KBytes&lt;/code&gt;. Let's test it.&lt;/p&gt;

&lt;p&gt;The header, clearly states that this is a stereo file (2 channels), recorded at 32 bits per channel, sampled 48k times per second. If you remember from the image above, the chunks repeat at approximately 0.7 seconds.&lt;/p&gt;

&lt;p&gt;Let's get a rough approximation on the chunk bytes so we know the ballpark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1 second of data &lt;span class="o"&gt;=&lt;/span&gt; 2 channels &lt;span class="k"&gt;*&lt;/span&gt; 32 bits &lt;span class="k"&gt;*&lt;/span&gt; 48000 samples
1 second of data &lt;span class="o"&gt;=&lt;/span&gt; 384000 bytes
0.7 seconds ~ 268800 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;We're looking into chunks of around 268 KBytes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And just like that, the idea that data may be chunked to exFAT AUS of 128 Kbytes is instantly disproved.&lt;/p&gt;

&lt;p&gt;The next obvious step would be to travel upwards on base 2. Given that 4096 is a good balance for buffers, let's evaluate from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;4096 &lt;span class="k"&gt;*&lt;/span&gt; 32 &lt;span class="o"&gt;=&lt;/span&gt; 131072 &lt;span class="o"&gt;(&lt;/span&gt;falls short by about 1/2&lt;span class="o"&gt;)&lt;/span&gt;
4096 &lt;span class="k"&gt;*&lt;/span&gt; 64 &lt;span class="o"&gt;=&lt;/span&gt; 262144 &lt;span class="o"&gt;(&lt;/span&gt;is &lt;span class="k"&gt;in &lt;/span&gt;the ballpark of what we&lt;span class="s1"&gt;'re expecting)

262144/384000 ~ 0.682 seconds of data
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.0682 seconds matched our estimate of 0.7 seconds so perfectly that I immediately knew &lt;strong&gt;262144&lt;/strong&gt; was the constant we were after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reconstruction
&lt;/h2&gt;

&lt;p&gt;Conceptually, the problem was solved. Now I just had to build the tool. For that it would be necessary to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Untangle the files&lt;/strong&gt; directly from the image dump. Since pieces recovered by other carving software would be half the expected size (the data chunk contains two files, so it’s actually double the size reported).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn how to create a RIFF header&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a RIFF header with a BEXT chunk&lt;/strong&gt; to make the recovered files compatible with the "M3 ZOOM Edit &amp;amp; Play" software.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⌨️ &lt;a href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover" rel="noopener noreferrer"&gt;&lt;strong&gt;All the code is on GitHub&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And I'm pretty sure you'll feel more at home there.&lt;/p&gt;

&lt;p&gt;However, for the sake of Google indexing, I’m leaving the methods here that create both the RIFF and BEXT headers, something I couldn’t find, which unfortunately made the process take longer than I’d like to admit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RiffFile&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Creates a RIFF header with BEXT and fmt chunks
     *
     * @param sampleRate the sample rate of the audio (8000Hz, 44100Hz, 48000Hz, etc) times per second the audio is sampled
     * @param bitsPerSample the bits per sample (8bits, 16bits, 32bits, etc)
     * @param channels the number of channels (1 mono, 2 stereo, etc)
     * @param audioDataSize the size of the audio data in bytes
     * @return the RIFF header
     * @throws IOException if an I/O error occurs
     */&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;createRiffHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleRate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;bitsPerSample&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;audioDataSize&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// calculate the byte rate, block align and file size&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;byteRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bitsPerSample&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;blockAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bitsPerSample&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// stream that will carry the new RIFF file&lt;/span&gt;
        &lt;span class="nc"&gt;ByteArrayOutputStream&lt;/span&gt; &lt;span class="n"&gt;byteArrayOutputStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ByteArrayOutputStream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;DataOutputStream&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataOutputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byteArrayOutputStream&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// riff header&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RIFF"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WAVE"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 9-12 Format always WAVE&lt;/span&gt;

        &lt;span class="c1"&gt;// bext chunk&lt;/span&gt;
        &lt;span class="n"&gt;writeBextChunk&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// fmt chunk&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fmt "&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 13-16 chunkID is "fmt " with trailing whitespace&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 17-20 size of this chunk, is 16 byts&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 21-22 (2 bytes) audioFormat (1 PCM integer, 3 IEEE 754 float)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 23-24 (2 bytes) numChannels (1 mono, 2 stereo, 4, etc)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sampleRate&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 25-28 (4 bytes) sampleRate (8000, 44100, 48000, etc)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byteRate&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 29-32 (4 bytes) byteRate (sampleRate * numChannels * bitsPerSample/8)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blockAlign&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 33-34 (2 bytes) blockAlign (numChannels * bitsPerSample/8)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bitsPerSample&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 35-36 (2 bytes) bitsPerSample (8bits, 16bits, 32bits, etc)&lt;/span&gt;

        &lt;span class="c1"&gt;// data chunk&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 37-40 chunkID ID is "data"&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audioDataSize&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 41-44 size of this chunk varies&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// write the full size of the file on the 4-8 bytes&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;outArr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byteArrayOutputStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toByteArray&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outArr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;ByteBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wrap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outArr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ByteOrder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LITTLE_ENDIAN&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;putInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outArr&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;writeBextChunk&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataOutputStream&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// bext chunk&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bext"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// bext chunk size (fixed size for BWF)&lt;/span&gt;

        &lt;span class="c1"&gt;// description 256 bytes&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 256 bytes description&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ZOOM M3"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 32 bytes originator&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 32 bytes originator reference&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2023-10-01"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 10 bytes origination date&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"12:00:00"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 8 bytes origination time&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"12:00:00"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 8 bytes time reference&lt;/span&gt;

        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 8 bytes time reference&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverseBytes&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 2 bytes version&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="o"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// 180 bytes UMID&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFloat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes loudness value&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFloat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes loudness range&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFloat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes max true peak level&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFloat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes max momentary loudness&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFloat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes max short term loudness&lt;/span&gt;

        &lt;span class="c1"&gt;// zoom m3 needs this bit to allow file to be read from "zoom m3 edit &amp;amp; play"&lt;/span&gt;
        &lt;span class="n"&gt;writeToArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"A=PCM,F=48000,W=32,M=stereo,T=M3;VERSION=1.00;MSRAW=ON ;"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you may see, there wasn't much effort put into the BEXT chunk; I simply created it to ensure "Zoom M3 Edit &amp;amp; Play" would be compatible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool
&lt;/h2&gt;

&lt;p&gt;I hope you had an interesting read. I tried to lift the curtain on the thought process while keeping this engaging. The actual code is hopefully self-explanatory, and you will find it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover" rel="noopener noreferrer"&gt;https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The challenge wasn't just about recovering lost recordings - it was about understanding why traditional tools failed and developing a method that worked.&lt;/p&gt;

&lt;p&gt;While it might have been easier to re-record, the thrill of solving the puzzle made the effort worthwhile. In the end we got a custom-built solution that successfully restores Zoom M3 MicTrak recordings.&lt;/p&gt;

&lt;p&gt;If you ever find yourself in a similar situation, hopefully, this breakdown helps you out. And if not, well, at least you got to enjoy a little adventure into the world of data recovery.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/zoom-m3-mictrak-file-recovery/" rel="noopener noreferrer"&gt;https://wasteofserver.com/zoom-m3-mictrak-file-recovery/&lt;/a&gt;, you will find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>datarecovery</category>
      <category>programming</category>
      <category>java</category>
    </item>
    <item>
      <title>Permanently delete unwanted emails from Gmail. Out of sight, out of mind!</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Fri, 21 Feb 2025 05:07:59 +0000</pubDate>
      <link>https://dev.to/wasteofserver/permanently-delete-unwanted-emails-from-gmail-out-of-sight-out-of-mind-2lpd</link>
      <guid>https://dev.to/wasteofserver/permanently-delete-unwanted-emails-from-gmail-out-of-sight-out-of-mind-2lpd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1hgyl7cbahn3fn18ovg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1hgyl7cbahn3fn18ovg.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/permanently-delete-unwanted-emails-from-gmail-out-of-sight-out-of-mind/" rel="noopener noreferrer"&gt;https://wasteofserver.com/permanently-delete-unwanted-emails-from-gmail-out-of-sight-out-of-mind/&lt;/a&gt;, you will find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is a bit different from my usual content. While it includes a snippet of code, it's designed for a less technical audience, thus the flood of screenshots.&lt;/p&gt;

&lt;p&gt;I received an atypical cry for help. A couple is splitting and one of the partners is spamming the other with abusive emails. The receiver created a Gmail filter to trash the messages, but Gmail trash retention policy is 30 days and, in moments of weakness, can't resist clicking in the trash and reading them, which understandably exacerbates the issue.&lt;/p&gt;

&lt;p&gt;Given the violent nature of the emails, I didn’t want to risk deleting potential evidence so my suggestion was to create a process that would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-forward the emails from that sender to a lawyer&lt;/li&gt;
&lt;li&gt;Delete those emails and purge them from trash, bypassing Gmail's 30-day retention policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdm1qlhyo3m7jigeugbtw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdm1qlhyo3m7jigeugbtw.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Out of sight, out of mind can be the best policy.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a forward address
&lt;/h3&gt;

&lt;p&gt;On this particular case, the forward address will be the lawyer. Go to Gmail settings, select "Forwarding and POP/IMAP" and click on "Add a forwarding address".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx5s4z1l05j76do5i9sb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx5s4z1l05j76do5i9sb.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="346"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Remember to keep forwarding DISABLED, we only want to forward emails from a single sender&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After adding a forward address, Gmail will send a confirmation to that email - your lawyer - asking for permission. As soon as that's granted, you may proceed to the next step. Just remember to keep &lt;code&gt;Forwarding&lt;/code&gt; disabled!&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a new Filter
&lt;/h3&gt;

&lt;p&gt;Here we will be specifying that all emails that come from &lt;code&gt;someone@example.com&lt;/code&gt; will be forwarded to &lt;code&gt;laywer@example.com&lt;/code&gt; and then sent to the trash.&lt;/p&gt;

&lt;p&gt;Go to "Filters and Blocked Addresses" and then click on "Create a new filter".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qjkdb8t3rxyj8qe0lo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qjkdb8t3rxyj8qe0lo6.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The form for adding filters will open. You want &lt;strong&gt;all messages&lt;/strong&gt; from that specific address to be filtered, so just add "&lt;a href="mailto:someone@example.com"&gt;someone@example.com&lt;/a&gt;" to the filter and choose "Create filter".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fby7cqu2d24crb445kw4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fby7cqu2d24crb445kw4q.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="330"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;You can be picky with filters, but here we want ALL emails from sender to match&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now you'll have to select exactly what you want the filter to do. On our specific case, we'll forward the email to the lawyer, so tick that box. We also want to delete the email, so also tick that one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66gevfanvqtfq07mvdgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66gevfanvqtfq07mvdgm.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="596"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;And just like that, emails from this sender are forwarded and trashed!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This solves half of the issue and is the most straightforward part of the process. The tricky bit comes next, how to instantly purge those emails from the trash so that we're not tempted to read them. Google Apps Script to the rescue!&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a Google Apps Script
&lt;/h3&gt;

&lt;p&gt;Google Drive offers a feature that allows you to host and run scripts. While most developers are familiar with this, power users may not be aware of it. For the task at hand, this feature is absolutely perfect.&lt;/p&gt;

&lt;p&gt;Head into &lt;a href="https://script.google.com/" rel="noopener noreferrer"&gt;https://script.google.com/&lt;/a&gt;, follow the authentication procedures, if needed, and then click on "New Project".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpflfg6enq4bqm96s3dsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpflfg6enq4bqm96s3dsy.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="479"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Hurray for your first script!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're now on your project screen. You'll need to interact with Gmail so let's add that service. Click on the big &lt;code&gt;+&lt;/code&gt; next to "Services", look for &lt;code&gt;Gmail API&lt;/code&gt; and add it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vp3sw4yktndcvtfglpr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vp3sw4yktndcvtfglpr.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="530"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Your script needs the Gmail API to access your emails&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, replace the &lt;code&gt;myFunction&lt;/code&gt; with this piece of code. Remember, &lt;strong&gt;YOU MUST CHANGE &lt;a href="mailto:someone@example.com"&gt;someone@example.com&lt;/a&gt;&lt;/strong&gt; to the actual address you want to remove from trash!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteMailsFromTrash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;gmailSearchString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`in:trash from:someone@example.com`&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gmailSearchString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No threads matching search string &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gmailSearchString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%s threads matching action **%s**&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gmailSearchString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\t Thread# &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; [ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;]: [message : &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFirstMessageSubject&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;] deleted`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This script will look in the trash for emails from &lt;a href="mailto:someone@example.com"&gt;someone@example.com&lt;/a&gt; and delete them&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your screen should now resemble this. Go ahead and rename "Untitled project" to something more meaningful, like "Purge Specific Mails from Trash". Also change &lt;code&gt;myFunction&lt;/code&gt; to &lt;code&gt;deleteMailsFromTrash&lt;/code&gt; and then hit &lt;code&gt;Run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You'll be asked to give permissions to access your Google account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftaphrfdtths0b4xnzvzt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftaphrfdtths0b4xnzvzt.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you'll get an error! Google hasn't verified this app. &lt;em&gt;While the developer hasn't verified the app, you shouldn't use.&lt;/em&gt; In this particular case, &lt;strong&gt;you are the developer&lt;/strong&gt;! That's why I didn't release this solution as a pre-made script. It's safer to have the code running on your side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg85pa7zfzsa9caufwfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg85pa7zfzsa9caufwfe.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="639" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on that "Go to Purge Specific Mails from Trash (unsafe)" link and then continue. On your Apps Script window, you'll see the first execution of your script.&lt;/p&gt;

&lt;p&gt;On my case, since I don't have email from &lt;code&gt;someone@example.com&lt;/code&gt; on the "trash" the program simply prints a "No threads matching search string". On your specific case, you may see that a couple of emails have been deleted! Well done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fualhhlb82n91pb5uzdna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fualhhlb82n91pb5uzdna.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now everything is working, but we still need to set up a trigger to run the script automatically, ensuring unwanted emails are deleted in a timely manner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure a trigger!
&lt;/h3&gt;

&lt;p&gt;You'll want a time driven trigger. Something that runs on a schedule making sure that the emails that were placed in the trash by the filter you created above are purged before you can get to them.&lt;/p&gt;

&lt;p&gt;Click on the clock on the left sidebar, then on the big blue button on the bottom right that says "Add Trigger" and configure the filter as per the image below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fso71gzculs1pao6h22h1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fso71gzculs1pao6h22h1.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="729"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;And there you have it! Process is automated.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’ve set this script to run every 5 minutes, but you can adjust the interval to as low as 1 minute if you prefer. Tweak as needed. Setting a longer interval is simply a way to considerate of Google's infrastructure.&lt;/p&gt;

&lt;p&gt;Now, to make sure things are working as expected, on the left sidebar, click on executions. You'll see a table with all the times the script has run. As you've just implemented it, there should probably be two to three runs. One manual &lt;code&gt;Type: Editor&lt;/code&gt; from when you manually executed it and then a couple more from the time-driven trigger labeled with &lt;code&gt;Type: Time-Driven&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13qhupl3cck7qkukhl0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13qhupl3cck7qkukhl0s.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Peace.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As challenging as it may be to navigate these situations, it’s important to remember that protecting your health is a priority.&lt;/p&gt;

&lt;p&gt;While technology can help us minimize harmful distractions, healing takes time and self-compassion. Stay strong, take care of yourself, and don’t hesitate to seek support.&lt;/p&gt;

&lt;p&gt;You deserve peace and healing through this process.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/4kaAc1p" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6xuooleg9h7dng4erqh.jpg" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" width="800" height="747"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don’t know if you’ve tried rucking before, but it’s something that liberates my mind.   &lt;/p&gt;

&lt;p&gt;It’s simply &lt;a href="https://amzn.to/4kaAc1p" rel="noopener noreferrer"&gt;walking while carrying weight&lt;/a&gt;. Originally a military exercise, rucking is gaining popularity for its benefits to physical health, stability, and mental well-being. Give it a try!&lt;/p&gt;

</description>
      <category>gmail</category>
      <category>abuse</category>
      <category>privacy</category>
      <category>lifehack</category>
    </item>
    <item>
      <title>Stop 404 prying bots with HAProxy</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Mon, 17 Feb 2025 06:09:16 +0000</pubDate>
      <link>https://dev.to/wasteofserver/stop-404-prying-bots-with-haproxy-of5</link>
      <guid>https://dev.to/wasteofserver/stop-404-prying-bots-with-haproxy-of5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5e35o12gdsmdtrygzmi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5e35o12gdsmdtrygzmi1.png" alt="Stop 404 prying bots with HAProxy" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/stop-404-prying-bots-with-haproxy/" rel="noopener noreferrer"&gt;https://wasteofserver.com/stop-404-prying-bots-with-haproxy/&lt;/a&gt;, you will find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your logs are filled with 404 hits on &lt;code&gt;/.DS_Store&lt;/code&gt;, &lt;code&gt;/backup.sql&lt;/code&gt;, &lt;code&gt;/.vscode/sftp.json&lt;/code&gt; and a multitude of other URLs. While these requests are mostly harmless, unless of course, your server actually does have something to offer at those locations, you should placate the bots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hitting a server is a resource intensive task and, given that those bots have an extensive list of different URLs, there's no caching mechanism that can help you. Besides, stopping bots is always a safety measure.&lt;/p&gt;

&lt;p&gt;We've previously used HAProxy to &lt;a href="https://wasteofserver.com/how-to-prevent-attacks-on-wordpress-when-running-under-cloudflare/" rel="noopener noreferrer"&gt;mitigate attacks on Wordpress login page&lt;/a&gt;, the idea is to extend that approach to also cover 404 errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy71pqu40t0jacdae69sw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy71pqu40t0jacdae69sw.png" alt="Stop 404 prying bots with HAProxy" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Bots will try their best to create havoc in your server&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've taken &lt;a href="https://www.tekovic.com/blog/block-abusive-ips-based-on-404-error-rate-using-haproxy/" rel="noopener noreferrer"&gt;inspiration from Sasa Tekovic&lt;/a&gt;, namely on not blocking actual search engine crawlers and allowing 404 on static resources to prevent actual missing resources - an error on your part - from not blocking legitimate users.&lt;/p&gt;

&lt;p&gt;Before implementing, it's always good to spin up a local test environment. Let's start &lt;code&gt;HAProxy&lt;/code&gt; and &lt;code&gt;Apache&lt;/code&gt; using &lt;code&gt;Docker&lt;/code&gt;. We do need an actual backend server to give us those &lt;code&gt;404&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version : '3'

services:
    haproxy:
        image: haproxy:3.1.3-alpine
        ports:
            - "8100:80"
        volumes:
            - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
        networks:
            - webnet
    apache:
        image: httpd:latest
        container_name: apache1
        ports:
            - "8080:80"
        volumes:
            - ./html:/usr/local/apache2/htdocs/
        networks:
            - webnet

networks:
    webnet:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Then, simply run docker-compose up, and you can access localhost:8100 in your browser.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;haproxy.cfg&lt;/code&gt; file is pretty much self-explanatory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global
    log stdout format raw daemon debug

defaults
    log global
    mode http

frontend main
    bind *:80

    acl static_file path_end .css .js .jpg .jpeg .gif .ico .png .bmp .webp .csv .ttf .woff .svg .svgz
    acl excluded_user_agent hdr_reg(user-agent) -i (yahoo|yandex|kagi|(google|bing)bot)

    # tracks IPs but exclude hits on static files and search engine crawlers
    http-request track-sc0 src table mock_404_tracking if !static_file !excluded_user_agent
    # increment gpc0 if response code was 404
    http-response sc-inc-gpc0(0) if { status 404 }
    # checks if the 404 error rate limit was exceeded
    http-request deny deny_status 403 content-type text/html lf-string "404 abuse" if { sc0_gpc0_rate(mock_404_tracking) ge 5 }

    # whatever backend you're using
    use_backend apache_servers

backend apache_servers
    server apache1 apache1:80 maxconn 32

# mock backend to hold a stick table
backend mock_404_tracking
    stick-table type ip size 100k expire 10m store gpc0,gpc0_rate(1m)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If you get more than 5 hits on 404 requests in a single minute, bot will be banned for 10 minutes.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;As it stands, this setup effectively rate-limits bots generating excessive 404s. However, we also want to integrate it with our previous example, where we used &lt;code&gt;HAProxy&lt;/code&gt; to block attacks on &lt;code&gt;WordPress&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global
    log stdout format raw daemon debug

defaults
    log global
    mode http

frontend main
    bind *:80

    # We may, or may not, be running this with Cloudflare acting as a CDN.
    # If Cloudflare is in front of our servers, user/bot IP will be in 
    # 'CF-Connecting-IP', otherwise user IP with be in 'src'. So we make
    # sure to set a variable 'txn.actual_ip' that has the IP, no matter what
    http-request set-var(txn.actual_ip) hdr_ip(CF-Connecting-IP) if { hdr(CF-Connecting-IP) -m found }
    http-request set-var(txn.actual_ip) src if !{ hdr(CF-Connecting-IP) -m found }

    # gets the actual IP on logs
    log-format "%ci\ %hr\ %ft\ %b/%s\ %Tw/%Tc/%Tt\ %B\ %ts\ %r\ %ST\ %Tr IP:%{+Q}[var(txn.actual_ip)]"

    # common static files where we may get 404 errors and also common search engine
    # crawlers that we don't want blocked
    acl static_file path_end .css .js .jpg .jpeg .gif .ico .png .bmp .webp .csv .ttf .woff .svg .svgz
    acl excluded_user_agent hdr_reg(user-agent) -i (yahoo|yandex|kagi|google|bing)

    # paths where we will rate limit users to prevent Wordpress abuse
    acl is_wp_login path_end -i /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST

    # 404 abuse blocker
    # track IPs but exclude hits on static files and search engine crawlers
    # increment gpc0 counter if response status was 404 and deny if rate exceeded
    http-request track-sc0 var(txn.actual_ip) table mock_404_track if !static_file !excluded_user_agent
    http-response sc-inc-gpc0(0) if { status 404 }
    http-request deny deny_status 403 content-type text/html lf-string "404 abuse" if { sc0_gpc0_rate(mock_404_track) ge 5 }

    # wordpress abuse blocker
    # track IPs if the request hits one of the monitored paths with a POST request
    # increment gpc1 counter if path was hit and deny if rate exceeded
    http-request track-sc1 var(txn.actual_ip) table mock_wplogin_track if is_wp_login is_post
    http-request sc-inc-gpc1(1) if is_wp_login is_post 
    http-request deny deny_status 403 content-type text/html lf-string "login abuse" if { sc1_gpc1_rate(mock_wplogin_track) ge 5 }

    # your backend, here using apache for demonstration purposes
    use_backend apache_servers

backend apache_servers
    server apache1 apache1:80 maxconn 32

# mock backends for storing sticky tables
backend mock_404_track
    stick-table type ip size 100k expire 10m store gpc0,gpc0_rate(1m)
backend mock_wplogin_track
    stick-table type ip size 100k expire 10m store gpc1,gpc1_rate(1m)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Running with two stick tables, and stopping both threats.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And there you have it. HAProxy once again used for much more than as a simple reverse proxy. It's a little Swiss Knife!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/3CIuG5t" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F479ukzvwhhluusaniiim.jpg" alt="Stop 404 prying bots with HAProxy" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This headlamp has been a game-changer when working on repairs.&lt;/p&gt;

&lt;p&gt;I had one, but when it broke, hesitated to replace it, resorting to the phone’s flashlight. And sure, it works - but once &lt;a href="https://amzn.to/3CIuG5t" rel="noopener noreferrer"&gt;you experience the convenience of having both hands free again&lt;/a&gt; - there’s no going back. If you need reliable, hands-free lighting, this is a must-have!&lt;/p&gt;

</description>
      <category>haproxy</category>
      <category>security</category>
    </item>
    <item>
      <title>How to mitigate attacks on WordPress when running under a full CDN like Cloudflare</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Thu, 19 Sep 2024 17:04:48 +0000</pubDate>
      <link>https://dev.to/wasteofserver/how-to-mitigate-attacks-on-wordpress-when-running-under-a-full-cdn-like-cloudflare-46nk</link>
      <guid>https://dev.to/wasteofserver/how-to-mitigate-attacks-on-wordpress-when-running-under-a-full-cdn-like-cloudflare-46nk</guid>
      <description>

&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/how-to-prevent-attacks-on-wordpress-when-running-under-cloudflare/" rel="noopener noreferrer"&gt;wasteofserver.com&lt;/a&gt;, you may find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How to mitigate attacks on WordPress when running under a full CDN like Cloudflare
&lt;/h2&gt;

&lt;p&gt;The most effective way to prevent bots from spamming your server is to drop them at the firewall. This is generally achieved using tools like &lt;a href="https://github.com/denyhosts/denyhosts" rel="noopener noreferrer"&gt;Denyhosts&lt;/a&gt; or &lt;a href="https://github.com/fail2ban/fail2ban" rel="noopener noreferrer"&gt;fail2ban&lt;/a&gt;, which monitor your logs, identify suspicious activity, and block the offending IP addresses before they cause harm.&lt;/p&gt;

&lt;p&gt;Denyhosts works at the application level by adding entries to &lt;code&gt;/etc/hosts.deny&lt;/code&gt;, whereas fail2ban operates at the firewall level using &lt;code&gt;iptables&lt;/code&gt;, which makes it far more efficient.&lt;/p&gt;

&lt;p&gt;However, on resource-constrained machines, fail2ban can still be taxing. A few years ago, we shared a demo of a lightweight log parser called &lt;a href="https://github.com/wasteofserver/banbylog" rel="noopener noreferrer"&gt;&lt;strong&gt;banbylog&lt;/strong&gt;&lt;/a&gt;, tailored specifically for our needs (SSH and WordPress activity monitoring) at a much lower resource cost. If that sounds like a good fit, feel free to check it out, but keep in mind that it’s not production-ready!&lt;/p&gt;

&lt;p&gt;While the consensus is to parse logs and block hostile IPs at the firewall, &lt;strong&gt;it will not work&lt;/strong&gt;  &lt;strong&gt;under Cloudflare&lt;/strong&gt; umbrella!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtdhum5xsnfq1uzctf7l.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtdhum5xsnfq1uzctf7l.jpeg" alt="How to mitigate attacks on WordPress when running under a full CDN like Cloudflare" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Ideally the attack would stop here, but it won't work with a full CDN like Cloudflare&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why can't we flag offending IPs with Cloudflare?
&lt;/h2&gt;

&lt;p&gt;Because the IPs that are "attacking" your server are not the actual offending IPs, but Clouflare machines that are proxying the request to your server. Take a look at the diagram below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7d29wlnuign36hclcqu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7d29wlnuign36hclcqu7.png" alt="How to mitigate attacks on WordPress when running under a full CDN like Cloudflare" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare acts as a middleman between your server and the users. The only IP addresses visible to your firewall are from Cloudflare, not from the original user.&lt;/p&gt;

&lt;p&gt;In fact, you should explicitly whitelist &lt;a href="https://www.cloudflare.com/ips/" rel="noopener noreferrer"&gt;Cloudflare IP range&lt;/a&gt;. If you happen to block an IP that belongs to Cloudflare, legitimate users will see your site as down.&lt;/p&gt;

&lt;p&gt;😱&lt;/p&gt;

&lt;p&gt;&lt;em&gt;While this article's focus is on securing servers while working under a Content Distribution Network, Cloudflare offers a variety of tools - free and paid - you may leverage to achieve these same results.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  If the IP we see doesn't belong to the user making the request, what can we look for?
&lt;/h3&gt;

&lt;p&gt;Every request Cloudflare sends to your server has an attached header that carries the user original IP under &lt;code&gt;CF-Connecting-IP&lt;/code&gt;. And that's what we can leverage to get them! Unfortunately, reading &lt;code&gt;http&lt;/code&gt; headers is too upstream for &lt;code&gt;iptables&lt;/code&gt;, thus &lt;strong&gt;HAProxy to the rescue&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;iptables&lt;/code&gt; operates at Layer 4, &lt;code&gt;HAProxy&lt;/code&gt; can operate at OSI Layer 7.&lt;/p&gt;

&lt;p&gt;💡&lt;/p&gt;

&lt;p&gt;Remember the OSI model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Layer 7 - Application&lt;span class="p"&gt;;&lt;/span&gt; protocols &lt;span class="o"&gt;(&lt;/span&gt;HTTP, ...&lt;span class="o"&gt;)&lt;/span&gt;  
Layer 6 - Presentation&lt;span class="p"&gt;;&lt;/span&gt; character encoding &lt;span class="o"&gt;(&lt;/span&gt;ASCII, UTF8, ...&lt;span class="o"&gt;)&lt;/span&gt;  
Layer 5 - Session&lt;span class="p"&gt;;&lt;/span&gt; stick client to server  
Layer 4 - Transport&lt;span class="p"&gt;;&lt;/span&gt; protocols &lt;span class="o"&gt;(&lt;/span&gt;TCP, UDP, ...&lt;span class="o"&gt;)&lt;/span&gt;  
Layer 3 - Network&lt;span class="p"&gt;;&lt;/span&gt; routing protocols &lt;span class="o"&gt;(&lt;/span&gt;IP&lt;span class="o"&gt;)&lt;/span&gt;  
Layer 2 - Data Link&lt;span class="p"&gt;;&lt;/span&gt; physical to network &lt;span class="o"&gt;(&lt;/span&gt;ARP, Ethernet&lt;span class="o"&gt;)&lt;/span&gt;  
Layer 1 - Physical&lt;span class="p"&gt;;&lt;/span&gt; cabling, Wi-Fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The easiest way to test HAProxy configurations is to boot a Docker instance of HAProxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; haproxy &lt;span class="nt"&gt;-p&lt;/span&gt; 888:80 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/usr/local/etc/haproxy &lt;span class="nt"&gt;--sysctl&lt;/span&gt; net.ipv4.ip_unprivileged_port_start&lt;span class="o"&gt;=&lt;/span&gt;0 haproxy:2.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Start HAProxy listening on host port 888&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The above assumes you're using Powershell and are in the directory that contains &lt;code&gt;haproxy.cfg&lt;/code&gt;. Change &lt;code&gt;${pwd}&lt;/code&gt; accordingly if not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; HUP haproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This command forces HAProxy restart, which is useful to iterate different configs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Below is a straightforward &lt;code&gt;haproxy.cfg&lt;/code&gt; that will put HAProxy listening on port &lt;code&gt;80&lt;/code&gt;, log to &lt;code&gt;stdout&lt;/code&gt; so you can get an instant glimpse on what's going on, and also print the &lt;code&gt;CF-Connecting-IP&lt;/code&gt; header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;global
    &lt;span class="c"&gt;# output logs straight to stdout&lt;/span&gt;
    log stdout format raw daemon debug

defaults
    &lt;span class="c"&gt;# put HAProxy in Level 7 mode allowing to inspect http protocol&lt;/span&gt;
    log global
    mode http

frontend main
    &lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;:80

    &lt;span class="c"&gt;# capture original IP from header and put it in variable txn.cf_conn_ip&lt;/span&gt;
    http-request set-var&lt;span class="o"&gt;(&lt;/span&gt;txn.cf_conn_ip&lt;span class="o"&gt;)&lt;/span&gt; hdr&lt;span class="o"&gt;(&lt;/span&gt;CF-Connecting-IP&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;# get the original IP on logs (just to check if things are working)&lt;/span&gt;
    log-format &lt;span class="s2"&gt;"%ci&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%hr&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ft&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%b/%s&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%Tw/%Tc/%Tt&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%B&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ts&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%r&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ST&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%Tr CF-IP:%{+Q}[var(txn.cf_conn_ip)]"&lt;/span&gt;

    &lt;span class="c"&gt;# use backend ok, which always returns a 200, for debug&lt;/span&gt;
    use_backend ok

backend ok
    http-request &lt;span class="k"&gt;return &lt;/span&gt;status 200 content-type &lt;span class="s2"&gt;"text/plain"&lt;/span&gt; lf-string &lt;span class="s2"&gt;"ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This should be enough to test HAProxy&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's make a test request to HAProxy.&lt;/p&gt;

&lt;p&gt;I'm partial to &lt;a href="https://www.usebruno.com/" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt;, a portable and offline alternative to Postman. Download it, add the &lt;code&gt;CF-Connecting-IP&lt;/code&gt; header and make a &lt;code&gt;POST&lt;/code&gt; request to &lt;a href="http://localhost:888/wp-login.php" rel="noopener noreferrer"&gt;&lt;code&gt;http://localhost:888/wp-login.php&lt;/code&gt;&lt;/a&gt; to test if everything's working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmdwa1zn5sg45fktmvn7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmdwa1zn5sg45fktmvn7.png" alt="How to mitigate attacks on WordPress when running under a full CDN like Cloudflare" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Nothing to install, a POST request in under a minute!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If things went as planned, your HAProxy should have printed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;172.17.0.1 main ok/&amp;lt;NOSRV&amp;gt; &lt;span class="nt"&gt;-1&lt;/span&gt;/-1/0 78 LR POST /wp-login.php HTTP/1.1 200 &lt;span class="nt"&gt;-1&lt;/span&gt; CF-IP:&lt;span class="s2"&gt;"222.222.222.222"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Now that we have the offending IP (222.222.222.222), we can block it!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On our particular case, bots are hitting &lt;code&gt;wp-login.php&lt;/code&gt;, &lt;code&gt;xmlrpc.php&lt;/code&gt; and &lt;code&gt;xmrlpc.php&lt;/code&gt; (last one is a typo, but we've had more than 100k hits in the last 24h!). We also know that they're flooding the server with POST requests trying to brute-force passwords.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;frontend main
    &lt;span class="c"&gt;# requests that will be monitored and blocked if abused&lt;/span&gt;
    acl is_wp_login path_end &lt;span class="nt"&gt;-i&lt;/span&gt; /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Add this to end of the frontend main block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We now have a couple of options:&lt;/p&gt;

&lt;p&gt;a) Now that we have the offending IPs in the log, we could change &lt;code&gt;banbylog&lt;/code&gt; to write them to a file, and have HAProxy deny those requests. While this would work, HAProxy would need to be constantly reloaded.&lt;/p&gt;

&lt;p&gt;b) Or we may simply leverage HAProxy stick tables and do a rate-limiting on offending requests.&lt;/p&gt;

&lt;p&gt;While "&lt;em&gt;a"&lt;/em&gt; would allow us to ban the offending IP for an indefinite amount of time, &lt;em&gt;"b"&lt;/em&gt; has less moving pieces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;frontend main
    &lt;span class="c"&gt;# requests that will be monitored and blocked if abused&lt;/span&gt;
    acl is_wp_login path_end &lt;span class="nt"&gt;-i&lt;/span&gt; /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST

    &lt;span class="c"&gt;# table than can store 100k IPs, entries expire after 1 minute&lt;/span&gt;
    stick-table &lt;span class="nb"&gt;type &lt;/span&gt;ip size 100k expire 1m store http_req_rate&lt;span class="o"&gt;(&lt;/span&gt;1m&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;# we'll track (save to table) the original IP only if the request hits&lt;/span&gt;
    &lt;span class="c"&gt;# one of the monitored paths with a POST request&lt;/span&gt;
    http-request track-sc0 hdr&lt;span class="o"&gt;(&lt;/span&gt;CF-Connecting-IP&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;is_wp_login is_post

    &lt;span class="c"&gt;# we now query the stick-table and if the IP has made more than &lt;/span&gt;
    &lt;span class="c"&gt;# 5 requests of the offending type in the last minute, &lt;/span&gt;
    &lt;span class="c"&gt;# current request is denied&lt;/span&gt;
    http-request deny &lt;span class="k"&gt;if &lt;/span&gt;is_wp_login is_post &lt;span class="o"&gt;{&lt;/span&gt; sc_http_req_rate&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt; gt 5 &lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HAProxy has &lt;a href="https://www.haproxy.com/blog/use-haproxy-response-policies-to-stop-threats" rel="noopener noreferrer"&gt;multiple deny options&lt;/a&gt;, tarpit, silent drop, reject or shadowban. A tarpit deny would be something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# make request hang for 20 seconds before replying back with a 403&lt;/span&gt;
&lt;span class="nb"&gt;timeout &lt;/span&gt;tarpit 20s
http-request tarpit deny_status 403 &lt;span class="k"&gt;if &lt;/span&gt;is_wp_login is_post &lt;span class="o"&gt;{&lt;/span&gt; sc_http_req_rate&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt; gt 2 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Notice that this puts extra stress on both HAProxy and iptables as they must keep the connection open&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Just for reference, here the full, simplified, HAProxy config file that blocks requests if they hit one of the monitored URL's with more than 5 hits in less than a minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;global
    log stdout format raw daemon debug

defaults
    log global
    mode http

frontend main
    &lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;:80

    &lt;span class="c"&gt;# capture original IP from header and put it in variable txn.cf_conn_ip&lt;/span&gt;
    http-request set-var&lt;span class="o"&gt;(&lt;/span&gt;txn.cf_conn_ip&lt;span class="o"&gt;)&lt;/span&gt; hdr&lt;span class="o"&gt;(&lt;/span&gt;CF-Connecting-IP&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;# get the original IP on logs (just to check if things are working)&lt;/span&gt;
    log-format &lt;span class="s2"&gt;"%ci&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%hr&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ft&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%b/%s&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%Tw/%Tc/%Tt&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%B&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ts&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%r&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%ST&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;%Tr CF-IP:%{+Q}[var(txn.cf_conn_ip)]"&lt;/span&gt;

    acl is_wp_login path_end &lt;span class="nt"&gt;-i&lt;/span&gt; /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST
    stick-table &lt;span class="nb"&gt;type &lt;/span&gt;ip size 100k expire 1m store http_req_rate&lt;span class="o"&gt;(&lt;/span&gt;1m&lt;span class="o"&gt;)&lt;/span&gt;
    http-request track-sc0 var&lt;span class="o"&gt;(&lt;/span&gt;txn.cf_conn_ip&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;is_wp_login is_post
    http-request deny &lt;span class="k"&gt;if &lt;/span&gt;is_wp_login is_post &lt;span class="o"&gt;{&lt;/span&gt; sc_http_req_rate&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt; gt 5 &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;# obviously change this to whatever backend you're using&lt;/span&gt;
    use_backend ok

backend ok
    http-request &lt;span class="k"&gt;return &lt;/span&gt;status 200 content-type &lt;span class="s2"&gt;"text/plain"&lt;/span&gt; lf-string &lt;span class="s2"&gt;"ok"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;And there you have it, using your proxy as a gatekeeper!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhze2samh4ihed9k4z0vn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhze2samh4ihed9k4z0vn.png" alt="How to mitigate attacks on WordPress when running under a full CDN like Cloudflare" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The CPU toll of a wp-login.php brute-force attack and then HAProxy acting as a bouncer.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Re-Captcha, obviously!
&lt;/h2&gt;

&lt;p&gt;While this article focus on the server side of things, the first thing you should obviously do, is to foolproof forms with Re-Captcha, which you can easily do with a plugin such as &lt;a href="https://wordpress.org/plugins/google-captcha/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;reCaptcha by BestWebSoft&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;EDIT:&lt;/strong&gt; A user queried: &lt;em&gt;«I have some WordPress installations under Cloudflare and some exposing the server directly. Can it work on both?»&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can. It's as simple as doing something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# if CF-Connecting-IP is not set, put src IP in txn.client_ip&lt;/span&gt;
http-request set-var&lt;span class="o"&gt;(&lt;/span&gt;txn.client_ip&lt;span class="o"&gt;)&lt;/span&gt; hdr&lt;span class="o"&gt;(&lt;/span&gt;CF-Connecting-IP&lt;span class="o"&gt;)&lt;/span&gt;
http-request set-var&lt;span class="o"&gt;(&lt;/span&gt;txn.client_ip&lt;span class="o"&gt;)&lt;/span&gt; src &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!{&lt;/span&gt; var&lt;span class="o"&gt;(&lt;/span&gt;txn.client_ip&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; found &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;But beware, if some sites are under Cloudflare and some aren't, a spammer can forge CF-Connecting-IP to trick the simple if/else above. You must first check if src belongs to Cloudflare before extracting txn.client_ip from the header.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/4e8v3DL" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59l0oa2m1jdyh9z13pav.jpg" alt="How to mitigate attacks on WordPress when running under a full CDN like Cloudflare" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This week, I assisted a friend in upgrading to professional &lt;a href="https://amzn.to/4e8v3DL" rel="noopener noreferrer"&gt;TP-Link access points&lt;/a&gt;. I'm a strong advocate for devices that excel at a single task, and these EAP610 access points do just that. Highly recommended!&lt;/p&gt;

</description>
      <category>haproxy</category>
      <category>security</category>
    </item>
    <item>
      <title>Premature optimization, where software thrives unless you kill it first - a tale of Java GC</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Sat, 30 Mar 2024 17:45:49 +0000</pubDate>
      <link>https://dev.to/wasteofserver/premature-optimization-where-software-thrives-unless-you-kill-it-first-a-tale-of-java-gc-5fhc</link>
      <guid>https://dev.to/wasteofserver/premature-optimization-where-software-thrives-unless-you-kill-it-first-a-tale-of-java-gc-5fhc</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dVHfm-0_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/java_future_gabage_collector_illustration.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dVHfm-0_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/java_future_gabage_collector_illustration.jpg" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/premature-optimization-where-software-thrives-unless-you-kill-it-first-a-tale-of-java-gc/" rel="noopener noreferrer"&gt;wasteofserver.com&lt;/a&gt;, you may find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Before going heads on into Java and the ways to tackle interference, either from the garbage collector or from context switching, let's first glance over the fundamentals of writing code for your future self.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Premature optimization is the root of all evil.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've heard it before; &lt;em&gt;premature optimization is the root of all evil&lt;/em&gt;. Well, sometimes. When writing software, I'm a firm believer of being:&lt;/p&gt;

&lt;p&gt;1) as &lt;strong&gt;descriptive as possible&lt;/strong&gt; ; you should try to narrate intentions as if you were writing a story.&lt;/p&gt;

&lt;p&gt;2) as &lt;strong&gt;optimal as possible&lt;/strong&gt; ; which means that you should know the fundamentals of the language and apply them accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  As descriptive as possible
&lt;/h2&gt;

&lt;p&gt;Your code should speak intention, and a lot of it pertains to the way you name methods and variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;array1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// bad&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;numItems&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// better&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;backPackItems&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// great&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Just by the variable name, you can already infer functionality&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;numItems&lt;/code&gt; is abstract, &lt;code&gt;backPackItems&lt;/code&gt; tells you a lot about expected behaviour.&lt;/p&gt;

&lt;p&gt;Or say you have this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Countries&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;visitedCountries&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noCountryVisitedYet&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// (...)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;listOfVisitedCountries&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;As far as code goes, this looks more or less ok.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can we do better? We definitely can!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Countries&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;visitedCountries&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noCountryVisitedYet&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emptyList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// (...)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;listOfVisitedCountries&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Reading Collections.emptyList() is much more descriptive than new ArrayList&amp;lt;&amp;gt;(0);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine you're reading the above code for the first time and stumble on the &lt;em&gt;guard clause&lt;/em&gt; that checks if the user has actually visited countries. Also imagine this is buried in a lengthy class, reading &lt;code&gt;Collections.emptyList()&lt;/code&gt; is definitely more descriptive than &lt;code&gt;new ArrayList&amp;lt;&amp;gt;(0)&lt;/code&gt;, you're also making sure it's immutable making sure client code can't modify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  As optimal as possible
&lt;/h2&gt;

&lt;p&gt;Know your language and use it accordingly. If you need a &lt;code&gt;double&lt;/code&gt; there's no need to wrap it in a &lt;code&gt;Double&lt;/code&gt; object. The same goes to using a &lt;code&gt;List&lt;/code&gt; if all you actually need is an &lt;code&gt;Array&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Know that you should concatenate Strings using &lt;code&gt;StringBuilder&lt;/code&gt; or &lt;code&gt;StringBuffer&lt;/code&gt; if you're sharing state between threads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't do this&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;votesByCounty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;County&lt;/span&gt; &lt;span class="n"&gt;county&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;votesByCounty&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;county&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// do this instead&lt;/span&gt;
&lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;votesByCounty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;County&lt;/span&gt; &lt;span class="n"&gt;county&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;votesByCounty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Know how to index your database. Anticipate bottlenecks and cache accordingly. All the above are optimizations. They are the kind of optimizations that you should be aware and implement as first citizens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you kill it first?
&lt;/h2&gt;

&lt;p&gt;I'll never forget about a hack I read a couple of years ago. Truth be said, the author backtracked quickly, but it goes to show how a lot of evil can spur from good intention.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// do not do this, ever!&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// business logic&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;//prevent long gc&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Ignored&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;A garbage collector hack from hell!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can read more on why and how the above code works &lt;a href="https://levelup.gitconnected.com/why-does-my-colleague-use-thread-sleep-0-in-the-code-a3fd12dc98b7" rel="noopener noreferrer"&gt;in the original article&lt;/a&gt; and, while the exploit is definitely interesting, this is one of those things you should &lt;strong&gt;never&lt;/strong&gt; ever do.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works by side effects, &lt;code&gt;Thread.sleep(0)&lt;/code&gt; has no purpose in this block&lt;/li&gt;
&lt;li&gt;Works by exploiting a deficiency of code downstream&lt;/li&gt;
&lt;li&gt;For anyone inheriting this code, it's obscure and magical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only start forging something a bit more involved if, after writing with all the &lt;strong&gt;default optimizations the language provides&lt;/strong&gt; , you've hit a bottleneck. But steer away from concoctions as the above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q7C2Mzfa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/garbage_collector.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q7C2Mzfa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/garbage_collector.jpg" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An interpretation of Java's future Garbage Collector "imagined" by Microsoft Copilot&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tackle &lt;em&gt;that&lt;/em&gt; Garbage Collector?
&lt;/h2&gt;

&lt;p&gt;If after all's done, the Garbage Collector is still the piece that's offering resistance, these are some of the things you may try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If your service is so latency sensitive that you can't allow for GC, run with &lt;strong&gt;"Epsilon GC" and avoid GC altogether&lt;/strong&gt;. &lt;code&gt;-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC&lt;/code&gt; This will obviously grow your memory until you get an OOM exception, so either it's a short-lived scenario or your program is optimized not to create objects  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your service is somewhat latency sensitive, but &lt;strong&gt;the allowed tolerance permits some leeway&lt;/strong&gt; , run GC1 and feed it something like &lt;code&gt;-XX:MaxGCPauseTimeMillis=100&lt;/code&gt; (default is 250ms)  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the issue spurs from external libraries&lt;/strong&gt; , say one of them calls &lt;code&gt;System.gc()&lt;/code&gt; or &lt;code&gt;Runtime.getRuntime().gc()&lt;/code&gt; which are stop-the-world garbage collectors, you can override offending behaviour by running with &lt;code&gt;-XX:+DisableExplicitGC&lt;/code&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you're running on a JVM above 11, do try the &lt;a href="https://wiki.openjdk.org/display/zgc" rel="noopener noreferrer"&gt;Z Garbage Collector (ZGC)&lt;/a&gt;, performance improvements are monumental! &lt;code&gt;-XX:+UnlockExperimentalVMOptions -XX:+UseZGC&lt;/code&gt;. You may also want to check this &lt;a href="https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html" rel="noopener noreferrer"&gt;JDK 21 GC benchmark&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version Start&lt;/th&gt;
&lt;th&gt;Version End&lt;/th&gt;
&lt;th&gt;Default GC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java 1&lt;/td&gt;
&lt;td&gt;Java 4&lt;/td&gt;
&lt;td&gt;Serial Garbage Collector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 5&lt;/td&gt;
&lt;td&gt;Java 8&lt;/td&gt;
&lt;td&gt;Parallel Garbage Collector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 9&lt;/td&gt;
&lt;td&gt;ongoing&lt;/td&gt;
&lt;td&gt;G1 Garbage Collector&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note 1: since Java 15, &lt;code&gt;ZGC&lt;/code&gt; is &lt;a href="https://wiki.openjdk.org/display/zgc/Main" rel="noopener noreferrer"&gt;production ready&lt;/a&gt;, but you still have to explicitly activate it with &lt;code&gt;-XX:+UseZGC&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note 2: The VM considers machines as server-class if the VM detects more than two processors and a heap size larger or equal to 1792 MB. If not server-class, it &lt;a href="https://docs.oracle.com/en/java/javase/20/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781" rel="noopener noreferrer"&gt;will default to the Serial GC&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In essence, opt for GC tuning when it's clear that the application's performance constraints are directly tied to garbage collection behavior and you have the necessary expertise to make informed adjustments. Otherwise, trust the JVM's default settings and focus on optimizing application-level code.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/user/shiphe/" rel="noopener noreferrer"&gt;u/shiphe&lt;/a&gt; - you'll want to read the &lt;a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxu9hff/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;full comment&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Other relevant libraries you may want to explore:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/openjdk/jmh" rel="noopener noreferrer"&gt;Java Microbenchmark Harness (JMH)&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;If you're &lt;em&gt;optimizing&lt;/em&gt; out of feeling without any real benchmarking, you're doing yourself a disservice. JMH is the &lt;em&gt;de facto&lt;/em&gt; Java library to test your algorithms' performance. Use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/OpenHFT/Java-Thread-Affinity" rel="noopener noreferrer"&gt;Java-Thread-Affinity&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Pinning a process to a specific core may improve cache hits. It will depend on the underlying hardware and how your routine is dealing with data. Nonetheless, this library makes it so easy to implement that, if a CPU intensive method is dragging you, you'll want to test it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://lmax-exchange.github.io/dis" rel="noopener noreferrer"&gt;LMAX Disruptor&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is one of those libraries that, even if you don't need, you'll want to study. The idea is to allow for ultra low latency concurrency. But the way it's implemented, from &lt;em&gt;mechanical sympathy&lt;/em&gt; to the &lt;em&gt;ring buffer,&lt;/em&gt; brings a lot of new concepts. I still remember when I first discovered it, seven years ago, pulling an all-nighter to digest it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/Netflix-Skunkworks/jvmquake" rel="noopener noreferrer"&gt;Netflix jvmquake&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The premiss of &lt;code&gt;jvmquake&lt;/code&gt; is that when things go sideways with the JVM, you want it to die and not hang. A couple of years ago, I was running simulations on an HTCondor cluster that was on tight memory constraints and sometimes jobs would get stuck due to "out of memory" errors. This library forces the JVM to die, allowing you to deal with the actual error. On this specific case, HTCondor would auto re-schedule the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The code that made me write this post? I've written way worse. I still do. The best we can hope for is to continuously mess up less.&lt;/p&gt;

&lt;p&gt;I'm expecting to be disgruntled looking at my own code a few years down the road.&lt;/p&gt;

&lt;p&gt;And that's a good sign.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/3TZrPLg" rel="noopener noreferrer"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1Qbv960o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/amazon_basics_shredder.jpg" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC" width="800" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given the nature of this post, I found it appropriate to promote a product I've had for quite some time, &lt;a href="https://amzn.to/3TZrPLg" rel="noopener noreferrer"&gt;&lt;strong&gt;a home shredder!&lt;/strong&gt;&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;This is the 3rd different model/brand I've had and can definitely attest to Amazon Basics sturdiness.   &lt;/p&gt;

&lt;p&gt;While I do prefer signed and encrypted PDFs, there are a lot of financial institutions that still share data via paper. I shred those. Then I shred some other trivial stuff just to make it safe by obscurity.  &lt;/p&gt;

&lt;p&gt;In all honesty, I find it soothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edits &amp;amp; Thank You:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;to &lt;a href="https://www.florian-schaetz.de/" rel="noopener noreferrer"&gt;FlorianSchaetz&lt;/a&gt; for catching the mutability error in &lt;code&gt;visitedCountries()&lt;/code&gt; and the detailed explanation.&lt;/li&gt;
&lt;li&gt;to &lt;a href="https://www.reddit.com/user/brunocborges/" rel="noopener noreferrer"&gt;u/brunocborges&lt;/a&gt; and &lt;a href="https://www.reddit.com/user/BikingSquirrel/" rel="noopener noreferrer"&gt;u/BikingSquirrel&lt;/a&gt; for explaining that &lt;a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxtizb4/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;on lower end machines&lt;/a&gt;, you get Serial GC&lt;/li&gt;
&lt;li&gt;to &lt;a href="https://www.reddit.com/user/shiphe/" rel="noopener noreferrer"&gt;u/shiphe&lt;/a&gt; for taking the time to better explain when you should mess with the GC and &lt;a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxu9hff/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;when you shouldn't&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;to &lt;a href="https://www.reddit.com/user/tomwhoiscontrary/" rel="noopener noreferrer"&gt;u/tomwhoiscontrary&lt;/a&gt; to put me in the right track regarding what &lt;a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxwln4u/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;should be considered standard&lt;/a&gt; practice&lt;/li&gt;
&lt;li&gt;to &lt;a href="https://www.reddit.com/user/BikingSquirrel/" rel="noopener noreferrer"&gt;u/BikingSquirrel&lt;/a&gt; (again) for providing the link to JDK 21 garbage collectors benchmark&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>benchmark</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to calculate P&amp;L</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Thu, 07 Mar 2024 06:29:30 +0000</pubDate>
      <link>https://dev.to/wasteofserver/how-to-calculate-pl-1o8o</link>
      <guid>https://dev.to/wasteofserver/how-to-calculate-pl-1o8o</guid>
      <description>&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/how-to-calculate-pnl/" rel="noopener noreferrer"&gt;wasteofserver.com&lt;/a&gt;, you may find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Calculating profits and losses from a mathematical perspective is trivial, just subtract all things sold to all things bought. The remainder is your profit. That's pretty much how mom-and-pop retail works.&lt;/p&gt;

&lt;p&gt;In future markets tough, people may get confused as you'll have open positions, which in retail would translate to inventory, that may not have been bought yet (if you're short selling).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--az9rqmGP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--az9rqmGP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg" alt="How to calculate P&amp;amp;L" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Say you're trading in &lt;code&gt;Mini S&amp;amp;P Futures&lt;/code&gt; from CME.&lt;/p&gt;

&lt;p&gt;The contract &lt;code&gt;lot size&lt;/code&gt; is &lt;code&gt;50 units&lt;/code&gt; and it's currently trading at around &lt;code&gt;5140&lt;/code&gt;. Let's see how that works out in a simple BUY/SELL trade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// final value is num_contracts * lot_size * price&lt;/span&gt;
&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5140.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_000&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5141.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_100&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
                           &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PNL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's add a third order to the mix to see how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5140.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_000&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5141.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_100&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5142.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_200&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you think it through, you have an open position of &lt;code&gt;-2 ES&lt;/code&gt; contracts that you'll eventually have to close. To calculate the instant P&amp;amp;L you must assume current market price.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5140.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_000&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5141.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_100&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5142.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_200&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
&lt;span class="c1"&gt;// current market price is 5145.00&lt;/span&gt;
&lt;span class="c1"&gt;// you have -2 ES contracts to close&lt;/span&gt;
&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mf"&gt;5145.00&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;514_500&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt;
                           &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="no"&gt;USD&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PNL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;              
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple arithmetic. Buy orders are treated as negative values, sell orders are treated as positive values. That's really all you need to think about.&lt;/p&gt;

&lt;p&gt;If you're not flat, you'll have a position that you must pretend to close (at current market value) to calculate your current P&amp;amp;L.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mi"&gt;5100&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5100&lt;/span&gt;
&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mi"&gt;5150&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5150&lt;/span&gt;
&lt;span class="no"&gt;BUY&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mi"&gt;5200&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5200&lt;/span&gt;
&lt;span class="no"&gt;SELL&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="no"&gt;ESH4&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="mi"&gt;5300&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5300&lt;/span&gt;
&lt;span class="c1"&gt;// contracts open (-3) - 2 - 1 + 4 = -2&lt;/span&gt;
                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;current_market_price&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now that we've covered the basis, you really only need to iterate all the orders, count buys as negative, sells as positive and, in the end, calculate whatever is open at current market price.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about performance?
&lt;/h2&gt;

&lt;p&gt;It's pretty evident that given a large amount of orders, doing this calculation for every single tick will become troublesome very fast. But as you've probably guessed by now, the system only needs to do that calculation on &lt;code&gt;fills&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As soon as an order gets filled, you calculate the P&amp;amp;L for all filled positions and cache it. You also cache the number of contracts open. Then you just need to do a &lt;strong&gt;single calculation&lt;/strong&gt; on price change:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pnl_realized + contracts_open * lot_size * current_market_price&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your system is not doing any sort of risk control, you can even push that calculation to the user GUI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commission fees?
&lt;/h2&gt;

&lt;p&gt;Once you get the mechanics, they are very simple to add. Even if your current broker offers a flat fee, you'll want to delegate such calculation to its own class, as it will make code more extensible to future changes. I'm thinking about quantity rebates, tiers, etc.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/3TnkHGJ" rel="noopener noreferrer"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OJQpz0s6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/amazon-phone-holder.png" alt="How to calculate P&amp;amp;L" width="800" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've recently been gifted this &lt;a href="https://amzn.to/49IebBr" rel="noopener noreferrer"&gt;Amazon Basics phone holder&lt;/a&gt;, and I'm converted.&lt;/p&gt;

&lt;p&gt;I use it to browse online newspappers during breakfast and it also serves as a recipe holder in the kitchen. It has proven its worth daily. Highly recommended!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>trivial</category>
      <category>pnl</category>
    </item>
    <item>
      <title>SSH, Mosh and tmux</title>
      <dc:creator>wasteofserver</dc:creator>
      <pubDate>Mon, 30 Oct 2023 18:00:22 +0000</pubDate>
      <link>https://dev.to/wasteofserver/ssh-mosh-and-tmux-2clo</link>
      <guid>https://dev.to/wasteofserver/ssh-mosh-and-tmux-2clo</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A1o0f2Ix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2023/10/ssh_mosh_tmux.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A1o0f2Ix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2023/10/ssh_mosh_tmux.jpeg" alt="SSH, Mosh and tmux" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This post was originally posted on &lt;a href="https://wasteofserver.com/ssh-mosh-and-tmux/" rel="noopener noreferrer"&gt;wasteofserver.com&lt;/a&gt;, you may find newer revisions and additional comments there.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;In one way or another, this post has been written thousands of times across the web. I'm &lt;strong&gt;rewriting it for two reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Command reference &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mosh&lt;/code&gt; under &lt;code&gt;Cygwin&lt;/code&gt;error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mosh (mobile shell)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://mosh.org/#usage" rel="noopener noreferrer"&gt;Mosh&lt;/a&gt; uses UDP to create an unbreakable ssh session. If you're on the road with flaky connections, it's a lifesaver.&lt;/p&gt;

&lt;p&gt;On Windows, you can use &lt;code&gt;mosh&lt;/code&gt; either in WSL or Cygwin. If using via Cygwin, you may stumble on the error &lt;em&gt;«Did not find remote IP address (is SSH ProxyCommand disabled?)».&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;mosh&lt;/span&gt; &lt;span class="n"&gt;frankie&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;
&lt;span class="nc"&gt;CreateProcessW&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="nl"&gt;error:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nl"&gt;posix_spawnp:&lt;/span&gt; &lt;span class="nc"&gt;No&lt;/span&gt; &lt;span class="n"&gt;such&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="nl"&gt;mosh:&lt;/span&gt; &lt;span class="nc"&gt;Did&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="no"&gt;IP&lt;/span&gt; &lt;span class="nf"&gt;address&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="no"&gt;SSH&lt;/span&gt; &lt;span class="nc"&gt;ProxyCommand&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;?).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix it, it's a simple as passing a few extra parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;mosh&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="n"&gt;frankie&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--predict=&lt;/code&gt;controls use of speculative local echo&lt;br&gt;&lt;br&gt;
&lt;code&gt;--experimental-remote-ip=&lt;/code&gt; used to discover IP address mosh connects to&lt;/p&gt;

&lt;p&gt;Notice that if you're having errors connecting, you should check if you're actually allowed to UDP into the server. For that, use &lt;code&gt;netcat&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;check connection to mosh server (generally port 60001)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  tmux (terminal multiplexer)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tmux/tmux/wiki" rel="noopener noreferrer"&gt;tmux&lt;/a&gt; puts your terminal connection into a proper session. Say you're working on a remote server from your home. You can disconnect that session. Go to work and resume the session exactly as it was.&lt;/p&gt;

&lt;p&gt;On top of that, it allows you to easily split the screen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tmux new&lt;/code&gt; - to start&lt;br&gt;&lt;br&gt;
&lt;code&gt;tmux new -s session-name&lt;/code&gt; - to name the session&lt;br&gt;&lt;br&gt;
&lt;code&gt;tmux -t session-name&lt;/code&gt; - to attach to the named session&lt;/p&gt;

&lt;p&gt;During the session, your go-to command is &lt;code&gt;Ctrl-b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl+b %&lt;/code&gt; - splits the screen vertically&lt;br&gt;&lt;br&gt;
&lt;code&gt;Ctrl+b "&lt;/code&gt; - splits the screen horizontally&lt;br&gt;&lt;br&gt;
&lt;code&gt;Ctrl+b x&lt;/code&gt; - closes current session&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S9Bkq64A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2023/10/mosh_tmux_better_ssh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S9Bkq64A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2023/10/mosh_tmux_better_ssh.jpeg" alt="SSH, Mosh and tmux" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enjoy your super-powered ssh connection!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://amzn.to/3IAVUdq" rel="noopener noreferrer"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8rxBVPzF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://wasteofserver.com/content/images/2024/03/output-onlinepngtools.png" alt="SSH, Mosh and tmux" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've never had one of these iPhone cables fail.  &lt;/p&gt;

&lt;p&gt;Even after being chewed up by Roomba. It's funny how &lt;a href="https://amzn.to/3IAVUdq" rel="noopener noreferrer"&gt;Amazon does a better iPhone cable&lt;/a&gt; than Apple, but there you have it. Can't recommend them enough.&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>utilities</category>
      <category>trivial</category>
    </item>
  </channel>
</rss>
