init
Signed-off-by: Joseph Umana <joseph@josephumana.com>
This commit is contained in:
commit
da34a7b62e
9 changed files with 378 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.out
|
||||
gojrt
|
||||
.cache/
|
||||
compile_commands.json
|
2
Makefile
Normal file
2
Makefile
Normal file
|
@ -0,0 +1,2 @@
|
|||
all:
|
||||
gcc main.c structs.h serialization.c serialization.h -o gojrt -Wall -std=c23 -g
|
12
README.md
Normal file
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# GOJ-Runtime
|
||||
A lightweight, simple competitive programmer runtime and grader.
|
||||
|
||||
## Features
|
||||
[x] Time limits
|
||||
[x] Memory limits
|
||||
[x] Result Serialization (results)
|
||||
[] Grading
|
||||
[] Multiple Test Cases
|
||||
|
||||
## Usage
|
||||
`# gojrt <compile_command> <run_command> <runtime_seconds> <memory_limit>`
|
180
main.c
Normal file
180
main.c
Normal file
|
@ -0,0 +1,180 @@
|
|||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <stdarg.h>
|
||||
#include <errno.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include "structs.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#define COMPILE_TIMEOUT 120
|
||||
#define COMPILE_MEMORYLIMIT 1000000 // 1GB
|
||||
|
||||
#define RUNNER_UID 1000
|
||||
#define RUNNER_GID 1000
|
||||
|
||||
#ifndef RUNNER_UID
|
||||
#error "RUNNER_UID must be set"
|
||||
#endif
|
||||
|
||||
#ifndef RUNNER_GID
|
||||
#error "RUNNER_GID must be set"
|
||||
#endif
|
||||
|
||||
// TODO: Better Panics
|
||||
// Give an OE
|
||||
_Noreturn void panic(const char* msg, ...) {
|
||||
va_list vargs;
|
||||
va_start(vargs, msg);
|
||||
|
||||
fprintf(stderr, "panic: ");
|
||||
vfprintf(stderr, msg, vargs);
|
||||
|
||||
va_end(vargs);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void execCommand(char* command) {
|
||||
setgid(RUNNER_GID);
|
||||
setuid(RUNNER_UID);
|
||||
char* args[] = {"sh", "-c", command, NULL};
|
||||
execvp("sh", args);
|
||||
|
||||
// If exec returns, we're beyond cooked so panic
|
||||
panic("Failed to exec in process! Errno: %d", errno);
|
||||
}
|
||||
|
||||
void getExecStats(pid_t pid, RunResult* result) {
|
||||
puts("getExecStats");
|
||||
struct rusage usage;
|
||||
time_t start = time(NULL);
|
||||
|
||||
if (wait4(pid, &result->exitCode, 0, &usage) <= 0) {
|
||||
panic("wait4 non-zero! Errno: %d", errno);
|
||||
}
|
||||
|
||||
result->exitCode = WEXITSTATUS(result->exitCode);
|
||||
|
||||
result->maxRss = usage.ru_maxrss;
|
||||
|
||||
time_t end = time(NULL);
|
||||
|
||||
time_t executionTime = end - start;
|
||||
|
||||
result->runtime = executionTime;
|
||||
}
|
||||
|
||||
void setExecFlags(RunResult* result, time_t runtimeSeconds, size_t memoryLimit) {
|
||||
printf("%d\n", result->exitCode);
|
||||
if (result->exitCode == 69) {
|
||||
result->flags |= Nice;
|
||||
strcpy(result->feedback, "Nice!");
|
||||
}
|
||||
|
||||
bool errorFlag = false;
|
||||
|
||||
if (result->exitCode != 0) {
|
||||
result->flags |= ProgramError;
|
||||
errorFlag = true;
|
||||
}
|
||||
|
||||
if (result->runtime > runtimeSeconds) {
|
||||
result->flags |= TimeLimitExceeded;
|
||||
errorFlag = true;
|
||||
}
|
||||
|
||||
if (result->maxRss > memoryLimit) {
|
||||
result->flags |= MemoryLimitExceeded;
|
||||
errorFlag = true;
|
||||
}
|
||||
|
||||
if (errorFlag) {
|
||||
result->flags |= Err;
|
||||
} else {
|
||||
result->flags |= Ok;
|
||||
}
|
||||
}
|
||||
|
||||
void getRunSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs commands with given constraints
|
||||
*
|
||||
* @details
|
||||
* executeCommand will run the command string with <em>runtimeSeconds</em>
|
||||
* as a time limit and <em>memoryLimit
|
||||
* </em> as max RSS in KB. If time
|
||||
* is exceeded, the function will immediately return error. However,
|
||||
* a program that exceeds the memory limit will continue to run but, it's max
|
||||
* RSS usage will be checked and will fail if it exceeds the memory limit
|
||||
*
|
||||
* Command strings are also passed in as an argument to "shell -c" allowing
|
||||
* shell syntax to be used. This can be particularly useful for multi-step
|
||||
* compilation.
|
||||
*
|
||||
* @param[in] command The shell commmand/syntax to run
|
||||
* @param[in] runtimeSeconds Timeout in seconds
|
||||
* @param[in] memoryLimit Max RSS limit in KB
|
||||
*
|
||||
*/
|
||||
RunResult executeCommand(char* command, time_t runtimeSeconds, size_t memoryLimit) {
|
||||
int process = fork();
|
||||
|
||||
RunResult result;
|
||||
|
||||
// stop undefined behavior
|
||||
// from thinking a string is present
|
||||
result.feedback[0] = '\0';
|
||||
|
||||
if (process > 0) {
|
||||
getExecStats(process, &result);
|
||||
} else if (process == 0) {
|
||||
execCommand(command);
|
||||
} else {
|
||||
panic("Failed to spawn process");
|
||||
}
|
||||
|
||||
setExecFlags(&result, runtimeSeconds, memoryLimit);
|
||||
|
||||
result.flags |= ExecutionFinal;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (getuid() != 0) {
|
||||
panic("Must run as root");
|
||||
}
|
||||
|
||||
if (argc < 5) {
|
||||
panic("Not enough args");
|
||||
}
|
||||
|
||||
size_t runMemoryLimit = atol(argv[3]);
|
||||
time_t runRuntimeSeconds = atol(argv[4]);
|
||||
|
||||
if (runMemoryLimit <= 0 || runRuntimeSeconds <= 0) {
|
||||
panic("invalid args");
|
||||
}
|
||||
|
||||
RunResult compileResult = executeCommand(argv[1], COMPILE_TIMEOUT, COMPILE_MEMORYLIMIT);
|
||||
RunResult runResult = executeCommand(argv[2], runRuntimeSeconds, runMemoryLimit);
|
||||
|
||||
SerializationResult serialResult = serializeResult(&runResult);
|
||||
|
||||
write(STDOUT_FILENO, serialResult.data, serialResult.size);
|
||||
|
||||
free(serialResult.data);
|
||||
}
|
60
serialization.c
Normal file
60
serialization.c
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "structs.h"
|
||||
|
||||
#define WATERMARK {0x69, 0x6E, 0x79, 0x72, 0x74, 0x62, 0x75}
|
||||
#define VERSION 0x00
|
||||
|
||||
// Number of chars required to represent any 64 bit int/uint + NUL
|
||||
#define INT_BUFFER_SIZE 21
|
||||
|
||||
#define SUM_BYTES(x) totalBytes += x
|
||||
|
||||
#define CONCAT_BYTES(dst, src, size) memcpy(dst, src, size); \
|
||||
dst += size;
|
||||
|
||||
// len_fb | fb | runtime
|
||||
|
||||
// WARNING: This approach assumes uniform endiness and architechture
|
||||
// TODO: Make it more portable?
|
||||
// TODO: this is horrible. i need a better way. maybe a serialization lib or c++
|
||||
// rewrite?
|
||||
|
||||
uint64_t calcSize(RunResult* result) {
|
||||
uint64_t totalBytes = 0;
|
||||
|
||||
size_t feedbackLen = strlen(result->feedback);
|
||||
totalBytes += sizeof(feedbackLen);
|
||||
totalBytes += feedbackLen;
|
||||
|
||||
totalBytes += sizeof(result->runtime);
|
||||
totalBytes += sizeof(result->maxRss);
|
||||
totalBytes += sizeof(result->flags);
|
||||
totalBytes += sizeof(result->exitCode);
|
||||
return totalBytes;
|
||||
}
|
||||
|
||||
SerializationResult serializeResult(RunResult* result) {
|
||||
uint64_t size = calcSize(result);
|
||||
uint8_t* buf = malloc(size);
|
||||
uint8_t* bufCursor = buf;
|
||||
|
||||
size_t feedbackLen = strlen(result->feedback);
|
||||
CONCAT_BYTES(bufCursor, &feedbackLen, sizeof(feedbackLen));
|
||||
CONCAT_BYTES(bufCursor, result->feedback, feedbackLen);
|
||||
CONCAT_BYTES(bufCursor, &result->runtime, sizeof(result->runtime));
|
||||
CONCAT_BYTES(bufCursor, &result->maxRss, sizeof(result->maxRss));
|
||||
CONCAT_BYTES(bufCursor, &result->flags, sizeof(result->flags));
|
||||
CONCAT_BYTES(bufCursor, &result->exitCode, sizeof(result->exitCode));
|
||||
|
||||
SerializationResult serialResult = {
|
||||
.data = buf,
|
||||
.size = size
|
||||
};
|
||||
|
||||
return serialResult;
|
||||
}
|
4
serialization.h
Normal file
4
serialization.h
Normal file
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "structs.h"
|
||||
|
||||
SerializationResult serializeResult(RunResult* result);
|
70
structs.h
Normal file
70
structs.h
Normal file
|
@ -0,0 +1,70 @@
|
|||
#pragma once
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum ResultFlags {
|
||||
/// Execution Result ///
|
||||
// No other error occured, output may be graded
|
||||
// OK
|
||||
Ok = 1 << 0,
|
||||
// At least one error occured
|
||||
// See "Error" for all of them
|
||||
// ERR
|
||||
Err = 1 << 1,
|
||||
|
||||
/// Grader Result ///
|
||||
// The program gave correct output with no errors
|
||||
// AC
|
||||
Accepted = 1 << 2,
|
||||
// The program got Err OR Wrong Answer
|
||||
// RJ
|
||||
Rejected = 1 << 3,
|
||||
|
||||
/// Errors ///
|
||||
// Set if memory limit is exceeded
|
||||
// MLE
|
||||
MemoryLimitExceeded = 1 << 4,
|
||||
// Set if timelimit triggered termination
|
||||
// TLE
|
||||
TimeLimitExceeded = 1 << 5,
|
||||
// Code gave the wrong result
|
||||
// WA
|
||||
WrongAnswer = 1 << 6,
|
||||
// Given if program tampers with the runtime (unused)
|
||||
// TD
|
||||
TamperingDetected = 1 << 7,
|
||||
// Set if program retuns non-zero exit code
|
||||
// PE
|
||||
ProgramError = 1 << 8,
|
||||
// Occurs if a non-listed error occurs
|
||||
// (Details given in comment)
|
||||
// OE
|
||||
OtherError = 1 << 9,
|
||||
|
||||
/// Status ///
|
||||
// Set if the run result is final (all flags set)
|
||||
Final = 1 << 10,
|
||||
// If all Execution flags are set
|
||||
ExecutionFinal = 1 << 11,
|
||||
|
||||
/// Misc. ///
|
||||
// Nice!
|
||||
// NIC
|
||||
Nice = 1 << 31,
|
||||
};
|
||||
|
||||
struct {
|
||||
// Specifies extra feedback
|
||||
char feedback[256];
|
||||
time_t runtime;
|
||||
int maxRss;
|
||||
int flags;
|
||||
int exitCode;
|
||||
} typedef RunResult;
|
||||
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
uint64_t size;
|
||||
} SerializationResult;
|
||||
|
4
testprogs/nonzero.c
Normal file
4
testprogs/nonzero.c
Normal file
|
@ -0,0 +1,4 @@
|
|||
int main(void) {
|
||||
return 69;
|
||||
}
|
||||
|
42
tools/decodeFlags.py
Executable file
42
tools/decodeFlags.py
Executable file
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
flag_dict = {
|
||||
0: ['OK', 'Ok'],
|
||||
1: ['ERR', 'Err'],
|
||||
2: ['AC', 'Accepted'],
|
||||
3: ['RJ', 'Rejected'],
|
||||
4: ['MLE', 'MemoryLimitExceeded'],
|
||||
5: ['TLE', 'TimeLimitExceeded'],
|
||||
6: ['WA', 'WrongAnswer'],
|
||||
7: ['TD', 'TamperingDetected'],
|
||||
8: ['PE', 'ProgramError'],
|
||||
9: ['OE', 'OtherError'],
|
||||
10: [None, 'Final'],
|
||||
11: [None, 'ExecutionFinal'],
|
||||
31: ['NIC', 'Nice'],
|
||||
}
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='GOJ Flag Decoder',
|
||||
description='Decoder for GOJ status flags'
|
||||
)
|
||||
|
||||
parser.add_argument('flags', help='the flags you would like to decode (as a 32 bit signed int)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
flags = int(args.flags)
|
||||
|
||||
for flag in range(0, 32):
|
||||
if (flags & 2 ** flag) == 2 ** flag:
|
||||
desc_list = flag_dict.get(flag)
|
||||
|
||||
if desc_list is None:
|
||||
print(f'{flag}: Unknown')
|
||||
continue
|
||||
|
||||
if desc_list[0] is None:
|
||||
print(f'{flag}: {desc_list[1]}')
|
||||
else:
|
||||
print(f'{flag}: {desc_list[0]} ({desc_list[1]})')
|
Loading…
Add table
Reference in a new issue