Home > OS >  Running a batch script through PHP on Windows without waiting for the script to end
Running a batch script through PHP on Windows without waiting for the script to end

Time:01-03

I'm new to PHP and development in general and I'm asking, because I haven't found a working solution for my problem yet.

I apologize beforehand if this is a bit long, however this is the first time I am asking a question here and I don't want to leave out any small, potentially crucial details.

A little bit of backstory: I am currently working on my bachelors thesis and my task is to emulate the front end of an existing program that handles Petri nets via JS in a web browser and call the back end functions of said Petri net program (let's call it PN program for convenience) via PHP and AJAX. The PN program runs on a windows 11 PC. Some already graduated students have implemented parts of the front end and some back end functions. They have also implemented a ticket system so one can only execute those back end functions a limited amount of times.

As a part of that project I am trying to run a batch script from the Petri net program (let's call it PN program for convenience). The batch script creates some analysis files and crucially compiles 2 executables, a master and a slave for the specific Petri net I am giving to the script. The script also starts them as processes which I can see and stop with the task manager. I also have to communicate with the master process at a later step. As a prerequisite I must not change anything on the batch script, as that would impact the functionality of the PN program.

To start the script, I request a ticket and then I compose a call $cmd where I set 2 environment variables: the path to the PN program and the directory where the script runs and temporarily stores all files, including the 2 executables. I also add some necessary parameters that the script needs in order to start.

I then run

exec($cmd);

This call produces all files in the working directory, and starts the processes, however the batch script now waits for the master process to finish, as it cleans up some temporary files after the simulation is finished. At this point the PHP program stops executing and waits for the batch script to finish, which it doesn't do.

Over the course of the last 2 days I have tried multiple solutions proposed here

or

exec($cmd.' > NUL 2>&1 &')

and

pclose(popen("start /B " . $cmd . " 1> ".$scrptDir." 2>&1 &", "r"));

where $scrptDir is the working directory I described earlier.

I have not found a working solution, at best the script doesn't start at all and at worst it produces a syntax error when making an AJAX call, which admittedly I have not yet understood why exactly it does that, as AJAX was implemented by a predecessor and left pretty undocumented.

Edit: The syntax errors were caused by myself, because I forgot the semicolon.

Edit2: Is it maybe possible to start the runScript() function asynchroniously?

This is the PHP code:

index.php; receives an AJAX call 'tg-runscript' from the front end


[...] // include statements and some debugging tools

switch ($_POST['action']) {

[...] // various other cases

    case 'tg-runscript':
        //-- checks for valid session and ticket ----------------------
        $sessionid;
        if (!sessionCheck($sessionid)) {
            $log->Add(Warning::noSession());
            terminate();
        }

        $request = new Request($_POST['action']);
        
        // This prepares the Petri net for the script  
        $tgame = TGame::initTgame($_POST); 
        if ($tgame === null) {
            //-- an error occured during the init process -------------
            $request->setStatusCode(Request::FAILED);
            printRequest($request); // routine to print a JSON request
        }
        
        //-- the script function that executes the CLI call -----------
        $tgame->runScript(); 
        $request = new Request($_POST['action']);
        $request->setStatusCode(Request::SUCCESS); // report back to front end
        printRequest($request);
        break;

[...] // various other cases

Tgame.php runScript() function

    public function runScript() {
        //-- compose the command line to start script 
        $cmd = $this->getCall();

        //-- execute the script
        exec($cmd);        
      
    }
} 

Thank you in advance for the help.

Updates: Starting the script via

pclose(popen("start /B " . $cmd . " 1> NUL 2>&1 &", "r"));

or

popen("start /B " . $cmd . " 1> NUL 2>&1 &", "r");

or

exec('nohup ' . $cmd . ' 2>&1 &');

has the same outcome: the script runs, but master.exe and slave.exe close and the script performs the cleanup.

Using

exec($cmd.' > NUL 2>&1 &');

does not have different behaviour from

exec($cmd);

CodePudding user response:

This is one way to do it. Please be advised that this is not the best way to go about it, especially if you need to do something with the results of an AJAX call.

jQuery

Let's say that you initiate the AJAX request like this:

$(document).ready(function() {
    $.ajax({
        type: 'POST',
        url: 'myscript.php',
        data: {'val1':val1,'val2':val2,....},
        timeout: 100,
        success: function(data) {
            // whatever it is you need to do on success, although you could leave it empty
        },
        error: function (xmlhttprequest, textstatus, message, desc, err) {
            if(textstatus === "timeout") {
                // do something else
            } else {
                // cover the case of an error which is not a timeout
            }
        }
    });
});

The key part here is the timeout. This will abort the pending AJAX, meaning that the JS will not wait for the response, but the server-side script called with it will still execute.


Vanilla Javascript

If you're not using jQuery, you could do the same thing in vanilla Javascript, like this.

var xmlHttp = new XMLHttpRequest();
xmlHttp.open("POST", "myscript.php", true);

// Post the request with your data
xmlHttp.send(
    JSON.stringify({
        val1 : val1,
        val2 : val2,
        ... etc
    })
);

xmlHttp.onload = function(e) {
    // Check if the request was a success
    if (this.readyState === XMLHttpRequest.DONE && (this.status === 201 || this.staus === 200)) {
        clearTimeout(xmlHttpTimeout); 
        // do whatever it is you do on successful response
    }
}

// Timeout to abort in 100 miliseconds
var xmlHttpTimeout = setTimeout(ajaxTimeout,100);
function ajaxTimeout(){
   xmlHttp.abort();
   // do what you would normally do once initiating the AJAX
}

If you're unsure whether the AJAX request is reaching the backend, and there are no console errors or network errors, you can do the following.

First, create a script called test.php (or give it another name, if you already have a script called like that). Then, inside that script, place the following.

<?php

sleep(15);
$filename = "zztest.txt";
$file = fopen($filename,'a ');

fwrite($file, date("Y-m-d H:i:s"));
sleep(5);
fwrite($file, PHP_EOL.date("Y-m-d H:i:s").PHP_EOL."********".PHP_EOL);
fclose($file);

?>

What this will do is:

  1. Upon initialization, wait for 15 seconds (that's what sleep(15) does)
  2. Create (or open, if it already exists) a *.txt file, and write the current date and time to it
  3. Keep the file open - notice that there is no fclose(...) after fwrite(...). This is generally not good practice, because something might break the script execution, and the file would remain locked, but it's alright in this case, because it's for testing purposes
  4. Once the date and time have been written to the text file, wait for another 5 seconds (sleep(5)), and then write the current date and time to it. PHP_EOL is the newline character
  5. Finally, close the file

The next step is to replace myscript.php call inside your AJAX, so that it calls test.php. The rest of your AJAX setup can remain the same.

Then, execute AJAX as if you would normally. Wait for about 20 seconds, and check if a file called zztest.txt has been created, and if it has, what its contents are.

If it hasn't, then the problem is in the front-end (the AJAX call).

CodePudding user response:

Ok, I found a workaround.

Instead of

exec($cmd)

I used

$filename = "help.bat";
$file = fopen($filename,'a ');
fwrite($file, $cmd);
fclose($file);
pclose(popen("start /B ". $filename, "r"));

I wrote the composed command $cmd into a batch file and then executed that batch file instead. I think the reason why it is working now is that my composed call was a bit too complex for me, so I tried to make it simpler.

  • Related