Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use r+ with truncation when saving existing files #31733

Merged
merged 7 commits into from
Aug 15, 2017
Merged

Use r+ with truncation when saving existing files #31733

merged 7 commits into from
Aug 15, 2017

Conversation

seishun
Copy link
Contributor

@seishun seishun commented Jul 30, 2017

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
Copy link

@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
Copy link

@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
Copy link
Member

bpasero commented Aug 11, 2017

@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 bpasero added this to the On Deck milestone Aug 11, 2017
@seishun
Copy link
Contributor Author

seishun commented Aug 11, 2017

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
Copy link
Member

bpasero commented Aug 11, 2017

@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
Copy link
Contributor Author

seishun commented Aug 11, 2017

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
Member

bpasero commented Aug 11, 2017

@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
Contributor Author

seishun commented Aug 11, 2017

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

Copy link
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

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

@seishun thanks, some feedback provided

@@ -366,6 +366,34 @@ export class FileService implements IFileService {

// 5.) resolve
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
Member

Choose a reason for hiding this comment

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

@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
Member

Choose a reason for hiding this comment

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

@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
Contributor Author

seishun commented Aug 12, 2017

@bpasero PTAL

Copy link
Member

@bpasero bpasero left a comment

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.

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
Member

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)

@@ -315,6 +315,29 @@ export class FileService implements IFileService {
});
}

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

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
Member

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
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
Member

Choose a reason for hiding this comment

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

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

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
Member

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
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
Member

Choose a reason for hiding this comment

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

@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
Member

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
Member

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
Member

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
Contributor Author

seishun commented Aug 13, 2017

Nits applied.

@bpasero bpasero modified the milestones: August 2017, On Deck Aug 13, 2017
@seishun
Copy link
Contributor Author

seishun commented Aug 13, 2017

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

@bpasero
Copy link
Member

bpasero commented Aug 13, 2017

@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
Contributor Author

seishun commented Aug 13, 2017

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

@bpasero bpasero merged commit cc9d8fc into microsoft:master Aug 15, 2017
@bpasero
Copy link
Member

bpasero commented Aug 15, 2017

@seishun thanks, merging 👍

@seishun seishun deleted the hidden branch September 8, 2017 19:13
@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