# -*- coding: utf-8 -*-
#
# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
# more information about the licensing of this file.
import array
import os
import signal
import socket
import tempfile
import msgpack
import zmq
[docs]def run_student(cmd, container=None,
time_limit=0, hard_time_limit=0,
memory_limit=0, share_network=False,
working_dir=None, stdin=None, stdout=None, stderr=None,
signal_handler_callback=None):
"""
Run a command inside a student container
:param cmd: command to be ran (as a string, with parameters)
:param container: container to use. Must be present in the current agent. By default it is None, meaning the current
container type will be used.
:param time_limit: time limit in seconds. By default it is 0, which means that it will be the same as the current
container (NB: it does not count in the "host" container timeout!)
:param hard_time_limit: hard time limit. By default it is 0, which means that it will be the same as the current
container (NB: it *does* count in the "host" container *hard* timeout!)
:param memory_limit: memory limit in megabytes. By default it is 0, which means that it will be the same as the current
container (NB: it does not count in the "host" container memory limit!)
:param share_network: share the network with the host container if True. Default is False.
:param working_dir: The working directory for the distant command. By default, it is os.getcwd().
:param stdin: File descriptor for stdin. Can be None, in which case a file descriptor is open to /dev/null.
:param stdout: File descriptor for stdout. Can be None, in which case a file descriptor is open to /dev/null.
:param stderr: File descriptor for stderr. Can be None, in which case a file descriptor is open to /dev/null.
:param signal_handler_callback: If not None, `run` will call this callback with a function as single argument.
this function can itself be called with a signal value that will immediately be sent
to the remote process. See the run_student script command for an example, or
the hack_signals function below.
:return: the return value of the calling process. There are special values:
- 252 means that the command was killed due to an out-of-memory
- 253 means that the command timed out
- 254 means that an error occurred while running the proxy
"""
if working_dir is None:
working_dir = os.getcwd()
if stdin is None:
stdin = open(os.devnull, 'rb').fileno()
if stdout is None:
stdout = open(os.devnull, 'rb').fileno()
if stderr is None:
stderr = open(os.devnull, 'rb').fileno()
try:
# creates a placeholder for the socket
DIR = "/sockets/"
_, path = tempfile.mkstemp('', 'p', DIR)
# Gets the socket id
socket_id = os.path.split(path)[-1]
socket_path = os.path.join(DIR, socket_id + ".sock")
# Start the socket
server = socket.socket(socket.AF_UNIX)
try:
os.unlink(socket_path)
except OSError:
if os.path.exists(socket_path):
raise
server.bind(socket_path)
server.listen(0)
# Kindly ask the agent to start a new container linked to our socket
context = zmq.Context()
zmq_socket = context.socket(zmq.REQ)
zmq_socket.connect("ipc:///sockets/main.sock")
zmq_socket.send(msgpack.dumps({"type": "run_student", "environment": container,
"time_limit": time_limit, "hard_time_limit": hard_time_limit,
"memory_limit": memory_limit, "share_network": share_network,
"socket_id": socket_id}, use_bin_type=True))
# Check if the container was correctly started
message = msgpack.loads(zmq_socket.recv(), use_list=False)
assert message["type"] == "run_student_started"
# Send a dummy message to ask for retval
zmq_socket.send(msgpack.dumps({"type": "run_student_ask_retval", "socket_id": socket_id}, use_bin_type=True))
# Serve one and only one connection
connection, addr = server.accept()
# _run_student_intern should say hello
datagram = connection.recv(1)
assert datagram == b'H'
# send the fds and the command/workdir
connection.sendmsg([b'S'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [stdin, stdout, stderr]))])
connection.send(msgpack.dumps({"command": cmd, "working_dir": working_dir}))
# Allow to send signals
if signal_handler_callback is not None:
def receive_signal(signum_s):
signum_data = str(signum_s).zfill(3).encode("utf8")
connection.send(signum_data)
signal_handler_callback(receive_signal)
# Wait for everything to end
message = msgpack.loads(zmq_socket.recv(), use_list=False)
# Unlink unneeded files
try:
os.unlink(socket_path)
os.unlink(path)
except:
pass
return message["retval"]
except:
return 254
[docs]def run_student_simple(cmd, cmd_input=None, container=None,
time_limit=0, hard_time_limit=0,
memory_limit=0, share_network=False,
working_dir=None, stdout_err_fuse=False, text="utf-8"):
"""
A simpler version of `run`, which takes an input string and return the output of the command.
This disallows interactive processes.
:param cmd: cmd to be run.
:param cmd_input: input of the command. Can be a string or a bytes object, or None.
:param container: container to use. Must be present in the current agent. By default it is None, meaning the current
container type will be used.
:param time_limit: time limit in seconds. By default it is 0, which means that it will be the same as the current
container (NB: it does not count in the "host" container timeout!)
:param hard_time_limit: hard time limit. By default it is 0, which means that it will be the same as the current
container (NB: it *does* count in the "host" container *hard* timeout!)
:param memory_limit: memory limit in megabytes. By default it is 0, which means that it will be the same as the current
container (NB: it does not count in the "host" container memory limit!)
:param share_network: share the network with the host container if True. Default is False.
:param working_dir: The working directory for the distant command. By default, it is os.getcwd().
:param stdout_err_fuse: Weither to fuse stdout and stderr (i.e. make them use the same file descriptor)
:param text: By default, run_simple assumes that stdout/stderr will be encoded in UTF-8. Putting another encoding
will make the streams encoded using this encoding. text=False indicates that the streams should be
opened in binary mode. In this case, run_simple returns streams in the form of binary, unencoded,
strings.
:return: The output of the command, as a tuple of objects (stdout, stderr, retval). If stdout_err_fuse is True, the
output is in the form (stdout, retval) is returned.
The type of the returned strings (stdout, stderr) is dependent of the `text` arg.
"""
stdin = None
if cmd_input is not None:
r, w = os.pipe()
fdo = os.fdopen(w, 'w')
fdo.write(cmd_input)
fdo.close()
stdin = r
stdout_r, stdout_w = os.pipe()
if stdout_err_fuse:
stderr_r, stderr_w = stdout_r, stdout_w
else:
stderr_r, stderr_w = os.pipe()
retval = run_student(cmd, container, time_limit, hard_time_limit, memory_limit,
share_network, working_dir, stdin, stdout_w, stderr_w)
preprocess_out = (lambda x: x.decode(text)) if text is not False else (lambda x: x)
os.fdopen(stdout_w, 'w').close()
stdout = preprocess_out(os.fdopen(stdout_r, 'rb').read())
if not stdout_err_fuse:
os.fdopen(stderr_w, 'w').close()
stderr = preprocess_out(os.fdopen(stderr_r, 'rb').read())
return stdout, stderr, retval
else:
return stdout, retval
def _hack_signals(receive_signal):
""" Catch every signal, and send it to the remote process """
uncatchable = ['SIG_DFL', 'SIGSTOP', 'SIGKILL']
for i in [x for x in dir(signal) if x.startswith("SIG")]:
if i not in uncatchable:
try:
signum = getattr(signal, i)
signal.signal(signum, lambda x, _: receive_signal)
except:
pass