Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@atom/source-map-support": "^0.3.4",
"@babel/core": "7.18.6",
"@electron/remote": "2.1.2",
"@parcel/watcher": "^2.5.1",
"@pulsar-edit/fuzzy-native": "https://github.com/pulsar-edit/fuzzy-native.git#670c97d95ac22e4ac54aa6ebda0f2e3d3716764a",
"about": "file:packages/about",
"archive-view": "file:packages/archive-view",
Expand Down Expand Up @@ -68,6 +69,7 @@
"etch": "0.14.1",
"event-kit": "^2.5.3",
"exception-reporting": "file:packages/exception-reporting",
"fdir": "6.4.6",
"find-and-replace": "file:packages/find-and-replace",
"find-parent-dir": "^0.3.0",
"focus-trap": "6.3.0",
Expand Down
262 changes: 160 additions & 102 deletions spec/path-watcher-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('File', () => {
file.unsubscribeFromNativeChangeEvents();
fs.removeSync(filePath);
closeAllWatchers();
await stopAllWatchers();
await watchPath.reset();
await wait(100);
});

Expand Down Expand Up @@ -323,7 +323,7 @@ describe('watchPath', function () {

afterEach(async function () {
subs.dispose();
await stopAllWatchers();
await watchPath.reset();
});

function waitForChanges(watcher, ...fileNames) {
Expand All @@ -348,123 +348,181 @@ describe('watchPath', function () {
});
}

describe('watchPath()', function () {
it('resolves the returned promise when the watcher begins listening', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');
const WATCHER_IMPLEMENTATIONS = ['nsfw', 'parcel'];

const watcher = await watchPath(rootDir, {}, () => {});
expect(watcher.constructor.name).toBe('PathWatcher');
});
for (let impl of WATCHER_IMPLEMENTATIONS) {
describe(`watchPath() (${impl})`, function () {
let disposables;
beforeEach(async () => {
jasmine.useRealClock();
atom.config.set('core.fileSystemWatcher', impl);
// Changing the config setting will trigger an async transition to new
// file-watchers. This helper method lets us wait until that transition
// has finished.
await watchPath.waitForTransition();
disposables = new CompositeDisposable();
});

it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');
afterEach(() => {
disposables?.dispose();
});

const watcher0 = await watchPath(rootDir, {}, () => {});
const watcher1 = await watchPath(rootDir, {}, () => {});
it('resolves the returned promise when the watcher begins listening', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');
const watcher = await watchPath(rootDir, {}, () => {});
disposables.add(watcher);
expect(watcher.constructor.name).toBe('PathWatcher');
});

expect(watcher0.native).toBe(watcher1.native);
});
it('respects `core.ignoredNames`', async () => {
jasmine.useRealClock();

it("reuses existing native watchers even while they're still starting", async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');
let existing = atom.config.get('core.ignoredNames');
atom.config.set(
'core.ignoredNames',
[...existing, 'some-other-dir']
);

const [watcher0, watcher1] = await Promise.all([
watchPath(rootDir, {}, () => {}),
watchPath(rootDir, {}, () => {})
]);
expect(watcher0.native).toBe(watcher1.native);
});
const rootDir = await tempMkdir('atom-fsmanager-test-');

it("doesn't attach new watchers to a native watcher that's stopping", async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');
// Create a directory that will be affected by our `core.ignoredNames`
// value.
let ignoredDir = path.join(rootDir, 'some-other-dir');
await mkdir(ignoredDir, { recursive: true });

const watcher0 = await watchPath(rootDir, {}, () => {});
const native0 = watcher0.native;
let spy = jasmine.createSpy();

watcher0.dispose();
const watcher1 = await watchPath(rootDir, {}, () => {});
let watcher = await watchPath(rootDir, {}, spy);
disposables.add(watcher);

expect(watcher1.native).not.toBe(native0);
});
// Writing a file to a path within an ignored directory should not
// trigger the callback…
await writeFile(path.join(ignoredDir, 'foo.txt'), 'something');
// (file-watchers might have a debounce interval)
await wait(process.env.CI ? 3000 : 1000);
expect(spy).not.toHaveBeenCalled();

it('reuses an existing native watcher on a parent directory and filters events', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-').then(realpath);
const rootFile = path.join(rootDir, 'rootfile.txt');
const subDir = path.join(rootDir, 'subdir');
const subFile = path.join(subDir, 'subfile.txt');
// …but writing a file to a path outside of an ignored directory should
// trigger the callback.
await writeFile(path.join(rootDir, 'foo.txt'), 'something');
// (file-watchers might have a debounce interval)
await wait(process.env.CI ? 3000 : 1000);
expect(spy).toHaveBeenCalled();
});

await mkdir(subDir);
it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');

// Keep the watchers alive with an undisposed subscription
const rootWatcher = await watchPath(rootDir, {}, () => {});
const childWatcher = await watchPath(subDir, {}, () => {});
const watcher0 = await watchPath(rootDir, {}, () => {});
const watcher1 = await watchPath(rootDir, {}, () => {});

expect(rootWatcher.native).toBe(childWatcher.native);
expect(rootWatcher.native.isRunning()).toBe(true);
disposables.add(watcher0, watcher1);

const firstChanges = Promise.all([
waitForChanges(rootWatcher, subFile),
waitForChanges(childWatcher, subFile)
]);
await writeFile(subFile, 'subfile\n', { encoding: 'utf8' });
await firstChanges;
expect(watcher0.native).toBe(watcher1.native);
});

const nextRootEvent = waitForChanges(rootWatcher, rootFile);
await writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' });
await nextRootEvent;
});
it("reuses existing native watchers even while they're still starting", async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');

it('adopts existing child watchers and filters events appropriately to them', async function () {
const parentDir = await tempMkdir('atom-fsmanager-test-').then(realpath);

// Create the directory tree
const rootFile = path.join(parentDir, 'rootfile.txt');
const subDir0 = path.join(parentDir, 'subdir0');
const subFile0 = path.join(subDir0, 'subfile0.txt');
const subDir1 = path.join(parentDir, 'subdir1');
const subFile1 = path.join(subDir1, 'subfile1.txt');

await mkdir(subDir0);
await mkdir(subDir1);
await Promise.all([
writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' }),
writeFile(subFile0, 'subfile 0\n', { encoding: 'utf8' }),
writeFile(subFile1, 'subfile 1\n', { encoding: 'utf8' })
]);

// Begin the child watchers and keep them alive
const subWatcher0 = await watchPath(subDir0, {}, () => {});
const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0);

const subWatcher1 = await watchPath(subDir1, {}, () => {});
const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1);

expect(subWatcher0.native).not.toBe(subWatcher1.native);

// Create the parent watcher
const parentWatcher = await watchPath(parentDir, {}, () => {});
const parentWatcherChanges = waitForChanges(
parentWatcher,
rootFile,
subFile0,
subFile1
);
const [watcher0, watcher1] = await Promise.all([
watchPath(rootDir, {}, () => {}),
watchPath(rootDir, {}, () => {})
]);
expect(watcher0.native).toBe(watcher1.native);
});

it("doesn't attach new watchers to a native watcher that's stopping", async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-');

const watcher0 = await watchPath(rootDir, {}, () => {});
const native0 = watcher0.native;

expect(subWatcher0.native).toBe(parentWatcher.native);
expect(subWatcher1.native).toBe(parentWatcher.native);

// Ensure events are filtered correctly
await Promise.all([
appendFile(rootFile, 'change\n', { encoding: 'utf8' }),
appendFile(subFile0, 'change\n', { encoding: 'utf8' }),
appendFile(subFile1, 'change\n', { encoding: 'utf8' })
]);

await Promise.all([
subWatcherChanges0,
subWatcherChanges1,
parentWatcherChanges
]);
watcher0.dispose();
const watcher1 = await watchPath(rootDir, {}, () => {});

expect(watcher1.native).not.toBe(native0);
});

it('reuses an existing native watcher on a parent directory and filters events', async function () {
const rootDir = await tempMkdir('atom-fsmanager-test-').then(realpath);
const rootFile = path.join(rootDir, 'rootfile.txt');
const subDir = path.join(rootDir, 'subdir');
const subFile = path.join(subDir, 'subfile.txt');

await mkdir(subDir);

// Keep the watchers alive with an undisposed subscription
const rootWatcher = await watchPath(rootDir, {}, () => {});
const childWatcher = await watchPath(subDir, {}, () => {});

expect(rootWatcher.native).toBe(childWatcher.native);
expect(rootWatcher.native.isRunning()).toBe(true);

const firstChanges = Promise.all([
waitForChanges(rootWatcher, subFile),
waitForChanges(childWatcher, subFile)
]);
await writeFile(subFile, 'subfile\n', { encoding: 'utf8' });
await firstChanges;

const nextRootEvent = waitForChanges(rootWatcher, rootFile);
await writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' });
await nextRootEvent;
});

