For reasons that (I swear!) have nothing to do with my nationality, I have always avoided acquiring any modicum of French language skills. I took Latin in high school and while at university, Spanish sounded like the clearly superior investment, especially for my social life. Hearing people speak French, I was having trouble holding it together, because, obviously this was a fake language designed to make you emit as many silly noncoherent noises as possible.
Similarly, I had put off learning Javascript until very recently. The language of “sprinkle some ads on it”, the language of ADHD-riddled kids who import a package to left-align a number, the language that makes the web an increasingly unpleasant experience. Javascript did not appeal to me.
l’ordinateur - the computer
$ 'wtf' instanceof String
> false
$ typeof 'wtf'
> 'string'
As happens from time to time, it is not by choice but rather by necessity that you have to pick up a skill. So while I am now forced to learn French for future foreign assignments, I also had to delve into the murky waters that are the Javascript “ecosystem”.
Ubuntu Touch & just a small bug
Flashing custom firmware on mobile phones has historically been a manual and painful process. All the more was I happy to hear that my buddy Konrad had worked hard to submit my favourite device, the Sony Xperia XZ, for inclusion in Ubuntu Touch’s fabulous interactive GUI installer.
There was just a small issue with the (Electron/Javascript-based) installer failing to verify a specific file.
error: Error: core:manual_download: Error: checksum mismatch
I know about Android, Linux administration and hardware debugging. I know where to look and I can solve issues rather quickly. From investing quite a bit of time, I’ve seen a lot of errors and know how to look for the part in the whole source tree that might be responsible.
With Javascript however - or any new and unfamiliar environment - hunting down bugs takes me ages. It’s incredibly frustrating, like you’re a preschooler being made to navigate complex algebra.
le magazine - the magazine; le magasin - the store
There are so many convenitions, syntax oddities and build/ecosystem tooling to learn, none of which I could give a rat’s ass about. I am not interested in any of the individual tools involved, I just want to solve a problem that has been caused by them.
Not to be deterred though, I dug in. This, treasured reader, is the story of that problem.
Promises and Lies
The code I am about to reference makes heavy use of
asynchronous Promise
functionality.
A basic usage pattern would look like this:
const prom = new Promise( (resolutionFunc, rejectionFunc) => {
console.log("Look, ma!");
return true;
});
With this block, a new Promise
object would be created (and immediately
executed). It executes the log statement and calls back to one of the two
callback functions, one for success (fulfilled) and one for failure
(rejected).
More commonly, developers use .then
to handle promise resolution:
const myPromise =
(new Promise(resolutionFunc))
.then(() =>
// handle first fulfilled, return second fulfilled
dostuff;
)
.then(() =>
// handle second fulfilled, return something
domorestuff;
)
.then(resolved => {
// handle domorestuff success
}, rejected {
// handle domorestuff failure
})
This is called a “chained promise” and is basically a try/catch
, but for
Javascript and asynchrooonouuuuus.
in, an, en, eau, eux - all pronounced pretty much the same
Having learned the semantics of promises, this block of code looked perfectly innocent to me:
return new Promise(resolve => {
// get user input for path - simplified here
return "/home/user/path/file.zip";
})
.then(downloadedFilePath => {
fs.ensureDir(
path.join(this.cachePath, this.props.config.codename, group)
)
.then(() =>
fs.copyFile(
downloadedFilePath,
path.join(
this.cachePath,
this.props.config.codename,
group,
file.name
)
)
);
})
.then(() =>
checkFile(
{
checksum: file.checksum,
path: path.join(
this.cachePath,
this.props.config.codename,
group,
file.name
)
},
true
)
)
.then(ok => {
if (ok) {
return ok;
} else {
throw new Error("checksum mismatch");
}
});
Simple, right? The first promise passes the downloadedFilePath
to the
ensureDir
func, which then triggers copyFile
. Once the block is resolved,
checkFile
is run on the copied file. All functions politely wait for the
previous func to resolve via the .then()
statement.
Or do they? Has this all been a lie?
Les Nombres
Similarly, lulled into a false sense of security, I thought I might approach learning French numbers above the single-digit range. Une, deux, trois… that’s practically Spanish, right? So how hard can those until 100 be?
4, 5, 6, 7, 8, 9, 10 - quatre, cinq, six, sept, huit, neuf, dix
So far, so good.
11, 12, 13, 14, 15, 16 - onze, douze, treize, quatorze, quinze, seize
Great!
17, 18, 19, 20, 21 - dix-sept, dix-huit, dix-neuf, vingt, vingt-et-un
Okay, small inconsistency, but I can get used to that…
30, 40, 50 - trente, quarante, cinquante
So from 20 onwards, there’s a nice pattern again. Sweet.
Continuing on with Javascript.
The intended flow of the specific part of the program I was looking at was:
- If file already present, checksum and compare with predefined checksum, on success skip all the next steps
- If checksum mismatch or file not present:
- Let user pick file with system file picker
- Return path of picked file as promise
- Ensure target dir inside cache is present
- Copy user-picked file to cache location (
~/.cache/ubports/<device>/firmware/
) - Checksum newly copied file
- Unpack newly copied file (extract
.img
from zip file)
To reiterate, the bug which I was encountering looked like this:
error: Error: core:manual_download: Error: checksum mismatch
I quickly narrowed down the issue to a call of checkFile()
in
src/core/plugins/core/plugin.js.
At first, I thought maybe the crypto library was failing to call the right hash functions, but when a file was already in the right place, the hashes were verified as correct.
I then quickly disabled the checksum verification after the file had been
copied and was met with a failure of the unpack
step, which complained about a
missing input file. The file was being copied alright, but it seems the checksum
and unpack steps were winning a race against the copyFile()
operation even
though I thought they were running in sequence.
After some further digging, maybe what I needed was the synchronous variant of
copyFile
? Swapped it for copyFileSync
, but no change in results.
I added an artificial delay of 100ms after the whole ensuredir/copy block and before the checksum, and everything went smoothly. So, why was the copy operation not being finished in time?
Having spent way too much time on this issue already, there must’ve been something wrong with all this asynchronous promising going on…
Shadows looming
Helpfully, Mozilla’s excellent MDN includes an admonition about the subtle ways you can shoot yourself in the foot:
An action can be assigned to an already “settled” promise. In that case, the action (if appropriate) will be performed at the first asynchronous opportunity. Note that promises are guaranteed to be asynchronous. Therefore, an action for an already “settled” promise will occur only after the stack has cleared and a clock-tick has passed. The effect is much like that of
setTimeout(action,10)
.
and this little snippet:
const promiseA = new Promise( (resolutionFunc,rejectionFunc) => {
resolutionFunc(777);
});
// At this point, "promiseA" is already settled.
promiseA.then( (val) => console.log("asynchronous logging has val:",val) );
console.log("immediate logging");
// produces output in this order:
// immediate logging
// asynchronous logging has val: 777
Not quite the problem I was facing, but a harbinger of things to come.
Now for the final nail in the coffin: Implied return statements.
This:
() => 1 // returns 1
is the same as this:
() => { return 1; } // returns 1
but not the same as this:
() => { 1; } // returns nothing
With all the pieces of the puzzle collected, we are ready to solve this riddle.
But not before going back again to the French numbers. We left off at 50 and I’m excited for what lies ahead:
60 - soixante
Excellent. This is beginning to make a lot of sense to me.
70 - soixante-dix (60 + 10)
What. The. Fuck.
71 - soixante et onze (60 + 11)
So from 70 on, they just use 60 as a base? Alright, very confusing, but I’m in it for the long haul, I can deal with that. How do the numbers continue after 70?
80 - quatre-vingts (4 x 20)
At this point I’m scrambling to cross-reference, because clear as day my study material has been written by a prankster. That can’t possibly be right, no?
90 - quatre-vingt-dix ((4 x 20) + 10)
By now I have consulted several sources and confirmed this is indeed how the French count. We might have been premature in mocking the Anglos for their imperial units. This is worse.
But I’ll leave les nombres be for now.
Deadly void
Aaaand back into the joyous world of machines! Let’s look at the mutinous code block again.
return new Promise(resolve => {
// Return downloadedFilePath
return "/home/user/path/file.zip";
})
// block start
.then(downloadedFilePath => {
fs.ensureDir("/home/user/path")
.then(() =>
fs.copyFile(
downloadedFilePath,
"/home/user/.cache/path/file.zip"
)
);
}) // block end
.then(() =>
checkFile();
)
.then(ok => {
if (ok) { // handle stuff
}
I’ve simplified it for better readibility. Astute readers will have spotted the error by now:
- The whole “block” was never going to resolve its promise with an actual return
value (inside curly braces but no
return
statement) checkFile()
was being called before any of the promise functions inside the previous block had the chance to resolve- The copied file was not yet available
checkFile
tried to verify a non-existent path- Checksum mismatch error.
This is the sort of bug that makes you want to strangle a kitten. To shout obscenities à la Captain Haddock. To permanently disable the power grid and return to farming sheep.
parle, parles, parle - I speak, you speak, he speaks - all pronounced the same
Solution & Reflections
The first solution I landed on was to make the whole block explicitly return something, in this case the value of the last chained promise:
.then(downloadedFilePath => {
// Added return statement below
return fs.ensureDir("/home/user/path")
.then(() =>
fs.copyFile(
downloadedFilePath,
"/home/user/.cache/path/file.zip"
)
);
})
I finally settled (after consultation with Marijn) on removing the curly braces to convert the block into an implied return, as that would fit better with the syntax of the surrounding code:
// Removed curly braces:
.then(downloadedFilePath =>
fs.ensureDir("/home/user/path")
.then(() =>
fs.copyFile(
downloadedFilePath,
"/home/user/.cache/path/file.zip"
)
)
)
I submitted a Pull Request, as you do nowadays, and waited for the fix to propagate to the users.
The saddest part about all this is that none of these functions need to be asynchronous. They are executed in sequence and the UI blocks until they return anyway.
The developer who wrote all this, Jan Sprinz, is a perfectly nice guy, and if it wasn’t for him, people would probably still have to fumble around with arcane command line tools to install Ubuntu Touch. I can’t fault him for seeing common patterns in Javascript development and, thinking they’re good practices, writing the whole function using promises.
In conclusion, programming is terrible, hug your loved ones, go outside and enjoy the sun.
P.S.
Huge thanks to my buddy Marijn for proofreading and giving corrections!