diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..e85cfec
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,52 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(Robinwaita LANGUAGES C)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
+find_package(PkgConfig REQUIRED)
+
+pkg_check_modules(GTK REQUIRED gtk4)
+pkg_check_modules(ADWAITA REQUIRED libadwaita-1)
+pkg_check_modules(GLIB REQUIRED glib-2.0)
+pkg_check_modules(GIOUNIX REQUIRED gio-unix-2.0)
+pkg_check_modules(GRAPHENE REQUIRED graphene-1.0)
+
+include_directories(
+ ${Python3_INCLUDE_DIRS}
+ ${GIOUNIX_INCLUDE_DIRS}
+ ${GTK_INCLUDE_DIRS}
+ ${ADWAITA_INCLUDE_DIRS}
+ ${GLIB_INCLUDE_DIRS}
+ ${GRAPHENE_INCLUDE_DIRS}
+)
+
+add_executable(robinwaita
+ main.c
+ process.c
+ process.h
+ scheduler.c
+ scheduler.h
+ main.h
+)
+
+target_link_libraries(robinwaita
+ ${Python3_LIBRARIES}
+ ${GTK_LIBRARIES}
+ ${ADWAITA_LIBRARIES}
+ ${GLIB_LIBRARIES}
+ ${GIOUNIX_LIBRARIES}
+ ${GRAPHENE_LIBRARIES}
+)
+
+link_directories(
+ ${GTK_LIBRARY_DIRS}
+ ${ADWAITA_LIBRARY_DIRS}
+ ${GLIB_LIBRARY_DIRS}
+ ${GRAPHENE_LIBRARY_DIRS}
+ ${GIOUNIX_LIBRARY_DIRS}
+)
+
+target_compile_options(robinwaita PRIVATE ${GTK_CFLAGS_OTHER})
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a4d09a1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# Robinwaita
+Robinwaita is user-friendly round-robin process scheduler for Linux written in C. It comes with a modern Libadwaita & GTK 4-based user interface.
+
+## Screenshots
+![A screenshot of Robinwaita having completed process scheduling](screenshots/img1.png "Robinwaita heading image")
+![A screenshot of Robinwaita showing how processes are added to the scheduler](screenshots/scheduler.png "Adding processes to the scheduler")
+![A screenshot of Robinwaita's statistics screen, showing context switches and average running time](screenshots/statistics.png "Robinwaita's statistics tab")
+![A picture of the source code, showing a segment of the timerfd implementation](screenshots/timerfd.png "Robinwaita uses timerfd")
+
+## Details
+* Easily add processes to the scheduling queue
+* Round-robin scheduling algorithm, with context switches initiated by `timerfd`
+* See how processes get managed by the algorithm in real-time
+* Modern GTK 4 interface with light and dark modes
+* Check basic statistics like total context switches in a separate tab
+
+## Motivation for this project
+* Before we get into anything -- yes, this project is useless. I know.
+* That being said, given the additional low-level programming experience I've been getting recently, as well as my growing experience with operating systems concepts, I wanted to challenge myself by programming a process scheduler from scratch.
+* I've also loved GNOME and its technologies like GTK for several years. I've thought about making a GTK app for a while now, so combining the two seemed like a fun fit. Ultimately, while the user interface for this project is undeniably basic, I'm still really happy with how it turned out.
+
+## Try Robinwaita for yourself
+### Dependencies
+* Robinwaita has been developed with the following versions of these packages:
+ * `libadwaita`/`libadwaita-devel` version 1.5
+ * `libgtk-4-dev`/`gtk4-devel` 4.14
+ * `libglib2.0-dev`/`glib2-devel`
+ * `libgio-unix-2.0-dev`/`gio-unix-devel` 2.0
+ * `libgraphene-1.0-dev`/`graphene-devel` 1.0
+ * `python3-dev`/`python3-devel` 3.12.6
+ * `gcc` 14.2.1
+ * `cmake` 3.28.2
+ * `pkg-config`/`pkgconf-pkg-config`
+* Other versions may work, but I provide no guarantees.
+* I provide an ARM64 build under Releases, but it's expected your system still satisfies dependencies above.
+* My build environment was a Fedora 40 VM under Parallels on my M2 Pro MacBook Pro.
+
+### Building the project
+1. Clone the repository:
+```
+git clone https://
+cd robinwaita
+```
+2. Install dependencies:
+* Install the dependencies noted above. Keep in mind these instructions are different across distributions, so instructions aren't provided here.
+3. Generate the build system using CMake, and build:
+```
+mkdir build
+cd build
+cmake ..
+cmake --build .
+```
+4. Run the application:
+```
+chmod +x robinwaita # set file as executable
+./robinwaita # run it! (you can also open it from your file manager)
+```
\ No newline at end of file
diff --git a/desktop_integration/robinwaita.desktop b/desktop_integration/robinwaita.desktop
new file mode 100755
index 0000000..bf92a81
--- /dev/null
+++ b/desktop_integration/robinwaita.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Name=Robinwaita
+Exec=/path/to/Robinwaita/build/robinwaita
+Icon=robinwaita
+Terminal=false
+Type=Application
+Categories=Utility;
+Path=/path/to/Robinwaita/build/
diff --git a/desktop_integration/robinwaita.svg b/desktop_integration/robinwaita.svg
new file mode 100644
index 0000000..fedc4eb
--- /dev/null
+++ b/desktop_integration/robinwaita.svg
@@ -0,0 +1,22 @@
+
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..85dc6ae
--- /dev/null
+++ b/main.c
@@ -0,0 +1,314 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "main.h"
+
+int main(int argc, char **argv) {
+
+ g_autoptr(AdwApplication) app = NULL;
+ int status;
+
+ //allocate initialData
+ struct initData *initialData = malloc(sizeof(struct initData));
+ if (initialData == NULL) {
+ g_print("Failed to allocate memory for initialData");
+ return -1;
+ }
+
+ //create the gtk application and pass initialData to activate
+ app = adw_application_new("com.asolonari.Robinwaita", G_APPLICATION_DEFAULT_FLAGS);
+ g_signal_connect(app, "activate", G_CALLBACK(activate), initialData);
+
+ //start gtk application
+ status = g_application_run(G_APPLICATION(app), argc, argv);
+ fflush(stdout);
+
+ //free memory after the event loop exits
+ freeAll(initialData);
+
+ return status;
+}
+
+void updateStatistics(gpointer userData){
+ initData *data = (initData *) userData;
+ AdwActionRow *averageRunningTimeRow, *totalContextSwitchesRow;
+ double averageTime = data->totalTime / data->processAmt;
+
+ //get objects from builder
+ averageRunningTimeRow = ADW_ACTION_ROW(gtk_builder_get_object(data->builder, "averageRunningTimeRow"));
+ if(!averageRunningTimeRow) g_print("Error: averageRunningTimeRow is NULL\n");
+ totalContextSwitchesRow = ADW_ACTION_ROW(gtk_builder_get_object(data->builder, "totalContextSwitchesRow"));
+ if(!totalContextSwitchesRow) g_print("Error: totalContextSwitchesRow is NULL\n");
+
+ //format strings
+ gchar *averageTimeStr = g_strdup_printf("%.1lf seconds", averageTime);
+ gchar *totalContextSwitchesStr = g_strdup_printf("%d context switches", data->contextSwitches);
+
+ //update subtitles with information
+ adw_action_row_set_subtitle(averageRunningTimeRow, averageTimeStr);
+ adw_action_row_set_subtitle(totalContextSwitchesRow, totalContextSwitchesStr);
+
+ //free new strings
+ g_free(averageTimeStr);
+ g_free(totalContextSwitchesStr);
+}
+
+gboolean schedulerUpdateFromTimerfd(gint fd, GIOCondition condition, gpointer userData) {
+ initData *data = (initData *) userData;
+
+ if (condition & G_IO_IN) {
+ uint64_t expirations;
+ ssize_t bytes = read(fd, &expirations, sizeof(expirations));
+ if (bytes != sizeof(expirations)) {
+ g_print("Warning: read unexpected number of bytes from timerfd\n");
+ }
+
+ //scheduler logic
+ manageQueue(data);
+ }
+
+ return TRUE; //while monitoring file descriptor
+}
+
+void addProcess(int processType, struct initData *data) {
+ Process* createdProcess = newProcess(processType);
+
+ enQueue(data->allProcesses, createdProcess);
+ displayProcess(createdProcess, data->allProcesses, data);
+}
+
+void addCPUProcess(GtkWidget *widget, gpointer userData) {
+ struct initData *data = (struct initData *) userData;
+ addProcess(1, data); // 1 = CPU-bound
+}
+
+void addIOProcess(GtkWidget *widget, gpointer userData) {
+ struct initData *data = (struct initData *) userData;
+ addProcess(2, data); // 2 = IO-bound
+}
+
+void addMixedProcess(GtkWidget *widget, gpointer userData) {
+ struct initData *data = (struct initData *) userData;
+ addProcess(3, data); // 3 = Mixed-bound
+}
+
+void updateButtonSensitivity(initData *data, gboolean sensitive) {
+ //find buttons by id
+ GtkWidget *cpuButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "cpuButton"));
+ GtkWidget *ioButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "ioButton"));
+ GtkWidget *mixedButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "mixedButton"));
+ GtkWidget *runAllButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "runAllButton"));
+
+ //set button sensitivity
+ gtk_widget_set_sensitive(cpuButton, sensitive);
+ gtk_widget_set_sensitive(ioButton, sensitive);
+ gtk_widget_set_sensitive(mixedButton, sensitive);
+ gtk_widget_set_sensitive(runAllButton, sensitive);
+}
+
+void runScheduler(GtkButton *button, gpointer userData) {
+ initData *data = (initData *)userData;
+
+ if (data->schedulerRunning) {
+ g_print("Scheduler is already running.\n");
+ return;
+ }
+
+ //reset statistics variables
+ data->contextSwitches = 0;
+ data->processAmt = 0;
+ data->totalTime = 0;
+
+ //call timerSetup to initialize timerfd
+ data->timerfd = timerSetup(data->allProcesses);
+
+ if (data->timerfd == -1) {
+ g_print("Failed to set up the scheduler timer.\n");
+ return;
+ }
+
+ //add timerfd to glib main loop to monitor it
+ data->timerWatchID = g_unix_fd_add(data->timerfd, G_IO_IN | G_IO_HUP, (GUnixFDSourceFunc) schedulerUpdateFromTimerfd, data);
+ if (data->timerWatchID == 0) {
+ g_print("Failed to add timerfd to main loop.\n");
+ close(data->timerfd);
+ return;
+ }
+
+ updateButtonSensitivity(data, FALSE);
+
+ data->schedulerRunning = 1;
+ appendTextToView("Scheduler started.\n", data->textView);
+}
+
+static void activate(GtkApplication *app, gpointer userData) {
+ GtkWidget *window, *runButton, *processListView, *cpuButton, *ioButton, *mixedButton;
+ struct initData *data = (struct initData *) userData;
+
+ //init gtk ui
+ data->builder = gtk_builder_new_from_file("../user_interface.xml");
+ window = GTK_WIDGET(gtk_builder_get_object(data->builder, "main_window"));
+ gtk_window_set_application(GTK_WINDOW(window), GTK_APPLICATION(app));
+
+ //retrieve textview
+ processListView = GTK_WIDGET(gtk_builder_get_object(data->builder, "ProcessList"));
+ if (!processListView) {
+ g_print("Error: processListView is NULL\n");
+ } else {
+ data->textView = GTK_TEXT_VIEW(processListView);
+ }
+
+ //connect "run all" button to scheduler
+ runButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "runAllButton"));
+ if (!runButton)
+ g_print("Error: runButton is NULL\n");
+ else
+ g_signal_connect(runButton, "clicked", G_CALLBACK(runScheduler), data);
+
+ //connect all buttons
+ cpuButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "cpuButton"));
+ if (!cpuButton)
+ g_print("Error: cpuButton is NULL\n");
+ else
+ g_signal_connect(cpuButton, "clicked", G_CALLBACK(addCPUProcess), data);
+
+ ioButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "ioButton"));
+ if (!ioButton) {
+ g_print("Error: ioButton is NULL\n");
+ } else {
+ g_signal_connect(ioButton, "clicked", G_CALLBACK(addIOProcess), data);
+ }
+
+ mixedButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "mixedButton"));
+ if (!mixedButton) {
+ g_print("Error: mixedButton is NULL\n");
+ } else {
+ g_signal_connect(mixedButton, "clicked", G_CALLBACK(addMixedProcess), data);
+ }
+
+ //add a timeout to continue updating the textview every 100ms
+ g_timeout_add(100, updateTextviewFromLog, data);
+
+ //initialize rest of scheduler
+ data->schedulerRunning = 0;
+ data->timerWatchID = 0;
+ data->logBuffer = g_string_new(NULL);
+
+ //allocate queue
+ data->allProcesses = (Queue *) malloc(sizeof(Queue));
+ if (data->allProcesses == NULL) {
+ perror("main: Couldn't allocate memory for process queue");
+ exit(1);
+ }
+ initQueue(data->allProcesses); //initialize variables
+
+ //show gtk window
+ gtk_window_present(GTK_WINDOW(window));
+}
+
+void scrollToBottom(GtkTextView *textView) {
+ GtkAdjustment *adjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(textView));
+
+ //get upper limit of the vertical adjustment (the bottom)
+ gdouble upper = gtk_adjustment_get_upper(adjustment);
+
+ //set adjustment value to the upper limit (scroll to bottom)
+ gtk_adjustment_set_value(adjustment, upper);
+}
+
+void appendTextToView(const char *text, GtkTextView *textView) {
+ if (!textView) {
+ g_print("Error: textView is NULL in appendTextToView\n");
+ return;
+ }
+
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer(textView);
+ if (!buffer) {
+ g_print("Error: GtkTextBuffer is NULL\n");
+ return;
+ }
+
+ GtkTextIter start, end;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+
+ //keep textview buffer within certain size
+ int current_size = gtk_text_iter_get_offset(&end);
+ if (current_size > 10000) {
+ GtkTextIter new_start;
+ gtk_text_buffer_get_iter_at_offset(buffer, &new_start, current_size - 10000);
+ gtk_text_buffer_delete(buffer, &start, &new_start);
+ }
+
+ //insert new text
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ gtk_text_buffer_insert(buffer, &end, text, -1);
+
+ //scroll to bottom
+ scrollToBottom(textView);
+}
+
+gboolean updateTextviewFromLog(gpointer userData) {
+ initData *data = (initData *) userData;
+
+ if (data->logBuffer->len > 0) {
+ //copy logBuffer content
+ gchar *newText = g_strdup(data->logBuffer->str);
+
+ //clear logBuffer
+ g_string_truncate(data->logBuffer, 0);
+
+ //append new text to GtkTextView
+ appendTextToView(newText, data->textView);
+
+ g_free(newText);
+ }
+
+ return TRUE; //call this function continuously
+}
+
+void freeAll(struct initData *initialData) {
+ Queue *q = initialData->allProcesses;
+ Node *current = q->front;
+ Node *nextNode;
+
+ while (current != NULL) {
+ nextNode = current->next;
+
+ //free process
+ if (current->process != NULL) {
+ free(current->process->name);
+ for (int i = 0; current->process->args[i] != NULL; i++) {
+ free(current->process->args[i]);
+ }
+ free(current->process->args);
+ free(current->process);
+ }
+
+ //free node
+ free(current);
+ current = nextNode;
+ }
+
+ q->front = NULL;
+ q->rear = NULL;
+ q->numElements = 0;
+
+ free(q);
+
+ //unref gtk objects
+ if (initialData->builder) {
+ g_object_unref(initialData->builder);
+ }
+
+ if(initialData->logBuffer) {
+ g_string_free(initialData->logBuffer, TRUE);
+ }
+
+ free(initialData);
+}
\ No newline at end of file
diff --git a/main.h b/main.h
new file mode 100644
index 0000000..cc02a6a
--- /dev/null
+++ b/main.h
@@ -0,0 +1,52 @@
+#ifndef ROBINWAITA_MAIN_H
+#define ROBINWAITA_MAIN_H
+#include
+#include
+#include
+#include "scheduler.h"
+
+struct Queue;
+
+//data that gets passed through gpointer, contains queue and other info
+typedef struct initData {
+ //whether scheduler is running
+ int schedulerRunning;
+
+ //statistics variables
+ int timerfd;
+ int processAmt;
+ double totalTime;
+ int contextSwitches;
+
+ //timer info
+ guint timerWatchID;
+
+ //allProcesses queue
+ Queue *allProcesses;
+
+ //gtk text view
+ GtkTextView *textView;
+
+ //gtk builder
+ GtkBuilder *builder;
+
+ //in-memory buffer
+ GString *logBuffer;
+
+} initData;
+
+void updateStatistics(gpointer userData); //calculate & update statistics screen
+gboolean schedulerUpdateFromTimerfd(gint fd, GIOCondition condition, gpointer userData); //call scheduler from timerfd updates
+void addProcess(int processType, struct initData *data); //enqueue process and display newest
+void addCPUProcess(GtkWidget *widget, gpointer userData); //button to add cpu process to queue
+void addIOProcess(GtkWidget *widget, gpointer userData); //button to add io process to queue
+void addMixedProcess(GtkWidget *widget, gpointer userData); //button to add mixed-bound process to queue
+void updateButtonSensitivity(initData *data, gboolean sensitive); //gray out buttons if scheduler is running
+void runScheduler(GtkButton *button, gpointer userData); //init & start running scheduler
+static void activate(GtkApplication *app, gpointer userData); //init gtk window objects
+void scrollToBottom(GtkTextView *textView); //keep gtk textview scrolled to bottom
+void appendTextToView(const char *text, GtkTextView *textView); //add text to gtk textview
+gboolean updateTextviewFromLog(gpointer userData); //copy text from buffer and call appendTextToView with the text
+void freeAll(struct initData *initialData); //free dynamically allocated memory
+
+#endif //ROBINWAITA_MAIN_H
diff --git a/process.c b/process.c
new file mode 100644
index 0000000..3571565
--- /dev/null
+++ b/process.c
@@ -0,0 +1,149 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "process.h"
+#include "main.h"
+
+Process* newProcess(int processType){
+
+ Process* createdProcess = (Process*) malloc(sizeof(Process));
+
+ if(createdProcess == NULL){
+ perror("newProcess: Failed to create process struct");
+ return NULL;
+ }
+
+ char newName[16];
+ char testProcess[35];
+
+ //init integer values
+ createdProcess->state = 0;
+ createdProcess->pid = 0;
+
+ //set process information
+ if(processType == 1) {
+ strcpy(newName, "CPUBound");
+ strcpy(testProcess, "../testing_processes/cpubound.py");
+ } else if(processType == 2) {
+ strcpy(newName, "IOBound");
+ strcpy(testProcess, "../testing_processes/iobound.py");
+ } else if(processType == 3) {
+ strcpy(newName, "MixedBound");
+ strcpy(testProcess, "../testing_processes/mixbound.py");
+ }
+
+ //allocate memory for name
+ createdProcess->name = (char*) malloc(sizeof(char) * (strlen(newName)+1));
+ if (createdProcess->name == NULL) {
+ perror("newProcess: Failed to allocate memory for process struct name");
+ free(createdProcess);
+ return NULL;
+ }
+ strcpy(createdProcess->name, newName);
+
+ //allocate memory for the arguments array
+ createdProcess->args = (char**) malloc(3 * sizeof(char*)); //allocate python3, path, NULL terminator, and amount of addl arguments
+ if (createdProcess->args == NULL) {
+ perror("newProcess: Failed to allocate memory for process struct arguments");
+ free(createdProcess->name);
+ free(createdProcess);
+ return NULL;
+ }
+
+ //allocate and set the python argument
+ createdProcess->args[0] = (char*) malloc(strlen("python3")+1);
+ if (createdProcess->args[0] == NULL) {
+ perror("newProcess: Failed to allocate memory for python3 argument");
+ free(createdProcess->args);
+ free(createdProcess->name);
+ free(createdProcess);
+ return NULL;
+ }
+ strcpy(createdProcess->args[0], "python3");
+
+ //allocate and set the name of the script
+ createdProcess->args[1] = (char*) malloc(strlen(testProcess)+1);
+ if (createdProcess->args[1] == NULL) {
+ perror("newProcess: Failed to allocate memory for script argument");
+ for (int i = 0; createdProcess->args[i] != NULL; i++) {
+ free(createdProcess->args[i]);
+ }
+ free(createdProcess->args);
+ free(createdProcess->name);
+ free(createdProcess);
+ return NULL;
+ }
+ strcpy(createdProcess->args[1], testProcess);
+
+ //null-terminate arguments array
+ createdProcess->args[2] = NULL;
+
+ return createdProcess;
+}
+
+void startProcess(Process *process, initData *data) {
+ pid_t pid;
+ pid = fork(); //fork process
+
+ if (pid == 0) { //child process
+ execvp(process->args[0], process->args);
+
+ //will only run if execvp fails
+ perror("startProcess: execvp failed");
+ exit(EXIT_FAILURE);
+ } else if (pid > 0) { //parent process
+ process->pid = pid;
+ process->state = 1;
+
+ //append message for gtktextview
+ gchar *message = g_strdup_printf("Started process %d (%s)\n", pid, process->name);
+ g_string_append(data->logBuffer, message);
+ g_free(message);
+ } else { //fork failed
+ perror("startProcess: fork failed");
+ }
+}
+
+void stopProcess(Process *process, initData *data) {
+ int status;
+ pid_t result = waitpid(process->pid, &status, WNOHANG);
+
+ if (result == 0) {
+ //process is still running, safe to send SIGSTOP
+ if (kill(process->pid, SIGSTOP) == -1) {
+ perror("Failed to stop process");
+ exit(EXIT_FAILURE);
+ } else {
+ process->state = 2;
+
+ gchar *message = g_strdup_printf("Stopped process %d (%s)\n", process->pid, process->name);
+ g_string_append(data->logBuffer, message);
+ g_free(message);
+ }
+ } else if (result > 0) {
+ //process has already finished
+ process->state = 3;
+ fprintf(stderr, "Program %d has already completed, cannot stop\n", process->pid);
+ } else {
+ //an error occurred in waitpid
+ perror("stopProcess: waitpid failed");
+ exit(EXIT_FAILURE);
+ }
+}
+
+void continueProcess(Process *process, initData *data) {
+ if (kill(process->pid, SIGCONT) == -1) { //resume process by sending SIGSTOP signal
+ fprintf(stderr, "Program %d failed to resume\n", process->pid); //print error to stderr
+ exit(EXIT_FAILURE);
+ } else {
+ gchar *message = g_strdup_printf("Continued process %d (%s)\n", process->pid, process->name);
+ g_string_append(data->logBuffer, message);
+ g_free(message);
+
+ process->state = 1;
+ }
+}
\ No newline at end of file
diff --git a/process.h b/process.h
new file mode 100644
index 0000000..2e8fe5a
--- /dev/null
+++ b/process.h
@@ -0,0 +1,22 @@
+#ifndef ROBINWAITA_PROCESS_H
+#define ROBINWAITA_PROCESS_H
+#include
+#include
+
+typedef struct initData initData;
+
+typedef struct {
+ char *name; //process name
+ char **args; //any arguments we're passing to the process
+ int state; //STATES || 0: not started, not running | 1: running | 2: started, paused | 3: completed
+ pid_t pid; //process ID
+ struct timeval startTime; //process start time
+ struct timeval endTime; //process end time
+} Process;
+
+Process *newProcess(int processType); //create new process struct
+void startProcess(Process *process, initData *data); //start process (fork into child and then execute python process)
+void stopProcess(Process *process, initData *data); //stop process
+void continueProcess(Process *process, initData *data); //continue process from stop
+
+#endif //ROBINWAITA_PROCESS_H
diff --git a/scheduler.c b/scheduler.c
new file mode 100644
index 0000000..04822dd
--- /dev/null
+++ b/scheduler.c
@@ -0,0 +1,193 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "main.h"
+
+void displayProcess(Process *p, Queue *q, initData *data){
+ gchar *output = g_strdup_printf("Process %d: %s\n", q->numElements, p->name);
+ g_string_append(data->logBuffer, output);
+ g_free(output);
+}
+
+void initQueue(Queue *q){
+ q->front = NULL;
+ q->rear = NULL;
+ q->numElements = 0;
+}
+
+int isEmpty(Queue *q){
+ return (q->front == NULL);
+}
+
+void enQueue(Queue *q, Process *p) {
+ Node *newNode = (Node*) malloc(sizeof(Node));
+ if (newNode == NULL) {
+ perror("enQueue: Failed to allocate memory for new node");
+ return;
+ }
+
+ newNode->process = p;
+ newNode->next = NULL;
+
+ if (isEmpty(q)) {
+ q->front = newNode;
+ q->rear = newNode;
+ } else {
+ q->rear->next = newNode;
+ q->rear = newNode;
+ }
+
+ q->numElements++;
+}
+
+void deQueue(Queue *q) {
+ if (isEmpty(q)) {
+ perror("deQueue: Attempt to dequeue from empty queue");
+ return;
+ }
+
+ Node *temp = q->front;
+ q->front = q->front->next;
+
+ if (temp->process->state == 3) {
+ //free if completed
+ free(temp->process->name);
+ for (int i = 0; temp->process->args[i] != NULL; i++) {
+ free(temp->process->args[i]);
+ }
+ free(temp->process->args);
+ free(temp->process);
+ }
+
+ free(temp);
+
+ if (q->front == NULL) {
+ q->rear = NULL;
+ }
+
+ q->numElements--;
+}
+
+int timerSetup(Queue *q){
+ struct itimerspec its;
+ int timerfd = 0;
+
+ timerfd = timerfd_create(CLOCK_REALTIME, 0);
+ if (timerfd == -1) {
+ perror("timerSetup: timerfd_create failed");
+ exit(EXIT_FAILURE);
+ }
+
+ //configure timer expiration intervals
+ its.it_value.tv_sec = 1; //first expiration after 1 second
+ its.it_value.tv_nsec = 0;
+ its.it_interval.tv_sec = 1; //recurring interval every 1 second
+ its.it_interval.tv_nsec = 0;
+
+ if (timerfd_settime(timerfd, 0, &its, NULL) == -1) {
+ perror("timerSetup: timerfd_settime failed");
+ exit(EXIT_FAILURE);
+ }
+
+ return timerfd;
+
+}
+
+void checkCompletion(Process *p){
+
+ //if the process hasn't started or the process has already been marked as completed
+ if(p == NULL || p->state == 0 || p->state == 3) return;
+
+ int status;
+ pid_t result = waitpid(p->pid, &status, WNOHANG);
+ if (result > 0 && (WIFEXITED(status) || result == WIFSIGNALED(status))) { //process has completed, so log that
+ p->state = 3;
+ gettimeofday(&p->endTime, NULL);
+ } else if (result < 0) { //error occurred
+ perror("checkCompletion: waitpid encountered an error when checking status\n");
+ }
+
+}
+
+void manageQueue(struct initData *data) {
+
+ Process *currentProcess;
+
+ //check if there's even anything to schedule
+ if (isEmpty(data->allProcesses)){
+ if(close(data->timerfd) == -1){
+ perror("manageQueue: Error closing timerfd");
+ } else {
+ data->schedulerRunning = 0;
+ gchar *message = g_strdup_printf("All processes have finished executing. Timer stopped.\n");
+ g_string_append(data->logBuffer, message);
+ g_free(message);
+
+ updateStatistics(data);
+ updateButtonSensitivity(data, TRUE);
+
+ //remove the giochannel watch
+ if (data->timerWatchID != 0) {
+ g_source_remove(data->timerWatchID);
+ data->timerWatchID = 0;
+ }
+
+ }
+
+ return; //exit loop
+
+ }
+
+ currentProcess = data->allProcesses->front->process;
+ checkCompletion(currentProcess);
+
+ if (currentProcess->state != 1) { //if the process isn't running
+
+ if (currentProcess->state == 3) { //if the process has completed
+ data->processAmt += 1;
+ addElapsedTime(currentProcess, &data->totalTime, data);
+ deQueue(data->allProcesses);
+ } else if (currentProcess->state == 0) { //if the process hasn't even started
+ startProcess(currentProcess, data);
+ gettimeofday(¤tProcess->startTime, NULL); //set process start time
+ } else if (currentProcess->state == 2) { //if process has started but is paused
+ data->contextSwitches += 1;
+ continueProcess(currentProcess, data);
+ }
+
+ return; //start the loop again
+
+ }
+
+ //section: if the process is running
+
+ //only apply timer-based scheduling logic if there's more than one process in the queue
+ if(data->allProcesses->numElements > 1){ //stop current process and start next one
+ data->contextSwitches += 1;
+ stopProcess(currentProcess, data);
+ deQueue(data->allProcesses); //increments front
+ enQueue(data->allProcesses, currentProcess); //enqueue process at end of queue
+ return;
+ }
+
+}
+
+void addElapsedTime(Process *p, double *totalTime, initData *data) {
+
+ long seconds = p->endTime.tv_sec - p->startTime.tv_sec;
+ long microseconds = p->endTime.tv_usec - p->startTime.tv_usec;
+ double processTime = (double) seconds + (double) microseconds * 1e-6;
+
+ gchar *message = g_strdup_printf("The %s process took %.0f seconds to complete.\n", p->name, processTime);
+ g_string_append(data->logBuffer, message);
+ g_free(message);
+
+ *totalTime += processTime;
+
+}
\ No newline at end of file
diff --git a/scheduler.h b/scheduler.h
new file mode 100644
index 0000000..98e19f2
--- /dev/null
+++ b/scheduler.h
@@ -0,0 +1,28 @@
+#ifndef ROBINWAITA_SCHEDULER_H
+#define ROBINWAITA_SCHEDULER_H
+#include "process.h"
+
+typedef struct initData initData;
+struct Queue;
+
+typedef struct Node {
+ Process *process;
+ struct Node *next;
+} Node;
+
+typedef struct Queue {
+ Node *front;
+ Node *rear;
+ int numElements;
+} Queue;
+
+void displayProcess(Process *p, Queue *q, initData *data); //display new process when added to queue
+void initQueue(Queue *q); //initialize process queue
+int isEmpty(Queue *q); //check if queue empty
+void enQueue(Queue *q, Process *p); //add process struct to queue
+void deQueue(Queue *q); //remove process struct from queue
+int timerSetup(Queue *q); //setup timerfd
+void manageQueue(struct initData *data); //scheduling logic, gets called repeatedly
+void addElapsedTime(Process *p, double *totalTime, initData *data); //get total time for running processes and display
+
+#endif //ROBINWAITA_SCHEDULER_H
diff --git a/screenshots/img1.png b/screenshots/img1.png
new file mode 100644
index 0000000..0b54be8
Binary files /dev/null and b/screenshots/img1.png differ
diff --git a/screenshots/scheduler.png b/screenshots/scheduler.png
new file mode 100644
index 0000000..e7abc69
Binary files /dev/null and b/screenshots/scheduler.png differ
diff --git a/screenshots/statistics.png b/screenshots/statistics.png
new file mode 100644
index 0000000..d886609
Binary files /dev/null and b/screenshots/statistics.png differ
diff --git a/screenshots/timerfd.png b/screenshots/timerfd.png
new file mode 100644
index 0000000..e088c8d
Binary files /dev/null and b/screenshots/timerfd.png differ
diff --git a/testing_processes/cpubound.py b/testing_processes/cpubound.py
new file mode 100644
index 0000000..0893f1f
--- /dev/null
+++ b/testing_processes/cpubound.py
@@ -0,0 +1,28 @@
+import os
+
+def is_prime(n):
+ if n <= 1:
+ return False
+ if n <= 3:
+ return True
+ if n % 2 == 0 or n % 3 == 0:
+ return False
+ i = 5
+ while i * i <= n:
+ if n % i == 0 or n % (i + 2) == 0:
+ return False
+ i += 6
+ return True
+
+def cpu_bound(passed_pid):
+ count = 0
+ for i in range(2, 1000000): # check how many prime numbers exist between 2 and 100,000
+ if is_prime(i):
+ count += 1
+ print(f"CPU-bound process {passed_pid} found that {count} numbers were prime.")
+
+
+if __name__ == "__main__":
+ process_id = os.getpid()
+ print(f"Running CPU-bound process {process_id}...")
+ cpu_bound(process_id)
\ No newline at end of file
diff --git a/testing_processes/iobound.py b/testing_processes/iobound.py
new file mode 100644
index 0000000..9355659
--- /dev/null
+++ b/testing_processes/iobound.py
@@ -0,0 +1,18 @@
+import time
+import os
+
+def io_bound(passed_pid):
+ file_name = f"iobound_test_{passed_pid}.txt"
+ with open(file_name, "a") as f:
+ for i in range(10):
+ f.write(f"Writing iteration {i + 1} from process {passed_pid}\n")
+ f.flush() # ensure data written
+ time.sleep(0.1) # simulate i/o delay
+ print(f"I/O-bound process {passed_pid} iteration {i + 1} completed.")
+ print(f"I/O-bound process {passed_pid} completed.")
+
+
+if __name__ == "__main__":
+ process_id = os.getpid()
+ print(f"Running I/O bound process {process_id}...")
+ io_bound(process_id)
diff --git a/testing_processes/mixbound.py b/testing_processes/mixbound.py
new file mode 100644
index 0000000..667acc3
--- /dev/null
+++ b/testing_processes/mixbound.py
@@ -0,0 +1,36 @@
+import time
+import os
+
+def is_prime(n):
+ if n <= 1:
+ return False
+ if n <= 3:
+ return True
+ if n % 2 == 0 or n % 3 == 0:
+ return False
+ i = 5
+ while i * i <= n:
+ if n % i == 0 or n % (i + 2) == 0:
+ return False
+ i += 6
+ return True
+
+def mix_bound(passed_pid):
+ file_name = f"mixbound_test_{passed_pid}.txt"
+ count = 0
+ for i in range(10):
+ for j in range(2, 500000):
+ if is_prime(j):
+ count += 1
+ with open(file_name, "a") as f:
+ f.write(f"Writing iteration {i + 1} from process {passed_pid}\n")
+ f.flush() # ensure data written
+ time.sleep(0.05) # simulate i/o delay
+ print(f"Mixed-bound process {passed_pid} iteration {i + 1} completed.")
+ print(f"Mixed-bound process {passed_pid} found that {count} numbers were prime.")
+
+
+if __name__ == "__main__":
+ process_id = os.getpid()
+ print(f"Running mixed-bound process {process_id}...")
+ mix_bound(process_id)
diff --git a/user_interface.xml b/user_interface.xml
new file mode 100644
index 0000000..005f55e
--- /dev/null
+++ b/user_interface.xml
@@ -0,0 +1,318 @@
+
+
+
+
\ No newline at end of file