Sunday, 29 November 2020

Micro-optimisation #1874: NPM script targets

These days I spend most of my working day writing TypeScript/Node/React apps. I still love (and work with) Scala but in the main, it's the faster-moving Javascript world where most of the changes are taking place. One of the best things about the NPM/Yarn workflow that these apps all share, is the ability to declare "scripts" to shortcut common development tasks. It's not new (make has entered the chat) but it's very flexible and powerful. The only downside is, there's no definitive convention for naming the tasks.

One project might use start (i.e. yarn start) to launch the application in development mode (e.g. with hot-reload and suchlike) while another might use run:local (i.e. yarn run:local) for a similar thing. The upshot being, a developer ends up opening package.json in some way, scrolling down to the scripts stanza and looking for their desired task, before carefully typing it in at the command prompt. Can we do better?


Phase 1: The 's' alias

Utilising the wonderful jq, we can very easily get a very nice first pass at streamlining the flow:
alias s='cat package.json | jq .scripts'
This eliminates scrolliing past all the unwanted noise of the package.json (dependencies, jest configuration, etc etc) and just gives a nice list of the scripts:
john$ s
{
  "build": "rm -rf dist && yarn compile && node scripts/build.js ",
  "compile": "tsc -p .",
  "compile:watch": "tsc --watch",
  "lint": "yarn eslint . --ext .ts",
  "start:dev": "source ./scripts/init_dev.sh && concurrently \"yarn compile:watch\" \"nodemon\"",
  "start": "source ./scripts/init_dev.sh && yarn compile && node dist/index",
  "test": "NODE_ENV=test jest --runInBand",
  "test:watch": "yarn test --watch",
  "test:coverage": "yarn test --coverage"
}
john$ 
A nice start. But now while you can see the list of targets, you've still got to (ugh) type one in.
What if ...

Phase 2: Menu-driven

TIL about the select BASH built-in command which will make an interactive menu out of a list of options. So let's do it!

~/bin/menufy_package_json.sh
#!/bin/bash
  
# Show the scripts in alphabetical order, so as to match the
# numbered options shown later
cat package.json | jq '.scripts | to_entries | sort_by(.key) | from_entries'

SCRIPTS=$(cat package.json | jq '.scripts | keys | .[]' --raw-output)

select script in $SCRIPTS
do
  yarn $script
  break
done
I've got that aliased to sm (for "script menu") so here's what the flow looks like now:
john$ sm
{
  "build": "rm -rf dist && yarn compile && node scripts/build.js ",
  "compile": "tsc -p .",
  "compile:watch": "tsc --watch",
  "lint": "yarn eslint . --ext .ts",
  "start": "source ./scripts/init_dev.sh && yarn compile && node dist/index",
  "start:dev": "source ./scripts/init_dev.sh && concurrently \"yarn compile:watch\" \"nodemon\"",
  "test": "NODE_ENV=test jest --runInBand",
  "test:coverage": "yarn test --coverage",
  "test:watch": "yarn test --watch"
}
1) build	  4) lint	    7) test
2) compile	  5) start	    8) test:coverage
3) compile:watch  6) start:dev	    9) test:watch
#? 9
yarn run v1.21.1
$ yarn test --watch
$ NODE_ENV=test jest --runInBand --watch
... and away it goes. For a typical command like yarn test:watch I've gone from 15 keystrokes plus [Enter] to sm[Enter]9[Enter] => five keystrokes, and that's not even including the time/keystroke saving of showing the potential targets in the first place instead of opening package.json in some way and scrolling. For something I might do tens of times a day, I call that a win!

Saturday, 31 October 2020

Micro-optimisation #392: Log-macros!

Something I find myself doing a lot in the Javascript/Node/TypeScript world is logging out an object to the console. But of course if you're not careful you end up logging the oh-so-useful [Object object], so you need to wrap your thing in JSON.stringify() to get something readable.

