-
Notifications
You must be signed in to change notification settings - Fork 132
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
When spago run
, if the target cli app manipulates console interface it may not terminate properly
#1048
Comments
Also dug a little bit around node documentation. Must be some reason that there exist three flavors of stream piping API |
Hm... This is probably a bug with |
We're not using the latest execa version - @JordanMartinez do you think that "just" upgrading to latest might fix this? |
I'm not sure. I'll have to see what goes on in |
So, after I updated Spec.itOnly "can run interactive programs" \{ spago, spago' } -> do
spago [ "init" ] >>= shouldBeSuccess
spago [ "install", "node-readline" ] >>= shouldBeSuccess
FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) $ Array.intercalate "\n"
[ "module Main where"
, ""
, "import Prelude"
, ""
, "import Effect (Effect)"
, "import Effect.Console (log)"
, "import Node.EventEmitter (on_)"
, "import Node.ReadLine (close, createConsoleInterface, closeH, lineH, noCompletion, prompt, setPrompt)"
, ""
, "main :: Effect Unit"
, "main = do"
, " interface <- createConsoleInterface noCompletion"
, " setPrompt \"(type 'quit' to stop)\\n> \" interface"
, " prompt interface"
, " interface # on_ closeH do"
, " log \"interface was closed\""
, " interface # on_ lineH \\s -> do"
, " log $ \"You typed: \" <> show s"
, " if s == \"quit\" then do"
, " log $ \"calling rl.close()\""
, " close interface"
, " log $ \"called rl.close()\""
, " else do"
, " log $ \"Looping\""
, " prompt interface"
]
spagoProcess <- spago' StdinNewPipe [ "run" ]
let delay200Ms = Aff.delay $ Milliseconds 200.0
delay200Ms
spagoProcess.stdin.writeUtf8 "foo\n"
delay200Ms
spagoProcess.stdin.writeUtf8 "bar\n"
delay200Ms
spagoProcess.stdin.writeUtf8 "quit\n"
delay200Ms
spagoProcess.stdin.writeUtf8End "quit\n"
delay200Ms
spagoProcess.result >>= shouldBeSuccess which outputs the following: ✅ Selecting package to build: 7368613235362d376c5766502b59557a4c3459473333656f2b
Downloading dependencies...
Building...
[1 of 2] Compiling Main
[2 of 2] Compiling Test.Main
Src Lib All
Warnings 0 0 0
Errors 0 0 0
✅ Build succeeded.
(type 'quit' to stop)
> You typed: "foo"
Looping
(type 'quit' to stop)
> You typed: "bar"
Looping
(type 'quit' to stop)
> You typed: "quit"
calling rl.close()
interface was closed
called rl.close()
You typed: "quit"
calling rl.close()
called rl.close()
STDOUT:
(type 'quit' to stop)
> You typed: "foo"
Looping
(type 'quit' to stop)
> You typed: "bar"
Looping
(type 'quit' to stop)
> You typed: "quit"
calling rl.close()
interface was closed
called rl.close()
You typed: "quit"
calling rl.close()
called rl.close() I noticed that the program hangs if I don't call |
After more investigation (and re-reading the opening thread), the issue here seems to be in Also, from the Node docs:
|
This might be it. At the very bottom of readline.createInterface(options):
|
That makes sense, but I still don't get why it behaves differently when run directly with Node vs running it through Spago - it can't just be readline, it sounds like there is something that we are missing in how we set up the streams? |
Yeah, that's what I'm trying to figure out myself... |
There's seemingly a difference between
and
|
Regardless, the program written in the opening thread should not need to be changed in order for So far, my experimentation has shown that the program only terminates if we AFAIK, the child process does not terminate because something is keeping the event loop alive, and that something appears to be the lack of an EOF sent to the child process' stdin in this unique situation where Maybe this changes if we run the child process directly via a shell? Otherwise, this might be one of those quirky Node bugs. On a different note, I recall James Brock hitting something similar with |
I'd suggest using one of these three node libraries to see what's causing the child process to stay alive, but they're all written via CJS modules. |
cc @jamesdbrock hopefully this is the same issue you had 😄 |
Tried "use strict"
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const whyRunningJS = require('why-is-node-running')
export const whyRunningLog = function() {
whyRunningJS()
} and module WhyIsNodeRunning where
import Prelude
import Effect (Effect)
foreign import whyRunningLog :: Effect Unit Inserting the line
and yet, it terminates normally. If
|
Maybe the hang is caused by execa's _.result waiting for something to finish, and so the resulting Aff never returns? |
But the inner node instance should terminate when asked, no? So at that point the middle one (spawned with execa) should return too. It's as if we need a cp.on('exit', (code) => {
process.exit(code);
}); |
My point is that Aff doesn't return until you tell it to. So, if I'm using Aff incorrectly at some point, the child process could have terminated but the corresponding Aff callback isn't called, thereby making the child process appear to hang. Perhaps the example code should add more listeners to verify whether the child process is terminating or not. And we could add listeners to the child process itself to see what kind of events are being emitted. |
I added listeners to the child process, and can guarantee that it's not emitting an event after it initially spawns once I type 'quit'. |
Found the solution. We need to use However... In the execa version currently used in Spago, that's not possible. Moreover, if you updated I first thought we should verify that a node process (like spago) starting another node process (like the program) should or shouldn't terminate when we type 'quit'. This eliminates I bundled the following program via spago to a file called module Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
import Effect.Ref as Ref
import Node.EventEmitter (on_)
import Node.ReadLine (close, createConsoleInterface, closeH, lineH, noCompletion, prompt, setPrompt)
main :: Effect Unit
main = do
interface <- createConsoleInterface noCompletion
setPrompt "(type 'quit' to stop)\n> " interface
prompt interface
countRef <- Ref.new 0
interface # on_ closeH do
log "interface was closed"
interface # on_ lineH \s -> do
log $ "You typed: " <> show s
count <- Ref.read countRef
case s, count of
"quit", 0 -> do
Ref.write 1 countRef
log $ "calling rl.close()"
close interface
log $ "called rl.close()"
"quit", _ -> do
log "This should not be printed"
_, _ -> do
log $ "Looping"
prompt interface I then wrote the following JS program called import process from "node:process";
import cp from "node:child_process";
const stdioVal = "pipe";
const subprocess = cp.spawn("node", [ "index.js"], {
stdio: [stdioVal, stdioVal, stdioVal]
});
subprocess.on("spawn", () => console.log("spawned"));
subprocess.on("exit", () => console.log("exited"));
subprocess.on("closed", () => console.log("closed"));
process.stdin.pipe(subprocess.stdin);
subprocess.stdout.pipe(process.stdout); I then called |
Man, that's some great digging 😄 I gather the next step is to patch execa to allow this without going the unsafe route? |
Mm... I'm not sure. The 'unsafeness' is the fact that certain properties don't exist on the child process depending on what values get passed to Some of this I need to verify personally because I've assumed that writing content to a child process' |
What I found out yesterday is that as soon as one puts node process in a pipe, its Could be relevant. |
A workaround: close interface
+ Stream.destroy Process.stdin A previous comment mentioned that there is a missing |
The "fix" is most unusual. I was trying to attack this again by adding this new test to diff --git a/test/Test/Node/Library/Execa.purs b/test/Test/Node/Library/Execa.purs
index e508ff1..72d369c 100644
--- a/test/Test/Node/Library/Execa.purs
+++ b/test/Test/Node/Library/Execa.purs
@@ -88,6 +88,18 @@ spec = describe "execa" do
result.timedOut `shouldEqual` true
_ ->
fail $ "Timeout should work: " <> show result
+ it "can run interactive programs" do
+ let
+ interactiveJSFile = Path.concat [ "test", "fixtures", "interactive.js" ]
+ spawned <- execa "node" [ interactiveJSFile ]
+ (_ { timeout = Just { milliseconds: Milliseconds 400.0, killSignal: stringSignal "SIGTERM" } })
+ spawned.stdin.writeUtf8 "node\n"
+ spawned.stdin.writeUtf8 "quit\n"
+ result <- spawned.getResult
+ case result.exit of
+ Normally 0 -> result.stdout `shouldContain` "You typed: node"
+ _ ->
+ fail $ (if result.timedOut then "Should not timeout: " else "Other error: ") <> show result
describe "execaSync" do
describe "`cat` tests" do
it "input is file" do and this js file: const readline = require('node:readline')
const { stdin: input, stdout: output } = require('node:process')
const rl = readline.createInterface({ input, output })
rl.setPrompt('(type "quit" to stop)\n> ')
rl.prompt()
rl.on('line', (line) => {
if (line === 'quit') {
rl.close()
} else {
console.log(`You typed: ${line}`)
rl.prompt()
}
}) It failed as expected. BUT the following will succeed even being run piped: ...
const rl = readline.createInterface({ input, output })
rl.input._readableState.highWaterMark = 0
... It has to do when running piped, the underlying |
Probably not. We've already identified a non-hacky fix; why consider this hacky idea? I'm not seeing the advantage. |
That's fair. But it at least shows that the purescript side is not necessary at fault here. Instead, the implementation detail of the node side leaks which leads to observable behavior difference from outside. |
* Add note about stdin, pipe, and inherit See purescript/spago#1048 * Drop unused functions * Add variant of waitSpawned
|
Blocked by #1122. |
This should now be unblocked |
I think this already fixed actually now that we've merged #1129. And because it is interactive, I'm wondering how we'd even test this as the usage of There's already a test for this here: #1048 (comment). It's just a matter of figuring out how to add that to our test suite. |
I just released 0.93.24 with all the latest patches and it doesn't seem to be fixed - the example in the first post still doesn't terminate when run with |
I just tried that out myself and it terminated fine. $ npx spago run
Reading Spago workspace configuration...
✅ Selecting package to build: runbug
Downloading dependencies...
Building...
Src Lib All
Warnings 0 0 0
Errors 0 0 0
✅ Build succeeded.
(type 'quit' to stop)
> hello1
You typed: hello1
(type 'quit' to stop)
> helo2
You typed: helo2
(type 'quit' to stop)
> quit
$ Versions used:
|
Just use the example from
purescript-node-readline
repo:If
spago bundle
thennode index.js
, after typing quit the app terminates. But ifspago run
directly, it hangs. It terminates only after pressingctrl-d
. Withspago run --verbose
, it can be deduced that the hang must have happened here: src/Spago/Cmd.purs#L59.This happened with
spago
version0.93.9
. I also checkout out this repo and built a fresh version from head. It also happened there.The text was updated successfully, but these errors were encountered: