Skip to content

Use r+ with truncation when saving existing files - #31733

Merged
Benjamin Pasero (bpasero) merged 7 commits into
microsoft:masterfrom
seishun:hidden
Aug 15, 2017
Merged

Benjamin Pasero (bpasero) merged 7 commits into
microsoft:masterfrom
seishun:hidden

Conversation

@seishun

@seishun Nikolai Vavilov (seishun) commented Jul 30, 2017 •

Copy link
Copy Markdown
Contributor

Fixes #931

Here's how things got this way:

  1. Windows API requires the FILE_ATTRIBUTE_HIDDEN flag when calling CreateFile on a hidden file if CREATE_ALWAYS is specified.
  2. Microsoft C run-time library never uses this flag, consequently fopen in w mode fails on hidden files.
  3. libuv follows MS CRT's behavior on Windows and fails with UV_EPERM when uv_fs_open is called with O_WRONLY|O_CREAT|O_TRUNC (aka "w") on a hidden file.
  4. Node.js uses uv_fs_open to implement fs.open and thus fails with EPERM when trying to open a hidden file with flags='w'.
  5. Visual Studio Code opens the file with flags='w' when saving a file, which fails on hidden files. We've gone full circle.

This PR uses r+ when opening existing files and truncates them. We can't do it just for hidden files since Node.js doesn't provide an API to query the hiddenness of a file. We also can't do it for all files since r+ fails if the file doesn't exist.

@msftclas

@seishun,
Thanks for your contribution.
To ensure that the project team has proper rights to use your work, please complete the Contribution License Agreement at https://cla.microsoft.com.

It will cover your contributions to all Microsoft-managed open source projects.
Thanks,
Microsoft Pull Request Bot

@msftclas

Nikolai Vavilov (@seishun), thanks for signing the contribution license agreement. We will now validate the agreement and then the pull request.

Thanks, Microsoft Pull Request Bot

@bpasero

Benjamin Pasero (bpasero) commented Aug 11, 2017 •

Copy link
Copy Markdown
Contributor

Nikolai Vavilov (@seishun) good start, thanks for looking into this! Instead of running this code everytime we save a file, I would feel more comfortable to catch the EPERM situation and only in that case do your trick. How does that sound?

@bpasero Benjamin Pasero (bpasero) added this to the On Deck milestone Aug 11, 2017
@seishun

Copy link
Copy Markdown
Contributor Author

That's fine, but it will add two indentation levels rather than one. I've submitted #32327 to get rid of the callback hell. If it's merged, then it will no longer be a problem.

@bpasero

Benjamin Pasero (bpasero) commented Aug 11, 2017 •

Copy link
Copy Markdown
Contributor

Nikolai Vavilov (@seishun) hm I see, I added a comment to that PR because changes like that are on the edge of being controversal (for various reasons: should all code use async/await or not, does the author prefer async/await over promises, etc.).

Can we get my suggestion in without forcing the discussion around async/await in the file service?

@seishun

Nikolai Vavilov (seishun) commented Aug 11, 2017 •

Copy link
Copy Markdown
Contributor Author

Sure, no problem. I had already converted it as an experiment so I figured that I might as well submit a PR.

An alternative approach would be a chain of then callbacks. It would also avoid multi-level nesting, but it would require declaring some variables (e.g. exists) at the top of the function. Would that work, or do you prefer keeping the current style?

@bpasero

Copy link
Copy Markdown
Contributor

Nikolai Vavilov (@seishun) yeah that is totally fine.

As for async/await, it just seems weird to have a small part of a file use this style and the rest the other style. And it goes beyond that, we need to agree in general where we adopt async/await, it should be consistent imho.

@seishun

Copy link
Copy Markdown
Contributor Author

Done. I decided to keep the existing style because assigning function arguments to variables in outer scope doesn't look pretty.

@bpasero Benjamin Pasero (bpasero) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nikolai Vavilov (@seishun) thanks, some feedback provided

