Zulip Chat Archive

Stream: lean4

Topic: Interrupting command in VSCode does not kill it


Yicheng Qian (Nov 07 2023 at 12:10):

It seems that interrupting commands that invokes IO.Process.spawn in VSCode does not kill the child being spawned. Here is a mwe (in Linux(WSL)):

import Lean
open Lean

def spawnSleep (n : Nat) : IO UInt32 := do
  let proc  IO.Process.spawn
    {stdin := .piped, stdout := .piped, stderr := .piped, cmd := "sleep", args := #[s!"{n}"]}
  proc.wait

-- Type something here --> <-- multiple times to interrupt the normal execution of the following command
#eval spawnSleep 30

If we type at the --> <-- multiple times with short pauses in between, we can see multiple sleep running in the background.

Yicheng Qian (Nov 07 2023 at 12:14):

Some context: I'm working on a tactic that invokes external commands. If some theorem in a file invokes my tactic, and I keep editing that file for a while, my computer becomes extremely slow and sometimes even crashes.

Yicheng Qian (Nov 07 2023 at 17:17):

Restarting lean server on the file (ctrl+shift+x) kills the spawned process

Yicheng Qian (Nov 07 2023 at 17:18):

But re-loading the imports can take a long time.

Sebastian Ullrich (Nov 07 2023 at 18:09):

wait maps more or less directly to the OS primitive, there isn't really anything the elaborator could do to interrupt it. I would suggest busy-waiting with a small sleep and using Child.kill if IO.checkCanceled returns true, which the elaborator sets on cancellation starting with 4.3.0-rc1.

Joe Hendrix (Nov 07 2023 at 18:46):

@Yicheng Qian I'm working on Lean bindings to libuv as a side project. Right now, there isn't much there, but I am working on TCP. I plan to start adding process creation once that's done.

I'll see if we can have a way of creating a libuv async that is triggered when IO.checkCanceled is set. That should provide a mechanism for waiting for processes but killing them if the elaborator sets cancellation. This doesn't help immediately, but should provide a more robust long term API.

Yicheng Qian (Nov 08 2023 at 03:27):

Sebastian Ullrich said:

wait maps more or less directly to the OS primitive, there isn't really anything the elaborator could do to interrupt it. I would suggest busy-waiting with a small sleep and using Child.kill if IO.checkCanceled returns true, which the elaborator sets on cancellation starting with 4.3.0-rc1.

Sorry, I'm confused about the approach, and I can't find busy-waiting primitives (non-blocking ones) in Lean. Can you provide a modified spawnSleep that kills the child when interrupted?

Yicheng Qian (Nov 08 2023 at 03:33):

I tried this:

def spawnSleep (n : Nat) : IO Nat := do
  let proc  IO.Process.spawn
    {stdin := .piped, stdout := .piped, stderr := .piped, cmd := "sleep", args := #[s!"{n}"]}
  let _  IO.asTask (do
    let start  IO.monoMsNow
    while true do
      IO.sleep 100
      if  IO.checkCanceled then
        proc.kill
        break
      if ( IO.monoMsNow) - start > n * 2000 then
        break)
  let _  proc.wait
  return 0

But the sleep is not killed when the command is interrupted.

Sebastian Ullrich (Nov 08 2023 at 06:53):

Oh, if you do that in a separate Task, it will have its own cancellation flag. If you switch it around, i.e. do the Child.wait in a task and busy-wait on it in the main thread using IO.hasFinished, I believe it should work on 4.3.0-rc1+.

Sebastian Ullrich (Nov 08 2023 at 06:54):

If we had https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait, the additional task would not be necessary

Yicheng Qian (Nov 08 2023 at 07:30):

Great! Now it works

def spawnSleep₂ (n : Nat) : IO Nat := do
  let proc  IO.Process.spawn
    {stdin := .piped, stdout := .piped, cmd := "sleep", args := #[s!"{n}"]}
  let tsk  IO.asTask proc.wait
  while true do
    IO.sleep 100
    if  IO.checkCanceled then
      proc.kill
      break
    if  IO.hasFinished tsk then
      break
  return 0

-- Type something here --> <-- multiple times to interrupt the normal execution of the following command
#eval spawnSleep₂ 5

Yicheng Qian (Nov 08 2023 at 07:31):

But this seems a bit magical to me.

Yicheng Qian (Nov 08 2023 at 07:31):

Why proc.kill is still executed even after the command is interrupted ...

Sebastian Ullrich (Nov 08 2023 at 07:36):

Yicheng Qian said:

But this seems a bit magical to me.

I wouldn't call it ergonomic, but from my point of view it would be more magical if the original code just worked :)

Sebastian Ullrich (Nov 08 2023 at 07:36):

Yicheng Qian said:

Why proc.kill is still executed even after the command is interrupted ...

What do you mean?

Yicheng Qian (Nov 08 2023 at 07:41):

I'm thinking that, when the command #eval spawnSleep₂ 5 is interrupted (when I type something above it), it's likely that we're in IO.sleep 100, and everything below will not be executed after the interruption. So I'm surprised that proc.kill is still executed and sleep is killed.

Sebastian Ullrich (Nov 08 2023 at 07:43):

Task cancellation is cooperative, nothing will be skipped until someone uses checkCanceled. I/O primitives like sleep don't check it, and the sleep itself is not interruptible either.

Yicheng Qian (Nov 08 2023 at 07:59):

Ah, I see.

Yicheng Qian (Nov 08 2023 at 08:15):

I certainly didn't know that before. I though that lean would just interrupt the task right at the command it's executing.

def spin : IO Unit := do
  let mut i := 0
  while true do
    i := i + 1
    if i % 10000000 == 0 then
      IO.println "Oh"

-- Type something here --> <-- multiple times to interrupt the normal execution of the following command
#eval spin

If I type at --> <-- for k times, top will tell me that lean uses (k+1) * 100% CPU :)

Yicheng Qian (Nov 08 2023 at 08:15):

So this issue is not necessarily related to IO.spawn.

Notification Bot (Nov 08 2023 at 08:17):

This topic was moved here from #lean4 > Interrupting spawn in VSCode does not kill child by Yicheng Qian.

Yicheng Qian (Nov 08 2023 at 08:19):

Anyway checkCanceled still solves the issue

def spin : IO Unit := do
  let mut i := 0
  while true do
    i := i + 1
    if i % 10000000 == 0 then
      IO.println "Oh"
    if  IO.checkCanceled then
      break

Yicheng Qian (Nov 08 2023 at 08:20):

Btw

def spin : IO Unit := do
  let mut i := 0
  while true do
    i := i + 1

#eval spin

causes

Lean server printed an error: libc++abi: terminating due to uncaught exception of type lean::exception: unreachable code

should this be considered an issue?

Sebastian Ullrich (Nov 08 2023 at 08:34):

That does look like an issue


Last updated: Dec 20 2023 at 11:08 UTC