-
Notifications
You must be signed in to change notification settings - Fork 29.5k
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
Conversation
@seishun, It will cover your contributions to all Microsoft-managed open source projects. |
@seishun, thanks for signing the contribution license agreement. We will now validate the agreement and then the pull request. |
@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 |
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. |
@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? |
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 |
@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. |
'w' doesn't work with hidden files. Fixes: #931
Done. I decided to keep the existing style because assigning function arguments to variables in outer scope doesn't look pretty. |
There was a problem hiding this 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') { |
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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 👍
@bpasero PTAL |
There was a problem hiding this 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.
src/vs/base/node/pfs.ts
Outdated
let queueKey = toQueueKey(path); | ||
|
||
return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, encoding)); | ||
return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, options)); |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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 theencoding.encode
method instead
|
||
// Otherwise use encoding lib | ||
else { | ||
const encoded = encoding.encode(value, encodingToWrite, { addBOM: addBom }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Nits applied. |
Also removed the |
@seishun almost good, the only complaint I have now is that |
I removed the argument since it defaults to 'utf8' anyway. |
@seishun thanks, merging 👍 |
Fixes #931
Here's how things got this way:
FILE_ATTRIBUTE_HIDDEN
flag when callingCreateFile
on a hidden file ifCREATE_ALWAYS
is specified.fopen
inw
mode fails on hidden files.UV_EPERM
whenuv_fs_open
is called withO_WRONLY|O_CREAT|O_TRUNC
(aka "w") on a hidden file.uv_fs_open
to implementfs.open
and thus fails withEPERM
when trying to open a hidden file withflags='w'
.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 sincer+
fails if the file doesn't exist.