return this.resolve(resource);
}, error => {
// Can't use 'w' for hidden files, so truncate and use 'r+' if the file exists
if (!exists || error.code !== 'EPERM') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nikolai Vavilov (@seishun) should we add a check for windows here? as far as I know, hidden files (and this issue) only manifest on windows


// 5.) truncate
return pfs.truncate(absolutePath, 0).then(() => {
let writeFilePromise: TPromise<void>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nikolai Vavilov (@seishun) I still suggest to extract those lines which are now duplicate to a separate method that can be called then once from the normal path and once from the error path. that should make the change much smaller and easier to understand 👍

@seishun

Copy link
Copy Markdown
Contributor Author

Benjamin Pasero (@bpasero) PTAL

@bpasero Benjamin Pasero (bpasero) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, much better. Only minor requests now.

Comment thread src/vs/base/node/pfs.ts
let queueKey = toQueueKey(path);

return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, encoding));
return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, options));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this change, can we update extfs.writeFileAndFlush to no longer check for the options bag being a string (in https://github.com/Microsoft/vscode/blob/master/src/vs/base/node/extfs.ts#L342)

});
}

public setContentsAndResolve(resource: uri, absolutePath: string, value: string, addBom: boolean, encodingToWrite: string, options: { encoding?: string; mode?: number; flag?: string; }): TPromise<IFileStat> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions:

  • make the method private
  • rename it to doSetContentsAndResolve
  • move it below the method updateContent
  • remove the encoding from the options, it is not being used from the caller and it is also dangerous because in order to support all encodings, we need to use the encoding.encode method instead


// Otherwise use encoding lib
else {
const encoded = encoding.encode(value, encodingToWrite, { addBOM: addBom });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can actually be simplified to { addBOM } as last argument to encode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't, addBOM vs addBom. Or do you propose renaming the parameter?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nikolai Vavilov (@seishun) oh yeah I see, then maybe just rename the parameter in this method so that you can use the shorthand.

Comment thread src/vs/base/node/pfs.ts Outdated
export function writeFile(path: string, data: string, encoding?: string): TPromise<void>;
export function writeFile(path: string, data: NodeBuffer, encoding?: string): TPromise<void>;
export function writeFile(path: string, data: any, encoding: string = 'utf8'): TPromise<void> {
export function writeFile(path: string, data: string, options?: string | { encoding?: string; mode?: number; flag?: string; }): TPromise<void>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can safely remove the encoding option because it was always UTF-8 being used. The reason why an explicit encoding field is dangerous is because not all encodings the VS Code supports are supported by node (that is why we use a third party library for encoding conversion). Let's remove this option and use UTF-8 always in the method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the string option or the encoding property?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nikolai Vavilov (@seishun) the encoding property within the options bag. I think it is good that you removed the previous string property and replaced with a more explicit option bag 👍

return writeFilePromise.then(() => {
// 4.) set contents and resolve
return this.setContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { mode: 0o666, flag: 'w' }).then(undefined, error => {
// Can't use 'w' for hidden files, so truncate and use 'r+' if the file exists

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to reference the issue here in the comment: #931

return this.setContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { mode: 0o666, flag: 'w' }).then(undefined, error => {
// Can't use 'w' for hidden files, so truncate and use 'r+' if the file exists
if (!exists || error.code !== 'EPERM' || !isWindows) {
throw error;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to use TPromise.wrapError(error) to be consistent with other places in the file


// 5.) resolve
return this.resolve(resource);
// 5.) truncate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add a little bit more comment here why the trick of truncate and r+ mode actually works for hidden files. Currently it is hard to understand what is going on in the error case.

@seishun

Copy link
Copy Markdown
Contributor Author

Nits applied.

@seishun

Copy link
Copy Markdown
Contributor Author

Also removed the string overload (?) for the options parameter in writeFile since it doesn't work anymore.

@bpasero

Copy link
Copy Markdown
Contributor

Nikolai Vavilov (@seishun) almost good, the only complaint I have now is that extfs.writeFile still has an encoding option which imho we should not expose but just set to UTF-8. E.g. where we access options.encoding in that method, just use utf8 directly and do not expose the option to set it.

@seishun

Copy link
Copy Markdown
Contributor Author

I removed the argument since it defaults to 'utf8' anyway.

@bpasero
Benjamin Pasero (bpasero) merged commit cc9d8fc into microsoft:master Aug 15, 2017
@bpasero

Copy link
Copy Markdown
Contributor

Nikolai Vavilov (@seishun) thanks, merging 👍

@github-actions github-actions Bot locked and limited conversation to collaborators Mar 27, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: Cannot save hidden files

3 participants