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

Writable stream is not immediatly closed after .destroy() call #31776

Closed
Veetaha opened this issue Feb 13, 2020 · 11 comments
Closed

Writable stream is not immediatly closed after .destroy() call #31776

Veetaha opened this issue Feb 13, 2020 · 11 comments
Labels
stream Issues and PRs related to the stream subsystem.

Comments

@Veetaha
Copy link

Veetaha commented Feb 13, 2020

  • Version: 12.6.0
  • Platform:
  • 64-bit (Windows 10 Education version 1809),
  • Linux lenovo520 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
  • Subsystem: fs, stream

What steps will reproduce the bug?

Run this code with node

const fs = require("fs");
const { spawnSync } = require("child_process");

const pipeline = require("util").promisify(require("stream").pipeline);

async function main() {
    const src  = fs.createReadStream("src.exe");
    const dest = fs.createWriteStream("dest.exe", { mode: 0o755 });
    await pipeline(src, dest);
    dest.destroy();

    console.dir(spawnSync("dest.exe", ["--version"]));
}

main().finally(() => console.log("DONE"));

How often does it reproduce? Is there a required condition?

It doesn't reproduce on all platforms stably. On my laptop it does always reproduce on Windows 10. I am not sure what is the required condition for this...

What is the expected behavior?

As per documentation of .destroy():

This is a destructive and immediate way to destroy a stream

I expect write-stream to release the file handle during the call to .destroy(), so that the next call to spawnSync of the copied file does execute the binary successfully.

What do you see instead?

Windows 10 x64
D:\junk\catch>node index.js
{
  error: Error: spawnSync dest.exe EBUSY
      at Object.spawnSync (internal/child_process.js:1041:20)
      at spawnSync (child_process.js:609:24)
      at main (D:\junk\catch\index.js:12:17)
      at processTicksAndRejections (internal/process/task_queues.js:85:5) {
    errno: 'EBUSY',
    code: 'EBUSY',
    syscall: 'spawnSync dest.exe',
    path: 'dest.exe',
    spawnargs: [ '--version' ]
  },
  status: null,
  signal: null,
  output: null,
  pid: 0,
  stdout: null,
  stderr: null
}
DONE

D:\junk\catch>
Ubuntu 18.04.3
~/my/junk/stream $ node index.js 
{
  error: Error: spawnSync ./dest.exe EACCES
      at Object.spawnSync (internal/child_process.js:1045:20)
      at spawnSync (child_process.js:597:24)
      at main (/home/veetaha/my/junk/stream/index.js:12:17)
      at processTicksAndRejections (internal/process/task_queues.js:97:5) {
    errno: -13,
    code: 'EACCES',
    syscall: 'spawnSync ./dest.exe',
    path: './dest.exe',
    spawnargs: [ '--version' ]
  },
  status: null,
  signal: null,
  output: null,
  pid: 6261,
  stdout: null,
  stderr: null
}
DONE
~/my/junk/stream $ 

Additional information

The original problem was when downloading a binary executable from GitHub releases for rust-analyzer. You can see the initial discussion on the bug here.

The workaround for this that we use now is to wait for "close" event on writable-stream after the call to .destroy(), i.e.

await stream.pipeline(src, dest);
return new Promise(resolve => {
    dest.on("close", resolve);
    dest.destroy();
});
@addaleax addaleax added the stream Issues and PRs related to the stream subsystem. label Feb 13, 2020
@addaleax
Copy link
Member

@nodejs/streams

@Veetaha
Copy link
Author

Veetaha commented Feb 13, 2020

@addaleax, the link you provided doesn't work...

@mcollina
Copy link
Member

I think this is a bug in fs, not on pipeline or streams. Essentially you should not be caling .destroy() manually or wait for 'close'. I think we should change

if (this.autoClose) {
this.destroy();
}
callback();

to

  if (this.autoClose) {
    this.destroy(callback);
  } else {
    callback();
  }

Essentially we are not waiting to close the handle before emitting 'finish', and as a result pipeline thinks the stream is done.

@mcollina
Copy link
Member

cc @ronag

@ronag
Copy link
Member

ronag commented Feb 14, 2020

@mcollina Yea, I think you are right.

This is probably fixed through #29656. I can add an additional test.

Would you consider this a bug worth back-porting? i.e. should I prepare a PR narrowed to this specific issue?

@ronag ronag pinned this issue Feb 14, 2020
@ronag ronag unpinned this issue Feb 14, 2020
@Veetaha
Copy link
Author

Veetaha commented Feb 14, 2020

@mcollina

Essentially you should not be caling .destroy() manually or wait for 'close'

Sorry, not perfect in English, did you mean we should neither call .destroy() nor wait for "close"?

The documentation says:

stream.pipeline() will call stream.destroy(err) on all streams except:

Readable streams which have emitted 'end' or 'close'.
Writable streams which have emitted 'finish' or 'close'.

I guess during the call to .pipeline(src, dest), dest doesn't emit neither "finish" nor "close"
So, please-please correct me if I am wrong. The following code is expected to work properly, right?

await pipeline(src, dest);
// Notice, no call to .destroy(), not waiting for "close" here, we go use the copied file right away
spawnSync("dest.exe", ["--version"]));

@mcollina
Copy link
Member

I’m saying that you have found a bug in node core ;). The stream is emitting ‘finish’ before the handle is destroyed, and it shouldn’t.

ronag added a commit to nxtedition/node that referenced this issue Feb 14, 2020
WriteStream autoClose was implemented by manually
calling .destroy() instead of using autoDestroy
and callback. This caused some invariants related
to order of events to be broken.

Fixes: nodejs#31776
@Veetaha
Copy link
Author

Veetaha commented Feb 14, 2020

@mcollina, sure, I am glad that we spotted a bug!
Though, I am just a bit concerned about our use-case.
Maybe this is not the right place to put such questions, but should we manually call .destroy() on write-stream after pipeline(src, dst) anyway?
I just want to understand, assuming the bug is fixed, what the correct code would look like for our current workaround?

await stream.pipeline(src, dest);
return new Promise(resolve => {
    dest.on("close", resolve); // resolve only when "close" is emited
    dest.destroy();
});

@ronag
Copy link
Member

ronag commented Feb 14, 2020

Maybe this is not the right place to put such questions, but should we manually call .destroy() on write-stream after pipeline(src, dst) anyway?

You would not need to call destroy.

I just want to understand, assuming the bug is fixed, what the correct code would look like for our current workaround?

await stream.pipeline(src, dest);

@ronag ronag closed this as completed in 568fdfb Feb 16, 2020
@Veetaha
Copy link
Author

Veetaha commented Feb 16, 2020

@ronag, will this fix be backported to nodejs 12.17.0 or 12.16.1 ?

@ronag
Copy link
Member

ronag commented Feb 16, 2020

@Veetaha I think it might be backported to a 12.x release. I'm not too familiar with the backport process yet. @targos might have better input. I've labeled the PR lts-watch-v12.x

ronag added a commit to nxtedition/node that referenced this issue Mar 10, 2020
WriteStream autoClose was implemented by manually
calling .destroy() instead of using autoDestroy
and callback. This caused some invariants related
to order of events to be broken.

Fixes: nodejs#31776

PR-URL: nodejs#31790
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Backport-PR-URL: nodejs#32163
MylesBorins pushed a commit that referenced this issue Mar 11, 2020
WriteStream autoClose was implemented by manually
calling .destroy() instead of using autoDestroy
and callback. This caused some invariants related
to order of events to be broken.

Fixes: #31776

Backport-PR-URL: #32163
PR-URL: #31790
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants