I am trying to make an application using python code and C++, but I need some kind of protection against infinite loops or too long executions. I tried following some threads and seeing how other people solved their issues, but couldn't find a working solution. I have this example code:
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include "pybind11/pybind11.h"
#include "pybind11/embed.h"
namespace py = pybind11;
std::atomic<bool> stopped(false);
std::atomic<bool> executed(false);
void python_executor(const std::string &code)
{
  py::gil_scoped_acquire acquire;
  try
  {
      std::cout << "+ Executing python script..." << std::endl;
      py::exec(code);
      std::cout << "+ Finished python script" << std::endl;
  }
  catch (const std::exception &e)
  {
      std::cout << "@ " << e.what() << std::endl;
  }
  executed = true;
  std::cout << "+ Terminated normal" << std::endl;
}
int main()
{
    std::cout << "+ Starting..." << std::endl;
    std::string code = R"(
# infinite_writer.py
import time
file_path = r"C:\Temp\loop.txt"
counter = 1
while True:
    with open(file_path, "a") as f:
        f.write(f"Line {counter}\n")
    counter += 1
    time.sleep(1)  # optional: wait 1 second between writes
)";
    py::scoped_interpreter interpreterGuard{};
    py::gil_scoped_release release;
    std::thread th(python_executor, code);
    auto threadId = th.get_id();
    std::cout << "+ Thread: " << threadId << std::endl;
    // stopped = true;
    int maxExecutionTime = 10;
    auto start = std::chrono::steady_clock::now();
    while (!executed)
    {
      auto elapsed = std::chrono::steady_clock::now() - start;
      if (elapsed > std::chrono::seconds(maxExecutionTime)) {
        std::cout << "Interrupting...";
        PyErr_SetInterrupt();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        if (th.joinable()) {
          th.join();
          executed = true;
          break;
        }
      }
      std::this_thread::sleep_for(std::chrono::seconds(1));
      std::cout << "+ Waiting..." << std::endl;
    }
    // Make sure to join the thread if it's still running
    if (th.joinable()) {
        th.join();
    }
    std::cout << "+ Finished" << std::endl;
    return EXIT_SUCCESS;
}
It's purposely an infinite loop in python, to try and stop it when timeout hits, but it never finishes, I got the following console response:
+ Starting...
+ Thread: 28596
+ Executing python script...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
+ Waiting...
Interrupting...
It stays at this part infinitely, proving that it didn't finish execution of the python code. What can I do to correctly end the execution?
I tried exactly like the example code, with PyErr_SetInterrupt, and also tried using PyEval_SetTrace lie said in this thread on github: https://github.com/pybind/pybind11/issues/2677 with the same result, code still runs after trying to stop it.
You can isolate user's Python code execution in a child process, and after timeout, send signal to it.
Fork child process
Start executing user's Python code in it
In main process, start a thread that will send a signal to that child process when the time is over
In main process, wait for the child to finish (normally or by signal)
Example for Unix-like OS:
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <csignal>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h> 
std::vector<char*> vecstr2vecc(const std::vector<std::string>& str_vec)
{
    std::vector<char*> res(str_vec.size() + 1, nullptr);
    for (size_t i = 0; i < str_vec.size(); i++)
        res[i] = const_cast<char*>(str_vec[i].c_str());
    return res;
}
void task(const std::string& code, size_t maxExecutionTime)
{
    pid_t pid = fork();
    if (pid)  // parent
    {
        std::cout << "Started task with pid=" << pid << std::endl;
        std::thread task_timeout_stopper([pid, maxExecutionTime]() {
            std::this_thread::sleep_for(std::chrono::seconds(maxExecutionTime));
            if (kill(pid, SIGINT) == 0)
            {
                std::cout << "Sent signal to pid=" << pid << std::endl;
            }
        });
        task_timeout_stopper.detach();
        waitpid(pid, NULL, 0);
        std::cout << "pid=" << pid << " finished." << std::endl;
    }
    else  // child
    {
        std::vector<std::string> args = { "python", "-c", code };
        execvp(args.front().c_str(), vecstr2vecc(args).data());
    }
}
int main()
{
    std::string code = R"(
import time
file_path = r"loop.txt"
counter = 1
while counter < 10:  # execute for 10 seconds
    with open(file_path, "a") as f:
        f.write(f"Line {counter}\n")
    counter += 1
    time.sleep(1)  # optional: wait 1 second between writes
)";
    task(code, 15); // this task will end in 10 seconds, before timeout of 15
    task(code, 5);  // this task will be interrupted after 5 seconds
    return 0;
}
This gives me the following output:
Started task with pid=120700
pid=120700 finished.
Started task with pid=120737
Sent signal to pid=120737
Traceback (most recent call last):
  File "<string>", line 12, in <module>
KeyboardInterrupt
pid=120737 finished.
The results in loop.txt file are as expected:
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line 4
Line 5
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With