Monday, August 3, 2026

Firefox settings

Settings needed in order to make Firefox load bookmarks in a background tab and without closing the open bookmarks folder dropdown:
 
browser.tabs.loadBookmarksInBackground = true 
browser.bookmarks.openInTabClosesMenu = false 

Automatically start downloads:
 
browser.download.useDownloadDir 

Enable revamped sidebar:

sidebar.revamp = true


Sunday, March 8, 2026

ELRS tooling

Tooling for configuring ELRS for drones & controllers.

Install missing drivers for Windows.

Flash EdgeTX on controllers.

Friday, September 12, 2025

Mass Effect 2 Lower Afterlife Music (Lo Fi Epic)

So for you Mass Effect 2 Fans here is perhaps one of the oddest "making of stories" you will hear. This track "Lo Fi Epic" (The lower afterlife track) was actually composed for the EA Sports Arena Football game in February of 2006 but never saw the light of day, hitting the cutting room floor after a producer nixed it for being "Too Techno" . The track went into the music archives and lived on the main server network for years.

Fast forward to 2010 when I was sitting down with my new copy of Mass Effect 2. I am a huge Mass Effect fan, I had played through version 1 many times. So one can only imagine my reaction when I walked into the lower level of "Club Afterlife" and heard this track... At first I said "hummm that sounds very familiar" then it hit me "Holy crap I wrote that!".

Sadly I was not credited in the game for this piece of music but neither was:

Saki Kaskas - Callista - Upper Club Afterlife (Originally used in Need For Speed Hot Pursuit)

John Morgan - Happiness - Dark Star Lounge

Comaduster - To Hide To Seek - Eternity Bar

This story got even crazier when I Googled the name of the ME2 planet this music was found on, there was a user on You Tube that had extracted the file from the PC version of the game and identified me as the composer; his video had over 100,000 views with many comments about who wrote it and where they could find it. EA Traxx or whoever passed along the cue to Bioware renamed the track "The Techno Madness Mix" but had left my name in file name or metadata.

I had actually met the sound design team for Mass Effect up in Canada during a sound design for video game meeting; Simon Pressy the Mass Effect audio director and I talked at length during that meeting, I expressed my interest and support for their projects. While working on Warhammer I even helped produced some sounds for their games to use. So I can't say I don't know how this all happened but in my wildest dreams I would have never imagined Lo Fi Epic making it into Mass Effect 2 much less the fans reaction to this track (Thank you all). I even got an email from the Mass Effects' main composer Jack Wall that stated he heard so many good things from fans about "Lo Fi Epic" (Of course I am in awe of Jacks score for the game).

Hearing "Lo Fi Epic" in game reminds me of the days when I use to have acetates cut of my music and taking it to clubs. That feeling of watching hundreds of people dance to something I created was beyond words... this track in Mass Effect immortalized that feeling for me. Anyway that is the crazy story of Lo Fi Epic and Mass Effect 2.

Monday, March 3, 2025

Manifest V2

 To continue using Manifest V2 extensions:
  1. Go to: chrome://flags/
  2. Search for "mv2"
  3. Set "Allow legacy extension manifest versions" to Enabled and the 3 others to Disabled:
    1. "Extension Manifest V2 Deprecation Warning Stage"
    2. "Extension Manifest V2 Deprecation Disabled Stage"
    3. "Extension Manifest V2 Deprecation Unsupported Stage"

Einhell

  • TC = classic you want to avoid
  • TE = expert, could be good, especially taking the price into account.
  • EP = ‘professional’, the best they offer

Tuesday, October 1, 2024

Type 'Express' is not assignable to type 'Application'.

We've had a lot of problems with the fact that the DefinitelyTyped packages for Express consists of multiple separate packages (notably @types/express and @types/express-serve-static-core) and that new versions of @types/express don't work unless you update @types/express-serve-static-core too but that the dependencies between them are not declared tightly enough.

https://github.com/apollographql/apollo-server/issues/5280

Use:

npm dedup

Thursday, September 12, 2024

/etc/wsl.conf

 My WSL configuration

[automount]
enabled = true
options = "metadata,umask=22,fmask=11"

Monday, July 1, 2024

Twitter: remove all likes

 The following block will remove all your Twitter likes, if you are on your profile's /likes page:

{
  const sleep = t => new Promise(r => setTimeout(r, t));
  let i = 0;
  while (true) {
    const nodes = document.querySelectorAll('button[data-testid="unlike"]');
    if (nodes.length === 0) {
      console.log('NOOP');
      await sleep(2_000);
      if (++i === 10) {
        console.log('DONE');
        break;
      }
    } else {
      i = 0;
    }  
        
    let j = 0;
    for (const node of nodes) {
      console.log(`${++j} / ${nodes.length}`);
      node.scrollIntoView();
      node.click();
      await sleep(2_000);
    }
  }
}

