rovf has asked for the wisdom of the Perl Monks concerning the following question:

Here is a stripped-down example BATCH file:

@echo off echo starting setlocal cd c:\tmp set A=B perl -e "$ENV{X}='Y'; system('cmd')" echo finished
I run this script (on Windows 7) with working directory set in c:\. The script opens, as expected, a new command shell with working directory set to c:\tmp and the environment set up as defined in my script.

As a next step in this experiment, I type Control-C within this shell (instead of ending it "properly" by EXIT). When I do this, I observe the following strange behaviour:

First, I'm asked Terminate batch job (Y/N)?. Answering this with Y, I get the error message 'Y' is not recognized as an internal or external command, operable program or batch file. I get, however, a shell prompt, the current directory shown as c:\tmp. Let's say I type Control-C again. Now I see:

c:\tmp>^C finished c:\>
My prompt shows that my working directory is back to C:\ However, this shell shows a funny behaviour: If I repeatedly hit ENTER in this shell, I get the following output:
c:\> c:\tmp> c:\> c:\tmp>
Further experiments on this prompts indeed show that I have two shells running, alternately getting my input: One with working directory set to C:\, the other one to C:\tmp.

At this point, I didn't think of this riddle as a Perl question yet, and thought it might be due to some weird behaviour of Windows CMD.EXE and its way to handle Batch scripts; but when I replace in my batch script the line perl -e "$ENV{X}='Y'; system('cmd')" by its equivalent in Ruby, i.e. ruby -e "ENV['X']='Y'; system('cmd')", I get a completely different behaviour: Hitting Control-C withing the sub-commandshell doesn't have any effect (I just stay in the shell, and am required to type EXIT in order to leave). However, even the Ruby version is not without weirdness: After leaving the shell, I'm back on C:\ without seeing the message "finished" being output...

Perl and Ruby seem to differ in their behaviour when running an external program using system, so I don't think anymore that this is a "pure" Windows question; it has at least some Perl-related part in it; that's why I thought it is OK to post it here.

Any idea what had happened here? Could someone having Windows 7 try out my example batch script with his/her Perl version? BTW, I was running it using Perl 5.8.8, since this is the version which is used in this project.

-- 
Ronald Fischer <ynnor@mm.st>

Replies are listed 'Best First'.
Re: Interaction of Windows Batch files and Perl's system() function
by BrowserUk (Patriarch) on Sep 07, 2011 at 11:25 UTC

    When you run the batch file, once you get to the prompt of the secondary shell, you have a process tree like this:

    cmd.exe ## running the batch file perl.exe ## running -e ... system() cmd.exe ## 2

    The second copy of cmd.exe (tagged ##2) is now in control of the console window hence the c:\tmp> prompt.

    When you type ^C, all the processes in the tree receive the message. The action they take depends upon their nature.

    1. The second shell does nothing except re-prompt, just as you see in a normal cmd window.
    2. Perl.exe emits Terminating on signal SIGINT(2) and terminates.
    3. the first level shell prompts with the normal (for a shell running a batch file) message:
      Terminate batch job (Y/N)?

    At this point, with the perl process having ended, the (grandparent/grandchild) relationship between the first and second shells has been broken and the (relevant) process "tree" has become:

    cmd.exe ## running the batch file cmd.exe ## 2

    In other words, both shells are still running, but as peers. More importantly, they are sharing the same console session. And that is the source of your confusion.

    When you see the prompt Terminate batch job (Y/N)?, you respond to it. The problem is, as both shells are sharing the console session, and both are looking for input, when you respond, it is the other shell that reads the response. It tries to run your 'Y' as a command and you get:

    'y' is not recognized as an internal or external command, operable program or batch file. c:\tmp>

    So now you think you are talking to the second shell and probably type a command. Say dir, and you get:

    c:\tmp>dir Terminate batch job (Y/N)?

    Because this time, its the first shells turn to receive the input.

    Think of it like talking to someone long distance on a call that has been routed via a satellite which introduces that 1/4 second delay into the conversation.

    The way out of the impasse at the keyboard is to type exit in response to the Terminate batch job (Y/N)? message. The exit will actually be read by the secondary shell, and it will terminate.

    Then hit enter and you will get the Terminate batch job (Y/N)? prompt again and this time your response will be seen by the "correct" (first) shell, as it is the only one still running:

    Terminate batch job (Y/N)? exit Terminate batch job (Y/N)? y c:\test>

    The solution to avoiding the creation of the problem would be for perl to ignore ^C until any active system call completed. Obviously, you can arrange this to happen yourself on a case by case basis using %SIG{INT}.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      This means that the example using Ruby probably works because Ruby itself (by design) catches the interrupt before spawning a sub-process?

      Thank you for your suggestions and your elaborate explanation. Actually, in my application it indeed makes sense to catch SIGINT in my Perl program, so this is what I will do.

      -- 
      Ronald Fischer <ynnor@mm.st>
        This means that the example using Ruby probably works because Ruby itself (by design) catches the interrupt before spawning a sub-process?

        Hm. The first thing is that Windows doesn't do signals. perl.exe simulates POSIX-style signal handling to some extent.

        ^C generates a "Console Event". How this is processed by related processes depends in large part by whether the processes share a console or not; and b) whether they are formally related into a process group or not.

        For more of the gory details see the discussion about CREATE_NEW_PROCESS_GROUP in the Process Creation Flags passed to the CreateProcess() function. See also the related discussion for GenerateConsoleCtrlEvent Function.

        There are several ways that Ruby might achieve it's behaviour. Without reading the source it isn't possible to know which is used.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Interaction of Windows Batch files and Perl's system() function
by Anonymous Monk on Sep 07, 2011 at 12:00 UTC

    Windows7 won't change anything :)

    What you have here, is cmd being cmd, the cmd way , and perl being perl

    cmd.exe is a special kind of program

    Instead of striking out the incorrect parts of my investigation below, everything is in readmore

    What you want to do is use $SIG{INT}='IGNORE'; so that pressing Ctrl+C doesn't end the shell launched by perl, force the user to type exit, like
    call perl -le " $SIG{INT}=q[IGNORE]; $ENV{X}='Y'; $ret = system('cmd') +; print qq[system returned $ret ]; "

    But that still leaves the problem of being prompted by the original shell running the batch file

    $ exit system returned 0 Terminate batch job (Y/N)? y
    $ exit system returned 0 Terminate batch job (Y/N)? n finished

    I'm not sure how to signal to it, that everything is ok , no need to prompt, the answer probably lies in that dllhook business :/