Skip to content

Commit

Permalink
Change stack size to unlimited before running (#103)
Browse files Browse the repository at this point in the history
* Add function for unlimiting stack size

* Catch errors when changing stack size

* Change severity of the error

* Set stack size to max memory limit

* Add tests

* Fix tests for Linux

* Set hard limit when it's lower than wanted soft limit

* Fix tests

* Refactor finding max memory

* Change package for testing stack size

* Change stack size to unlimited

* Kill solution if it is running with memory limit exceeded

* Add tests

* Add comment to `change_stack_size_to_unlimited`

* Change `stc` package

* Change memory limit in `stc` package
  • Loading branch information
MasloMaslane authored Sep 15, 2023
1 parent e5705ef commit 2ea7989
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 11 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ install_requires =
PyYAML
dictdiffer
importlib-resources
psutil

[options.packages.find]
where = src
Expand Down
1 change: 1 addition & 0 deletions src/sinol_make/commands/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def run(self, args: argparse.Namespace):
shutil.rmtree(export_package_path)
os.makedirs(export_package_path)

util.change_stack_size_to_unlimited()
self.copy_package_required_files(export_package_path)
self.create_makefile_in(export_package_path, config)
archive = self.compress(export_package_path)
Expand Down
1 change: 1 addition & 0 deletions src/sinol_make/commands/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def run(self, args: argparse.Namespace):
self.correct_solution = gen_util.get_correct_solution(self.task_id)
self.ingen_exe = gen_util.compile_ingen(self.ingen, self.args, self.args.weak_compilation_flags)

util.change_stack_size_to_unlimited()
if gen_util.run_ingen(self.ingen_exe):
print(util.info('Successfully generated input files.'))
else:
Expand Down
1 change: 1 addition & 0 deletions src/sinol_make/commands/inwer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def run(self, args: argparse.Namespace):
else:
print('Verifying tests: ' + util.bold(', '.join(self.tests)))

util.change_stack_size_to_unlimited()
self.compile_inwer(args)
results: Dict[str, TestResult] = self.verify_and_print_table()
print('')
Expand Down
39 changes: 32 additions & 7 deletions src/sinol_make/commands/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import subprocess
import signal
import threading
from io import StringIO
import time
import psutil
import glob
from io import StringIO
from typing import Dict

from sinol_make import contest_types, oiejq
Expand Down Expand Up @@ -503,7 +505,10 @@ def sigint_handler(signum, frame):

def execute_time(self, command, name, result_file_path, input_file_path, output_file_path, answer_file_path,
time_limit, memory_limit, hard_time_limit):

executable = package_util.get_executable(name)
timeout = False
mem_limit_exceeded = False
with open(input_file_path, "r") as input_file:
process = subprocess.Popen(command, stdin=input_file, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
preexec_fn=os.setsid)
Expand All @@ -516,11 +521,27 @@ def sigint_handler(signum, frame):
sys.exit(1)
signal.signal(signal.SIGINT, sigint_handler)

try:
output, _ = process.communicate(timeout=hard_time_limit)
except subprocess.TimeoutExpired:
timeout = True
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
start_time = time.time()
while process.poll() is None:
try:
time_process = psutil.Process(process.pid)
executable_process = None
for child in time_process.children():
if child.name() == executable:
executable_process = child
break
if executable_process is not None and executable_process.memory_info().rss > memory_limit * 1024:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
mem_limit_exceeded = True
break
except psutil.NoSuchProcess:
pass

if time.time() - start_time > hard_time_limit:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
timeout = True
break
output, _ = process.communicate()

result = ExecutionResult()
program_exit_code = None
Expand All @@ -546,7 +567,7 @@ def sigint_handler(signum, frame):
Command terminated by signal 11
"""
program_exit_code = int(lines[0].strip().split(" ")[-1])
else:
elif not mem_limit_exceeded:
result.Status = Status.RE
result.Error = "Unexpected output from time command: " + "\n".join(lines)
return result
Expand All @@ -555,6 +576,9 @@ def sigint_handler(signum, frame):
result.Status = Status.RE
elif timeout:
result.Status = Status.TL
elif mem_limit_exceeded:
result.Memory = memory_limit + 1 # Add one so that the memory is red in the table
result.Status = Status.ML
elif result.Time > time_limit:
result.Status = Status.TL
elif result.Memory > memory_limit:
Expand Down Expand Up @@ -1147,6 +1171,7 @@ def run(self, args):
self.failed_compilations = []
solutions = self.get_solutions(self.args.solutions)

util.change_stack_size_to_unlimited()
for solution in solutions:
lang = package_util.get_file_lang(solution)
for test in self.tests:
Expand Down
14 changes: 14 additions & 0 deletions src/sinol_make/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import tempfile
import shutil
import hashlib
import subprocess
import threading
import resource
from typing import Union

import sinol_make
Expand Down Expand Up @@ -249,6 +251,18 @@ def stringify_keys(d):
return d


def change_stack_size_to_unlimited():
"""
Function to change the stack size to unlimited.
"""
try:
resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
except (resource.error, ValueError):
# We can't run `ulimit -s unlimited` in the code, because since it failed, it probably requires root.
print(error(f'Failed to change stack size to unlimited. Please run `ulimit -s unlimited` '
f'to make sure that solutions with large stack size will work.'))


def is_wsl():
"""
Function to check if the program is running on Windows Subsystem for Linux.
Expand Down
29 changes: 26 additions & 3 deletions tests/commands/run/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
import sys
import time
import pytest
import copy

Expand All @@ -11,7 +12,7 @@
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
get_checker_package_path(), get_library_package_path(),
get_library_string_args_package_path(), get_limits_package_path(),
get_limits_package_path(), get_override_limits_package_path()],
get_override_limits_package_path()],
indirect=True)
def test_simple(create_package, time_tool):
"""
Expand All @@ -30,7 +31,7 @@ def test_simple(create_package, time_tool):
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
get_checker_package_path(), get_library_package_path(),
get_library_string_args_package_path(), get_limits_package_path(),
get_limits_package_path(), get_override_limits_package_path()],
get_override_limits_package_path()],
indirect=True)
def test_no_expected_scores(capsys, create_package, time_tool):
"""
Expand Down Expand Up @@ -66,7 +67,7 @@ def test_no_expected_scores(capsys, create_package, time_tool):
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
get_checker_package_path(), get_library_package_path(),
get_library_string_args_package_path(), get_limits_package_path(),
get_limits_package_path(), get_override_limits_package_path()],
get_override_limits_package_path()],
indirect=True)
def test_apply_suggestions(create_package, time_tool):
"""
Expand Down Expand Up @@ -423,3 +424,25 @@ def test_override_limits(create_package, time_tool):
"points": 0
}
}


@pytest.mark.parametrize("create_package", [get_stack_size_package_path()], indirect=True)
def test_mem_limit_kill(create_package, time_tool):
"""
Test if `sinol-make` kills solution if it runs with memory limit exceeded.
"""
package_path = create_package
command = get_command()
create_ins_outs(package_path)

parser = configure_parsers()
args = parser.parse_args(["run", "--time-tool", time_tool])
command = Command()
start_time = time.time()
with pytest.raises(SystemExit) as e:
command.run(args)
end_time = time.time()

assert e.value.code == 1
assert end_time - start_time < 5 # The solution runs for 20 seconds, but it immediately exceeds memory limit,
# so it should be killed.
9 changes: 9 additions & 0 deletions tests/packages/stc/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Package for testing if changing stack size works

memory_limit: 1000
time_limit: 10000

sinol_expected_scores:
stc.cpp:
expected: {1: OK}
points: 100
Empty file added tests/packages/stc/in/.gitkeep
Empty file.
Empty file added tests/packages/stc/out/.gitkeep
Empty file.
19 changes: 19 additions & 0 deletions tests/packages/stc/prog/stc.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <bits/stdc++.h>
#include <chrono>

using namespace std;
using namespace std::chrono_literals;


int main() {
char array[30000000]; // 30 MB
for (int i = 0; i < 30000000; i++) {
array[i] = 'a';
}
this_thread::sleep_for(5s);
int a, b;
cin >> a >> b;
array[a] = (char)b;
cout << a + array[a];
return 0;
}
9 changes: 9 additions & 0 deletions tests/packages/stc/prog/stcingen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include <bits/stdc++.h>

using namespace std;

int main() {
ofstream f("stc1a.in");
f << "1 2\n";
f.close();
}
6 changes: 5 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import json
import tempfile
import requests
import resource
import requests_mock
import pytest

from sinol_make import util
from sinol_make import util, configure_parsers
from tests import util as test_util
from tests.fixtures import create_package
from tests.commands.run import util as run_util


def test_file_diff():
Expand Down
8 changes: 8 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ def get_handwritten_package_path():
return os.path.join(os.path.dirname(__file__), "packages", "hwr")


def get_stack_size_package_path():
"""
Get path to package for testing of changing stack size (/test/packages/stc)
"""
return os.path.join(os.path.dirname(__file__), "packages", "stc")



def get_override_limits_package_path():
"""
Get path to package with `override_limits` present in config (/test/packages/ovl)
Expand Down

0 comments on commit 2ea7989

Please sign in to comment.