Wednesday, June 26, 2024

Error 80072EFE during Windows 7 update

Best zstd compression

The highest zstd compression ratio is achieved with zstd -22 --ultra --single-thread
The -22 gives the highest supported compression level.
The --ultra is needed to get above -19.
The --single-thread preempts some issues with the file being corrupted.
(See: https://github.com/facebook/zstd/issues/3350#issuecomment-1352455838) 

Monday, May 13, 2024

Twitter: collect all links to posts on a page

The following snippet executed in the Developer Console will help you collect all the links to individual posts on a profile. Once the script has been executed, all you'll need to do is to scroll down (or navigate from post to post using the "J" key).

window.links = new Set();

window._updateLinks = () => {
    Array.from(document.querySelectorAll('a > time'))
        .map(node => node.parentNode.href)
        .forEach(link => window.links.add(link));
}

window._handleScroll = () => {
    _updateLinks();
    console.log("Links collected:", window.links.size);
}

window.addEventListener('scroll', _handleScroll);

Once you're ready to collect the data, you can copy it to the clipboard using:

copy(Array.from(window.links).join("\n"));

This will copy the links to all the posts you've scrolled through, one link per line.
Useful e.g. when wanting to archive the content from a profile.

Thursday, December 14, 2023

Java: streaming WARC files

Previously, I published in a separate post a minimal example of some Java code that reads WARC data, using the "jwat-warc" library. After that, I wanted to compare the performance of the two major WARC parsing libraries ("jwat-warc" and "jwarc"), in the context of a more complex process.

The difficulty in doing this is that the major libraries have different approaches to how they expect you to consume the data.

e.g.

  • "jwat-warc" exposes a stream of the underlying HTML, while "jwarc" exposes a stream containing both the underlying HTTP headers and the HTML.
  • "jwat-warc" exposes the HTTP response code, while when using "jwarc" they have to be extracted from the headers.
  • etc. 

Here is the interface I came up with that represents a fully loaded WARC record, independent of the underlying implementation, alongside two static utility functions for constructing an XWarcRecord instance from the library-specific WarcRecord instance:

public abstract class XWarcRecord {
    protected String _uri;
    protected String _payload;

    abstract public String responseCode();
    abstract public String payload();
    abstract public String uri();
 
    public static XWarcRecord from(org.jwat.warc.WarcRecord r) {
        return new XWarcRecord_JWat(r);
    }
    
    public static XWarcRecord from(org.netpreserve.jwarc.WarcRecord r) {
        return new XWarcRecord_JWarc(r);
    }
}

And the particular implementations, with some optimizations to defer as much processing as possible to the time when a particular data point is needed:

I. "jwat-warc"

For some reason, if you don't read the stream manually, the reader will throw an error when advancing to the next record. So we have to do that in the constructor. 

public class XWarcRecord_JWat extends XWarcRecord {
    private final WarcRecord r;

    XWarcRecord_JWat(WarcRecord r) {
        this.r = r;

        try {
            InputStream contentStream = r.getPayloadContent();
            this._payload = new String(contentStream.readAllBytes(), Charsets.UTF_8);
        } catch (IOException | NullPointerException e) {
            this._payload = null;
        }
    }

    @Override
    public String responseCode() {
        if (r.getHttpHeader() == null) {
            return "0"; 
        }
        return r.getHttpHeader().statusCodeStr;
    }

    @Override
    public String payload() {
        return _payload;
    }

    @Override
    public String uri() {
        if (_uri == null) {
            _uri = r.getHeader("WARC-Target-URI").value;
        }
        return _uri;
    }
}

II. "jwarc" 

The stream they expose includes both the HTTP headers and the HTML (or other content), so we have to extract them manually. 

public class XWarcRecord_JWarc extends XWarcRecord {
    protected String _headers = null;
    protected String _responseCode = null; 
    private final WarcRecord r;

    XWarcRecord_JWarc(WarcRecord r) {
        this.r = r;
    }

    @Override
    public String responseCode() {
        if (_responseCode == null) {
            _parseContent();
            _responseCode = _responseCode(this._headers);
        }
        return _responseCode;
    }

    @Override
    public String payload() {
        if (this._payload == null) {
            this._parseContent();
        }
        return this._payload;
    }

    @Override
    public String uri() {
        if (_uri == null) {
            _uri = r.headers().first("WARC-Target-URI").orElse(null);
        }
        return _uri;
    }

    private void _parseContent() {
        List<String> h = new ArrayList<>();
        List<String> p = new ArrayList<>();

        try (MessageBody body = r.body();
             BufferedReader reader = new BufferedReader(new InputStreamReader(body.stream(), StandardCharsets.UTF_8))) {

            String line;
            while ((line = reader.readLine()) != null) {
                if (line.isEmpty()) {
                    break;
                }
                h.add(line);
            }

            while ((line = reader.readLine()) != null) {
                p.add(line);
            }

            this._headers = String.join("\n", h);
            this._payload = String.join("\n", p);
        } catch (IOException | NullPointerException e) {
            throw new RuntimeException(e);
        }
    }

    private static final Pattern P = Pattern.compile("HTTP/\\d\\.\\d\\s+(\\d{3})");

    private static String _responseCode(String input) {
        Matcher matcher = P.matcher(input);
        if (matcher.find()) {
            return matcher.group(1);
        } else {
            return "0";
        }
    }
}
 
There was no observed performance difference between the two, but coming up with a solution to abstract away the underlying implementation was an interesting exercise. 

Monday, December 11, 2023

Throttle vs Debounce

The following page will offer a very nice JavaScript illustration of the difference between throttle and debounce: https://web.archive.org/web/20220128120157/http://demo.nimius.net/debounce_throttle/

The terms make sense in the context when you want to control the time when an "effect" is triggered, based on the timing of the "cause" (I am using these terms very generally: "cause and effect").

You "throttle" when you want the effect to be spaced apart by a minimum interval of X time.
You "debounce" when you want the effect to be triggered after the cause has "cooled off" for enough (X) time. 

 

Friday, December 8, 2023

Install Node.js / npm on Linux

The easiest way to manage Node.js / npm on Linux is by using the Node Version Manager:
https://github.com/nvm-sh/nvm

I. Install NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

II. Use NVM to install the latest LTS release of Node.js / npm

nvm install --lts

III. List versions

nvm ls

This will output a mix of both installed and available versions (including all LTS releases)

            N/A
default -> lts/* (-> N/A)
iojs -> N/A (default)
node -> stable (-> N/A) (default)
unstable -> N/A (default)
lts/* -> lts/iron (-> N/A)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.2 (-> N/A)
lts/hydrogen -> v18.19.0 (-> N/A)
lts/iron -> v20.10.0 (-> N/A)

When installing and uninstalling specific versions, you can use both the numeric version, or the release designation (e.g. v16.20.2 and lts/gallium are interchangeable)

IV. Uninstall a specific version

nvm uninstall lts/iron

V. Install a specific version

nvm install lts/hydrogen

VI. Use a specific version

nvm use lts/gallium

If you're running an older version of Linux, you may only have access to older Node.js versions, because a dependency on the GNU C Library (glibc).

Trying to run anything newer than lts/gallium on Amazon Linux 2 will throw the following:

node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)

You can update the NPM version independently from Node.js, but this too has a limit. Gallium comes with version 8. At the time of this writing, it prompts that version 10 is available, but you can only upgrade up to version 9.

npm install -g npm@9

(It will complain about incompatible versions if you try to install version 10).

Saturday, November 25, 2023

IMDSv2

v2 of the AWS "Instance Metadata Service" has been around for a while.

  • It is optional for now, but they will make it mandatory some time during 2024 [1].
  • The IMDS has to do with how EC2 instances get their metadata and IAM permissions.
  • If you're using a recent version of cloud-init, AWS CLI and SDKs, these should support v2.
  • By default, most instances that have been around for a while still use v1.

To list instances that are still using v1 [2]:

aws ec2 describe-instances \
  --filters "Name=metadata-options.http-tokens,Values=optional" \
  --query "Reservations[*].Instances[*].[InstanceId]" \
  --output text

To enable v2 on a per-instance basis:

aws ec2 modify-instance-metadata-options \
  --instance-id "${EC2_ID}" \
  --http-endpoint enabled \
  --http-tokens required

To change an AMI so that instances launched from it have v2 enabled:

aws ec2 modify-image-attribute \
  --image-id "${AMI_ID}" \
  --imds-support v2.0

You will have to change how you retrieve instance metadata URIs [3].

v1:

curl -s http://169.254.169.254/latest/meta-data/instance-id

v2:

TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")"

curl -s -H "X-aws-ec2-metadata-token: $TOKEN"  \

Thursday, November 9, 2023

Debugging Linux input events

I recently had some issues with a laptop running Linux receiving spurious keyboard events.
The following two commands are useful in debugging this:

sudo tail -f /dev/input/event*

And:

xinput test-xi2 --root 5

You can replace the "5" with any other value to view input from other devices.


Saturday, November 4, 2023

Is your AWS instance Xen or Nitro?

This command will let you know whether the AWS instance you are logged into is Xen or Nitro-based:

curl http://169.254.169.254/latest/meta-data/system/

AWS has a page about Nitro, but not one about Xen, as that is considered a legacy virtualizer:
https://aws.amazon.com/ec2/nitro/