Skip to content

Commit 1ae11c0

Browse files
nija-atrix0rrr
authored andcommitted
fix(aws-cdk): Move version check TTL file to home directory (#2774)
Originally, the version check TTL file was located alongside the CDK node installation directory. This caused issues for people who had CDK installed on a readonly filesystem. This change moves this file to the user's home directory, specifically under $HOMEDIR/.cdk/cache. This directory is already being used by account-cache.ts. The old logic is maintained where a repo check is run once a day against NPM, when a CDK command is invoked, and when a new version is present will generate a console banner notifying the user of this. The edge case with the updated implementation is when there are 2+ active installations of CDK and they are used interchangably. When this happens, the user will only get one notification per non-latest installation of CDK per day.
1 parent bbc9ef7 commit 1ae11c0

File tree

3 files changed

+93
-28
lines changed

3 files changed

+93
-28
lines changed

packages/aws-cdk/.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ dist
77

88
# Generated by generate.sh
99
build-info.json
10-
.LAST_VERSION_CHECK
1110

1211
.LAST_BUILD
1312
.nyc_output

packages/aws-cdk/lib/version.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { exec as _exec } from 'child_process';
22
import colors = require('colors/safe');
3-
import { close as _close, open as _open, stat as _stat } from 'fs';
3+
import fs = require('fs-extra');
4+
import os = require('os');
5+
import path = require('path');
46
import semver = require('semver');
57
import { promisify } from 'util';
6-
import { debug, print, warning } from '../lib/logging';
8+
import { debug, print } from '../lib/logging';
79
import { formatAsBanner } from '../lib/util/console-formatters';
810

911
const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60;
1012

11-
const close = promisify(_close);
1213
const exec = promisify(_exec);
13-
const open = promisify(_open);
14-
const stat = promisify(_stat);
1514

1615
export const DISPLAY_VERSION = `${versionNumber()} (build ${commit()})`;
1716

@@ -23,23 +22,39 @@ function commit(): string {
2322
return require('../build-info.json').commit;
2423
}
2524

26-
export class TimestampFile {
25+
export class VersionCheckTTL {
26+
public static timestampFilePath(): string {
27+
// Get the home directory from the OS, first. Fallback to $HOME.
28+
const homedir = os.userInfo().homedir || os.homedir();
29+
if (!homedir || !homedir.trim()) {
30+
throw new Error('Cannot determine home directory');
31+
}
32+
// Using the same path from account-cache.ts
33+
return path.join(homedir, '.cdk', 'cache', 'repo-version-ttl');
34+
}
35+
2736
private readonly file: string;
2837

29-
// File modify times are accurate only till the second, hence using seconds as precision
38+
// File modify times are accurate only to the second
3039
private readonly ttlSecs: number;
3140

32-
constructor(file: string, ttlSecs: number) {
33-
this.file = file;
34-
this.ttlSecs = ttlSecs;
41+
constructor(file?: string, ttlSecs?: number) {
42+
this.file = file || VersionCheckTTL.timestampFilePath();
43+
try {
44+
fs.mkdirsSync(path.dirname(this.file));
45+
fs.accessSync(path.dirname(this.file), fs.constants.W_OK);
46+
} catch {
47+
throw new Error(`Directory (${path.dirname(this.file)}) is not writable.`);
48+
}
49+
this.ttlSecs = ttlSecs || ONE_DAY_IN_SECONDS;
3550
}
3651

3752
public async hasExpired(): Promise<boolean> {
3853
try {
39-
const lastCheckTime = (await stat(this.file)).mtimeMs;
54+
const lastCheckTime = (await fs.stat(this.file)).mtimeMs;
4055
const today = new Date().getTime();
4156

42-
if ((today - lastCheckTime) / 1000 > this.ttlSecs) { // convert ms to secs
57+
if ((today - lastCheckTime) / 1000 > this.ttlSecs) { // convert ms to sec
4358
return true;
4459
}
4560
return false;
@@ -52,15 +67,17 @@ export class TimestampFile {
5267
}
5368
}
5469

55-
public async update(): Promise<void> {
56-
const fd = await open(this.file, 'w');
57-
await close(fd);
70+
public async update(latestVersion?: string): Promise<void> {
71+
if (!latestVersion) {
72+
latestVersion = '';
73+
}
74+
await fs.writeFile(this.file, latestVersion);
5875
}
5976
}
6077

6178
// Export for unit testing only.
6279
// Don't use directly, use displayVersionMessage() instead.
63-
export async function latestVersionIfHigher(currentVersion: string, cacheFile: TimestampFile): Promise<string | null> {
80+
export async function latestVersionIfHigher(currentVersion: string, cacheFile: VersionCheckTTL): Promise<string|null> {
6481
if (!(await cacheFile.hasExpired())) {
6582
return null;
6683
}
@@ -74,7 +91,7 @@ export async function latestVersionIfHigher(currentVersion: string, cacheFile: T
7491
throw new Error(`npm returned an invalid semver ${latestVersion}`);
7592
}
7693
const isNewer = semver.gt(latestVersion, currentVersion);
77-
await cacheFile.update();
94+
await cacheFile.update(latestVersion);
7895

7996
if (isNewer) {
8097
return latestVersion;
@@ -83,14 +100,13 @@ export async function latestVersionIfHigher(currentVersion: string, cacheFile: T
83100
}
84101
}
85102

86-
const versionCheckCache = new TimestampFile(`${__dirname}/../.LAST_VERSION_CHECK`, ONE_DAY_IN_SECONDS);
87-
88103
export async function displayVersionMessage(): Promise<void> {
89104
if (!process.stdout.isTTY) {
90105
return;
91106
}
92107

93108
try {
109+
const versionCheckCache = new VersionCheckTTL();
94110
const laterVersion = await latestVersionIfHigher(versionNumber(), versionCheckCache);
95111
if (laterVersion) {
96112
const bannerMsg = formatAsBanner([
@@ -100,6 +116,6 @@ export async function displayVersionMessage(): Promise<void> {
100116
bannerMsg.forEach((e) => print(e));
101117
}
102118
} catch (err) {
103-
warning(`Could not run version check due to error ${err.message}`);
119+
debug(`Could not run version check - ${err.message}`);
104120
}
105-
}
121+
}

packages/aws-cdk/test/test.version.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import fs = require('fs-extra');
12
import { Test } from 'nodeunit';
3+
import os = require('os');
4+
import path = require('path');
5+
import sinon = require('sinon');
26
import { setTimeout as _setTimeout } from 'timers';
37
import { promisify } from 'util';
4-
import { latestVersionIfHigher, TimestampFile } from '../lib/version';
8+
import { latestVersionIfHigher, VersionCheckTTL } from '../lib/version';
59

610
const setTimeout = promisify(_setTimeout);
711

@@ -10,14 +14,29 @@ function tmpfile(): string {
1014
}
1115

1216
export = {
17+
'tearDown'(callback: () => void) {
18+
sinon.restore();
19+
callback();
20+
},
21+
22+
'initialization fails on unwritable directory'(test: Test) {
23+
test.expect(1);
24+
const cacheFile = tmpfile();
25+
sinon.stub(fs, 'mkdirsSync').withArgs(path.dirname(cacheFile)).throws('Cannot make directory');
26+
test.throws(() => new VersionCheckTTL(cacheFile), /not writable/);
27+
test.done();
28+
},
29+
1330
async 'cache file responds correctly when file is not present'(test: Test) {
14-
const cache = new TimestampFile(tmpfile(), 1);
31+
test.expect(1);
32+
const cache = new VersionCheckTTL(tmpfile(), 1);
1533
test.strictEqual(await cache.hasExpired(), true);
1634
test.done();
1735
},
1836

1937
async 'cache file honours the specified TTL'(test: Test) {
20-
const cache = new TimestampFile(tmpfile(), 1);
38+
test.expect(2);
39+
const cache = new VersionCheckTTL(tmpfile(), 1);
2140
await cache.update();
2241
test.strictEqual(await cache.hasExpired(), false);
2342
await setTimeout(1001); // Just above 1 sec in ms
@@ -26,14 +45,16 @@ export = {
2645
},
2746

2847
async 'Skip version check if cache has not expired'(test: Test) {
29-
const cache = new TimestampFile(tmpfile(), 100);
48+
test.expect(1);
49+
const cache = new VersionCheckTTL(tmpfile(), 100);
3050
await cache.update();
3151
test.equal(await latestVersionIfHigher('0.0.0', cache), null);
3252
test.done();
3353
},
3454

3555
async 'Return later version when exists & skip recent re-check'(test: Test) {
36-
const cache = new TimestampFile(tmpfile(), 100);
56+
test.expect(3);
57+
const cache = new VersionCheckTTL(tmpfile(), 100);
3758
const result = await latestVersionIfHigher('0.0.0', cache);
3859
test.notEqual(result, null);
3960
test.ok((result as string).length > 0);
@@ -44,9 +65,38 @@ export = {
4465
},
4566

4667
async 'Return null if version is higher than npm'(test: Test) {
47-
const cache = new TimestampFile(tmpfile(), 100);
68+
test.expect(1);
69+
const cache = new VersionCheckTTL(tmpfile(), 100);
4870
const result = await latestVersionIfHigher('100.100.100', cache);
4971
test.equal(result, null);
5072
test.done();
5173
},
74+
75+
'No homedir for the given user'(test: Test) {
76+
test.expect(1);
77+
sinon.stub(os, 'homedir').returns('');
78+
sinon.stub(os, 'userInfo').returns({ username: '', uid: 10, gid: 11, shell: null, homedir: ''});
79+
test.throws(() => new VersionCheckTTL(), /Cannot determine home directory/);
80+
test.done();
81+
},
82+
83+
async 'Version specified is stored in the TTL file'(test: Test) {
84+
test.expect(1);
85+
const cacheFile = tmpfile();
86+
const cache = new VersionCheckTTL(cacheFile, 1);
87+
await cache.update('1.1.1');
88+
const storedVersion = fs.readFileSync(cacheFile, 'utf8');
89+
test.equal(storedVersion, '1.1.1');
90+
test.done();
91+
},
92+
93+
async 'No Version specified for storage in the TTL file'(test: Test) {
94+
test.expect(1);
95+
const cacheFile = tmpfile();
96+
const cache = new VersionCheckTTL(cacheFile, 1);
97+
await cache.update();
98+
const storedVersion = fs.readFileSync(cacheFile, 'utf8');
99+
test.equal(storedVersion, '');
100+
test.done();
101+
},
52102
};

0 commit comments

Comments
 (0)