it('adopts existing child watchers and filters events appropriately to them', async function () {
const parentDir = await tempMkdir('atom-fsmanager-test-').then(realpath);

// Create the directory tree
const rootFile = path.join(parentDir, 'rootfile.txt');
const subDir0 = path.join(parentDir, 'subdir0');
const subFile0 = path.join(subDir0, 'subfile0.txt');
const subDir1 = path.join(parentDir, 'subdir1');
const subFile1 = path.join(subDir1, 'subfile1.txt');

await mkdir(subDir0);
await mkdir(subDir1);
await Promise.all([
writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' }),
writeFile(subFile0, 'subfile 0\n', { encoding: 'utf8' }),
writeFile(subFile1, 'subfile 1\n', { encoding: 'utf8' })
]);

// Begin the child watchers and keep them alive
const subWatcher0 = await watchPath(subDir0, {}, () => {});
const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0);

const subWatcher1 = await watchPath(subDir1, {}, () => {});
const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1);

expect(subWatcher0.native).not.toBe(subWatcher1.native);

// Create the parent watcher
const parentWatcher = await watchPath(parentDir, {}, () => {});
const parentWatcherChanges = waitForChanges(
parentWatcher,
rootFile,
subFile0,
subFile1
);

expect(subWatcher0.native).toBe(parentWatcher.native);
expect(subWatcher1.native).toBe(parentWatcher.native);

// Ensure events are filtered correctly
await Promise.all([
appendFile(rootFile, 'change\n', { encoding: 'utf8' }),
appendFile(subFile0, 'change\n', { encoding: 'utf8' }),
appendFile(subFile1, 'change\n', { encoding: 'utf8' })
]);

await Promise.all([
subWatcherChanges0,
subWatcherChanges1,
parentWatcherChanges
]);
});
});
});
}

});
15 changes: 15 additions & 0 deletions src/atom-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ class AtomEnvironment {
// need other disposing objects to be able to check it. We won't need to
// reset it because another environment will be created.
this.isDestroying = true;
this.emitter.emit('will-destroy');

this.disposables.dispose();
if (this.workspace) this.workspace.destroy();
Expand All @@ -482,6 +483,20 @@ class AtomEnvironment {
this.uninstallWindowEventHandler();
}

/**
* @memberof AtomEnvironment
* @function onWillDestroy
* @desc Invoke the given callback when the environment is destroying, as
* happens during window close or reload.
* @param {function} callback - Function to be called when the environment is
* destroying.
* @returns {Disposable} on which `.dispose()` can be called to unsubscribe.
* @category Event Subscription
*/
onWillDestroy (callback) {
return this.emitter.on('will-destroy', callback);
}

/**
* @memberof AtomEnvironment
* @function onDidBeep
Expand Down
21 changes: 15 additions & 6 deletions src/config-schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This is loaded by atom-environment.coffee. See
// https://atom.io/docs/api/latest/Config for more information about config TODO: Link to Pulsar API site when documented
// schemas.
// https://atom.io/docs/api/latest/Config for more information about config
//
// TODO: Link to Pulsar API site when documented schemas.
const configSchema = {
core: {
type: 'object',
Expand Down Expand Up @@ -347,13 +348,21 @@ const configSchema = {
},
fileSystemWatcher: {
description:
'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Pulsar, but may help prevent crashes or freezes.',
'Choose the underlying implementation used to watch for filesystem changes. It’s best to let Pulsar manage this, but you can change this value if you want to opt into a specific watcher that may work better for your platform.',
type: 'string',
default: 'native',
default: 'default',
enum: [
{
value: 'native',
description: 'Native operating system APIs'
value: 'default',
description: 'Default (let Pulsar decide)'
},
{
value: 'nsfw',
description: 'Node Sentinel File Watcher'
},
{
value: 'parcel',
description: '@parcel/watcher'
}
]
},
Expand Down
Loading
Loading