From e40730f283124f92f81ad8956de327efea0e0e1a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 15 Nov 2023 13:12:43 -0800 Subject: [PATCH] Handle fsWatch event with accesstime change on mac os (#56403) --- src/compiler/sys.ts | 22 ++- .../helpers/virtualFileSystemWithWatch.ts | 3 + .../unittests/tscWatch/watchEnvironment.ts | 36 ++++ .../fsWatch/fsWatchWithTimestamp-false.js | 177 ++++++++++++++++++ .../fsWatch/fsWatchWithTimestamp-true.js | 163 ++++++++++++++++ 5 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false.js create mode 100644 tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true.js diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index d888a3d599bb3..be0d973032edf 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -914,6 +914,7 @@ export interface CreateSystemWatchFunctions { useNonPollingWatchers?: boolean; tscWatchDirectory: string | undefined; inodeWatching: boolean; + fsWatchWithTimestamp: boolean | undefined; sysLog: (s: string) => void; } @@ -934,6 +935,7 @@ export function createSystemWatchFunctions({ useNonPollingWatchers, tscWatchDirectory, inodeWatching, + fsWatchWithTimestamp, sysLog, }: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } { const pollingWatches = new Map>(); @@ -1189,7 +1191,7 @@ export function createSystemWatchFunctions({ return watchPresentFileSystemEntryWithFsWatchFile(); } try { - const presentWatcher = fsWatchWorker( + const presentWatcher = (!fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)( fileOrDirectory, recursive, inodeWatching ? @@ -1286,6 +1288,18 @@ export function createSystemWatchFunctions({ ); } } + + function fsWatchWorkerHandlingTimestamp(fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback): FsWatchWorkerWatcher { + let modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + return fsWatchWorker(fileOrDirectory, recursive, (eventName, relativeFileName, currentModifiedTime) => { + if (eventName === "change") { + currentModifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + if (currentModifiedTime.getTime() === modifiedTime.getTime()) return; + } + modifiedTime = currentModifiedTime || getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + callback(eventName, relativeFileName, modifiedTime); + }); + } } /** @@ -1482,7 +1496,8 @@ export let sys: System = (() => { from?(input: string, encoding?: string): any; } = require("buffer").Buffer; - const isLinuxOrMacOs = process.platform === "linux" || process.platform === "darwin"; + const isMacOs = process.platform === "darwin"; + const isLinuxOrMacOs = process.platform === "linux" || isMacOs; const platform: string = _os.platform(); const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); @@ -1495,7 +1510,7 @@ export let sys: System = (() => { // Note that if we ever emit as files like cjs/mjs, this check will be wrong. const executingFilePath = __filename.endsWith("sys.js") ? _path.join(_path.dirname(__dirname), "__fake__.js") : __filename; - const fsSupportsRecursiveFsWatch = process.platform === "win32" || process.platform === "darwin"; + const fsSupportsRecursiveFsWatch = process.platform === "win32" || isMacOs; const getCurrentDirectory = memoize(() => process.cwd()); const { watchFile, watchDirectory } = createSystemWatchFunctions({ pollingWatchFileWorker: fsWatchFileWorker, @@ -1515,6 +1530,7 @@ export let sys: System = (() => { useNonPollingWatchers: !!process.env.TSC_NONPOLLING_WATCHER, tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, inodeWatching: isLinuxOrMacOs, + fsWatchWithTimestamp: isMacOs, sysLog, }); const nodeSystem: System = { diff --git a/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts b/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts index 493ec2cd2aff9..842a4ef3bfad1 100644 --- a/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts +++ b/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts @@ -79,6 +79,7 @@ export interface TestServerHostCreationParameters { runWithoutRecursiveWatches?: boolean; runWithFallbackPolling?: boolean; inodeWatching?: boolean; + fsWatchWithTimestamp?: boolean; } export function createWatchedSystem(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost { @@ -377,6 +378,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, runWithoutRecursiveWatches, runWithFallbackPolling, inodeWatching, + fsWatchWithTimestamp, }: TestServerHostCreationParameters = {}, ) { this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames; @@ -414,6 +416,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, tscWatchFile, tscWatchDirectory, inodeWatching: !!this.inodeWatching, + fsWatchWithTimestamp, sysLog: s => this.write(s + this.newLine), }); this.watchFile = watchFile; diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 1bb40df865dd2..c8278c047b0f5 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -689,6 +689,42 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po }); }); + describe("with fsWatch with fsWatchWithTimestamp", () => { + function verify(fsWatchWithTimestamp: boolean) { + verifyTscWatch({ + scenario, + subScenario: `fsWatch/fsWatchWithTimestamp ${fsWatchWithTimestamp}`, + commandLineArgs: ["-w", "--extendedDiagnostics"], + sys: () => + createWatchedSystem( + { + [libFile.path]: libFile.content, + "/user/username/projects/myproject/main.ts": `export const x = 10;`, + "/user/username/projects/myproject/tsconfig.json": jsonToReadableText({ files: ["main.ts"] }), + }, + { + currentDirectory: "/user/username/projects/myproject", + fsWatchWithTimestamp, + }, + ), + edits: [ + { + caption: "emulate access", + edit: sys => sys.invokeFsWatches("/user/username/projects/myproject/main.ts", "change", /*modifiedTime*/ undefined, /*useTildeSuffix*/ undefined), + timeouts: sys => sys.runQueuedTimeoutCallbacks(), + }, + { + caption: "modify file contents", + edit: sys => sys.appendFile("/user/username/projects/myproject/main.ts", "export const y = 10;"), + timeouts: sys => sys.runQueuedTimeoutCallbacks(), + }, + ], + }); + } + verify(/*fsWatchWithTimestamp*/ true); + verify(/*fsWatchWithTimestamp*/ false); + }); + verifyTscWatch({ scenario, subScenario: "fsEvent for change is repeated", diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false.js new file mode 100644 index 0000000000000..e8cfa9431277a --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false.js @@ -0,0 +1,177 @@ +currentDirectory:: /user/username/projects/myproject useCaseSensitiveFileNames: false +Input:: +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + +//// [/user/username/projects/myproject/main.ts] +export const x = 10; + +//// [/user/username/projects/myproject/tsconfig.json] +{ + "files": [ + "main.ts" + ] +} + + +/a/lib/tsc.js -w --extendedDiagnostics +Output:: +[12:00:21 AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 undefined Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 undefined Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Type roots +[12:00:24 AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = void 0; +exports.x = 10; + + + +PolledWatches:: +/user/username/projects/myproject/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: *new* + {} +/user/username/projects/myproject/main.ts: *new* + {} +/user/username/projects/myproject/tsconfig.json: *new* + {} + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/user/username/projects/myproject/main.ts (used version) + +exitCode:: ExitStatus.undefined + +Change:: emulate access + +Input:: + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file + + +Timeout callback:: count: 1 +1: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +1: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program + + + + +exitCode:: ExitStatus.undefined + +Change:: modify file contents + +Input:: +//// [/user/username/projects/myproject/main.ts] +export const x = 10;export const y = 10; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file + + +Timeout callback:: count: 1 +2: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +2: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program +[12:00:27 AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +[12:00:31 AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.y = exports.x = void 0; +exports.x = 10; +exports.y = 10; + + + + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Completely +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/main.ts (computed .d.ts) + +exitCode:: ExitStatus.undefined diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true.js new file mode 100644 index 0000000000000..0d41c11b03e81 --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true.js @@ -0,0 +1,163 @@ +currentDirectory:: /user/username/projects/myproject useCaseSensitiveFileNames: false +Input:: +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + +//// [/user/username/projects/myproject/main.ts] +export const x = 10; + +//// [/user/username/projects/myproject/tsconfig.json] +{ + "files": [ + "main.ts" + ] +} + + +/a/lib/tsc.js -w --extendedDiagnostics +Output:: +[12:00:21 AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 undefined Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 undefined Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Type roots +[12:00:24 AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = void 0; +exports.x = 10; + + + +PolledWatches:: +/user/username/projects/myproject/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: *new* + {} +/user/username/projects/myproject/main.ts: *new* + {} +/user/username/projects/myproject/tsconfig.json: *new* + {} + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/user/username/projects/myproject/main.ts (used version) + +exitCode:: ExitStatus.undefined + +Change:: emulate access + +Input:: + +Before running Timeout callback:: count: 0 + +After running Timeout callback:: count: 0 + + +exitCode:: ExitStatus.undefined + +Change:: modify file contents + +Input:: +//// [/user/username/projects/myproject/main.ts] +export const x = 10;export const y = 10; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 undefined Source file + + +Timeout callback:: count: 1 +1: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +1: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program +[12:00:27 AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +[12:00:31 AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.y = exports.x = void 0; +exports.x = 10; +exports.y = 10; + + + + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Completely +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/main.ts (computed .d.ts) + +exitCode:: ExitStatus.undefined