proj: Add exam-2020W-2A
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ bin/
|
|||||||
*.so
|
*.so
|
||||||
*.o
|
*.o
|
||||||
*.log
|
*.log
|
||||||
|
*.pdf
|
||||||
|
|||||||
327
proj/exam-2020W-2A/check.py
Normal file
327
proj/exam-2020W-2A/check.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import string
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from libosue import *
|
||||||
|
from libosue import CSnippet as C
|
||||||
|
from libosue import FunctionPrototype as FP
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# INITIALIZE THE EXAM OBJECT
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# These functions are not allowed.
|
||||||
|
not_allowed = [
|
||||||
|
"DEMO_setup_connection",
|
||||||
|
"DEMO_server_request_handler",
|
||||||
|
"DEMO_execute_command",
|
||||||
|
]
|
||||||
|
|
||||||
|
prototypes = {
|
||||||
|
"parse_arguments": FP("void")("int argc", "char **argv", "struct arguments *args"),
|
||||||
|
"DEMO_setup_connection": FP("int")("const char *port_str"),
|
||||||
|
"DEMO_server_request_handler": FP("void")("int sockfd", "const char *command", "int flags"),
|
||||||
|
"DEMO_execute_command": FP("FILE *")("const char *command", "const char *argument"),
|
||||||
|
}
|
||||||
|
|
||||||
|
PORT = 692
|
||||||
|
|
||||||
|
exam.initialize(
|
||||||
|
"./server",
|
||||||
|
server_port=7220,
|
||||||
|
not_allowed=not_allowed + exam.DEFAULT_RESTRICTED_FUNCTIONS,
|
||||||
|
call_limit=200,
|
||||||
|
prototypes=prototypes,
|
||||||
|
report_file="report.txt",
|
||||||
|
result_file="result.txt",
|
||||||
|
debug_file="libosue.dbg",
|
||||||
|
make_targets=["clean", "all"],
|
||||||
|
initialize_stack=["setup_connection", "server_request_handler", "execute_command"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionAborted(EvalError):
|
||||||
|
"""Raised when a function should be aborted."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Kill programs on port 692.
|
||||||
|
# This is slow, so we only do it once every deliver call and not for every session.
|
||||||
|
utils.free_port(PORT)
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# TASK 1: SETUP CONNECTION
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def bind_cb(s, args):
|
||||||
|
"""Call setsockopt(...) before binding."""
|
||||||
|
reuse = Value.from_type("char")
|
||||||
|
reuse.value = 1
|
||||||
|
s.c.setsockopt(args["sockfd"], socket.SOL_SOCKET, socket.SO_REUSEADDR, reuse.address, 1)
|
||||||
|
|
||||||
|
|
||||||
|
with exam.task("Task 1: setup connection", 10) as task:
|
||||||
|
with task.subtask("initialize server socket", 8) as st:
|
||||||
|
with exam.session([]) as s:
|
||||||
|
# Setup tracing for hints.
|
||||||
|
s.c.socket.tracing = True
|
||||||
|
s.c.bind.tracing = True
|
||||||
|
s.c.listen.tracing = True
|
||||||
|
|
||||||
|
# Make sure setsockopt(...) is called before bind().
|
||||||
|
s.c.bind.call_cb = bind_cb
|
||||||
|
|
||||||
|
# Call the student's setup_connection connection function
|
||||||
|
sockfd = s.c.setup_connection(str(PORT).encode())
|
||||||
|
|
||||||
|
# Check if the server socket works and creates valid connection sockets.
|
||||||
|
utils.check_server_socket(s, sockfd, PORT)
|
||||||
|
|
||||||
|
# Give the students some hints for their implementation
|
||||||
|
if st.points != 8:
|
||||||
|
if "socket" not in s.trace:
|
||||||
|
exam.log("[INFO] socket() was never called")
|
||||||
|
if "bind" not in s.trace:
|
||||||
|
exam.log("[INFO] bind() was never called")
|
||||||
|
if "listen" not in s.trace:
|
||||||
|
exam.log("[INFO] listen() was never called")
|
||||||
|
|
||||||
|
exam.log("The following subtasks are skipped because of errors:")
|
||||||
|
exam.log("* error handling (2 points)")
|
||||||
|
task.skip_subtasks()
|
||||||
|
|
||||||
|
with task.subtask("error handling", 2) as st:
|
||||||
|
with st.subtask("bind()", 1):
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.listen.tracing = True
|
||||||
|
s.c.bind.override_return = C("((int) -1)")
|
||||||
|
|
||||||
|
with exam.raises(ExitError, "program did not exit after bind() failed") as excinfo:
|
||||||
|
sockfd = s.c.setup_connection(str(PORT).encode())
|
||||||
|
assert excinfo.value.exitcode == 1, "wrong exit code"
|
||||||
|
assert "listen" not in s.trace, "listen() called after bind() failed"
|
||||||
|
|
||||||
|
with st.subtask("listen()", 1):
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.listen.override_return = C("((int) -1)")
|
||||||
|
|
||||||
|
with exam.raises(ExitError, "program did not exit after listen() failed") as excinfo:
|
||||||
|
sockfd = s.c.setup_connection(str(PORT).encode())
|
||||||
|
assert excinfo.value.exitcode == 1, "wrong exit code"
|
||||||
|
|
||||||
|
|
||||||
|
with exam.task("Task 2: handle requests", 20) as task:
|
||||||
|
with task.subtask("read argument and execute command", 7) as st:
|
||||||
|
# Check if the program correctly accepts a connection and reads the argument
|
||||||
|
# from the connection socket. Execution stops when task 3 is called.
|
||||||
|
def accept_return_cb(s, rv):
|
||||||
|
rv = rv.convert()
|
||||||
|
if rv > 0:
|
||||||
|
s["connfd"] = rv
|
||||||
|
|
||||||
|
def read_call_cb(s, args):
|
||||||
|
connfd = args.get("sockfd") or args.get("fd")
|
||||||
|
s["read"] = connfd.convert() == s["connfd"]
|
||||||
|
s["buffer"] = args.get("buf")
|
||||||
|
|
||||||
|
def read_return_cb(s, rv):
|
||||||
|
if s["read"]:
|
||||||
|
n = rv.convert()
|
||||||
|
s["result"] += s["buffer"].convert(shape=n, cast="char *")
|
||||||
|
|
||||||
|
def execute_command_cb(s, args):
|
||||||
|
s["ec_called"] = True
|
||||||
|
try:
|
||||||
|
s["command"] = args["command"].convert()
|
||||||
|
s["argument"] = args["argument"].convert()
|
||||||
|
except InvalidMemoryError:
|
||||||
|
pass
|
||||||
|
raise FunctionAborted("This is where execution ends.")
|
||||||
|
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.accept.return_cb = accept_return_cb
|
||||||
|
s.c.read.call_cb = read_call_cb
|
||||||
|
s.c.read.return_cb = read_return_cb
|
||||||
|
s.c.recv.call_cb = read_call_cb
|
||||||
|
s.c.recv.return_cb = read_return_cb
|
||||||
|
|
||||||
|
s.c.execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.allowed = True
|
||||||
|
|
||||||
|
# Setup the tracing variables.
|
||||||
|
s["connfd"] = None
|
||||||
|
s["result"] = b""
|
||||||
|
s["ec_called"] = False
|
||||||
|
s["command"] = None
|
||||||
|
s["argument"] = None
|
||||||
|
|
||||||
|
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
|
||||||
|
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
|
||||||
|
with utils.SingleShotClient(PORT, argument) as client:
|
||||||
|
with contextlib.suppress(FunctionAborted):
|
||||||
|
s.c.server_request_handler(sockfd, b"./osue-tool")
|
||||||
|
assert s["result"], "no argument read from client"
|
||||||
|
assert s["result"] == argument, "read wrong argument from client"
|
||||||
|
assert s["ec_called"], "execute_command() not called"
|
||||||
|
assert s["command"] == b"./osue-tool", "invalid command passt to execute_command()"
|
||||||
|
assert (
|
||||||
|
s["argument"] == argument
|
||||||
|
), "Invalid argument passed to execute_command()\nDid you terminate it?"
|
||||||
|
|
||||||
|
with task.subtask("response", 8) as st:
|
||||||
|
# Execute the full function and check if the client receives the correct result.
|
||||||
|
def execute_command_cb(s, args):
|
||||||
|
with s.c.fork.save():
|
||||||
|
s.c.fork.allowed = True
|
||||||
|
return s.c.DEMO_execute_command(args["command"], args["argument"])
|
||||||
|
|
||||||
|
with st.subtask("single line output", 4) as pt:
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.allowed = True
|
||||||
|
|
||||||
|
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
|
||||||
|
|
||||||
|
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
|
||||||
|
expected = argument + b"\n"
|
||||||
|
with utils.SingleShotClient(PORT, argument) as client:
|
||||||
|
s.c.server_request_handler(sockfd, b"./osue-tool")
|
||||||
|
assert client.response, "didn't receive a response for single-line output"
|
||||||
|
assert client.response == expected, "received wrong response for single-line output"
|
||||||
|
|
||||||
|
with st.subtask("multi line output", 4) as pt:
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.allowed = True
|
||||||
|
|
||||||
|
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
|
||||||
|
|
||||||
|
argument = "".join(random.choices(string.ascii_letters, k=128)).encode()
|
||||||
|
expected = b"\n".join(argument[i : i + 32] for i in range(0, len(argument), 32)) + b"\n"
|
||||||
|
with utils.SingleShotClient(PORT, argument) as client:
|
||||||
|
s.c.server_request_handler(sockfd, b"./osue-tool")
|
||||||
|
assert client.response, "didn't receive a response for multi-line output"
|
||||||
|
assert client.response == expected, "received wrong response for multi-line output"
|
||||||
|
|
||||||
|
if task.points != 15:
|
||||||
|
exam.log("The following subtasks are skipped because of errors:")
|
||||||
|
exam.log("* repeated write/send calls (5 points)")
|
||||||
|
task.skip_subtasks()
|
||||||
|
|
||||||
|
with task.subtask("repeated write/send calls", 5) as st:
|
||||||
|
|
||||||
|
def accept_return_cb(s, rv):
|
||||||
|
s["connfd"] = rv.convert()
|
||||||
|
|
||||||
|
def write_cb(s, args):
|
||||||
|
fd = (args.get("fd") or args.get("sockfd")).convert()
|
||||||
|
|
||||||
|
if fd == s["connfd"]:
|
||||||
|
n = args.get("count") or args.get("len")
|
||||||
|
buf = args.get("buf")
|
||||||
|
s["messages"].append(buf.convert(shape=n.convert(), cast="char *"))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.call_cb = execute_command_cb
|
||||||
|
s.c.DEMO_execute_command.allowed = True
|
||||||
|
|
||||||
|
s.c.accept.return_cb = accept_return_cb
|
||||||
|
s.c.write.call_cb = write_cb
|
||||||
|
s.c.send.call_cb = write_cb
|
||||||
|
|
||||||
|
s["connfd"] = None
|
||||||
|
s["messages"] = []
|
||||||
|
|
||||||
|
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
|
||||||
|
expected = argument + b"\n"
|
||||||
|
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
|
||||||
|
with utils.SingleShotClient(PORT, argument) as client:
|
||||||
|
s.c.server_request_handler(sockfd, b"./osue-tool")
|
||||||
|
|
||||||
|
assert len(s["messages"]) > 1, "write/send not called multiple times"
|
||||||
|
message = b"".join(m[:1] for m in s["messages"])
|
||||||
|
assert message == expected, "received wrong response"
|
||||||
|
|
||||||
|
|
||||||
|
with exam.task("Task 3: execute command", 20) as task:
|
||||||
|
with task.subtask("fork/exec pattern", 5) as st:
|
||||||
|
pattern = utils.ForkNode(parent=None, child=utils.ExecNode("./osue-tool"))
|
||||||
|
with utils.ForkExecTraces(pattern) as tracegen:
|
||||||
|
for fork_exec_run in tracegen:
|
||||||
|
with exam.session() as s:
|
||||||
|
with fork_exec_run(s):
|
||||||
|
s.c.execute_command(b"./osue-tool", b"just a random string")
|
||||||
|
|
||||||
|
assert tracegen.correct, tracegen.default_message
|
||||||
|
|
||||||
|
if task.points != 5:
|
||||||
|
exam.log("The following subtasks are skipped because of errors:")
|
||||||
|
exam.log("* wait for child process to finish (5 points)")
|
||||||
|
exam.log("* result (10 points)")
|
||||||
|
task.skip_subtasks()
|
||||||
|
|
||||||
|
with task.subtask("wait for child process to finish", 5) as st:
|
||||||
|
# Correctly wait for the child process to finish.
|
||||||
|
# Follow the parent process and trace the return value of fork() and wait().
|
||||||
|
def fork_return_cb(s, rv):
|
||||||
|
s["fork_result"] = rv.convert()
|
||||||
|
|
||||||
|
def wait_return_cb(s, rv):
|
||||||
|
s["wait_result"] = rv.convert()
|
||||||
|
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.fork.allowed = True
|
||||||
|
s.c.fork.return_cb = fork_return_cb
|
||||||
|
s.c.wait.return_cb = wait_return_cb
|
||||||
|
s.c.waitpid.return_cb = wait_return_cb
|
||||||
|
s.c.execute_command(b"./osue-tool", b"just a random string")
|
||||||
|
|
||||||
|
assert "wait_result" in s, "wait never called"
|
||||||
|
assert s.get("wait_result") == s.get("fork_result"), "program did not wait for child process"
|
||||||
|
|
||||||
|
if task.points != 10:
|
||||||
|
exam.log("The following subtasks are skipped because of errors:")
|
||||||
|
exam.log("* result (10 points)")
|
||||||
|
task.skip_subtasks()
|
||||||
|
|
||||||
|
with task.subtask("result", 10) as st:
|
||||||
|
# Handle a single request and check the result.
|
||||||
|
with exam.session([]) as s:
|
||||||
|
s.c.fork.allowed = True
|
||||||
|
|
||||||
|
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
|
||||||
|
expected = argument + b"\n"
|
||||||
|
|
||||||
|
fp = s.c.execute_command(b"./osue-tool", argument)
|
||||||
|
assert fp.intaddr(), "returned file pointer not valid"
|
||||||
|
|
||||||
|
bufsize = 8192
|
||||||
|
try:
|
||||||
|
buf = s.c.malloc(bufsize, cast="char *")
|
||||||
|
n = s.c.fread(buf, 1, bufsize, fp)
|
||||||
|
except FunctionKilledError:
|
||||||
|
st.fail("reading from returned file pointer never terminated")
|
||||||
|
except SignalError:
|
||||||
|
st.fail("reading from returned file pointer failed")
|
||||||
|
|
||||||
|
result = buf.convert(shape=n.convert())
|
||||||
|
assert result == expected, "invalid response"
|
||||||
37
proj/exam-2020W-2A/src/Makefile
Normal file
37
proj/exam-2020W-2A/src/Makefile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
##
|
||||||
|
# Makefile for server and client libs.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# config
|
||||||
|
|
||||||
|
DEFS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_SVID_SOURCE -D_POSIX_C_SOURCE=200809 -D_XOPEN_SOURCE
|
||||||
|
override CFLAGS += -Wall -g -std=c99 -pedantic $(DEFS)
|
||||||
|
override LDFLAGS +=
|
||||||
|
override LIBS += -pthread -lrt
|
||||||
|
|
||||||
|
# objects for the server to build
|
||||||
|
OBJS = server_lib.o
|
||||||
|
|
||||||
|
|
||||||
|
# rules
|
||||||
|
|
||||||
|
all: $(OBJS) client osue-tool
|
||||||
|
make -f Makefile.student all
|
||||||
|
|
||||||
|
client: client.o
|
||||||
|
gcc $(LDFLAGS) -o $@ $^ $(LIBS)
|
||||||
|
|
||||||
|
osue-tool: osue-tool.c
|
||||||
|
gcc -o $@ $^
|
||||||
|
|
||||||
|
%.o: %.c
|
||||||
|
gcc $(CFLAGS) -c -o $@ $<
|
||||||
|
strip -S $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
make -f Makefile.student clean
|
||||||
|
rm -f client client.o $(OBJS) osue-tool
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
rm -rf *.txt __pycache__
|
||||||
38
proj/exam-2020W-2A/src/Makefile.student
Normal file
38
proj/exam-2020W-2A/src/Makefile.student
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
##
|
||||||
|
# Makefile for server.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# config
|
||||||
|
|
||||||
|
DEFS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_SVID_SOURCE -D_POSIX_C_SOURCE=200809 -D_XOPEN_SOURCE
|
||||||
|
override CFLAGS += -Wall -g -std=c99 -pedantic $(DEFS)
|
||||||
|
override LDFLAGS +=
|
||||||
|
override LIBS += -pthread -lrt
|
||||||
|
|
||||||
|
# objects to build
|
||||||
|
OBJS = server.o
|
||||||
|
|
||||||
|
# objects to link (already built)
|
||||||
|
LDOBJS = server_lib.o
|
||||||
|
|
||||||
|
|
||||||
|
# rules
|
||||||
|
|
||||||
|
.PHONY : all clean
|
||||||
|
|
||||||
|
all: server
|
||||||
|
|
||||||
|
server: $(OBJS) $(LDOBJS)
|
||||||
|
gcc $(LDFLAGS) -o $@ $^ $(LIBS)
|
||||||
|
|
||||||
|
%.o: %.c
|
||||||
|
gcc $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f server $(OBJS)
|
||||||
|
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
|
||||||
|
server.o: server.c
|
||||||
114
proj/exam-2020W-2A/src/client.c
Normal file
114
proj/exam-2020W-2A/src/client.c
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define BUFSIZE 1024
|
||||||
|
|
||||||
|
/** Name of the executable (for printing messages). */
|
||||||
|
char *program_name = "<not yet set>";
|
||||||
|
|
||||||
|
/** Structure for the arguments. */
|
||||||
|
struct arguments {
|
||||||
|
const char *port_str;
|
||||||
|
const char *argument;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Print a usage message and exit. */
|
||||||
|
void usage(const char *msg) {
|
||||||
|
fprintf(stderr, "Usage: %s [-p PORT] ARGUMENT\n%s\n", program_name, msg);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_message(const char *msg) {
|
||||||
|
fprintf(stderr, "[%s] %s\n", program_name, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Print an error message and exit with EXIT_FAILURE. */
|
||||||
|
void error_exit(const char *msg) {
|
||||||
|
if (errno == 0)
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s\n", program_name, msg);
|
||||||
|
else
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
|
||||||
|
strerror(errno));
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse arguments. */
|
||||||
|
void parse_arguments(int argc, char *argv[], struct arguments *args) {
|
||||||
|
memset(args, 0, sizeof(*args));
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "p:")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'p':
|
||||||
|
if (args->port_str)
|
||||||
|
usage("-p option allowed only once");
|
||||||
|
args->port_str = optarg;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage("invalid option");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args->port_str == NULL)
|
||||||
|
args->port_str = "2020";
|
||||||
|
|
||||||
|
if (argc - optind != 1)
|
||||||
|
usage("invalid number of arguments");
|
||||||
|
|
||||||
|
args->argument = argv[optind];
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *setup_connection(const char *port_str) {
|
||||||
|
struct addrinfo hints, *ai;
|
||||||
|
memset(&hints, 0, sizeof hints);
|
||||||
|
hints.ai_family = AF_INET;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
hints.ai_flags = AI_PASSIVE;
|
||||||
|
|
||||||
|
int res = getaddrinfo("localhost", port_str, &hints, &ai);
|
||||||
|
if (res != 0)
|
||||||
|
error_exit("getaddrinfo()");
|
||||||
|
|
||||||
|
int connfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||||
|
if (connfd < 0)
|
||||||
|
error_exit("socket()");
|
||||||
|
|
||||||
|
if (connect(connfd, ai->ai_addr, ai->ai_addrlen) < 0)
|
||||||
|
error_exit("connect()");
|
||||||
|
|
||||||
|
freeaddrinfo(ai);
|
||||||
|
|
||||||
|
return fdopen(connfd, "w+");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
struct arguments args;
|
||||||
|
program_name = argv[0];
|
||||||
|
|
||||||
|
parse_arguments(argc, argv, &args);
|
||||||
|
|
||||||
|
FILE *fp = setup_connection(args.port_str);
|
||||||
|
|
||||||
|
fprintf(fp, "%s", args.argument);
|
||||||
|
fflush(fp);
|
||||||
|
shutdown(fileno(fp), SHUT_WR);
|
||||||
|
|
||||||
|
char *lineptr = NULL;
|
||||||
|
size_t n = 0;
|
||||||
|
print_message("reading response:");
|
||||||
|
while (getline(&lineptr, &n, fp) != -1) {
|
||||||
|
fprintf(stdout, "[%s] >>> %s", program_name, lineptr);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
print_message("exiting regularly");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
25
proj/exam-2020W-2A/src/osue-tool.c
Normal file
25
proj/exam-2020W-2A/src/osue-tool.c
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Take a single string as an argument and format it into 32-character blocks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "invalid number of arguments\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < strlen(argv[1]); i++) {
|
||||||
|
int c = argv[1][i];
|
||||||
|
printf("%c", c);
|
||||||
|
if (i % 32 == 31)
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
if (i % 32 != 0)
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
171
proj/exam-2020W-2A/src/server.c
Normal file
171
proj/exam-2020W-2A/src/server.c
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
char *program_name = "<not yet set>";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Signal handler and quit flag for synchronization loop.
|
||||||
|
* DO NOT CHANGE THIS!
|
||||||
|
*/
|
||||||
|
volatile sig_atomic_t quit = 0;
|
||||||
|
|
||||||
|
static void signal_handler(int sig) {
|
||||||
|
quit = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prints a usage message and exits.
|
||||||
|
* @param msg Additional output message.
|
||||||
|
*/
|
||||||
|
void usage(const char *msg) {
|
||||||
|
fprintf(stderr, "Usage: %s [-p PORT] COMMAND\n%s\n", program_name, msg);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Print an error message and exit with EXIT_FAILURE.
|
||||||
|
* @param msg Additional output message.
|
||||||
|
*/
|
||||||
|
void error_exit(const char *msg) {
|
||||||
|
if (errno == 0)
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s\n", program_name, msg);
|
||||||
|
else
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
|
||||||
|
strerror(errno));
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************************************************
|
||||||
|
* Task 1 - Setup the connection for the server
|
||||||
|
*
|
||||||
|
* Create a passive socket of domain AF_INET and type SOCK_STREAM.
|
||||||
|
* Listen for connections on the port given by the argument `port_str'.
|
||||||
|
* Return the file descriptor of the communication socket.
|
||||||
|
*
|
||||||
|
* WARNING: The communication socket is NOT the connection socket,
|
||||||
|
* DO NOT USE accept(2) IN THIS FUNCTION!
|
||||||
|
*
|
||||||
|
* @param port_str The port string.
|
||||||
|
* @return File descriptor of the communication socket.
|
||||||
|
*
|
||||||
|
* Hints: getaddrinfo(3), socket(2), listen(2)
|
||||||
|
************************************************************************/
|
||||||
|
int setup_connection(const char *port_str) {
|
||||||
|
// put your code here
|
||||||
|
|
||||||
|
// Replace with a meaningful return value
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Task 2 - Handle client connections
|
||||||
|
*
|
||||||
|
* Wait for a connection on the communication socket and accept it.
|
||||||
|
*
|
||||||
|
* Read the argument transmitted by the client from the connection and save it
|
||||||
|
* to a buffer that can hold a C-string with up to MAX_ARGUMENT_LEN characters.
|
||||||
|
*
|
||||||
|
* Call execute_command() (or the respective DEMO function) with the command
|
||||||
|
* and the argument.
|
||||||
|
*
|
||||||
|
* Read the result from the file pointer returned by execute_command() and send
|
||||||
|
* it back to the client. Send the string "COMMAND_FAILED" if execute_command()
|
||||||
|
* fails.
|
||||||
|
*
|
||||||
|
* @param sockfd File descriptor of the communication socket.
|
||||||
|
* @param command Command to execute.
|
||||||
|
*
|
||||||
|
* Hints: accept(2), fdopen(3), fgets(3), fprintf(3), fclose(3)
|
||||||
|
******************************************************************************/
|
||||||
|
void server_request_handler(int sockfd, const char *command) {
|
||||||
|
// put your code here
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Task 3 - Execute command
|
||||||
|
*
|
||||||
|
* Create a pipe for redirection of standard output of the child process.
|
||||||
|
*
|
||||||
|
* Fork a child process, close stdin of the new process and redirect stdout to
|
||||||
|
* the pipe.
|
||||||
|
*
|
||||||
|
* Execute the command in the variable 'command' with the single argument in
|
||||||
|
* the variable 'argument'.
|
||||||
|
*
|
||||||
|
* Wait for the child process to finish. If the child process exits with
|
||||||
|
* exit-status 0, create a FILE object for the read-end of the pipe and return
|
||||||
|
* it. Return NULL if the child process fails.
|
||||||
|
*
|
||||||
|
* Do not read from the pipe in this function.
|
||||||
|
*
|
||||||
|
* @param command The command that will be executed.
|
||||||
|
* @param argument The argument for the command.
|
||||||
|
*
|
||||||
|
* @return Address of the FILE-object for the read end of the child's standard
|
||||||
|
* output pipe.
|
||||||
|
*
|
||||||
|
* Hints: pipe(2), dup2(2), fork(2), exec(3), fdopen(3), wait(3)
|
||||||
|
******************************************************************************/
|
||||||
|
FILE *execute_command(const char *command, const char *argument) {
|
||||||
|
// put your code here
|
||||||
|
|
||||||
|
// Replace with a meaningful return value
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Main program
|
||||||
|
* @param argc Number of elements in argv
|
||||||
|
* @param argv Array of command line arguments
|
||||||
|
* @return EXIT_SUCCESS if everything is okay
|
||||||
|
*/
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
// DO NOT CHANGE THE FOLLOWING DECLARATIONS
|
||||||
|
int sockfd = -1;
|
||||||
|
struct arguments args; // struct for storing the parsed arguments
|
||||||
|
program_name = argv[0];
|
||||||
|
|
||||||
|
// Register signal handlers
|
||||||
|
struct sigaction s;
|
||||||
|
s.sa_handler = signal_handler;
|
||||||
|
s.sa_flags = 0; // no SA_RESTART!
|
||||||
|
if (sigfillset(&s.sa_mask) < 0) {
|
||||||
|
error_exit("sigfillset");
|
||||||
|
}
|
||||||
|
if (sigaction(SIGINT, &s, NULL) < 0) {
|
||||||
|
error_exit("sigaction SIGINT");
|
||||||
|
}
|
||||||
|
if (sigaction(SIGTERM, &s, NULL) < 0) {
|
||||||
|
error_exit("sigaction SIGTERM");
|
||||||
|
}
|
||||||
|
// register function to free resources at normal process termination
|
||||||
|
if (atexit(free_resources) == -1) {
|
||||||
|
error_exit("atexit failed");
|
||||||
|
}
|
||||||
|
parse_arguments(argc, argv, &args);
|
||||||
|
|
||||||
|
sockfd = setup_connection(args.port_str);
|
||||||
|
//sockfd = DEMO_setup_connection(args.port_str);
|
||||||
|
|
||||||
|
while (!quit) {
|
||||||
|
server_request_handler(sockfd, args.command);
|
||||||
|
//DEMO_server_request_handler(sockfd, args.command, 0); // internally calls execute_command()
|
||||||
|
//DEMO_server_request_handler(sockfd, args.command, USE_DEMO); // internally calls DEMO_execute_command()
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)fprintf(stderr, "server exiting regularly\n");
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
33
proj/exam-2020W-2A/src/server.h
Normal file
33
proj/exam-2020W-2A/src/server.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @brief Declarations used by the server application.
|
||||||
|
**/
|
||||||
|
|
||||||
|
#ifndef _SERVE_H_
|
||||||
|
#define _SERVE_H_
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define USE_DEMO 0x01
|
||||||
|
|
||||||
|
#define MAX_ARGUMENT_LEN 1024
|
||||||
|
|
||||||
|
/** Structure for the arguments. */
|
||||||
|
struct arguments {
|
||||||
|
const char *port_str;
|
||||||
|
const char *command;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parse_arguments(int argc, char *argv[], struct arguments *args);
|
||||||
|
void free_resources(void);
|
||||||
|
|
||||||
|
// forward declaration of student functions.
|
||||||
|
int setup_connection(const char *port_str);
|
||||||
|
void server_request_handler(int sockfd, const char *command);
|
||||||
|
FILE *execute_command(const char *command, const char *argument);
|
||||||
|
|
||||||
|
// demo solutions
|
||||||
|
int DEMO_setup_connection(const char *port_str);
|
||||||
|
void DEMO_server_request_handler(int sockfd, const char *command, int flags);
|
||||||
|
FILE *DEMO_execute_command(const char *command, const char *argument);
|
||||||
|
|
||||||
|
#endif
|
||||||
260
proj/exam-2020W-2A/src/server_lib.c
Normal file
260
proj/exam-2020W-2A/src/server_lib.c
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
extern char *program_name;
|
||||||
|
|
||||||
|
extern volatile sig_atomic_t quit;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prints a usage message and exits.
|
||||||
|
* @param msg Additional output message.
|
||||||
|
*/
|
||||||
|
static void usage(const char *msg) {
|
||||||
|
fprintf(stderr, "Usage: %s [-p PORT] COMMAND\n%s\n", program_name, msg);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Print an error message and exit with EXIT_FAILURE.
|
||||||
|
* @param msg Additional output message.
|
||||||
|
*/
|
||||||
|
static void error_exit(const char *msg) {
|
||||||
|
if (errno == 0)
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s\n", program_name, msg);
|
||||||
|
else
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
|
||||||
|
strerror(errno));
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_error(const char *msg) {
|
||||||
|
fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
|
||||||
|
strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_message(const char *msg) {
|
||||||
|
fprintf(stderr, "[%s] %s\n", program_name, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function freeing the resources.
|
||||||
|
*/
|
||||||
|
void free_resources(void) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse Arguments
|
||||||
|
*/
|
||||||
|
void parse_arguments(int argc, char *argv[], struct arguments *args) {
|
||||||
|
memset(args, 0, sizeof(*args));
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "p:")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'p':
|
||||||
|
if (args->port_str)
|
||||||
|
usage("-p option allowed only once");
|
||||||
|
args->port_str = optarg;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage("invalid option");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args->port_str == NULL)
|
||||||
|
args->port_str = "2020";
|
||||||
|
|
||||||
|
if (argc - optind != 1)
|
||||||
|
usage("invalid number of arguments");
|
||||||
|
|
||||||
|
args->command = argv[optind];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Custom getaddrinfo implementation for localhost only.
|
||||||
|
*/
|
||||||
|
int getaddrinfo(const char *node, const char *service,
|
||||||
|
const struct addrinfo *hints, struct addrinfo **res) {
|
||||||
|
struct in_addr localhost;
|
||||||
|
localhost.s_addr = htonl(0x7f000001); // 127.0.0.1
|
||||||
|
|
||||||
|
// check hostname (allow only `localhost' or any string which `inet_aton()'
|
||||||
|
// converts to 127.0.0.1)
|
||||||
|
struct in_addr node_addr;
|
||||||
|
if (node != NULL &&
|
||||||
|
(inet_aton(node, &node_addr) == 0 ||
|
||||||
|
node_addr.s_addr != localhost.s_addr) &&
|
||||||
|
strcmp(node, "localhost") != 0)
|
||||||
|
return EAI_NONAME;
|
||||||
|
|
||||||
|
// check port (allow only numerical ports)
|
||||||
|
char *endptr;
|
||||||
|
int port = strtol(service, &endptr, 10);
|
||||||
|
|
||||||
|
if (*endptr != '\0')
|
||||||
|
return EAI_NONAME;
|
||||||
|
|
||||||
|
// check hints (NULL is acceptable)
|
||||||
|
if (hints != NULL) {
|
||||||
|
// check family
|
||||||
|
if (hints->ai_family != AF_UNSPEC && hints->ai_family != AF_INET)
|
||||||
|
return EAI_FAMILY;
|
||||||
|
|
||||||
|
// check socket type
|
||||||
|
if (hints->ai_socktype != 0 && hints->ai_socktype != SOCK_STREAM)
|
||||||
|
return EAI_SOCKTYPE;
|
||||||
|
|
||||||
|
// check protocol
|
||||||
|
if (hints->ai_protocol != 0 && hints->ai_protocol != IPPROTO_TCP)
|
||||||
|
return EAI_SOCKTYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocate memory for result
|
||||||
|
*res = malloc(sizeof(struct addrinfo));
|
||||||
|
struct sockaddr_in *sin = malloc(sizeof(struct sockaddr_in));
|
||||||
|
|
||||||
|
if (*res == NULL || sin == NULL)
|
||||||
|
return EAI_MEMORY;
|
||||||
|
|
||||||
|
memset(sin, 0, sizeof(struct sockaddr_in));
|
||||||
|
sin->sin_family = AF_INET;
|
||||||
|
sin->sin_addr = localhost;
|
||||||
|
sin->sin_port = htons(port);
|
||||||
|
|
||||||
|
(*res)->ai_flags = (AI_V4MAPPED | AI_ADDRCONFIG);
|
||||||
|
(*res)->ai_family = AF_INET;
|
||||||
|
(*res)->ai_socktype = SOCK_STREAM;
|
||||||
|
(*res)->ai_protocol = IPPROTO_TCP;
|
||||||
|
(*res)->ai_addrlen = sizeof(struct sockaddr_in);
|
||||||
|
(*res)->ai_addr = (struct sockaddr *)sin;
|
||||||
|
(*res)->ai_canonname = NULL;
|
||||||
|
(*res)->ai_next = NULL;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************
|
||||||
|
* DEMO FUNCTIONS
|
||||||
|
*****************************************************************************/
|
||||||
|
int DEMO_setup_connection(const char *port_str) {
|
||||||
|
struct addrinfo hints, *ai;
|
||||||
|
memset(&hints, 0, sizeof hints);
|
||||||
|
hints.ai_family = AF_INET;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
hints.ai_flags = AI_PASSIVE;
|
||||||
|
|
||||||
|
int res = getaddrinfo(NULL, port_str, &hints, &ai);
|
||||||
|
if (res != 0)
|
||||||
|
error_exit("getaddrinfo() failed");
|
||||||
|
|
||||||
|
int sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||||
|
if (sockfd < 0)
|
||||||
|
error_exit("socket() failed");
|
||||||
|
|
||||||
|
int reuse = 1;
|
||||||
|
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
|
||||||
|
error_exit("setsockopt() failed");
|
||||||
|
|
||||||
|
if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0)
|
||||||
|
error_exit("bind() failed");
|
||||||
|
|
||||||
|
freeaddrinfo(ai);
|
||||||
|
|
||||||
|
if (listen(sockfd, 1) < 0)
|
||||||
|
error_exit("listen() failed");
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DEMO_server_request_handler(int sockfd, const char *command, int flags) {
|
||||||
|
int connfd = accept(sockfd, NULL, NULL);
|
||||||
|
if (connfd < 0)
|
||||||
|
error_exit("accept() failed");
|
||||||
|
|
||||||
|
print_message("Accepted connection");
|
||||||
|
|
||||||
|
FILE *fps = fdopen(connfd, "w+");
|
||||||
|
|
||||||
|
char argument[MAX_ARGUMENT_LEN + 1] = {0};
|
||||||
|
fgets(argument, sizeof(argument), fps);
|
||||||
|
|
||||||
|
FILE *fpc = NULL;
|
||||||
|
if (flags & USE_DEMO) {
|
||||||
|
print_message("Calling DEMO_execute_command()");
|
||||||
|
fpc = DEMO_execute_command(command, argument);
|
||||||
|
} else {
|
||||||
|
print_message("Calling execute_command()");
|
||||||
|
fpc = execute_command(command, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fpc) {
|
||||||
|
int c;
|
||||||
|
while ((c = fgetc(fpc)) != EOF)
|
||||||
|
fputc(c, fps);
|
||||||
|
fclose(fpc);
|
||||||
|
} else {
|
||||||
|
fputs("COMMAND_FAILED", fps);
|
||||||
|
}
|
||||||
|
fflush(fps);
|
||||||
|
fclose(fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *DEMO_execute_command(const char *command, const char *argument) {
|
||||||
|
// file stream to read the output of the process
|
||||||
|
FILE *proc_output = NULL;
|
||||||
|
|
||||||
|
// file descriptors for the pipe for parent-child process communication
|
||||||
|
int c2p[2];
|
||||||
|
|
||||||
|
// create the pipes
|
||||||
|
if (pipe(c2p) == -1)
|
||||||
|
error_exit("cannot create pipe");
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
switch (pid) {
|
||||||
|
case 0:
|
||||||
|
// duplicate write end to stdout
|
||||||
|
if (dup2(c2p[1], STDOUT_FILENO) == -1) {
|
||||||
|
error_exit("dup2() failed");
|
||||||
|
}
|
||||||
|
close(c2p[0]);
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
|
// exec the command
|
||||||
|
if (execlp(command, command, argument, NULL) == -1) {
|
||||||
|
error_exit("exec() failed");
|
||||||
|
}
|
||||||
|
case -1:
|
||||||
|
error_exit("fork() failed");
|
||||||
|
|
||||||
|
default:
|
||||||
|
// parent process
|
||||||
|
proc_output = fdopen(c2p[0], "r");
|
||||||
|
if (proc_output == NULL) {
|
||||||
|
error_exit("fdopen() failed");
|
||||||
|
}
|
||||||
|
close(c2p[1]);
|
||||||
|
|
||||||
|
int stat = 0;
|
||||||
|
wait(&stat);
|
||||||
|
|
||||||
|
if (WIFEXITED(stat) && WEXITSTATUS(stat) == 0) {
|
||||||
|
print_message("child process exited normally");
|
||||||
|
} else {
|
||||||
|
print_error("child process exited abnormally");
|
||||||
|
proc_output = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return proc_output;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user