I got heartily sick of doing this so created a couple of custom keybindings for VS Code to automate things.

Wrap in JSON.stringify - [ Cmd + Shift + J ]

Takes the selected text and wraps it in a call to JSON.stringify() with null, 2 as the second and third args to make it nicely indented (because why not given it's a macro?); e.g.:

console.log(`Received backEndResponse`)
becomes:
console.log(`Received ${JSON.stringify(backEndResponse, null, 2)}`)

Label and Wrap in JSON.stringify - [ Cmd + Shift + Alt + J ]

As the previous macro, but repeats the name of the variable with a colon followed by the JSON, for clarity as to what's being logged; e.g.:

console.log(`New localState`)
becomes:
console.log(`New localState: ${JSON.stringify(localState, null, 2)}`)

How do I set these?

On the Mac you can use ⌘-K-S to see the pretty shortcut list:

// Place your key bindings in this file to override the defaults
[
  {
    "key": "cmd+shift+j",
    "command": "editor.action.insertSnippet",
    "when": "editorTextFocus",
    "args": {
      "snippet": "JSON.stringify(${TM_SELECTED_TEXT}$1, null, 2)$0"
    }
  },
  {
    "key": "cmd+shift+alt+j",
    "command": "editor.action.insertSnippet",
    "when": "editorTextFocus",
    "args": {
      "snippet": "${TM_SELECTED_TEXT}: ${JSON.stringify(${TM_SELECTED_TEXT}$1, null, 2)}$0"
    }
  }
]

Sunday, 13 September 2020

Micro-optimisation #9725: Checkout the mainline

Very soon (October 1, 2020) Github will be making main the default branch of all new repositories instead of master. While you make the transition over to the new naming convention, it's handy to have an abstraction over the top for frequently-issued commands. For me, git checkout master is one of my faves, so much so that I've already aliased it to gcm. Which actually makes this easier - main and master start with the same letter...

Now when I issue the gcm command, it'll check if main exists, and if not, try master and remind me that this repo needs to be migrated. Here's the script

~/bin/checkout-main-or-master.sh:
#!/bin/bash

# Try main, else master and warn about outdated branch name

MAIN_BRANCH=`git branch -l | grep main`

if [[ ! -z ${MAIN_BRANCH} ]]; then
  git checkout main
else 
  echo "No main branch found, using master... please fix this repo!"
  git checkout master
fi



I run it using this alias:

alias gcm='~/bin/checkout-main-or-master.sh'

So a typical execution looks like this:

mymac:foo john$ gcm
No main branch found, using master... please fix this repo!
Switched to branch 'master'
Your branch is up to date with 'origin/master'.       
mymac:foo john$ 

Monday, 24 August 2020

Micro-optimisation #6587: Git push to Github

I've said it before; sometimes the best automations are the tiny ones that save a few knob-twirls, keystrokes or (as in this case) a drag-copy-paste, each and every day.

It's just a tiny thing, but I like it when a workflow gets streamlined. If you work on a modern Github-hosted codebase with a Pull-Request-based flow, you'll spend more than a few seconds a week looking at this kind of output, which happens the first time you try to git push to a remote that doesn't have your branch:

mymac:foo john$ git push
fatal: The current branch red-text has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin red-text

mymac:foo john$ git push --set-upstream origin red-text
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (24/24), 2.79 KiB | 953.00 KiB/s, done.
Total 24 (delta 9), reused 0 (delta 0)
remote: Resolving deltas: 100% (9/9), completed with 9 local objects.
remote: 
remote: Create a pull request for 'red-text' on GitHub by visiting:
remote:      https://github.com/my-org/foo/pull/new/red-text
remote: 
To https://github.com/my-org/foo.git
 * [new branch]        red-text -> red-text
Branch 'red-text' set up to track remote branch 'red-text' from 'origin'.

The desired workflow is clear and simple:

  • Push and set the upstream as suggested
  • Grab the suggested PR URL
  • Use the Mac open command to launch a new browser window for that URL
... so lets automate it!

~/bin/push-to-github.sh:
#!/bin/bash

# Try and push, catch the suggested action if there is one:

SUGG=`git push --porcelain 2>&1 | grep "git push --set-upstream origin"` 

if [[ ! -z ${SUGG} ]]; then
  echo "Doing suggested: ${SUGG}"

  URL=`${SUGG} --porcelain 2>&1 | grep remote | grep new | grep -o "https.*"`

  if [[ ! -z ${URL} ]]; then
    echo "Opening URL ${URL}"
    open $URL
  else
    echo "No PR URL found, doing nothing"
  fi
fi

I run it using this alias:

alias gpgh='~/bin/push-to-github.sh'

So a typical execution looks like this:

mymac:foo john$ gpgh
Doing suggested:     git push --set-upstream origin mybranch
Opening URL https://github.com/my-org/someproject/pull/new/mybranch        
mymac:foo john$ 

Wednesday, 29 July 2020

TASCAM FireOne on MacOS High Sierra: finally dead

I suppose it had to happen, but today, my TASCAM FireOne Firewire audio interface just ceased to work properly - namely, the audio input has a constant clicking sound making it unusable.

I suppose I should feel fortunate that it has lasted this long; I mean, look at the MacOS compatibility chart:

- yep 10.4 and 10.5 only, yet here I am on High Sierra (10.13) and it's only just turned up its toes.

It's even less whelming on the Windows side:

... XP only (!)

So now I'm on the hunt for a good interface that will last as long as this one did. Firewire seems to have been effectively killed by Apple, and Thunderbolt interfaces are incredibly expensive, so it'll be back to good ol' USB I guess. I'm thinking that with the rise of podcasts etc, Apple will be obligated to ensure USB audio interfaces that use no additional drivers (aka "Class-Compliant devices") work really well for the foreseeable future...

Thursday, 25 June 2020

Home-Grown Mesh Networking Part 2 - When It Doesn't Work ...

A few months ago I shared some tips on using existing Wifi gear to make your own mesh network.

Turns out though, it might not be as easy as I initially made out. In particular, I was noticing the expected switchover as I walked down the corridor in the middle of my house:


... was simply not happening. I would be "stuck" on Channel 3 (red) or Channel 9 (green) based on whatever my Mac had woken up with.

Lots of Googling later, and the simplest diagnostic tool on the Mac turns out to be Wireless Diagnostics -> Info - take a snapshot, turn off that AP, and wait for the UI to update. Then stick them side by side and eyeball them:


I wasted quite some time following a wild goose because of the differing Country Codes - it's not really something you can change in most consumer AP/routers so I thought I was in trouble, until I discovered that "X1" really just means "not broadcast" so I decided to ignore it, which turns out to be fine.

Now the other thing that was most definitely not fine was the different Security policies. I thought I had these set up to match perfectly, but as you can see, MacOS thought different (pun not intended).

When is WPA2 Not WPA2?


The D-Link AP (on Channel 9, on the right in the above screenshot) was supposedly in "WPA2 Personal" mode but the Mac was diagnosing it as just WPA v1. This is most definitely enough of a difference for it to NOT seamlessly switch channels. Even more confusingly, some parts of the MacOS network stack will report this as WPA2. It's quite tricky to sort out, especially when you have access points of different vintages, from different manufacturers, who use different terminology, but what worked for me (on the problematic D-Link) was using Security Mode WPA-Personal together with WPA Mode WPA2 plus explicitly setting the Cipher Type to AES and not the default TKIP and AES:



This last change did the trick for me, and I was able to get some automatic channel-switching, but the Mac was still holding on to the Channel 3 network for much longer than I would have liked. I could even stand in the same room as the Channel 9 AP (i.e. in the bottom-left corner of the heat map above) and not switch to Channel 9.

Performance Anxiety


The clue, yet again, is in the Info window above. In particular, the Tx Rate field. It would seem that rather than just na├»vely choosing an AP based on signal strength, MacOS instead (or perhaps also) checks the network performance of the candidate AP. And look at the difference between the newer dual-band Linksys on Channel 3 (145 Mbps) and the D-Link on Channel 9 (26 Mbps)!

There are plenty of ways to increase your wireless data rate, the most effective being switching to be 802.11n exclusively, as supporting -b and -g devices slows down the whole network, but (as usual) I hit a snag - my Brother 2130w wireless (-only) laser printer needs to have an 802.11g network. As it lives in the study, a couple of metres from the D-Link AP, I'd had the D-Link running "mixed mode" to support it.

Printers were sent from hell to make us miserable


As it turns out, the mixed-mode signal from the Linksys at the other end of the house is good enough for the printer (being mains-powered it's probably got very robust WiFi) and so I could move the D-Link to "n-only". But there was a trick. There's always a trick ...
You need to make sure the printer powers up onto the 802.11g network - it doesn't seem to be able to "roam" - which, again, makes sense - it's a printer. It knows to join a network called MILLHOUSE and will attempt to do so - and if the "n-only" network is there, it'll try to join it and never succeed.
So powering down the n-only AP, rebooting the printer, checking it's online (ping it), then powering the n-only AP back up again should do the trick.

Summary


Moving the D-Link AP to n-only doubled the typical Tx Rate at the front of the house to around 50 Mbps, the result being that the Mac now considers Channel 9 to be good enough to switch to as I move towards that end of the house. It still doesn't switch quite as fast as I'd like, but it gets there, and doesn't drop any connections while it switches, which is great.

Here's the summary of the setup now:


Living RoomStudy
DeviceLinksys X6200D-Link DIR-655
IP Address10.240.0.210.240.0.3
Channel39
Modeb/g/nn-only
Band2.4 GHz2.4 GHz
Bandwidth20 MHz20 MHz
SecurityWPA2 PersonalWPA Personal (sic)
WPA Moden/aWPA2 Only
Cipher Typen/aAES Only


Stand by for even more excruciating detail about my home network in future updates!



Sunday, 17 May 2020

Home Automation In The Small; Part 2

Continuing on the theme of home automation in the small, here's another tiny but pleasing hack that leverages the Chromecast and Yamaha receiver bindings in OpenHAB.

To conclude a happy Spotify listening session, we like to tell the Google Home to "stop the music and turn off the Living Room TV" - "Living Room TV" being the name of the Chromecast attached to HDMI2 of the Yamaha receiver.

While this does stop the music and turn off the television, the amplifier remains powered up. Probably another weird HDMI control thing. It's just a small detail, but power wastage annoys me, so here's the fix.

The trick with this one is ensuring we catch the correct state transition; namely, that the Chromecast's running "app" is the Backdrop and the state is "idling". If those conditions are true, but the amp is still listening to HDMI2, there's obviously nothing else interesting being routed through the amp so it's safe to shut it down. Note that the type of LivingRoomTV_Idling.state is an OnOffType so we don't compare to "ON", it has to be ON (i.e. it's an enumerated value) - some fun Java legacy there ...

rules/chromecast-powerdown.rules

rule "Ensure Yamaha amp turns off when Chromecast does"
when
  Item LivingRoomTV_App changed
then
  logInfo("RULE.CCP", "Chromecast app: " + LivingRoomTV_App.state)
  logInfo("RULE.CCP", "Chromecast idle: " + LivingRoomTV_Idling.state)
  logInfo("RULE.CCP", "Yamaha input: " + Yamaha_Input.state )

  if (LivingRoomTV_App.state == "Backdrop") {
    if (LivingRoomTV_Idling.state == ON) {
       if (Yamaha_Input.state == "HDMI2") {
         logInfo("RULE.CCP", "Forcing Yamaha to power off") 
         Yamaha_Power.sendCommand("OFF")
       }
     }
  }
end