From c9f139ec4f7a24477c86b6e9faf87bf0dbacc005 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Wed, 20 Apr 2022 17:12:44 +0200 Subject: [PATCH 001/106] moved gui scripts into new branch --- emulators/exercise_overlay.py | 26 ++++++++++++++++++++++++ exercise_runner_overlay.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 emulators/exercise_overlay.py create mode 100644 exercise_runner_overlay.py diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py new file mode 100644 index 0000000..7e46d0a --- /dev/null +++ b/emulators/exercise_overlay.py @@ -0,0 +1,26 @@ +from random import randint +import tkinter as TK + +def overlay(devices:int = 3): + # top is planned to be reserved for a little description of controls in stepper + # if stepper is not chosen, this will not be displayed + master = TK.Tk() + canvas = TK.Canvas(master, height=1000, width=1000) + canvas.pack(side=TK.TOP) + for device in range(devices): + x = randint(100, 900) + y = randint(100, 900) + device_size = 100 + canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") + device_frame = TK.Frame(canvas) + device_frame.place(x=x+(device_size/4), y=y+(device_size/4)) + device_button = TK.Button(device_frame, text="Show data") + device_button.pack(side=TK.BOTTOM) + device_text = TK.Label(device_frame, text=f'Device #{device}') + device_text.pack(side=TK.BOTTOM) + bottom_text = TK.Label(master, text="last message") + bottom_text.pack(side=TK.TOP) + master.resizable(False,False) + master.mainloop() + +overlay() \ No newline at end of file diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py new file mode 100644 index 0000000..5bcaf6f --- /dev/null +++ b/exercise_runner_overlay.py @@ -0,0 +1,38 @@ +import tkinter as TK +from exercise_runner import run_exercise + +def input_builder(master, title:str, entry_content:str): + frame = TK.Frame(master) + text = TK.Label(frame, text=title) + entry = TK.Entry(frame) + entry.insert(TK.END, entry_content) + frame.pack(side=TK.LEFT) + text.pack(side=TK.TOP) + entry.pack(side=TK.BOTTOM) + return frame, entry + +def run(): + lecture = int(lecture_entry.get()) + algorithm = algorithm_entry.get() + type = type_entry.get() + devices = int(devices_entry.get()) + print("running exercise") + print(f'with data: {lecture, algorithm, type, devices}') + run_exercise(lecture, algorithm, type, devices) + + +master = TK.Tk() + + +input_area = TK.LabelFrame(text="Input") +input_area.pack(side=TK.BOTTOM) + +lecture_frame, lecture_entry = input_builder(input_area, "Lecture", 0) +algorithm_frame, algorithm_entry = input_builder(input_area, "Algorithm", "PingPong") +type_frame, type_entry = input_builder(input_area, "Type", "stepping") +devices_frame, devices_entry = input_builder(input_area, "Devices", 3) + +start_button = TK.Button(master, text="Start", command=run) +start_button.pack(side=TK.TOP) + +master.mainloop() \ No newline at end of file From e4dc918fa023a73dc2efb6a9d1c75c2f7465d60a Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Wed, 20 Apr 2022 18:01:39 +0200 Subject: [PATCH 002/106] got a lot of the gui ready to be used with stepper --- emulators/AsyncEmulator.py | 2 ++ emulators/exercise_overlay.py | 40 ++++++++++++++++++++++++++--------- exercise_runner_overlay.py | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index b17a4b3..143fbd6 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -7,6 +7,7 @@ from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub +from emulators.exercise_overlay import overlay class AsyncEmulator(EmulatorStub): @@ -16,6 +17,7 @@ def __init__(self, number_of_devices: int, kind): self._terminated = 0 self._messages = {} self._messages_sent = 0 + overlay(self) def run(self): self._progress.acquire() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 7e46d0a..aa3073b 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,26 +1,46 @@ from random import randint +from threading import Thread import tkinter as TK -def overlay(devices:int = 3): +from emulators.EmulatorStub import EmulatorStub + +def overlay(emulator:EmulatorStub): # top is planned to be reserved for a little description of controls in stepper # if stepper is not chosen, this will not be displayed master = TK.Tk() - canvas = TK.Canvas(master, height=1000, width=1000) + height = 500 + width = 1000 + + def show_info(): + window = TK.Toplevel(master) + data = TK.Label(window, text="___placeholder___") + window.resizable(False, False) + data.pack(side=TK.TOP) + + def step(): + #insert stepper function + emulator._stepping = False + bottom_text.config(text=emulator._last_message) + pass + + canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) - for device in range(devices): - x = randint(100, 900) - y = randint(100, 900) + for device in range(len(emulator._devices)): device_size = 100 + x = randint(device_size, width-device_size) + y = randint(device_size, height-device_size) canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") device_frame = TK.Frame(canvas) - device_frame.place(x=x+(device_size/4), y=y+(device_size/4)) - device_button = TK.Button(device_frame, text="Show data") + device_frame.place(x=x+(device_size/5), y=y+(device_size/5)) + device_button = TK.Button(device_frame, text="Show data", command=show_info) device_button.pack(side=TK.BOTTOM) device_text = TK.Label(device_frame, text=f'Device #{device}') device_text.pack(side=TK.BOTTOM) - bottom_text = TK.Label(master, text="last message") + bottom_frame = TK.LabelFrame(master, text="Inputs") + bottom_frame.pack(side=TK.TOP) + step_button = TK.Button(bottom_frame, text="Step", command=step) + step_button.pack(side=TK.TOP) + bottom_text = TK.Label(bottom_frame, text="last message") bottom_text.pack(side=TK.TOP) master.resizable(False,False) master.mainloop() - -overlay() \ No newline at end of file diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 5bcaf6f..324a69c 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -29,7 +29,7 @@ def run(): lecture_frame, lecture_entry = input_builder(input_area, "Lecture", 0) algorithm_frame, algorithm_entry = input_builder(input_area, "Algorithm", "PingPong") -type_frame, type_entry = input_builder(input_area, "Type", "stepping") +type_frame, type_entry = input_builder(input_area, "Type", "async") devices_frame, devices_entry = input_builder(input_area, "Devices", 3) start_button = TK.Button(master, text="Start", command=run) From 65e22da3d6f64ff61b192e19132a7569a8b63b2b Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Wed, 20 Apr 2022 18:23:44 +0200 Subject: [PATCH 003/106] moved progress --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7e86cf0..d56b3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ dmypy.json # Cython debug symbols cython_debug/ +# remove at merge to main +emulators/SteppingEmulator.py \ No newline at end of file From 3b75dc6da7a8222de5cbef5754caf361cef00692 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Wed, 20 Apr 2022 18:24:32 +0200 Subject: [PATCH 004/106] moved progress from other branch --- emulators/exercise_overlay.py | 3 +-- exercise_runner_overlay.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index aa3073b..0f72b90 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -19,9 +19,8 @@ def show_info(): def step(): #insert stepper function - emulator._stepping = False + emulator._single = True bottom_text.config(text=emulator._last_message) - pass canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 324a69c..09bd868 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -35,4 +35,6 @@ def run(): start_button = TK.Button(master, text="Start", command=run) start_button.pack(side=TK.TOP) +master.resizable(False,False) + master.mainloop() \ No newline at end of file From a2cce40bb1f240ed5a5714922d5510d12c364dd5 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Wed, 20 Apr 2022 18:56:26 +0200 Subject: [PATCH 005/106] the final code from today --- emulators/AsyncEmulator.py | 1 - emulators/exercise_overlay.py | 15 +++++++++------ exercise_runner.py | 5 ++++- exercise_runner_overlay.py | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 143fbd6..30bf4be 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -17,7 +17,6 @@ def __init__(self, number_of_devices: int, kind): self._terminated = 0 self._messages = {} self._messages_sent = 0 - overlay(self) def run(self): self._progress.acquire() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 0f72b90..2c80417 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,5 +1,4 @@ from random import randint -from threading import Thread import tkinter as TK from emulators.EmulatorStub import EmulatorStub @@ -13,14 +12,18 @@ def overlay(emulator:EmulatorStub): def show_info(): window = TK.Toplevel(master) - data = TK.Label(window, text="___placeholder___") window.resizable(False, False) + data = TK.Label(window, text="___placeholder___") data.pack(side=TK.TOP) + for device in emulator._devices: + pass def step(): #insert stepper function emulator._single = True - bottom_text.config(text=emulator._last_message) + + def end(): + emulator._stepping = False canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) @@ -38,8 +41,8 @@ def step(): bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) step_button = TK.Button(bottom_frame, text="Step", command=step) - step_button.pack(side=TK.TOP) - bottom_text = TK.Label(bottom_frame, text="last message") - bottom_text.pack(side=TK.TOP) + step_button.pack(side=TK.LEFT) + end_button = TK.Button(bottom_frame, text="End", command=end) + end_button.pack(side=TK.LEFT) master.resizable(False,False) master.mainloop() diff --git a/exercise_runner.py b/exercise_runner.py index 0c6ac61..58edd26 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -15,6 +15,7 @@ import exercises.demo from emulators.AsyncEmulator import AsyncEmulator from emulators.SyncEmulator import SyncEmulator +from emulators.SteppingEmulator import SteppingEmulator def fetch_alg(lecture: str, algorithm: str): @@ -39,6 +40,8 @@ def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_d emulator = AsyncEmulator elif network_type == 'sync': emulator = SyncEmulator + elif network_type == 'stepping': + emulator = SteppingEmulator instance = None if lecture_no == 0: alg = fetch_alg('demo', 'PingPong') @@ -65,7 +68,7 @@ def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_d parser.add_argument('--algorithm', metavar='alg', type=str, nargs=1, help='Which algorithm from the exercise to run', required=True) parser.add_argument('--type', metavar='nw', type=str, nargs=1, - help='whether to use [async] or [sync] network', required=True, choices=['async', 'sync']) + help='whether to use [async] or [sync] network', required=True, choices=['async', 'sync', 'stepping']) parser.add_argument('--devices', metavar='N', type=int, nargs=1, help='Number of devices to run', required=True) args = parser.parse_args() diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 09bd868..9b01b0a 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -29,7 +29,7 @@ def run(): lecture_frame, lecture_entry = input_builder(input_area, "Lecture", 0) algorithm_frame, algorithm_entry = input_builder(input_area, "Algorithm", "PingPong") -type_frame, type_entry = input_builder(input_area, "Type", "async") +type_frame, type_entry = input_builder(input_area, "Type", "stepping") devices_frame, devices_entry = input_builder(input_area, "Devices", 3) start_button = TK.Button(master, text="Start", command=run) From f090b8c3c480d600e6cc0d57b3f5a7964b12bb37 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Fri, 22 Apr 2022 01:38:27 +0200 Subject: [PATCH 006/106] resized posix elements --- emulators/exercise_overlay.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 2c80417..1f2a320 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,6 +1,8 @@ from random import randint import tkinter as TK +from platformdirs import os + from emulators.EmulatorStub import EmulatorStub def overlay(emulator:EmulatorStub): @@ -28,7 +30,10 @@ def end(): canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) for device in range(len(emulator._devices)): - device_size = 100 + if os.name == 'posix': + device_size=150 + else: + device_size = 100 x = randint(device_size, width-device_size) y = randint(device_size, height-device_size) canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") From 0183565a57e66d5d3b438ee0fe72b7a799e62101 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Tue, 26 Apr 2022 17:20:26 +0200 Subject: [PATCH 007/106] fixed end of script issue --- emulators/exercise_overlay.py | 9 ++++----- exercise_runner.py | 26 +++++++++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 2c80417..d5bb809 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,9 +1,9 @@ from random import randint import tkinter as TK -from emulators.EmulatorStub import EmulatorStub +from emulators.SteppingEmulator import SteppingEmulator -def overlay(emulator:EmulatorStub): +def overlay(emulator:SteppingEmulator): # top is planned to be reserved for a little description of controls in stepper # if stepper is not chosen, this will not be displayed master = TK.Tk() @@ -16,7 +16,7 @@ def show_info(): data = TK.Label(window, text="___placeholder___") data.pack(side=TK.TOP) for device in emulator._devices: - pass + data_label = TK.Label() def step(): #insert stepper function @@ -44,5 +44,4 @@ def end(): step_button.pack(side=TK.LEFT) end_button = TK.Button(bottom_frame, text="End", command=end) end_button.pack(side=TK.LEFT) - master.resizable(False,False) - master.mainloop() + master.resizable(False,False) \ No newline at end of file diff --git a/exercise_runner.py b/exercise_runner.py index 58edd26..7297ab3 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -1,5 +1,7 @@ import argparse import inspect +from threading import Thread +from emulators.exercise_overlay import overlay import exercises.exercise1 import exercises.exercise2 @@ -49,17 +51,19 @@ def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_d else: alg = fetch_alg(f'exercise{lecture_no}', algorithm) instance = emulator(number_of_devices, alg) - - if instance is not None: - instance.run() - print(f'Execution Complete') - instance.print_result() - print('Statistics') - instance.print_statistics() - else: - raise NotImplementedError( - f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') - + def run_instance(): + if instance is not None: + instance.run() + print(f'Execution Complete') + instance.print_result() + print('Statistics') + instance.print_statistics() + else: + raise NotImplementedError( + f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') + Thread(target=run_instance).start() + if isinstance(instance, SteppingEmulator): + overlay(instance) if __name__ == "__main__": parser = argparse.ArgumentParser(description='For exercises in Distributed Systems.') From 303c9b079d860828d85e63ee05ef58ce0058403f Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Tue, 26 Apr 2022 18:15:35 +0200 Subject: [PATCH 008/106] added some data to devices in overlay --- emulators/AsyncEmulator.py | 1 - emulators/exercise_overlay.py | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 30bf4be..b17a4b3 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -7,7 +7,6 @@ from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub -from emulators.exercise_overlay import overlay class AsyncEmulator(EmulatorStub): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 4cc9cb6..2eab9c4 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -17,8 +17,19 @@ def show_info(): window.resizable(False, False) data = TK.Label(window, text="___placeholder___") data.pack(side=TK.TOP) + frames = list() for device in emulator._devices: - data_label = TK.Label() + dev_frame = TK.Frame(window) + dev_frame.pack(side=TK.LEFT) + frames.append(dev_frame) + dev_name = TK.Label(dev_frame, text=f'Device id: {device._id}') + dev_name.pack(side=TK.TOP) + for data in emulator._last_messages: + device_id = data[5] + data_label = TK.Label(frames[device_id], text=data) + data_label.pack(side=TK.BOTTOM) + + def step(): #insert stepper function From 556e377db1c3b8717eb79d0cd99140e4aeabc1ee Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Wed, 27 Apr 2022 20:07:01 +0200 Subject: [PATCH 009/106] started displaying actual data in overlay --- emulators/exercise_overlay.py | 61 ++++++++++++++++++++++++----------- exercise_runner.py | 2 +- exercise_runner_overlay.py | 8 +++-- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 2eab9c4..0ef86d4 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,34 +1,51 @@ +from copy import copy from random import randint import tkinter as TK +import tkinter.ttk as TTK from os import name from emulators.SteppingEmulator import SteppingEmulator -def overlay(emulator:SteppingEmulator): +def overlay(emulator:SteppingEmulator, run_function): # top is planned to be reserved for a little description of controls in stepper # if stepper is not chosen, this will not be displayed master = TK.Tk() height = 500 width = 1000 - def show_info(): + def show_all_data(): window = TK.Toplevel(master) + window.title("All data") window.resizable(False, False) - data = TK.Label(window, text="___placeholder___") - data.pack(side=TK.TOP) frames = list() for device in emulator._devices: - dev_frame = TK.Frame(window) + dev_frame = TK.LabelFrame(window, text=f'Device {emulator._devices.index(device)}') dev_frame.pack(side=TK.LEFT) frames.append(dev_frame) - dev_name = TK.Label(dev_frame, text=f'Device id: {device._id}') + dev_name = TTK.Label(dev_frame, text=f'Device id: {device._id}') dev_name.pack(side=TK.TOP) - for data in emulator._last_messages: - device_id = data[5] - data_label = TK.Label(frames[device_id], text=data) + for data in emulator._list_messages_received: + device_id = data._destination + data_label = TTK.Label(frames[device_id], text=data) data_label.pack(side=TK.BOTTOM) + def show_data(device_id): + def _window(): + window = TK.Toplevel(master, width=width/5, height=height/5) + window.title(f'Device {device_id}') + received_frame = TK.LabelFrame(window, text="Received") + received_frame.pack(side=TK.LEFT) + for data in emulator._list_messages_received: + if data._destination == device_id: + TTK.Label(received_frame, text=data).pack(side=TK.TOP) + sent_frame = TK.LabelFrame(window, text="Sent") + sent_frame.pack(side=TK.LEFT) + for data in emulator._list_messages_sent: + if data._source == device_id: + TTK.Label(sent_frame, text=data).pack(side=TK.TOP) + return _window + def step(): @@ -40,24 +57,30 @@ def end(): canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) + if name == "posix": + device_size = 150 + else: + device_size = 100 + for device in range(len(emulator._devices)): - if name == 'posix': - device_size = 150 - else: - device_size = 100 x = randint(device_size, width-device_size) y = randint(device_size, height-device_size) canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") - device_frame = TK.Frame(canvas) + device_frame = TTK.Frame(canvas) device_frame.place(x=x+(device_size/5), y=y+(device_size/5)) - device_button = TK.Button(device_frame, text="Show data", command=show_info) + device_button = TTK.Button(device_frame, text="Show data", command=show_data(device)) device_button.pack(side=TK.BOTTOM) - device_text = TK.Label(device_frame, text=f'Device #{device}') + device_text = TTK.Label(device_frame, text=f'Device #{device}') device_text.pack(side=TK.BOTTOM) bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) - step_button = TK.Button(bottom_frame, text="Step", command=step) + step_button = TTK.Button(bottom_frame, text="Step", command=step) step_button.pack(side=TK.LEFT) - end_button = TK.Button(bottom_frame, text="End", command=end) + end_button = TTK.Button(bottom_frame, text="End", command=end) end_button.pack(side=TK.LEFT) - master.resizable(False,False) \ No newline at end of file + start_new_button = TK.Button(bottom_frame, text="Restart algorithm", command=run_function) + start_new_button.pack(side=TK.LEFT) + show_all_data_button = TK.Button(bottom_frame, text="show all data", command=show_all_data) + show_all_data_button.pack(side=TK.LEFT) + master.resizable(False,False) + master.title("Stepping algorithm") \ No newline at end of file diff --git a/exercise_runner.py b/exercise_runner.py index 7297ab3..25c43fa 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -63,7 +63,7 @@ def run_instance(): f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') Thread(target=run_instance).start() if isinstance(instance, SteppingEmulator): - overlay(instance) + overlay(instance, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices)) if __name__ == "__main__": parser = argparse.ArgumentParser(description='For exercises in Distributed Systems.') diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 9b01b0a..8c9eca8 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,10 +1,11 @@ import tkinter as TK +import tkinter.ttk as TTK from exercise_runner import run_exercise def input_builder(master, title:str, entry_content:str): frame = TK.Frame(master) - text = TK.Label(frame, text=title) - entry = TK.Entry(frame) + text = TTK.Label(frame, text=title) + entry = TTK.Entry(frame) entry.insert(TK.END, entry_content) frame.pack(side=TK.LEFT) text.pack(side=TK.TOP) @@ -32,9 +33,10 @@ def run(): type_frame, type_entry = input_builder(input_area, "Type", "stepping") devices_frame, devices_entry = input_builder(input_area, "Devices", 3) -start_button = TK.Button(master, text="Start", command=run) +start_button = TTK.Button(master, text="Start", command=run) start_button.pack(side=TK.TOP) master.resizable(False,False) +master.title("Distributed Exercises AAU") master.mainloop() \ No newline at end of file From a93268127f5c70d4b90620c4fd2b0a750491b07d Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Thu, 28 Apr 2022 19:52:52 +0200 Subject: [PATCH 010/106] moved device object and added some fixes to window size --- emulators/exercise_overlay.py | 50 ++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 0ef86d4..d6a0465 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,4 +1,3 @@ -from copy import copy from random import randint import tkinter as TK import tkinter.ttk as TTK @@ -23,29 +22,38 @@ def show_all_data(): dev_frame = TK.LabelFrame(window, text=f'Device {emulator._devices.index(device)}') dev_frame.pack(side=TK.LEFT) frames.append(dev_frame) - dev_name = TTK.Label(dev_frame, text=f'Device id: {device._id}') - dev_name.pack(side=TK.TOP) for data in emulator._list_messages_received: device_id = data._destination data_label = TTK.Label(frames[device_id], text=data) data_label.pack(side=TK.BOTTOM) def show_data(device_id): - def _window(): - window = TK.Toplevel(master, width=width/5, height=height/5) - window.title(f'Device {device_id}') - received_frame = TK.LabelFrame(window, text="Received") - received_frame.pack(side=TK.LEFT) - for data in emulator._list_messages_received: - if data._destination == device_id: - TTK.Label(received_frame, text=data).pack(side=TK.TOP) - sent_frame = TK.LabelFrame(window, text="Sent") - sent_frame.pack(side=TK.LEFT) - for data in emulator._list_messages_sent: - if data._source == device_id: - TTK.Label(sent_frame, text=data).pack(side=TK.TOP) - return _window + def _show_data(): + if len(emulator._list_messages_received) > 0: + window = TK.Toplevel(master) + window.title(f'Device {device_id}') + received_frame = TK.LabelFrame(window, text="Received") + received_frame.pack(side=TK.LEFT) + for data in emulator._list_messages_received: + if data._destination == device_id: + TTK.Label(received_frame, text=data).pack(side=TK.TOP) + sent_frame = TK.LabelFrame(window, text="Sent") + sent_frame.pack(side=TK.LEFT) + for data in emulator._list_messages_sent: + if data._source == device_id: + TTK.Label(sent_frame, text=data).pack(side=TK.TOP) + else: + return + return _show_data + def build_device(master:TK.Canvas, device_id, x, y, device_size): + circle = canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") + frame = TTK.Frame(master) + frame.place(x=x+(device_size/5), y=y+(device_size/5)) + button = TTK.Button(frame, command=show_data(device_id), text="Show data") + button.pack(side=TK.BOTTOM) + text = TTK.Label(frame, text=f'Device #{device_id}') + text.pack(side=TK.BOTTOM) def step(): @@ -65,13 +73,7 @@ def end(): for device in range(len(emulator._devices)): x = randint(device_size, width-device_size) y = randint(device_size, height-device_size) - canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") - device_frame = TTK.Frame(canvas) - device_frame.place(x=x+(device_size/5), y=y+(device_size/5)) - device_button = TTK.Button(device_frame, text="Show data", command=show_data(device)) - device_button.pack(side=TK.BOTTOM) - device_text = TTK.Label(device_frame, text=f'Device #{device}') - device_text.pack(side=TK.BOTTOM) + build_device(canvas, device, x, y, device_size) bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) step_button = TTK.Button(bottom_frame, text="Step", command=step) From 9812fd4a005472e88c423ce0412d1488a7e6c731 Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Fri, 29 Apr 2022 20:21:28 +0200 Subject: [PATCH 011/106] added table module and implemented it in gui --- emulators/exercise_overlay.py | 45 +++++++++--------- emulators/table.py | 90 +++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 emulators/table.py diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index d6a0465..da3d532 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -4,28 +4,31 @@ from os import name from emulators.SteppingEmulator import SteppingEmulator - +from emulators.table import table def overlay(emulator:SteppingEmulator, run_function): # top is planned to be reserved for a little description of controls in stepper - # if stepper is not chosen, this will not be displayed master = TK.Tk() height = 500 width = 1000 + def show_all_data(): window = TK.Toplevel(master) - window.title("All data") - window.resizable(False, False) - frames = list() - for device in emulator._devices: - dev_frame = TK.LabelFrame(window, text=f'Device {emulator._devices.index(device)}') - dev_frame.pack(side=TK.LEFT) - frames.append(dev_frame) - for data in emulator._list_messages_received: - device_id = data._destination - data_label = TTK.Label(frames[device_id], text=data) - data_label.pack(side=TK.BOTTOM) + content:list[list] = [] + messages = emulator._list_messages_sent + header = TK.Frame(window) + header.pack(side=TK.TOP) + TK.Label(header, text="Source | ").pack(side=TK.LEFT) + TK.Label(header, text="Destination | ").pack(side=TK.LEFT) + TK.Label(header, text="Message | ").pack(side=TK.LEFT) + TK.Label(header, text="Sequence number").pack(side=TK.LEFT) + + content = [[messages[i].source, messages[i].destination, messages[i], i] for i in range(len(messages))] + + + tab = table(window, content, width=15, scrollable="y") + tab.pack(side=TK.BOTTOM) def show_data(device_id): def _show_data(): @@ -35,12 +38,12 @@ def _show_data(): received_frame = TK.LabelFrame(window, text="Received") received_frame.pack(side=TK.LEFT) for data in emulator._list_messages_received: - if data._destination == device_id: + if data.destination == device_id: TTK.Label(received_frame, text=data).pack(side=TK.TOP) sent_frame = TK.LabelFrame(window, text="Sent") sent_frame.pack(side=TK.LEFT) for data in emulator._list_messages_sent: - if data._source == device_id: + if data.source == device_id: TTK.Label(sent_frame, text=data).pack(side=TK.TOP) else: return @@ -76,13 +79,9 @@ def end(): build_device(canvas, device, x, y, device_size) bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) - step_button = TTK.Button(bottom_frame, text="Step", command=step) - step_button.pack(side=TK.LEFT) - end_button = TTK.Button(bottom_frame, text="End", command=end) - end_button.pack(side=TK.LEFT) - start_new_button = TK.Button(bottom_frame, text="Restart algorithm", command=run_function) - start_new_button.pack(side=TK.LEFT) - show_all_data_button = TK.Button(bottom_frame, text="show all data", command=show_all_data) - show_all_data_button.pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="show all data", command=show_all_data).pack(side=TK.LEFT) master.resizable(False,False) master.title("Stepping algorithm") \ No newline at end of file diff --git a/emulators/table.py b/emulators/table.py new file mode 100644 index 0000000..0d063b2 --- /dev/null +++ b/emulators/table.py @@ -0,0 +1,90 @@ +import tkinter as TK + +class table(TK.Frame): + rows:list[TK.Frame] + labels:list[list[TK.Label]] + master:TK.Toplevel + + def __init__(self, master = None, content:list[list]=[], width=2, height=2, title="", icon="", scrollable = "None"): + if master == None: + master = TK.Toplevel() + self.master = master + TK.Frame.__init__(self, self.master) + if type(self.master) is TK.Toplevel: + self.master.resizable(False,False) + self.master.title(title) + if not icon == "": + try: + self.master.iconbitmap(icon) + except: + print("failed to set window icon") + if not len(content) > 0 or not len(content[0]) > 0: + print("failed to make table: bad list") + return + rows = len(content) + cols = len(content[0]) + if scrollable == "None": + container = self + elif scrollable == "x": + if len(content) > 10: + mod = 10 + else: + mod = len(content) + __container = ScrollableFrame_X(self, width=width*7.5*len(content[0]), height=height*mod*20) + container = __container.scrollable_frame + __container.pack(side=TK.LEFT) + elif scrollable == "y": + if len(content) > 10: + mod = 10 + else: + mod = len(content) + __container = ScrollableFrame_Y(self, width=width*7.5*len(content[0]), height=height*mod*20) + container = __container.scrollable_frame + __container.pack(side=TK.LEFT) + self.rows = [TK.LabelFrame(container) for i in range(rows)] + [self.rows[i].pack(side=TK.TOP) for i in range(len(self.rows))] + self.labels = [[TK.Label(self.rows[i], text=content[i][j], width=width, height=height).pack(side=TK.LEFT) for i in range(rows)] for j in range(cols)] + +class ScrollableFrame_Y(TK.Frame): + ###source: https://blog.teclado.com/tkinter-scrollable-frames/ + def __init__(self, container, width=300, height=300, *args, **kwargs): + super().__init__(container, *args, **kwargs) + canvas = TK.Canvas(self) + scrollbar = TK.Scrollbar(self, orient="vertical", command=canvas.yview) + self.scrollable_frame = TK.Frame(canvas) + + self.scrollable_frame.bind( + "", + lambda e: canvas.configure( + scrollregion=canvas.bbox("all") + ) + ) + + canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") + + canvas.configure(yscrollcommand=scrollbar.set, width=width, height=height) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + +class ScrollableFrame_X(TK.Frame): + ###source: https://blog.teclado.com/tkinter-scrollable-frames/ + def __init__(self, container, width=300, height=300, *args, **kwargs): + super().__init__(container, *args, **kwargs) + canvas = TK.Canvas(self) + scrollbar = TK.Scrollbar(self, orient="horizontal", command=canvas.xview) + self.scrollable_frame = TK.Frame(canvas) + + self.scrollable_frame.bind( + "", + lambda e: canvas.configure( + scrollregion=canvas.bbox("all") + ) + ) + + canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") + + canvas.configure(xscrollcommand=scrollbar.set, width=width, height=height) + + canvas.pack(side="top", fill="both", expand=True) + scrollbar.pack(side="bottom", fill="x") \ No newline at end of file From 9c8daf57d11f2744cea4a1bf24559dc573e9f031 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Sun, 1 May 2022 02:28:26 +0200 Subject: [PATCH 012/106] started working on placing device representations correctly --- emulators/exercise_overlay.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index da3d532..2bd9313 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,3 +1,4 @@ +from math import cos, pi from random import randint import tkinter as TK import tkinter.ttk as TTK @@ -9,8 +10,9 @@ def overlay(emulator:SteppingEmulator, run_function): # top is planned to be reserved for a little description of controls in stepper master = TK.Tk() - height = 500 - width = 1000 + height = 360 + width = 360 + spacing = 10 def show_all_data(): @@ -49,6 +51,27 @@ def _show_data(): return return _show_data + def get_coordinates_from_index(center:tuple[int,int], r:int, device:int, n:int) -> tuple[int, int]: + # for index 0 we have values: center = (180, 180), r = 170, device = 0, n = 3 + # length should be 2pi/3 = ~2 + # x should be 2*0 = 0 + # y should be 1 + # function should return 180-(170*0), 180-(170*1) + # for index 1 we have values: center = (180, 180), r = 170, device = 1, n = 3 + # length should be 2pi/3 = ~2 + # x should be 2*1 = 2 + # y should be ~-0.4 + # function should return 180-(170*-2), 180-(170*-0.4) + length = (2*pi)/n + x = length*device + y = cos(x) + x = x/(2*pi) + print(x,y) + if x < pi: + return int(center[0]-(r*x)), int(center[1]-(r*y)) + else: + return int(center[0]-(r*-x)), int(center[1]-(r*y)) + def build_device(master:TK.Canvas, device_id, x, y, device_size): circle = canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") frame = TTK.Frame(master) @@ -74,8 +97,8 @@ def end(): device_size = 100 for device in range(len(emulator._devices)): - x = randint(device_size, width-device_size) - y = randint(device_size, height-device_size) + x,y = get_coordinates_from_index((int(width/2), int(width/2)), (int(width/2))-spacing, device, len(emulator._devices)) + print(x,y) build_device(canvas, device, x, y, device_size) bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) From b2023bbc9680a2e271fb9d7e5e0e7405a0fc75d0 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Sun, 1 May 2022 19:12:50 +0200 Subject: [PATCH 013/106] finished placement in exercise_overlay --- emulators/exercise_overlay.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 2bd9313..dcb86e5 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,4 +1,4 @@ -from math import cos, pi +from math import sin, cos, pi from random import randint import tkinter as TK import tkinter.ttk as TTK @@ -10,8 +10,8 @@ def overlay(emulator:SteppingEmulator, run_function): # top is planned to be reserved for a little description of controls in stepper master = TK.Tk() - height = 360 - width = 360 + height = 500 + width = 500 spacing = 10 @@ -51,22 +51,9 @@ def _show_data(): return return _show_data - def get_coordinates_from_index(center:tuple[int,int], r:int, device:int, n:int) -> tuple[int, int]: - # for index 0 we have values: center = (180, 180), r = 170, device = 0, n = 3 - # length should be 2pi/3 = ~2 - # x should be 2*0 = 0 - # y should be 1 - # function should return 180-(170*0), 180-(170*1) - # for index 1 we have values: center = (180, 180), r = 170, device = 1, n = 3 - # length should be 2pi/3 = ~2 - # x should be 2*1 = 2 - # y should be ~-0.4 - # function should return 180-(170*-2), 180-(170*-0.4) - length = (2*pi)/n - x = length*device - y = cos(x) - x = x/(2*pi) - print(x,y) + def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tuple[int, int]: + x = sin((i*2*pi)/n) + y = cos((i*2*pi)/n) if x < pi: return int(center[0]-(r*x)), int(center[1]-(r*y)) else: @@ -97,8 +84,7 @@ def end(): device_size = 100 for device in range(len(emulator._devices)): - x,y = get_coordinates_from_index((int(width/2), int(width/2)), (int(width/2))-spacing, device, len(emulator._devices)) - print(x,y) + x,y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) build_device(canvas, device, x, y, device_size) bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.TOP) From 60e31762bdc0efa91e87202d9cdfed2eec8f915e Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Sun, 1 May 2022 19:26:17 +0200 Subject: [PATCH 014/106] redefined stuff --- emulators/exercise_overlay.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index dcb86e5..c15783a 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -78,10 +78,7 @@ def end(): canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) - if name == "posix": - device_size = 150 - else: - device_size = 100 + device_size = 100 for device in range(len(emulator._devices)): x,y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) From 570724ddd382b49dc10f0817e52e071f2da7ff2d Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Mon, 2 May 2022 00:12:31 +0200 Subject: [PATCH 015/106] finished adding tables and tidying data printing --- emulators/exercise_overlay.py | 57 ++++++++++++++++++++++------------- exercises/demo.py | 5 ++- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index dcb86e5..2c51b7c 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -9,7 +9,7 @@ def overlay(emulator:SteppingEmulator, run_function): # top is planned to be reserved for a little description of controls in stepper - master = TK.Tk() + master = TK.Toplevel() height = 500 width = 500 spacing = 10 @@ -19,34 +19,49 @@ def show_all_data(): window = TK.Toplevel(master) content:list[list] = [] messages = emulator._list_messages_sent + message_content = list() + for message in messages: + temp = str(message) + temp = temp.replace(f'{message.source} -> {message.destination} : ', "") + temp = temp.replace(f'{message.source}->{message.destination} : ', "") + message_content.append(temp) header = TK.Frame(window) - header.pack(side=TK.TOP) - TK.Label(header, text="Source | ").pack(side=TK.LEFT) - TK.Label(header, text="Destination | ").pack(side=TK.LEFT) - TK.Label(header, text="Message | ").pack(side=TK.LEFT) - TK.Label(header, text="Sequence number").pack(side=TK.LEFT) + header.pack(side=TK.TOP, anchor=TK.NW) + TK.Label(header, text="Source", width=15).pack(side=TK.LEFT) + TK.Label(header, text="Destination", width=15).pack(side=TK.LEFT) + TK.Label(header, text="Message", width=15).pack(side=TK.LEFT) + TK.Label(header, text="Sequence number", width=15).pack(side=TK.LEFT) - content = [[messages[i].source, messages[i].destination, messages[i], i] for i in range(len(messages))] + content = [[messages[i].source, messages[i].destination, message_content[i], i] for i in range(len(messages))] - tab = table(window, content, width=15, scrollable="y") - tab.pack(side=TK.BOTTOM) + table(window, content, width=15, scrollable="y", title="All messages").pack(side=TK.BOTTOM) def show_data(device_id): def _show_data(): if len(emulator._list_messages_received) > 0: window = TK.Toplevel(master) window.title(f'Device {device_id}') - received_frame = TK.LabelFrame(window, text="Received") - received_frame.pack(side=TK.LEFT) - for data in emulator._list_messages_received: - if data.destination == device_id: - TTK.Label(received_frame, text=data).pack(side=TK.TOP) - sent_frame = TK.LabelFrame(window, text="Sent") - sent_frame.pack(side=TK.LEFT) - for data in emulator._list_messages_sent: - if data.source == device_id: - TTK.Label(sent_frame, text=data).pack(side=TK.TOP) + header = TK.Frame(window) + header.pack(side=TK.TOP, anchor=TK.NW) + TK.Label(header, text="Received", width=15).pack(side=TK.LEFT) + TK.Label(header, text="Sent", width=15).pack(side=TK.LEFT) + received = list() + sent = list() + for message in emulator._list_messages_received: + if message.destination == device_id: + received.append(str(message)) + if message.source == device_id: + sent.append(str(message)) + if len(received) > len(sent): + for _ in range(len(received)-len(sent)): + sent.append("") + elif len(sent) > len(received): + for _ in range(len(sent) - len(received)): + received.append("") + content = [[received[i], sent[i]] for i in range(len(received))] + + table(window, content, width=15, scrollable="y", title=f'Device {device_id}').pack(side=TK.BOTTOM) else: return return _show_data @@ -62,7 +77,7 @@ def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tu def build_device(master:TK.Canvas, device_id, x, y, device_size): circle = canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") frame = TTK.Frame(master) - frame.place(x=x+(device_size/5), y=y+(device_size/5)) + frame.place(x=x+(device_size/8), y=y+(device_size/4)) button = TTK.Button(frame, command=show_data(device_id), text="Show data") button.pack(side=TK.BOTTOM) text = TTK.Label(frame, text=f'Device #{device_id}') @@ -91,6 +106,6 @@ def end(): TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) - TTK.Button(bottom_frame, text="show all data", command=show_all_data).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) master.resizable(False,False) master.title("Stepping algorithm") \ No newline at end of file diff --git a/exercises/demo.py b/exercises/demo.py index ef3fc24..85e526d 100644 --- a/exercises/demo.py +++ b/exercises/demo.py @@ -18,7 +18,10 @@ def __init__(self, sender: int, destination: int, is_ping: bool): # remember to implement the __str__ method such that the debug of the framework works! def __str__(self): - return f'{self.source} -> {self.destination} : ping? {self.is_ping}' + if self.is_ping: + return f'{self.source} -> {self.destination} : Ping' + else: + return f'{self.source} -> {self.destination} : Pong' # This class extends on the basic Device class. We will implement the protocol in the run method From ca2778f702a561053c9614b4f2ed52f1fd528ceb Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Mon, 2 May 2022 00:46:57 +0200 Subject: [PATCH 016/106] added status label --- emulators/exercise_overlay.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 10e4e1f..32aaa46 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,5 +1,6 @@ from math import sin, cos, pi from random import randint +from threading import Thread import tkinter as TK import tkinter.ttk as TTK from os import name @@ -87,9 +88,16 @@ def build_device(master:TK.Canvas, device_id, x, y, device_size): def step(): #insert stepper function emulator._single = True + if emulator.all_terminated(): + bottom_label.config(text="Finished running", fg="green") + else: + bottom_label.config(text=f'Step {emulator._messages_sent}') def end(): emulator._stepping = False + while not emulator.all_terminated(): + pass + bottom_label.config(text="Finished running", fg="green") canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) @@ -98,11 +106,14 @@ def end(): for device in range(len(emulator._devices)): x,y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) build_device(canvas, device, x, y, device_size) + bottom_frame = TK.LabelFrame(master, text="Inputs") - bottom_frame.pack(side=TK.TOP) + bottom_frame.pack(side=TK.BOTTOM) TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) + bottom_label = TK.Label(master, text="Status") + bottom_label.pack(side=TK.BOTTOM) master.resizable(False,False) master.title("Stepping algorithm") \ No newline at end of file From 5a58c028ffa2d35b5aec01ccec51c5898c4e3928 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Mon, 2 May 2022 20:19:55 +0200 Subject: [PATCH 017/106] merge stuff --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index d56b3f6..14b7c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,3 @@ dmypy.json # Cython debug symbols cython_debug/ - -# remove at merge to main -emulators/SteppingEmulator.py \ No newline at end of file From 621fa20b514ff59b0083505cba969a49e7506c4b Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Mon, 2 May 2022 20:24:13 +0200 Subject: [PATCH 018/106] added new stuff in stepper --- emulators/SteppingEmulator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 425d4ab..88a5679 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -16,6 +16,8 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._stepper.start() self._stepping = True self._single = False + self._list_messages_received:list[MessageStub] = list() + self._list_messages_sent:list[MessageStub] = list() self._keyheld = False self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release) self.listener.start() @@ -42,6 +44,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: self._step("step?") m = self._messages[index].pop() print(f'\tRecieve {m}') + self._list_messages_received.append(m) self._progress.release() return m @@ -52,6 +55,7 @@ def queue(self, message: MessageStub): self._step("step?") self._messages_sent += 1 print(f'\tSend {message}') + self._list_messages_sent.append(message) if message.destination not in self._messages: self._messages[message.destination] = [] self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing From cb484440c651dcbddd559a88c66cd394aa7f2caa Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Tue, 10 May 2022 00:41:28 +0200 Subject: [PATCH 019/106] fixed listener issue, added more illustrations to gui --- emulators/exercise_overlay.py | 41 +++++++++++++++++++++++++---------- exercises/exercise1.py | 25 +++++++++++++++++++-- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 32aaa46..8311726 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -76,36 +76,53 @@ def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tu return int(center[0]-(r*-x)), int(center[1]-(r*y)) def build_device(master:TK.Canvas, device_id, x, y, device_size): - circle = canvas.create_oval(x, y, x+device_size, y+device_size, outline="black") + canvas.create_oval(x, y, x+device_size, y+device_size, outline="black", fill="gray") frame = TTK.Frame(master) frame.place(x=x+(device_size/8), y=y+(device_size/4)) - button = TTK.Button(frame, command=show_data(device_id), text="Show data") - button.pack(side=TK.BOTTOM) - text = TTK.Label(frame, text=f'Device #{device_id}') - text.pack(side=TK.BOTTOM) + TTK.Label(frame, text=f'Device #{device_id}').pack(side=TK.TOP) + TTK.Button(frame, command=show_data(device_id), text="Show data").pack(side=TK.TOP) + data_frame = TTK.Frame(frame) + data_frame.pack(side=TK.BOTTOM) + + def stop_emulator(): + emulator.listener.stop() + emulator.listener.join() + bottom_label.config(text="Finished running", fg="green") + def step(): #insert stepper function emulator._single = True if emulator.all_terminated(): - bottom_label.config(text="Finished running", fg="green") - else: - bottom_label.config(text=f'Step {emulator._messages_sent}') - + bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") + Thread(target=stop_emulator).start() + + if len(emulator._list_messages_sent) != 0: + message = emulator._list_messages_sent[len(emulator._list_messages_sent)-1] + canvas.delete("line") + canvas.create_line(coordinates[message.source][0]+(device_size/2), coordinates[message.source][1]+(device_size/2), coordinates[message.destination][0]+(device_size/2), coordinates[message.destination][1]+(device_size/2), tags="line") + msg = str(message) + msg = msg.replace(f'{message.source} -> {message.destination} : ', "") + msg = msg.replace(f'{message.source}->{message.destination} : ', "") + bottom_label.config(text=f'Last message sent: from {message.source} to {message.destination}, content: {msg}') def end(): emulator._stepping = False while not emulator.all_terminated(): pass - bottom_label.config(text="Finished running", fg="green") + bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") + Thread(target=stop_emulator).start() canvas = TK.Canvas(master, height=height, width=width) canvas.pack(side=TK.TOP) device_size = 100 - + canvas.create_line(0,0,0,0, tags="line") #create dummy lines + coordinates:list[tuple[TTK.Label]] = list() for device in range(len(emulator._devices)): - x,y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) + x, y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) build_device(canvas, device, x, y, device_size) + coordinates.append((x,y)) + bottom_frame = TK.LabelFrame(master, text="Inputs") bottom_frame.pack(side=TK.BOTTOM) diff --git a/exercises/exercise1.py b/exercises/exercise1.py index 32b83ba..5751c34 100644 --- a/exercises/exercise1.py +++ b/exercises/exercise1.py @@ -21,11 +21,32 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): # for this exercise we use the index as the "secret", but it could have been a new routing-table (for instance) # or sharing of all the public keys in a cryptographic system self._secrets = set([index]) + def run(self): # the following is your termination condition, but where should it be placed? - # if len(self._secrets) == self.number_of_devices(): - # return + while True: + + # merge my knowledge with received messages + for ingoing in self.medium().receive_all(): + self._secrets = self._secrets.union(ingoing.secrets) + + # create messages for neighbours + left_index = self.index() - 1 + if left_index < 0: + left_index = self.number_of_devices() - 1 + + right_index = self.index() + 1 # to avoid the following if; ..= (self.index() + 1) % self.number_of_devices() + if right_index >= self.number_of_devices(): + right_index = 0 + + for other in [left_index, right_index]: # to have totally connected, send to self.medium().ids() -- except ofc. self.index() + message = GossipMessage(self.index(), other, self._secrets) + self.medium().send(message) + + if len(self._secrets) == self.number_of_devices(): + return + self.medium().wait_for_next_round() return def print_result(self): From 22d4a18d17854c4f0ca264cf7429a792688a9f07 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Thu, 12 May 2022 01:03:55 +0200 Subject: [PATCH 020/106] fixed content field sizing issue --- emulators/exercise_overlay.py | 29 +++++++++++++++++------------ emulators/table.py | 26 +++++++++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 32aaa46..587ae2b 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -26,27 +26,25 @@ def show_all_data(): temp = temp.replace(f'{message.source} -> {message.destination} : ', "") temp = temp.replace(f'{message.source}->{message.destination} : ', "") message_content.append(temp) - header = TK.Frame(window) - header.pack(side=TK.TOP, anchor=TK.NW) - TK.Label(header, text="Source", width=15).pack(side=TK.LEFT) - TK.Label(header, text="Destination", width=15).pack(side=TK.LEFT) - TK.Label(header, text="Message", width=15).pack(side=TK.LEFT) - TK.Label(header, text="Sequence number", width=15).pack(side=TK.LEFT) content = [[messages[i].source, messages[i].destination, message_content[i], i] for i in range(len(messages))] - table(window, content, width=15, scrollable="y", title="All messages").pack(side=TK.BOTTOM) + tab = table(window, content, width=15, scrollable="y", title="All messages") + tab.pack(side=TK.BOTTOM) + + header = TK.Frame(window) + header.pack(side=TK.TOP, anchor=TK.NW) + TK.Label(header, text="Source", width=tab.column_width[0]).pack(side=TK.LEFT) + TK.Label(header, text="Destination", width=tab.column_width[1]).pack(side=TK.LEFT) + TK.Label(header, text="Message", width=tab.column_width[2]).pack(side=TK.LEFT) + TK.Label(header, text="Sequence number", width=tab.column_width[3]).pack(side=TK.LEFT) def show_data(device_id): def _show_data(): if len(emulator._list_messages_received) > 0: window = TK.Toplevel(master) window.title(f'Device {device_id}') - header = TK.Frame(window) - header.pack(side=TK.TOP, anchor=TK.NW) - TK.Label(header, text="Received", width=15).pack(side=TK.LEFT) - TK.Label(header, text="Sent", width=15).pack(side=TK.LEFT) received = list() sent = list() for message in emulator._list_messages_received: @@ -62,9 +60,16 @@ def _show_data(): received.append("") content = [[received[i], sent[i]] for i in range(len(received))] - table(window, content, width=15, scrollable="y", title=f'Device {device_id}').pack(side=TK.BOTTOM) + tab = table(window, content, width=15, scrollable="y", title=f'Device {device_id}') + tab.pack(side=TK.BOTTOM) + + header = TK.Frame(window) + header.pack(side=TK.TOP, anchor=TK.NW) + TK.Label(header, text="Received", width=tab.column_width[0]).pack(side=TK.LEFT) + TK.Label(header, text="Sent", width=tab.column_width[1]).pack(side=TK.LEFT) else: return + return _show_data def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tuple[int, int]: diff --git a/emulators/table.py b/emulators/table.py index 0d063b2..a4cdf09 100644 --- a/emulators/table.py +++ b/emulators/table.py @@ -4,8 +4,13 @@ class table(TK.Frame): rows:list[TK.Frame] labels:list[list[TK.Label]] master:TK.Toplevel + column_width:list[int] def __init__(self, master = None, content:list[list]=[], width=2, height=2, title="", icon="", scrollable = "None"): + if not len(content) > 0 or not len(content[0]) > 0: + print("failed to make table: bad list") + return + if master == None: master = TK.Toplevel() self.master = master @@ -18,11 +23,17 @@ def __init__(self, master = None, content:list[list]=[], width=2, height=2, titl self.master.iconbitmap(icon) except: print("failed to set window icon") - if not len(content) > 0 or not len(content[0]) > 0: - print("failed to make table: bad list") - return rows = len(content) cols = len(content[0]) + self.column_width = [width for _ in range(cols)] + for j in range(cols): + for i in range(rows): + if type(content[i][j]) == str: + if len(content[i][j]) > self.column_width[j]: + self.column_width[j] = len(content[i][j]) + elif width > self.column_width[j]: + self.column_width[j] = width + if scrollable == "None": container = self elif scrollable == "x": @@ -30,7 +41,7 @@ def __init__(self, master = None, content:list[list]=[], width=2, height=2, titl mod = 10 else: mod = len(content) - __container = ScrollableFrame_X(self, width=width*7.5*len(content[0]), height=height*mod*20) + __container = ScrollableFrame_X(self, width=sum(self.column_width), height=height*mod*20) container = __container.scrollable_frame __container.pack(side=TK.LEFT) elif scrollable == "y": @@ -38,13 +49,14 @@ def __init__(self, master = None, content:list[list]=[], width=2, height=2, titl mod = 10 else: mod = len(content) - __container = ScrollableFrame_Y(self, width=width*7.5*len(content[0]), height=height*mod*20) + __container = ScrollableFrame_Y(self, width=sum(self.column_width)*7.5, height=height*mod*20) container = __container.scrollable_frame __container.pack(side=TK.LEFT) self.rows = [TK.LabelFrame(container) for i in range(rows)] [self.rows[i].pack(side=TK.TOP) for i in range(len(self.rows))] - self.labels = [[TK.Label(self.rows[i], text=content[i][j], width=width, height=height).pack(side=TK.LEFT) for i in range(rows)] for j in range(cols)] - + for j in range(cols): + for i in range(rows): + TK.Label(self.rows[i], text=content[i][j], width=self.column_width[j], height=height).pack(side=TK.LEFT) class ScrollableFrame_Y(TK.Frame): ###source: https://blog.teclado.com/tkinter-scrollable-frames/ def __init__(self, container, width=300, height=300, *args, **kwargs): From 0ece677c5749e4ff66bbf04b5a4d3156bbb8d7d5 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Thu, 12 May 2022 01:07:57 +0200 Subject: [PATCH 021/106] fixed content field sizing issue --- exercises/exercise1.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/exercises/exercise1.py b/exercises/exercise1.py index 5751c34..32b83ba 100644 --- a/exercises/exercise1.py +++ b/exercises/exercise1.py @@ -21,32 +21,11 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): # for this exercise we use the index as the "secret", but it could have been a new routing-table (for instance) # or sharing of all the public keys in a cryptographic system self._secrets = set([index]) - def run(self): # the following is your termination condition, but where should it be placed? - while True: - - # merge my knowledge with received messages - for ingoing in self.medium().receive_all(): - self._secrets = self._secrets.union(ingoing.secrets) - - # create messages for neighbours - left_index = self.index() - 1 - if left_index < 0: - left_index = self.number_of_devices() - 1 - - right_index = self.index() + 1 # to avoid the following if; ..= (self.index() + 1) % self.number_of_devices() - if right_index >= self.number_of_devices(): - right_index = 0 - - for other in [left_index, right_index]: # to have totally connected, send to self.medium().ids() -- except ofc. self.index() - message = GossipMessage(self.index(), other, self._secrets) - self.medium().send(message) - - if len(self._secrets) == self.number_of_devices(): - return - self.medium().wait_for_next_round() + # if len(self._secrets) == self.number_of_devices(): + # return return def print_result(self): From fc0e9a88af69b88a65563e842ad785f92650e740 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Fri, 13 May 2022 01:04:00 +0200 Subject: [PATCH 022/106] added receiving lines to gui and added controls page in gui --- emulators/SteppingEmulator.py | 3 ++ emulators/exercise_overlay.py | 85 +++++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 88a5679..e8d68d4 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -18,6 +18,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._single = False self._list_messages_received:list[MessageStub] = list() self._list_messages_sent:list[MessageStub] = list() + self._last_message:tuple[str, MessageStub] = ("init") #type(received or sent), message self._keyheld = False self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release) self.listener.start() @@ -45,6 +46,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: m = self._messages[index].pop() print(f'\tRecieve {m}') self._list_messages_received.append(m) + self._last_message = ("received", m) self._progress.release() return m @@ -56,6 +58,7 @@ def queue(self, message: MessageStub): self._messages_sent += 1 print(f'\tSend {message}') self._list_messages_sent.append(message) + self._last_message = ("sent", message) if message.destination not in self._messages: self._messages[message.destination] = [] self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 019947b..f85de20 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,23 +1,31 @@ from math import sin, cos, pi -from random import randint from threading import Thread import tkinter as TK import tkinter.ttk as TTK -from os import name +from emulators.MessageStub import MessageStub from emulators.SteppingEmulator import SteppingEmulator from emulators.table import table def overlay(emulator:SteppingEmulator, run_function): - # top is planned to be reserved for a little description of controls in stepper + #master config master = TK.Toplevel() + master.resizable(False,False) + master.title("Stepping algorithm") + pages = TTK.Notebook(master) + main_page = TTK.Frame(pages) + controls_page = TTK.Frame(pages) + pages.add(main_page, text="Emulator") + pages.add(controls_page, text="Controls") + pages.pack(expand= 1, fill=TK.BOTH) height = 500 width = 500 spacing = 10 + #end of master config def show_all_data(): - window = TK.Toplevel(master) + window = TK.Toplevel() content:list[list] = [] messages = emulator._list_messages_sent message_content = list() @@ -43,30 +51,43 @@ def show_all_data(): def show_data(device_id): def _show_data(): if len(emulator._list_messages_received) > 0: - window = TK.Toplevel(master) + window = TK.Toplevel(main_page) window.title(f'Device {device_id}') - received = list() - sent = list() + received:list[MessageStub] = list() + sent:list[MessageStub] = list() for message in emulator._list_messages_received: if message.destination == device_id: - received.append(str(message)) + received.append(message) if message.source == device_id: - sent.append(str(message)) + sent.append(message) if len(received) > len(sent): for _ in range(len(received)-len(sent)): sent.append("") elif len(sent) > len(received): for _ in range(len(sent) - len(received)): received.append("") - content = [[received[i], sent[i]] for i in range(len(received))] + content = list() + for i in range(len(received)): + if received[i] == "": + msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + content.append(["", received[i], sent[i].destination, msg]) + elif sent[i] == "": + msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([received[i].source, msg, "", sent[i]]) + else: + sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([received[i].source, received_msg, sent[i].destination, sent_msg]) tab = table(window, content, width=15, scrollable="y", title=f'Device {device_id}') tab.pack(side=TK.BOTTOM) header = TK.Frame(window) header.pack(side=TK.TOP, anchor=TK.NW) - TK.Label(header, text="Received", width=tab.column_width[0]).pack(side=TK.LEFT) - TK.Label(header, text="Sent", width=tab.column_width[1]).pack(side=TK.LEFT) + TK.Label(header, text="Source", width=tab.column_width[0]).pack(side=TK.LEFT) + TK.Label(header, text="Message", width=tab.column_width[1]).pack(side=TK.LEFT) + TK.Label(header, text="Destination", width=tab.column_width[2]).pack(side=TK.LEFT) + TK.Label(header, text="Message", width=tab.column_width[3]).pack(side=TK.LEFT) else: return @@ -80,9 +101,9 @@ def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tu else: return int(center[0]-(r*-x)), int(center[1]-(r*y)) - def build_device(master:TK.Canvas, device_id, x, y, device_size): + def build_device(main_page:TK.Canvas, device_id, x, y, device_size): canvas.create_oval(x, y, x+device_size, y+device_size, outline="black", fill="gray") - frame = TTK.Frame(master) + frame = TTK.Frame(main_page) frame.place(x=x+(device_size/8), y=y+(device_size/4)) TTK.Label(frame, text=f'Device #{device_id}').pack(side=TK.TOP) TTK.Button(frame, command=show_data(device_id), text="Show data").pack(side=TK.TOP) @@ -103,22 +124,26 @@ def step(): bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") Thread(target=stop_emulator).start() - if len(emulator._list_messages_sent) != 0: - message = emulator._list_messages_sent[len(emulator._list_messages_sent)-1] + elif emulator._last_message[0] != "init": + message = emulator._last_message[1] canvas.delete("line") canvas.create_line(coordinates[message.source][0]+(device_size/2), coordinates[message.source][1]+(device_size/2), coordinates[message.destination][0]+(device_size/2), coordinates[message.destination][1]+(device_size/2), tags="line") msg = str(message) msg = msg.replace(f'{message.source} -> {message.destination} : ', "") msg = msg.replace(f'{message.source}->{message.destination} : ', "") - bottom_label.config(text=f'Last message sent: from {message.source} to {message.destination}, content: {msg}') + if emulator._last_message[0] == "sent": + bottom_label.config(text=f'Device {message.source} sent "{msg}" to {message.destination}') + elif emulator._last_message[0] == "received": + bottom_label.config(text=f'Device {message.destination} received {msg} from {message.source}') def end(): emulator._stepping = False while not emulator.all_terminated(): pass bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") Thread(target=stop_emulator).start() - - canvas = TK.Canvas(master, height=height, width=width) + + #Emulator page stuff + canvas = TK.Canvas(main_page, height=height, width=width) canvas.pack(side=TK.TOP) device_size = 100 canvas.create_line(0,0,0,0, tags="line") #create dummy lines @@ -129,13 +154,27 @@ def end(): coordinates.append((x,y)) - bottom_frame = TK.LabelFrame(master, text="Inputs") + bottom_frame = TK.LabelFrame(main_page, text="Inputs") bottom_frame.pack(side=TK.BOTTOM) TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) - bottom_label = TK.Label(master, text="Status") + bottom_label = TK.Label(main_page, text="Status") bottom_label.pack(side=TK.BOTTOM) - master.resizable(False,False) - master.title("Stepping algorithm") \ No newline at end of file + + #controls page stuff + + TTK.Label(controls_page, text="Controls", font=("Arial", 25)).pack(side=TK.TOP) + controls_frame = TTK.Frame(controls_page) + controls_frame.pack(side=TK.TOP) + name_frame = TTK.Frame(controls_frame) + name_frame.pack(side=TK.LEFT) + value_frame = TTK.Frame(controls_frame) + value_frame.pack(side=TK.RIGHT) + TTK.Label(name_frame, text="Space", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Step a single time through messages").pack(side=TK.BOTTOM, anchor=TK.NW) + TTK.Label(name_frame, text="f", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Fast-forward through messages").pack(side=TK.BOTTOM, anchor=TK.NW) + TTK.Label(name_frame, text="Enter", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Kill stepper daemon and run as an async emulator").pack(side=TK.BOTTOM, anchor=TK.NW) From 6835119aa0d855f9e36cd05ba2aa50c30fad8015 Mon Sep 17 00:00:00 2001 From: Mast3rwaf1z Date: Sat, 21 May 2022 12:21:16 +0200 Subject: [PATCH 023/106] work from last time... --- emulators/SteppingEmulator.py | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 425d4ab..2b798e4 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -5,6 +5,7 @@ from typing import Optional from emulators.MessageStub import MessageStub from pynput import keyboard +from os import system from getpass import getpass #getpass to hide input, cleaner terminal from threading import Thread #run getpass in seperate thread @@ -21,12 +22,14 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self.listener.start() msg = """ keyboard input: - space: Step a single time through messages - f: Fast-forward through messages - enter: Kill stepper daemon and run as an async emulator + space: Step a single time through messages + f: Fast-forward through messages + enter: Kill stepper daemon and run as an async emulator + tab: Show all messages currently waiting to be transmitted + s: Pick the next message waiting to be transmitted to transmit next + e: Toggle between sync and async emulation """ print(msg) - def dequeue(self, index: int) -> Optional[MessageStub]: #return super().dequeue(index) #uncomment to run as a normal async emulator (debug) @@ -39,7 +42,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: return None else: if self._stepping and self._stepper.is_alive(): #first expression for printing a reasonable amount, second to hide user input - self._step("step?") + index = self._step("step?", index) m = self._messages[index].pop() print(f'\tRecieve {m}') self._progress.release() @@ -78,6 +81,30 @@ def on_press(self, key:keyboard.KeyCode): self._stepping = False elif key == "space" and not self._keyheld: self._single = True + elif key == "tab": + print("Message queue:") + index = 0 + for messages in self._messages.values(): + for message in messages: + index+=1 + print(f'{index}: {message}') + elif key == "s": + try: + print("press return to proceed") + while self._stepper.is_alive(): + pass + _in = int(input("Specify index of which element to transmit next element to send next: ")) + index = 0 + for messages in self._messages.values(): + for message in messages: + index+=1 + if _in == index: + self.dequeue(message.destination) + except: + print("Invalid element") + if not self._stepper.is_alive(): + self._stepper = Thread(target=lambda: getpass(""), daemon=True) + self._stepper.start() self._keyheld = True def on_release(self, key:keyboard.KeyCode): @@ -89,4 +116,4 @@ def on_release(self, key:keyboard.KeyCode): key = key.name if key == "f": self._stepping = True - self._keyheld = False \ No newline at end of file + self._keyheld = False From 6d93e20edce974ad5a00ce90345c1082b26e92c6 Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Sat, 21 May 2022 12:47:20 +0200 Subject: [PATCH 024/106] made a more general stepping emulator --- emulators/SteppingEmulator.py | 38 +++++++++-------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 2b798e4..2677dd1 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -1,16 +1,15 @@ -import copy -import random -import time -from emulators.AsyncEmulator import AsyncEmulator +if True: + from emulators.AsyncEmulator import AsyncEmulator as Emulator +else: + from emulators.SyncEmulator import SyncEmulator as Emulator from typing import Optional from emulators.MessageStub import MessageStub from pynput import keyboard -from os import system from getpass import getpass #getpass to hide input, cleaner terminal from threading import Thread #run getpass in seperate thread -class SteppingEmulator(AsyncEmulator): +class SteppingEmulator(Emulator): def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object super().__init__(number_of_devices, kind) self._stepper = Thread(target=lambda: getpass(""), daemon=True) @@ -32,35 +31,18 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - #return super().dequeue(index) #uncomment to run as a normal async emulator (debug) self._progress.acquire() - if index not in self._messages: - self._progress.release() - return None - elif len(self._messages[index]) == 0: - self._progress.release() - return None - else: - if self._stepping and self._stepper.is_alive(): #first expression for printing a reasonable amount, second to hide user input - index = self._step("step?", index) - m = self._messages[index].pop() - print(f'\tRecieve {m}') - self._progress.release() - return m + if index in self._messages and not len(self._messages[index]) == 0 and self._stepping and self._stepper.is_alive(): + self._step("step?") + self._progress.release() + return super().dequeue(index) def queue(self, message: MessageStub): - #return super().queue(message) #uncomment to run as normal queue (debug) self._progress.acquire() if self._stepping and self._stepper.is_alive(): self._step("step?") - self._messages_sent += 1 - print(f'\tSend {message}') - if message.destination not in self._messages: - self._messages[message.destination] = [] - self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing - random.shuffle(self._messages[message.destination]) # shuffle to emulate changes in order - time.sleep(random.uniform(0.01, 0.1)) # try to obfuscate delays and emulate network delays self._progress.release() + return super().queue(message) def _step(self, message:str = ""): if not self._single: From 0d9fd11bb1c9c301a5e84a5b2510a20754425fe9 Mon Sep 17 00:00:00 2001 From: Thomas Jensen Date: Sat, 21 May 2022 13:11:00 +0200 Subject: [PATCH 025/106] pick command draft --- emulators/SteppingEmulator.py | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 2677dd1..bfd3e24 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -17,7 +17,8 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._stepping = True self._single = False self._keyheld = False - self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release) + self._pick = False + self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() msg = """ keyboard input: @@ -33,26 +34,42 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here def dequeue(self, index: int) -> Optional[MessageStub]: self._progress.acquire() if index in self._messages and not len(self._messages[index]) == 0 and self._stepping and self._stepper.is_alive(): - self._step("step?") + index = self._step(index=index) self._progress.release() return super().dequeue(index) def queue(self, message: MessageStub): self._progress.acquire() if self._stepping and self._stepper.is_alive(): - self._step("step?") + self._step() self._progress.release() return super().queue(message) - def _step(self, message:str = ""): + def _step(self, message:str = "Step?", index=0): if not self._single: print(f'\t{self._messages_sent}: {message}') while self._stepping: #run while waiting for input if self._single: #break while if the desired action is a single message self._single = False break + elif self._pick: + self._pick = False + try: + print("Press return to proceed") + while self._stepper.is_alive(): + pass + index = int(input("Specify index of the next element to send: ")) + except: + print("Invalid element!") + if not self._stepper.is_alive(): + self._stepper = Thread(target=lambda: getpass(""), daemon=True) + self._stepper.start() + self._stepping = True + print(message) + return index - def on_press(self, key:keyboard.KeyCode): + + def _on_press(self, key:keyboard.KeyCode): try: #for keycode class key = key.char @@ -71,25 +88,10 @@ def on_press(self, key:keyboard.KeyCode): index+=1 print(f'{index}: {message}') elif key == "s": - try: - print("press return to proceed") - while self._stepper.is_alive(): - pass - _in = int(input("Specify index of which element to transmit next element to send next: ")) - index = 0 - for messages in self._messages.values(): - for message in messages: - index+=1 - if _in == index: - self.dequeue(message.destination) - except: - print("Invalid element") - if not self._stepper.is_alive(): - self._stepper = Thread(target=lambda: getpass(""), daemon=True) - self._stepper.start() + self._pick = True self._keyheld = True - def on_release(self, key:keyboard.KeyCode): + def _on_release(self, key:keyboard.KeyCode): try: #for key class key = key.char From a679496789f7386de715f9afe6a58bd8d2a78e70 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Sat, 21 May 2022 15:39:38 +0200 Subject: [PATCH 026/106] progress on pick command --- emulators/SteppingEmulator.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index bfd3e24..8dbe5a6 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -45,7 +45,7 @@ def queue(self, message: MessageStub): self._progress.release() return super().queue(message) - def _step(self, message:str = "Step?", index=0): + def _step(self, message:str = "Step?", index=-1): if not self._single: print(f'\t{self._messages_sent}: {message}') while self._stepping: #run while waiting for input @@ -53,19 +53,20 @@ def _step(self, message:str = "Step?", index=0): self._single = False break elif self._pick: - self._pick = False - try: - print("Press return to proceed") - while self._stepper.is_alive(): - pass - index = int(input("Specify index of the next element to send: ")) - except: - print("Invalid element!") - if not self._stepper.is_alive(): - self._stepper = Thread(target=lambda: getpass(""), daemon=True) - self._stepper.start() - self._stepping = True - print(message) + if index != -1: + self._pick = False + try: + print("Press return to proceed") + while self._stepper.is_alive(): + pass + index = int(input("Specify index of the next element to send: ")) - 1 + except: + print("Invalid element!") + if not self._stepper.is_alive(): + self._stepper = Thread(target=lambda: getpass(""), daemon=True) + self._stepper.start() + self._stepping = True + print(message) return index From 49ddb435aad44d38b9ee3875073f9da3ce716d19 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 18 Jun 2022 13:56:34 +0200 Subject: [PATCH 027/106] changed default emulator to be sync, added a switch between async and sync --- emulators/SteppingEmulator.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 8dbe5a6..a20762b 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -1,15 +1,14 @@ -if True: - from emulators.AsyncEmulator import AsyncEmulator as Emulator -else: - from emulators.SyncEmulator import SyncEmulator as Emulator from typing import Optional +from emulators.AsyncEmulator import AsyncEmulator +from emulators.SyncEmulator import SyncEmulator from emulators.MessageStub import MessageStub from pynput import keyboard from getpass import getpass #getpass to hide input, cleaner terminal from threading import Thread #run getpass in seperate thread -class SteppingEmulator(Emulator): + +class SteppingEmulator(SyncEmulator, AsyncEmulator): def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object super().__init__(number_of_devices, kind) self._stepper = Thread(target=lambda: getpass(""), daemon=True) @@ -18,13 +17,15 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._single = False self._keyheld = False self._pick = False + self.current_parent = SyncEmulator + self.next_index = -1 self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() msg = """ keyboard input: space: Step a single time through messages f: Fast-forward through messages - enter: Kill stepper daemon and run as an async emulator + enter: Kill stepper daemon finish algorithm tab: Show all messages currently waiting to be transmitted s: Pick the next message waiting to be transmitted to transmit next e: Toggle between sync and async emulation @@ -32,18 +33,20 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: + result = self.current_parent.dequeue(self, index) self._progress.acquire() - if index in self._messages and not len(self._messages[index]) == 0 and self._stepping and self._stepper.is_alive(): + if result != None and self._stepping and self._stepper.is_alive(): index = self._step(index=index) self._progress.release() - return super().dequeue(index) + return result def queue(self, message: MessageStub): self._progress.acquire() if self._stepping and self._stepper.is_alive(): self._step() self._progress.release() - return super().queue(message) + + return self.current_parent.queue(self, message) def _step(self, message:str = "Step?", index=-1): if not self._single: @@ -59,7 +62,7 @@ def _step(self, message:str = "Step?", index=-1): print("Press return to proceed") while self._stepper.is_alive(): pass - index = int(input("Specify index of the next element to send: ")) - 1 + index = int(input("Specify index of the next element to send: ")) except: print("Invalid element!") if not self._stepper.is_alive(): @@ -67,7 +70,7 @@ def _step(self, message:str = "Step?", index=-1): self._stepper.start() self._stepping = True print(message) - return index + self.next_index = index def _on_press(self, key:keyboard.KeyCode): @@ -86,10 +89,16 @@ def _on_press(self, key:keyboard.KeyCode): index = 0 for messages in self._messages.values(): for message in messages: - index+=1 print(f'{index}: {message}') + index+=1 elif key == "s": self._pick = True + elif key == "e": + if self.current_parent is AsyncEmulator: + self.current_parent = SyncEmulator + elif self.current_parent is SyncEmulator: + self.current_parent = AsyncEmulator + print(f'Changed emulator to {self.current_parent.__name__}') self._keyheld = True def _on_release(self, key:keyboard.KeyCode): @@ -102,3 +111,5 @@ def _on_release(self, key:keyboard.KeyCode): if key == "f": self._stepping = True self._keyheld = False + + \ No newline at end of file From 7f26091eb9cacc3f7c56c376dfa07c2c1530cf3d Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 18 Jun 2022 14:54:30 +0200 Subject: [PATCH 028/106] added installation section --- README.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8afa26d..28dcef4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ # Exercises for Distributed Systems This repository contains a small framework written in python for emulating *asynchronous* and *synchronous* distributed systems. +## Install +The stepping emulator requires the following packages to run +* tkinter +* pynput + +These packages can be installed using pip as shown below: +```bash +pip3.10 install --user tk +pip3.10 install --user pynput +``` +For the development environment used for this framework (Arch Linux), tkinter also requires the following package installed via pacman +```bash +pacman -Sy tk +``` ## General Exercises will be described later in this document. @@ -12,19 +26,22 @@ I will provide new templates as the course progresses. You should be able to execute your solution to exercise 1 using the following lines: ```bash -python3.9 exercise_runner.py --lecture 1 --algorithm Gossip --type sync --devices 3 -python3.9 exercise_runner.py --lecture 1 --algorithm Gossip --type async --devices 3 +python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type sync --devices 3 +python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type async --devices 3 +python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type stepping --devices 3 ``` The first line will execute your implementation of the `Gossip` algorithm in a synchronous setting with three devices, while the second line will execute in an asynchronous setting. +The third line will execute your implementation in a synchronous setting, launching a GUI to visualize your implementation, the setting can be adjusted during execution. For usage of the framework, see `exercises/demo.py` for a lightweight example. The example can be run with: ```bash -python3.9 exercise_runner.py --lecture 0 --algorithm PingPong --type async --devices 3 +python3.10 exercise_runner.py --lecture 0 --algorithm PingPong --type async --devices 3 ``` + ## Pull Requests If you have any extensions or improvements you are welcome to create a pull request. @@ -56,7 +73,7 @@ Your tasks are as follows: You can have several copies of the `Gossip` class, just give the class another name in the `exercise1.py` document, for instance `ImprovedGossip`. You should then be able to call the framework with your new class via: ```bash -python3.9 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type async --devices 3 +python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type async --devices 3 ``` # Exercise 2 @@ -186,7 +203,7 @@ For all exercises today, you can use the `sync` network type - but most algorith NOTICE: To execute the code, issue for example: ```bash -python3.9 exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devices 7 +python3.10 exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devices 7 ``` # Exercise 9 @@ -200,7 +217,7 @@ python3.9 exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --d NOTICE: To execute the code, issue for example: ```bash -python3 exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async --devices 6 +python10 exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async --devices 6 ``` # Exercise 10 @@ -213,7 +230,7 @@ python3 exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async NOTICE: To execute the code, issue for example: ```bash -python3 exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type async --devices 4 +python3.10 exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type async --devices 4 ``` @@ -227,7 +244,7 @@ python3 exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type asy NOTICE: To execute the code, issue for example: ```bash -python3 exercise_runner.py --lecture 11 --algorithm BlockchainNetwork --type async --devices 10 +python3.10 exercise_runner.py --lecture 11 --algorithm BlockchainNetwork --type async --devices 10 ``` @@ -240,5 +257,5 @@ python3 exercise_runner.py --lecture 11 --algorithm BlockchainNetwork --type asy NOTICE: To execute the code, issue for example: ```bash -python3 exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 +python3.10 exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 ``` From 820a19515020ccb6b12d4400c8f8061f98549a4c Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 18 Jun 2022 15:09:54 +0200 Subject: [PATCH 029/106] added a last note about python version --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28dcef4..c124235 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,18 @@ The stepping emulator requires the following packages to run * tkinter * pynput -These packages can be installed using pip as shown below: +These packages can be installed using `pip` as shown below: ```bash pip3.10 install --user tk pip3.10 install --user pynput ``` -For the development environment used for this framework (Arch Linux), tkinter also requires the following package installed via pacman +For the development environment used for this framework (Arch Linux), tkinter also requires the following package installed via `pacman`: ```bash pacman -Sy tk ``` +Installation steps may vary depending on the operating system, for windows, installing tkinter through `pip` should be enough. + +The framework is tested in both `python3.9` and `python3.10`, both versions should work. ## General Exercises will be described later in this document. From 1159bc0d258dc0fa326f2363f1d766cbd260ff14 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 18 Jun 2022 15:41:52 +0200 Subject: [PATCH 030/106] added text about stepper and gui --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index c124235..9f688df 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,27 @@ The example can be run with: python3.10 exercise_runner.py --lecture 0 --algorithm PingPong --type async --devices 3 ``` +## Stepping emulator +The stepping emulator can be used to run the algorithm in steps where one message is sent or received for each step in this emulator. The Stepping emulator can be controlled with the following keyboard input: +``` +space: Step a single time through messages +f: Fast-forward through messages +enter: Kill stepper daemon and finish algorithm +tab: Show all messages currently waiting tobe transmitted +s: Pick the next message waiting to be transmitted to transmit next +e: Toggle between sync and async emulation +``` +## GUI +The framework can also be launched with an interface by executing the following line: +``` +python3.10 exercise_runner_overlay.py +``` +Where your solution can be executed through this GUI. + +### Stepping emulator GUI +If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your algorithm, an example of the windows this GUI will open is shown below: + + ## Pull Requests If you have any extensions or improvements you are welcome to create a pull request. From be41d3d9fcadbcc1d88a2fa9894ac2f5d8a3768c Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 18 Jun 2022 15:48:20 +0200 Subject: [PATCH 031/106] added figure to show gui --- README.md | 3 ++- figures/main_window.png | Bin 0 -> 41942 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 figures/main_window.png diff --git a/README.md b/README.md index 9f688df..f4cf217 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,9 @@ python3.10 exercise_runner_overlay.py Where your solution can be executed through this GUI. ### Stepping emulator GUI -If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your algorithm, an example of the windows this GUI will open is shown below: +If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your algorithm, an example of the Stepping GUI is shown below: +![](figures/main_window.png) ## Pull Requests diff --git a/figures/main_window.png b/figures/main_window.png new file mode 100644 index 0000000000000000000000000000000000000000..81b5cba3cf76a0f6ff66eab93d503d4af6e342a9 GIT binary patch literal 41942 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

wO`RgE$$_t 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 Date: Mon, 20 Jun 2022 14:54:18 +0200 Subject: [PATCH 032/106] worked on select command --- emulators/SteppingEmulator.py | 95 ++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index a20762b..1507fe6 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -17,8 +17,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._single = False self._keyheld = False self._pick = False - self.current_parent = SyncEmulator - self.next_index = -1 + self.parent = SyncEmulator self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() msg = """ @@ -33,10 +32,10 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - result = self.current_parent.dequeue(self, index) + result = self.parent.dequeue(self, index) self._progress.acquire() if result != None and self._stepping and self._stepper.is_alive(): - index = self._step(index=index) + self._step() self._progress.release() return result @@ -46,9 +45,9 @@ def queue(self, message: MessageStub): self._step() self._progress.release() - return self.current_parent.queue(self, message) + return self.parent.queue(self, message) - def _step(self, message:str = "Step?", index=-1): + def _step(self, message:str = "Step?"): if not self._single: print(f'\t{self._messages_sent}: {message}') while self._stepping: #run while waiting for input @@ -56,24 +55,30 @@ def _step(self, message:str = "Step?", index=-1): self._single = False break elif self._pick: - if index != -1: - self._pick = False - try: - print("Press return to proceed") - while self._stepper.is_alive(): - pass - index = int(input("Specify index of the next element to send: ")) - except: - print("Invalid element!") - if not self._stepper.is_alive(): - self._stepper = Thread(target=lambda: getpass(""), daemon=True) - self._stepper.start() - self._stepping = True - print(message) - self.next_index = index + self._pick = False + try: + print("Press return to proceed") + while self._stepper.is_alive(): + pass + self._print_transit() + if self.parent is AsyncEmulator: + print(f'Available devices: {self._messages.keys()}') + elif self.parent is SyncEmulator: + print(f'Available devices: {self._last_round_messages.keys()}') + device = int(input(f'Specify device to send to: ')) + self._print_transit_for_device(device) + index = int(input(f'Specify index of the next element to send: ')) + except Exception as e: + print(e) + print("Invalid element!") + if not self._stepper.is_alive(): + self._stepper = Thread(target=lambda: getpass(""), daemon=True) + self._stepper.start() + self._stepping = True + print(message) - def _on_press(self, key:keyboard.KeyCode): + def _on_press(self, key:keyboard.KeyCode | keyboard.Key): try: #for keycode class key = key.char @@ -85,23 +90,14 @@ def _on_press(self, key:keyboard.KeyCode): elif key == "space" and not self._keyheld: self._single = True elif key == "tab": - print("Message queue:") - index = 0 - for messages in self._messages.values(): - for message in messages: - print(f'{index}: {message}') - index+=1 + self._print_transit() elif key == "s": self._pick = True elif key == "e": - if self.current_parent is AsyncEmulator: - self.current_parent = SyncEmulator - elif self.current_parent is SyncEmulator: - self.current_parent = AsyncEmulator - print(f'Changed emulator to {self.current_parent.__name__}') + self.swap_emulator() self._keyheld = True - def _on_release(self, key:keyboard.KeyCode): + def _on_release(self, key:keyboard.KeyCode | keyboard.Key): try: #for key class key = key.char @@ -112,4 +108,35 @@ def _on_release(self, key:keyboard.KeyCode): self._stepping = True self._keyheld = False + def _print_transit(self): + print("Messages in transit:") + if self.parent is AsyncEmulator: + for messages in self._messages.values(): + for message in messages: + print(f'{message}') + elif self.parent is SyncEmulator: + for messages in self._last_round_messages.values(): + for message in messages: + print(f'{message}') + + def _print_transit_for_device(self, device): + print(f'Messages in transit to device #{device}') + index = 0 + if self.parent is AsyncEmulator: + for messages in self._messages.get(device): + for message in messages: + print(f'{index}: {message}') + index+=1 + elif self.parent is SyncEmulator: + for messages in self._last_round_messages.get(device): + for message in messages: + print(f'{index}: {message}') + index+=1 + + def swap_emulator(self): + if self.parent is AsyncEmulator: + self.parent = SyncEmulator + elif self.parent is SyncEmulator: + self.parent = AsyncEmulator + print(f'Changed emulator to {self.parent.__name__}') \ No newline at end of file From 2563e4cbedc88ea1c516137c83de34ba881f5dec Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Fri, 24 Jun 2022 14:59:28 +0200 Subject: [PATCH 033/106] finally found the issue with stepper --- emulators/AsyncEmulator.py | 2 +- emulators/SteppingEmulator.py | 31 +++++++++++++++++++------------ emulators/SyncEmulator.py | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index b17a4b3..e3dc88f 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -14,7 +14,7 @@ class AsyncEmulator(EmulatorStub): def __init__(self, number_of_devices: int, kind): super().__init__(number_of_devices, kind) self._terminated = 0 - self._messages = {} + self._messages:dict[int, list[MessageStub]] = {} self._messages_sent = 0 def run(self): diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 1507fe6..df311b5 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -17,7 +17,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._single = False self._keyheld = False self._pick = False - self.parent = SyncEmulator + self.parent = AsyncEmulator self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() msg = """ @@ -61,16 +61,26 @@ def _step(self, message:str = "Step?"): while self._stepper.is_alive(): pass self._print_transit() + keys = [] if self.parent is AsyncEmulator: - print(f'Available devices: {self._messages.keys()}') + for key in self._messages.keys(): + keys.append(key) elif self.parent is SyncEmulator: - print(f'Available devices: {self._last_round_messages.keys()}') + for key in self._last_round_messages.keys(): + keys.append(key) + print(f'Available devices: {keys}') device = int(input(f'Specify device to send to: ')) self._print_transit_for_device(device) index = int(input(f'Specify index of the next element to send: ')) + if self.parent is AsyncEmulator: + item = self._messages[device].pop(index) + self._messages[device].append(item) + + pass + elif self.parent is SyncEmulator: + pass except Exception as e: print(e) - print("Invalid element!") if not self._stepper.is_alive(): self._stepper = Thread(target=lambda: getpass(""), daemon=True) self._stepper.start() @@ -123,15 +133,12 @@ def _print_transit_for_device(self, device): print(f'Messages in transit to device #{device}') index = 0 if self.parent is AsyncEmulator: - for messages in self._messages.get(device): - for message in messages: - print(f'{index}: {message}') - index+=1 + messages:list[MessageStub] = self._messages.get(device) elif self.parent is SyncEmulator: - for messages in self._last_round_messages.get(device): - for message in messages: - print(f'{index}: {message}') - index+=1 + messages:list[MessageStub] = self._last_round_messages.get(device) + for message in messages: + print(f'{index}: {message}') + index+=1 def swap_emulator(self): if self.parent is AsyncEmulator: diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 214412e..d63d629 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -14,7 +14,7 @@ def __init__(self, number_of_devices: int, kind): self._round_lock = threading.Lock() self._done = [False for _ in self.ids()] self._awaits = [threading.Lock() for _ in self.ids()] - self._last_round_messages = {} + self._last_round_messages:dict[int, list[MessageStub]] = {} self._current_round_messages = {} self._messages_sent = 0 self._rounds = 0 From c2398531451836e51f14418d911235224cfd4f6b Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 25 Jun 2022 13:08:57 +0200 Subject: [PATCH 034/106] added a few comments, got pick working and renamed some stuff --- emulators/SteppingEmulator.py | 84 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index df311b5..d4a4b25 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -35,59 +35,31 @@ def dequeue(self, index: int) -> Optional[MessageStub]: result = self.parent.dequeue(self, index) self._progress.acquire() if result != None and self._stepping and self._stepper.is_alive(): - self._step() + self.step() self._progress.release() return result def queue(self, message: MessageStub): self._progress.acquire() if self._stepping and self._stepper.is_alive(): - self._step() + self.step() self._progress.release() return self.parent.queue(self, message) - def _step(self, message:str = "Step?"): + #the main program to stop execution + def step(self): if not self._single: - print(f'\t{self._messages_sent}: {message}') + print(f'\t{self._messages_sent}: Step?') while self._stepping: #run while waiting for input if self._single: #break while if the desired action is a single message self._single = False break elif self._pick: self._pick = False - try: - print("Press return to proceed") - while self._stepper.is_alive(): - pass - self._print_transit() - keys = [] - if self.parent is AsyncEmulator: - for key in self._messages.keys(): - keys.append(key) - elif self.parent is SyncEmulator: - for key in self._last_round_messages.keys(): - keys.append(key) - print(f'Available devices: {keys}') - device = int(input(f'Specify device to send to: ')) - self._print_transit_for_device(device) - index = int(input(f'Specify index of the next element to send: ')) - if self.parent is AsyncEmulator: - item = self._messages[device].pop(index) - self._messages[device].append(item) - - pass - elif self.parent is SyncEmulator: - pass - except Exception as e: - print(e) - if not self._stepper.is_alive(): - self._stepper = Thread(target=lambda: getpass(""), daemon=True) - self._stepper.start() - self._stepping = True - print(message) - + self.pick() + #listen for pressed keys def _on_press(self, key:keyboard.KeyCode | keyboard.Key): try: #for keycode class @@ -100,13 +72,14 @@ def _on_press(self, key:keyboard.KeyCode | keyboard.Key): elif key == "space" and not self._keyheld: self._single = True elif key == "tab": - self._print_transit() + self.print_transit() elif key == "s": self._pick = True elif key == "e": self.swap_emulator() self._keyheld = True + #listen for released keys def _on_release(self, key:keyboard.KeyCode | keyboard.Key): try: #for key class @@ -118,7 +91,8 @@ def _on_release(self, key:keyboard.KeyCode | keyboard.Key): self._stepping = True self._keyheld = False - def _print_transit(self): + #print all messages in transit + def print_transit(self): print("Messages in transit:") if self.parent is AsyncEmulator: for messages in self._messages.values(): @@ -129,7 +103,8 @@ def _print_transit(self): for message in messages: print(f'{message}') - def _print_transit_for_device(self, device): + #print all messages in transit to specified device + def print_transit_for_device(self, device): print(f'Messages in transit to device #{device}') index = 0 if self.parent is AsyncEmulator: @@ -140,10 +115,41 @@ def _print_transit_for_device(self, device): print(f'{index}: {message}') index+=1 + #swap between which parent class the program will run in between deliveries def swap_emulator(self): if self.parent is AsyncEmulator: self.parent = SyncEmulator elif self.parent is SyncEmulator: self.parent = AsyncEmulator print(f'Changed emulator to {self.parent.__name__}') - \ No newline at end of file + + #Pick command function, this lets the user alter the queue for a specific device + def pick(self): + try: + print("Press return to proceed") #prompt the user to kill the stepper daemon + while self._stepper.is_alive(): #wait for the stepper to be killed + pass + self.print_transit() + keys = [] + if self.parent is AsyncEmulator: + for key in self._messages.keys(): + keys.append(key) + elif self.parent is SyncEmulator: + for key in self._last_round_messages.keys(): + keys.append(key) + print(f'Available devices: {keys}') + device = int(input(f'Specify device to send to: ')) #ask for user input to specify which device queue to alter + self.print_transit_for_device(device) + index = int(input(f'Specify index of the next element to send: ')) #ask for user input to specify a message to send + if self.parent is AsyncEmulator: + self._messages[device].append(self._messages[device].pop(index)) #pop index from input and append to the end of the list + elif self.parent is SyncEmulator: + self._last_round_messages[device].append(self._last_round_messages[device].pop(index)) #pop index from input and append to the end of the list + except Exception as e: + print(e) + if not self._stepper.is_alive(): + self._stepper = Thread(target=lambda: getpass(""), daemon=True) #restart stepper daemon + self._stepper.start() + self._stepping = True + print(f'\t{self._messages_sent}: Step?') + From 3c4eb9de18d0abde95d89898a70f2194e930b578 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sun, 3 Jul 2022 15:40:39 +0200 Subject: [PATCH 035/106] finished creating a GUI version of the pick command, might wanna make a gui.isFocused method to avoid the keyboard listener to fire in the GUI --- emulators/SteppingEmulator.py | 3 +- emulators/exercise_overlay.py | 62 ++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 9593e9b..a3984c3 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -17,7 +17,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._single = False self._list_messages_received:list[MessageStub] = list() self._list_messages_sent:list[MessageStub] = list() - self._last_message:tuple[str, MessageStub] = ("init") #type(received or sent), message + self._last_message:tuple[str, MessageStub] = tuple() #type(received or sent), message self._keyheld = False self._pick = False self.parent = AsyncEmulator @@ -159,4 +159,3 @@ def pick(self): self._stepper.start() self._stepping = True print(f'\t{self._messages_sent}: Step?') - diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index f85de20..d11a1ea 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -2,9 +2,11 @@ from threading import Thread import tkinter as TK import tkinter.ttk as TTK +from emulators.AsyncEmulator import AsyncEmulator from emulators.MessageStub import MessageStub from emulators.SteppingEmulator import SteppingEmulator +from emulators.SyncEmulator import SyncEmulator from emulators.table import table def overlay(emulator:SteppingEmulator, run_function): @@ -124,7 +126,7 @@ def step(): bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") Thread(target=stop_emulator).start() - elif emulator._last_message[0] != "init": + elif emulator._last_message != tuple(): message = emulator._last_message[1] canvas.delete("line") canvas.create_line(coordinates[message.source][0]+(device_size/2), coordinates[message.source][1]+(device_size/2), coordinates[message.destination][0]+(device_size/2), coordinates[message.destination][1]+(device_size/2), tags="line") @@ -153,11 +155,69 @@ def end(): build_device(canvas, device, x, y, device_size) coordinates.append((x,y)) + def swap_emulator(button:TTK.Button): + emulator.swap_emulator() + button.configure(text=emulator.parent.__name__) + + def show_queue(): + pass + + def pick_gui(): + def execute(): + device = int(device_entry.get()) + index = int(message_entry.get()) + if emulator.parent is AsyncEmulator: + emulator._messages[device].append(emulator._messages[device].pop(index)) + elif emulator.parent is SyncEmulator: + emulator._last_round_messages[device].append(emulator._last_round_messages[device].pop(index)) + window.destroy() + + emulator.print_transit() + keys = [] + if emulator.parent is AsyncEmulator: + messages = emulator._messages + else: + messages = emulator._last_round_messages + for item in messages.items(): + keys.append(item[0]) + keys.sort() + window = TK.Toplevel(main_page) + header = TTK.Frame(window) + header.pack(side=TK.TOP) + TTK.Label(header, text=f'Pick a message to be transmitted next').pack(side=TK.LEFT) + max_size = 0 + for m in messages.values(): + if len(m) > max_size: + max_size = len(m) + content = [[messages[key][i] if len(messages[key]) > i else " " for key in keys] for i in range(max_size)] + content.insert(0, keys) + content[0].insert(0, "Message #") + for i in range(max_size): + content[i+1].insert(0, i) + tab = table(window, content, width=15, scrollable="y", title="Pick a message to be transmitted next") + tab.pack(side=TK.TOP) + footer = TTK.Frame(window) + footer.pack(side=TK.BOTTOM) + device_frame = TTK.Frame(footer) + device_frame.pack(side=TK.TOP) + TTK.Button(footer, text="Confirm", command=execute).pack(side=TK.BOTTOM) + message_frame = TTK.Frame(footer) + message_frame.pack(side=TK.BOTTOM) + TTK.Label(device_frame, text="Device: ").pack(side=TK.LEFT) + device_entry = TTK.Entry(device_frame) + device_entry.pack(side=TK.RIGHT) + TTK.Label(message_frame, text="Message: ").pack(side=TK.LEFT) + message_entry = TTK.Entry(message_frame) + message_entry.pack(side=TK.RIGHT) bottom_frame = TK.LabelFrame(main_page, text="Inputs") bottom_frame.pack(side=TK.BOTTOM) TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) + emulator_button = TTK.Button(bottom_frame, text=emulator.parent.__name__, command=lambda: swap_emulator(emulator_button)) + emulator_button.pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="Message queue", command=show_queue).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="Pick", command=pick_gui).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) bottom_label = TK.Label(main_page, text="Status") From 05389e455d9423c1b01cc3ec5a0fd71a6cfa2980 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Mon, 4 Jul 2022 12:34:21 +0200 Subject: [PATCH 036/106] added gui queue command --- emulators/exercise_overlay.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index d11a1ea..e5e72c7 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -160,7 +160,18 @@ def swap_emulator(button:TTK.Button): button.configure(text=emulator.parent.__name__) def show_queue(): - pass + window = TK.Toplevel(main_page) + content = [["Source", "Destination", "Message"]] + if emulator.parent is AsyncEmulator: + queue = emulator._messages.values() + else: + queue = emulator._last_round_messages.values() + for messages in queue: + for message in messages: + message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") + content.append([message.source, message.destination, message_stripped]) + tab = table(window, content, width=15, scrollable="y", title="Message queue") + tab.pack(side=TK.LEFT) def pick_gui(): def execute(): @@ -190,7 +201,8 @@ def execute(): if len(m) > max_size: max_size = len(m) content = [[messages[key][i] if len(messages[key]) > i else " " for key in keys] for i in range(max_size)] - content.insert(0, keys) + head = [f'Device {key}' for key in keys] + content.insert(0, head) content[0].insert(0, "Message #") for i in range(max_size): content[i+1].insert(0, i) @@ -219,7 +231,7 @@ def execute(): TTK.Button(bottom_frame, text="Message queue", command=show_queue).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Pick", command=pick_gui).pack(side=TK.LEFT) TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) - TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) + TTK.Button(bottom_frame, text="Delivered messages", command=show_all_data).pack(side=TK.LEFT) bottom_label = TK.Label(main_page, text="Status") bottom_label.pack(side=TK.BOTTOM) @@ -238,3 +250,9 @@ def execute(): TTK.Label(value_frame, text="Fast-forward through messages").pack(side=TK.BOTTOM, anchor=TK.NW) TTK.Label(name_frame, text="Enter", width=15).pack(side=TK.TOP) TTK.Label(value_frame, text="Kill stepper daemon and run as an async emulator").pack(side=TK.BOTTOM, anchor=TK.NW) + TTK.Label(name_frame, text="tab", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Show all messages currently waiting to be transmitted").pack(side=TK.BOTTOM, anchor=TK.NW) + TTK.Label(name_frame, text="s", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Pick the next message waiting to be transmitteed to transmit next").pack(side=TK.BOTTOM, anchor=TK.NW) + TTK.Label(name_frame, text="e", width=15).pack(side=TK.TOP) + TTK.Label(value_frame, text="Toggle between sync and async emulation").pack(side=TK.BOTTOM, anchor=TK.NW) \ No newline at end of file From e512da19ac9fbc883d0e64720e3348ebe2002753 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Fri, 15 Jul 2022 21:51:56 +0200 Subject: [PATCH 037/106] rewrote the GUI in PyQt6, need to test it in windows, so i'm taking down the PR --- emulators/exercise_overlay.py | 346 +++++++++++++++++----------------- emulators/table.py | 135 ++++--------- exercise_runner.py | 7 +- exercise_runner_overlay.py | 76 ++++---- icon.ico | Bin 0 -> 132141 bytes 5 files changed, 243 insertions(+), 321 deletions(-) create mode 100644 icon.ico diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index f85de20..21ce187 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,180 +1,176 @@ -from math import sin, cos, pi +from random import randint from threading import Thread -import tkinter as TK -import tkinter.ttk as TTK +from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt +from sys import argv +from math import cos, sin, pi from emulators.MessageStub import MessageStub +from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator -from emulators.table import table -def overlay(emulator:SteppingEmulator, run_function): - #master config - master = TK.Toplevel() - master.resizable(False,False) - master.title("Stepping algorithm") - pages = TTK.Notebook(master) - main_page = TTK.Frame(pages) - controls_page = TTK.Frame(pages) - pages.add(main_page, text="Emulator") - pages.add(controls_page, text="Controls") - pages.pack(expand= 1, fill=TK.BOTH) - height = 500 - width = 500 - spacing = 10 - #end of master config - - - def show_all_data(): - window = TK.Toplevel() - content:list[list] = [] - messages = emulator._list_messages_sent - message_content = list() - for message in messages: - temp = str(message) - temp = temp.replace(f'{message.source} -> {message.destination} : ', "") - temp = temp.replace(f'{message.source}->{message.destination} : ', "") - message_content.append(temp) - - content = [[messages[i].source, messages[i].destination, message_content[i], i] for i in range(len(messages))] - - - tab = table(window, content, width=15, scrollable="y", title="All messages") - tab.pack(side=TK.BOTTOM) - - header = TK.Frame(window) - header.pack(side=TK.TOP, anchor=TK.NW) - TK.Label(header, text="Source", width=tab.column_width[0]).pack(side=TK.LEFT) - TK.Label(header, text="Destination", width=tab.column_width[1]).pack(side=TK.LEFT) - TK.Label(header, text="Message", width=tab.column_width[2]).pack(side=TK.LEFT) - TK.Label(header, text="Sequence number", width=tab.column_width[3]).pack(side=TK.LEFT) - - def show_data(device_id): - def _show_data(): - if len(emulator._list_messages_received) > 0: - window = TK.Toplevel(main_page) - window.title(f'Device {device_id}') - received:list[MessageStub] = list() - sent:list[MessageStub] = list() - for message in emulator._list_messages_received: - if message.destination == device_id: - received.append(message) - if message.source == device_id: - sent.append(message) - if len(received) > len(sent): - for _ in range(len(received)-len(sent)): - sent.append("") - elif len(sent) > len(received): - for _ in range(len(sent) - len(received)): - received.append("") - content = list() - for i in range(len(received)): - if received[i] == "": - msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - content.append(["", received[i], sent[i].destination, msg]) - elif sent[i] == "": - msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([received[i].source, msg, "", sent[i]]) - else: - sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([received[i].source, received_msg, sent[i].destination, sent_msg]) - - tab = table(window, content, width=15, scrollable="y", title=f'Device {device_id}') - tab.pack(side=TK.BOTTOM) - - header = TK.Frame(window) - header.pack(side=TK.TOP, anchor=TK.NW) - TK.Label(header, text="Source", width=tab.column_width[0]).pack(side=TK.LEFT) - TK.Label(header, text="Message", width=tab.column_width[1]).pack(side=TK.LEFT) - TK.Label(header, text="Destination", width=tab.column_width[2]).pack(side=TK.LEFT) - TK.Label(header, text="Message", width=tab.column_width[3]).pack(side=TK.LEFT) - else: - return - - return _show_data - - def get_coordinates_from_index(center:tuple[int,int], r:int, i:int, n:int) -> tuple[int, int]: - x = sin((i*2*pi)/n) - y = cos((i*2*pi)/n) - if x < pi: - return int(center[0]-(r*x)), int(center[1]-(r*y)) - else: - return int(center[0]-(r*-x)), int(center[1]-(r*y)) - - def build_device(main_page:TK.Canvas, device_id, x, y, device_size): - canvas.create_oval(x, y, x+device_size, y+device_size, outline="black", fill="gray") - frame = TTK.Frame(main_page) - frame.place(x=x+(device_size/8), y=y+(device_size/4)) - TTK.Label(frame, text=f'Device #{device_id}').pack(side=TK.TOP) - TTK.Button(frame, command=show_data(device_id), text="Show data").pack(side=TK.TOP) - data_frame = TTK.Frame(frame) - data_frame.pack(side=TK.BOTTOM) - - - - def stop_emulator(): - emulator.listener.stop() - emulator.listener.join() - bottom_label.config(text="Finished running", fg="green") - - def step(): - #insert stepper function - emulator._single = True - if emulator.all_terminated(): - bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") - Thread(target=stop_emulator).start() - - elif emulator._last_message[0] != "init": - message = emulator._last_message[1] - canvas.delete("line") - canvas.create_line(coordinates[message.source][0]+(device_size/2), coordinates[message.source][1]+(device_size/2), coordinates[message.destination][0]+(device_size/2), coordinates[message.destination][1]+(device_size/2), tags="line") - msg = str(message) - msg = msg.replace(f'{message.source} -> {message.destination} : ', "") - msg = msg.replace(f'{message.source}->{message.destination} : ', "") - if emulator._last_message[0] == "sent": - bottom_label.config(text=f'Device {message.source} sent "{msg}" to {message.destination}') - elif emulator._last_message[0] == "received": - bottom_label.config(text=f'Device {message.destination} received {msg} from {message.source}') - def end(): - emulator._stepping = False - while not emulator.all_terminated(): - pass - bottom_label.config(text="Finished running, kill keyboard listener?... (press any key)") - Thread(target=stop_emulator).start() - - #Emulator page stuff - canvas = TK.Canvas(main_page, height=height, width=width) - canvas.pack(side=TK.TOP) - device_size = 100 - canvas.create_line(0,0,0,0, tags="line") #create dummy lines - coordinates:list[tuple[TTK.Label]] = list() - for device in range(len(emulator._devices)): - x, y = get_coordinates_from_index((int((width/2)-(device_size/2)), int((width/2)-(device_size/2))), (int((width/2)-(device_size/2)))-spacing, device, len(emulator._devices)) - build_device(canvas, device, x, y, device_size) - coordinates.append((x,y)) - - - bottom_frame = TK.LabelFrame(main_page, text="Inputs") - bottom_frame.pack(side=TK.BOTTOM) - TTK.Button(bottom_frame, text="Step", command=step).pack(side=TK.LEFT) - TTK.Button(bottom_frame, text="End", command=end).pack(side=TK.LEFT) - TTK.Button(bottom_frame, text="Restart algorithm", command=run_function).pack(side=TK.LEFT) - TTK.Button(bottom_frame, text="show all Messages", command=show_all_data).pack(side=TK.LEFT) - bottom_label = TK.Label(main_page, text="Status") - bottom_label.pack(side=TK.BOTTOM) - - #controls page stuff - - TTK.Label(controls_page, text="Controls", font=("Arial", 25)).pack(side=TK.TOP) - controls_frame = TTK.Frame(controls_page) - controls_frame.pack(side=TK.TOP) - name_frame = TTK.Frame(controls_frame) - name_frame.pack(side=TK.LEFT) - value_frame = TTK.Frame(controls_frame) - value_frame.pack(side=TK.RIGHT) - TTK.Label(name_frame, text="Space", width=15).pack(side=TK.TOP) - TTK.Label(value_frame, text="Step a single time through messages").pack(side=TK.BOTTOM, anchor=TK.NW) - TTK.Label(name_frame, text="f", width=15).pack(side=TK.TOP) - TTK.Label(value_frame, text="Fast-forward through messages").pack(side=TK.BOTTOM, anchor=TK.NW) - TTK.Label(name_frame, text="Enter", width=15).pack(side=TK.TOP) - TTK.Label(value_frame, text="Kill stepper daemon and run as an async emulator").pack(side=TK.BOTTOM, anchor=TK.NW) +def circle_button_style(size): + return f''' + QPushButton {{ + background-color: transparent; + border-style: solid; + border-width: 2px; + border-radius: {int(size/2)}px; + border-color: black; + max-width: {size}px; + max-height: {size}px; + min-width: {size}px; + min-height: {size}px; + }} + QPushButton:hover {{ + background-color: gray; + border-width: 2px; + }} + QPushButton:pressed {{ + background-color: transparent; + border-width: 1px + }} + ''' + +class Window(QWidget): + h = 600 + w = 600 + device_size = 80 + windows = list() + + def __init__(self, elements, restart_function, emulator:SteppingEmulator): + super().__init__() + self.emulator = emulator + self.setFixedSize(self.w, self.h) + layout = QVBoxLayout() + tabs = QTabWidget() + tabs.addTab(self.main(elements, restart_function), 'Main') + tabs.addTab(self.controls(), 'controls') + layout.addWidget(tabs) + self.setLayout(layout) + self.setWindowTitle("Test") + self.setWindowIcon(QIcon("icon.ico")) + + def coordinates(self, center, r, i, n): + x = sin((i*2*pi)/n) + y = cos((i*2*pi)/n) + if x < pi: + return int(center[0] - (r*x)), int(center[1] - (r*y)) + else: + return int(center[0] - (r*-x)), int(center[1] - (r*y)) + + def show_device_data(self, device_id): + def show(): + received:list[MessageStub] = list() + sent:list[MessageStub] = list() + for message in self.emulator._list_messages_received: + if message.destination == device_id: + received.append(message) + if message.source == device_id: + sent.append(message) + if len(received) > len(sent): + for _ in range(len(received)-len(sent)): + sent.append("") + elif len(sent) > len(received): + for _ in range(len(sent)-len(received)): + received.append("") + content = list() + for i in range(len(received)): + if received[i] == "": + msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + content.append(["", received[i], str(sent[i].destination), msg]) + elif sent[i] == "": + msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([str(received[i].source), msg, "", sent[i]]) + else: + sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([str(received[i].source), received_msg, str(sent[i].destination), sent_msg]) + content.insert(0, ['Source', 'Message', 'Destination', 'Message']) + table = Table(content, title=f'Device #{device_id}') + self.windows.append(table) + table.setFixedSize(300, 500) + table.show() + return table + return show + + def show_all_data(self): + content = [] + messages = self.emulator._list_messages_sent + message_content = [] + for message in messages: + temp = str(message) + temp = temp.replace(f'{message.source} -> {message.destination} : ', "") + temp = temp.replace(f'{message.source}->{message.destination} : ', "") + message_content.append(temp) + + content = [[str(messages[i].source), str(messages[i].destination), message_content[i], str(i)] for i in range(len(messages))] + content.insert(0, ['Source', 'Destination', 'Message', 'Sequence number']) + table = Table(content, title=f'All data') + self.windows.append(table) + table.setFixedSize(500, 500) + table.show() + return table + + def stop_stepper(self): + self.emulator.listener.stop() + self.emulator.listener.join() + + def end(self): + self.emulator._stepping = False + while not self.emulator.all_terminated(): + pass + Thread(target=self.stop_stepper, daemon=True).start() + + def step(self): + self.emulator._single = True + if self.emulator.all_terminated(): + Thread(target=self.stop_stepper, daemon=True).start() + + def main(self, num_devices, restart_function): + main_tab = QWidget() + layout = QVBoxLayout() + device_area = QWidget() + device_area.setFixedSize(500, 500) + layout.addWidget(device_area) + main_tab.setLayout(layout) + for i in range(num_devices): + x, y = self.coordinates((device_area.width()/2, device_area.height()/2), (device_area.height()/2) - (self.device_size/2), i, num_devices) + button = QPushButton(f'Device #{i}', main_tab) + button.resize(self.device_size, self.device_size) + button.setStyleSheet(circle_button_style(self.device_size)) + button.move(x, int(y - (self.device_size/2))) + button.clicked.connect(self.show_device_data(i)) + + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': restart_function, 'Show all messages': self.show_all_data} + inner_layout = QHBoxLayout() + for action in button_actions.items(): + button = QPushButton(action[0]) + button.clicked.connect(action[1]) + inner_layout.addWidget(button) + layout.addLayout(inner_layout) + + return main_tab + + def controls(self): + controls_tab = QWidget() + content = {'Space': 'Step a single time through messages', 'f': 'Fast forward through messages', 'Enter': 'Kill stepper daemon and run as an async emulator'} + main = QVBoxLayout() + main.setAlignment(Qt.AlignmentFlag.AlignTop) + controls = QLabel("Controls") + controls.setAlignment(Qt.AlignmentFlag.AlignCenter) + main.addWidget(controls) + for item in content.items(): + inner = QHBoxLayout() + inner.addWidget(QLabel(item[0])) + inner.addWidget(QLabel(item[1])) + main.addLayout(inner) + controls_tab.setLayout(main) + return controls_tab + +if __name__ == "__main__": + app = QApplication(argv) + window = Window(argv[1] if len(argv) > 1 else 10, lambda: print("Restart function")) + + app.exec() \ No newline at end of file diff --git a/emulators/table.py b/emulators/table.py index a4cdf09..ed63fbb 100644 --- a/emulators/table.py +++ b/emulators/table.py @@ -1,102 +1,33 @@ -import tkinter as TK - -class table(TK.Frame): - rows:list[TK.Frame] - labels:list[list[TK.Label]] - master:TK.Toplevel - column_width:list[int] - - def __init__(self, master = None, content:list[list]=[], width=2, height=2, title="", icon="", scrollable = "None"): - if not len(content) > 0 or not len(content[0]) > 0: - print("failed to make table: bad list") - return - - if master == None: - master = TK.Toplevel() - self.master = master - TK.Frame.__init__(self, self.master) - if type(self.master) is TK.Toplevel: - self.master.resizable(False,False) - self.master.title(title) - if not icon == "": - try: - self.master.iconbitmap(icon) - except: - print("failed to set window icon") - rows = len(content) - cols = len(content[0]) - self.column_width = [width for _ in range(cols)] - for j in range(cols): - for i in range(rows): - if type(content[i][j]) == str: - if len(content[i][j]) > self.column_width[j]: - self.column_width[j] = len(content[i][j]) - elif width > self.column_width[j]: - self.column_width[j] = width - - if scrollable == "None": - container = self - elif scrollable == "x": - if len(content) > 10: - mod = 10 - else: - mod = len(content) - __container = ScrollableFrame_X(self, width=sum(self.column_width), height=height*mod*20) - container = __container.scrollable_frame - __container.pack(side=TK.LEFT) - elif scrollable == "y": - if len(content) > 10: - mod = 10 - else: - mod = len(content) - __container = ScrollableFrame_Y(self, width=sum(self.column_width)*7.5, height=height*mod*20) - container = __container.scrollable_frame - __container.pack(side=TK.LEFT) - self.rows = [TK.LabelFrame(container) for i in range(rows)] - [self.rows[i].pack(side=TK.TOP) for i in range(len(self.rows))] - for j in range(cols): - for i in range(rows): - TK.Label(self.rows[i], text=content[i][j], width=self.column_width[j], height=height).pack(side=TK.LEFT) -class ScrollableFrame_Y(TK.Frame): - ###source: https://blog.teclado.com/tkinter-scrollable-frames/ - def __init__(self, container, width=300, height=300, *args, **kwargs): - super().__init__(container, *args, **kwargs) - canvas = TK.Canvas(self) - scrollbar = TK.Scrollbar(self, orient="vertical", command=canvas.yview) - self.scrollable_frame = TK.Frame(canvas) - - self.scrollable_frame.bind( - "", - lambda e: canvas.configure( - scrollregion=canvas.bbox("all") - ) - ) - - canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") - - canvas.configure(yscrollcommand=scrollbar.set, width=width, height=height) - - canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - -class ScrollableFrame_X(TK.Frame): - ###source: https://blog.teclado.com/tkinter-scrollable-frames/ - def __init__(self, container, width=300, height=300, *args, **kwargs): - super().__init__(container, *args, **kwargs) - canvas = TK.Canvas(self) - scrollbar = TK.Scrollbar(self, orient="horizontal", command=canvas.xview) - self.scrollable_frame = TK.Frame(canvas) - - self.scrollable_frame.bind( - "", - lambda e: canvas.configure( - scrollregion=canvas.bbox("all") - ) - ) - - canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") - - canvas.configure(xscrollcommand=scrollbar.set, width=width, height=height) - - canvas.pack(side="top", fill="both", expand=True) - scrollbar.pack(side="bottom", fill="x") \ No newline at end of file +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QScrollArea +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt + +class Table(QScrollArea): + def __init__(self, content:list[list[str]], title="table"): + super().__init__() + widget = QWidget() + self.setWindowIcon(QIcon('icon.ico')) + self.setWindowTitle(title) + columns = QVBoxLayout() + columns.setAlignment(Qt.AlignmentFlag.AlignTop) + for row in content: + column = QHBoxLayout() + for element in row: + label = QLabel(element) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + column.addWidget(label) + columns.addLayout(column) + widget.setLayout(columns) + self.setWidgetResizable(True) + self.setWidget(widget) + + + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + from sys import argv + app = QApplication(argv) + table = Table([[str(i+j) for i in range(5)] for j in range(5)]) + table.show() + app.exec() + \ No newline at end of file diff --git a/exercise_runner.py b/exercise_runner.py index 846012a..ae33102 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -1,7 +1,7 @@ import argparse import inspect from threading import Thread -from emulators.exercise_overlay import overlay +from emulators.exercise_overlay import Window import exercises.exercise1 import exercises.exercise2 @@ -19,7 +19,6 @@ from emulators.SyncEmulator import SyncEmulator from emulators.SteppingEmulator import SteppingEmulator - def fetch_alg(lecture: str, algorithm: str): if '.' in algorithm or ';' in algorithm: raise ValueError(f'"." and ";" are not allowed as names of solutions.') @@ -63,7 +62,9 @@ def run_instance(): f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') Thread(target=run_instance).start() if isinstance(instance, SteppingEmulator): - overlay(instance, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices)) + window = Window(number_of_devices, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices), instance) + window.show() + return window if __name__ == "__main__": parser = argparse.ArgumentParser(description='For exercises in Distributed Systems.') diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 8c9eca8..96a74a0 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,42 +1,36 @@ -import tkinter as TK -import tkinter.ttk as TTK from exercise_runner import run_exercise - -def input_builder(master, title:str, entry_content:str): - frame = TK.Frame(master) - text = TTK.Label(frame, text=title) - entry = TTK.Entry(frame) - entry.insert(TK.END, entry_content) - frame.pack(side=TK.LEFT) - text.pack(side=TK.TOP) - entry.pack(side=TK.BOTTOM) - return frame, entry - -def run(): - lecture = int(lecture_entry.get()) - algorithm = algorithm_entry.get() - type = type_entry.get() - devices = int(devices_entry.get()) - print("running exercise") - print(f'with data: {lecture, algorithm, type, devices}') - run_exercise(lecture, algorithm, type, devices) - - -master = TK.Tk() - - -input_area = TK.LabelFrame(text="Input") -input_area.pack(side=TK.BOTTOM) - -lecture_frame, lecture_entry = input_builder(input_area, "Lecture", 0) -algorithm_frame, algorithm_entry = input_builder(input_area, "Algorithm", "PingPong") -type_frame, type_entry = input_builder(input_area, "Type", "stepping") -devices_frame, devices_entry = input_builder(input_area, "Devices", 3) - -start_button = TTK.Button(master, text="Start", command=run) -start_button.pack(side=TK.TOP) - -master.resizable(False,False) - -master.title("Distributed Exercises AAU") -master.mainloop() \ No newline at end of file +from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QPushButton, QHBoxLayout, QLabel +from PyQt6.QtGui import QIcon +from sys import argv +app = QApplication(argv) + +windows = list() + + +window = QWidget() +window.setWindowIcon(QIcon('icon.ico')) +main = QVBoxLayout() +window.setFixedSize(500, 100) +start_button = QPushButton("start") +main.addWidget(start_button) +input_area_labels = QHBoxLayout() +input_area_areas = QHBoxLayout() +actions = {'Lecture':[print, 0], 'Algorithm': [print, 'PingPong'], 'Type': [print, 'stepping'], 'Devices': [print, 3]} + +for action in actions.items(): + input_area_labels.addWidget(QLabel(action[0])) + field = QLineEdit() + input_area_areas.addWidget(field) + field.setText(str(action[1][1])) + actions[action[0]][0] = field.text +main.addLayout(input_area_labels) +main.addLayout(input_area_areas) + + +def start_exercise(): + windows.append(run_exercise(int(actions['Lecture'][0]()), actions['Algorithm'][0](), actions['Type'][0](), int(actions['Devices'][0]()))) + +start_button.clicked.connect(start_exercise) +window.setLayout(main) +window.show() +app.exec() diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b138c53e753fe59997cc94a11c77bef769703724 GIT binary patch literal 132141 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%0D98o$j&of?Uhy1j~ud`^2wEcj( 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 literal 0 HcmV?d00001 From 1bcb38131908c154cd051f55c0911886496ef6dd Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 16 Jul 2022 01:44:34 +0200 Subject: [PATCH 038/106] forgot lines, changed it to colors to show it better --- emulators/SteppingEmulator.py | 3 +++ emulators/exercise_overlay.py | 49 +++++++++++++++++++++++++++++++---- exercise_runner_overlay.py | 3 ++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index e8d68d4..e5c56ba 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -16,6 +16,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._stepper.start() self._stepping = True self._single = False + self.last_action = "" self._list_messages_received:list[MessageStub] = list() self._list_messages_sent:list[MessageStub] = list() self._last_message:tuple[str, MessageStub] = ("init") #type(received or sent), message @@ -43,6 +44,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: else: if self._stepping and self._stepper.is_alive(): #first expression for printing a reasonable amount, second to hide user input self._step("step?") + self.last_action = "receive" m = self._messages[index].pop() print(f'\tRecieve {m}') self._list_messages_received.append(m) @@ -55,6 +57,7 @@ def queue(self, message: MessageStub): self._progress.acquire() if self._stepping and self._stepper.is_alive(): self._step("step?") + self.last_action = "send" self._messages_sent += 1 print(f'\tSend {message}') self._list_messages_sent.append(message) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 21ce187..7a6a89b 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,5 +1,6 @@ from random import randint from threading import Thread +from time import sleep from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt @@ -10,14 +11,14 @@ from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator -def circle_button_style(size): +def circle_button_style(size, color = "black"): return f''' QPushButton {{ background-color: transparent; border-style: solid; border-width: 2px; border-radius: {int(size/2)}px; - border-color: black; + border-color: {color}; max-width: {size}px; max-height: {size}px; min-width: {size}px; @@ -37,6 +38,8 @@ class Window(QWidget): h = 600 w = 600 device_size = 80 + last_message = None + buttons:dict[int, QPushButton] = {} windows = list() def __init__(self, elements, restart_function, emulator:SteppingEmulator): @@ -49,7 +52,7 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): tabs.addTab(self.controls(), 'controls') layout.addWidget(tabs) self.setLayout(layout) - self.setWindowTitle("Test") + self.setWindowTitle("Stepping Emulator") self.setWindowIcon(QIcon("icon.ico")) def coordinates(self, center, r, i, n): @@ -120,16 +123,47 @@ def stop_stepper(self): def end(self): self.emulator._stepping = False while not self.emulator.all_terminated(): - pass + self.set_device_color() Thread(target=self.stop_stepper, daemon=True).start() + def set_device_color(self): + sleep(.1) + messages = self.emulator._list_messages_sent if self.emulator.last_action == "send" else self.emulator._list_messages_received + if len(messages) != 0: + last_message = messages[len(messages)-1] + if not last_message == self.last_message: + for button in self.buttons.values(): + button.setStyleSheet(circle_button_style(self.device_size)) + if last_message.source == last_message.destination: + self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'yellow')) + else: + self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'green')) + self.buttons[last_message.destination].setStyleSheet(circle_button_style(self.device_size, 'red')) + self.last_message = last_message + def step(self): self.emulator._single = True if self.emulator.all_terminated(): Thread(target=self.stop_stepper, daemon=True).start() + self.set_device_color() + + def restart_algorithm(self, function): + self.windows.append(function()) def main(self, num_devices, restart_function): main_tab = QWidget() + green = QLabel("green: source", main_tab) + green.setStyleSheet("color: green;") + green.move(5, 0) + green.show() + red = QLabel("red: destination", main_tab) + red.setStyleSheet("color: red;") + red.move(5, 20) + red.show() + yellow = QLabel("yellow: same device", main_tab) + yellow.setStyleSheet("color: yellow;") + yellow.move(5, 40) + yellow.show() layout = QVBoxLayout() device_area = QWidget() device_area.setFixedSize(500, 500) @@ -142,8 +176,9 @@ def main(self, num_devices, restart_function): button.setStyleSheet(circle_button_style(self.device_size)) button.move(x, int(y - (self.device_size/2))) button.clicked.connect(self.show_device_data(i)) + self.buttons[i] = button - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': restart_function, 'Show all messages': self.show_all_data} + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data} inner_layout = QHBoxLayout() for action in button_actions.items(): button = QPushButton(action[0]) @@ -168,6 +203,10 @@ def controls(self): main.addLayout(inner) controls_tab.setLayout(main) return controls_tab + + def closeEvent(self, event): + Thread(target=self.end).start() + event.accept() if __name__ == "__main__": app = QApplication(argv) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 96a74a0..d44b312 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -9,8 +9,9 @@ window = QWidget() window.setWindowIcon(QIcon('icon.ico')) +window.setWindowTitle("Distributed Exercises AAU") main = QVBoxLayout() -window.setFixedSize(500, 100) +window.setFixedSize(600, 100) start_button = QPushButton("start") main.addWidget(start_button) input_area_labels = QHBoxLayout() From 91ff3594f2102cbf9e5a5560d2a8526d44a4a1a6 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 16 Jul 2022 01:51:48 +0200 Subject: [PATCH 039/106] updated readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f4cf217..1edf200 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,15 @@ This repository contains a small framework written in python for emulating *asynchronous* and *synchronous* distributed systems. ## Install The stepping emulator requires the following packages to run -* tkinter +* PyQt6 * pynput +* cryptography These packages can be installed using `pip` as shown below: ```bash -pip3.10 install --user tk +pip3.10 install --user pyqt6 pip3.10 install --user pynput -``` -For the development environment used for this framework (Arch Linux), tkinter also requires the following package installed via `pacman`: -```bash -pacman -Sy tk +pip3.10 install --user cryptography ``` Installation steps may vary depending on the operating system, for windows, installing tkinter through `pip` should be enough. From 5d0b31103e51c9f3ac74b622b59476a0e96bf55d Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 16 Jul 2022 01:55:53 +0200 Subject: [PATCH 040/106] updated figure --- README.md | 2 +- figures/main_window.png | Bin 41942 -> 0 bytes figures/stepping_gui.png | Bin 0 -> 320450 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 figures/main_window.png create mode 100644 figures/stepping_gui.png diff --git a/README.md b/README.md index 1edf200..743262a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Where your solution can be executed through this GUI. ### Stepping emulator GUI If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your algorithm, an example of the Stepping GUI is shown below: -![](figures/main_window.png) +![](figures/stepping_gui.png) ## Pull Requests diff --git a/figures/main_window.png b/figures/main_window.png deleted file mode 100644 index 81b5cba3cf76a0f6ff66eab93d503d4af6e342a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41942 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

wO`RgE$$_t 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

nA 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

w`3)1GQj^JeG|djfx^>cj}B}U*;QpNphn_2BeKb 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 literal 0 HcmV?d00001 From 4bba26b70f258b749e4d09f7a4d47bc70e288366 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 16 Jul 2022 03:12:48 +0200 Subject: [PATCH 041/106] updated controls --- emulators/AsyncEmulator.py | 22 ++++--- emulators/SteppingEmulator.py | 6 +- emulators/SyncEmulator.py | 22 ++++--- emulators/exercise_overlay.py | 104 +++++++++++++++++++++++++++++++--- 4 files changed, 126 insertions(+), 28 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index e3dc88f..52e31f2 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -34,8 +34,9 @@ def run(self): t.join() return - def queue(self, message: MessageStub): - self._progress.acquire() + def queue(self, message: MessageStub, stepper=False): + if not stepper: + self._progress.acquire() self._messages_sent += 1 print(f'\tSend {message}') if message.destination not in self._messages: @@ -43,20 +44,25 @@ def queue(self, message: MessageStub): self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing random.shuffle(self._messages[message.destination]) # shuffle to emulate changes in order time.sleep(random.uniform(0.01, 0.1)) # try to obfuscate delays and emulate network delays - self._progress.release() + if not stepper: + self._progress.release() - def dequeue(self, index: int) -> Optional[MessageStub]: - self._progress.acquire() + def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: + if not stepper: + self._progress.acquire() if index not in self._messages: - self._progress.release() + if not stepper: + self._progress.release() return None elif len(self._messages[index]) == 0: - self._progress.release() + if not stepper: + self._progress.release() return None else: m = self._messages[index].pop() print(f'\tRecieve {m}') - self._progress.release() + if not stepper: + self._progress.release() return m def done(self, index: int): diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index df27b8d..e9ba379 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -36,8 +36,8 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - result = self.parent.dequeue(self, index) self._progress.acquire() + result = self.parent.dequeue(self, index, True) if result != None and self._stepping and self._stepper.is_alive(): self.step() self._list_messages_received.append(result) @@ -53,9 +53,9 @@ def queue(self, message: MessageStub): self.last_action = "send" self._list_messages_sent.append(message) self._last_message = ("sent", message) - self._progress.release() - return self.parent.queue(self, message) + self.parent.queue(self, message, True) + self._progress.release() #the main program to stop execution def step(self): diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index d63d629..4783855 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -62,27 +62,33 @@ def run(self): t.join() return - def queue(self, message: MessageStub): - self._progress.acquire() + def queue(self, message: MessageStub, stepper=False): + if not stepper: + self._progress.acquire() self._messages_sent += 1 print(f'\tSend {message}') if message.destination not in self._current_round_messages: self._current_round_messages[message.destination] = [] self._current_round_messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing - self._progress.release() + if not stepper: + self._progress.release() - def dequeue(self, index: int) -> Optional[MessageStub]: - self._progress.acquire() + def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: + if not stepper: + self._progress.acquire() if index not in self._last_round_messages: - self._progress.release() + if not stepper: + self._progress.release() return None elif len(self._last_round_messages[index]) == 0: - self._progress.release() + if not stepper: + self._progress.release() return None else: m = self._last_round_messages[index].pop() print(f'\tReceive {m}') - self._progress.release() + if not stepper: + self._progress.release() return m def done(self, index: int): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 31523ca..3e2b9dd 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,11 +1,12 @@ from random import randint from threading import Thread from time import sleep -from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel +from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel, QLineEdit from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt from sys import argv from math import cos, sin, pi +from emulators.AsyncEmulator import AsyncEmulator from emulators.MessageStub import MessageStub from emulators.table import Table @@ -35,7 +36,7 @@ def circle_button_style(size, color = "black"): ''' class Window(QWidget): - h = 600 + h = 640 w = 600 device_size = 80 last_message = None @@ -116,6 +117,75 @@ def show_all_data(self): table.show() return table + def show_queue(self): + content = [["Source", "Destination", "Message"]] + if self.emulator.parent is AsyncEmulator: + queue = self.emulator._messages.values() + elif self.emulator: + queue = self.emulator._last_round_messages.values() + for messages in queue: + for message in messages: + message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") + content.append([str(message.source), str(message.destination), message_stripped]) + table = Table(content, "Message queue") + self.windows.append(table) + table.setFixedSize(500, 500) + table.show() + return table + + def pick(self): + def execute(): + device = int(footer_content['Device: '].text()) + index = int(footer_content['Message: '].text()) + if self.emulator.parent is AsyncEmulator: + self.emulator._messages[device].append(self.emulator._messages[device].pop(index)) + else: + self.emulator._last_round_messages[device].append(self.emulator._last_round_messages[device].pop(index)) + window.destroy(True, True) + + self.emulator.print_transit() + keys = [] + if self.emulator.parent is AsyncEmulator: + messages = self.emulator._messages + else: + messages = self.emulator._last_round_messages + if len(messages) == 0: + return + for item in messages.items(): + keys.append(item[0]) + keys.sort() + window = QWidget() + layout = QVBoxLayout() + header = QLabel("Pick a message to be transmitted next") + layout.addWidget(header) + max_size = 0 + for m in messages.values(): + if len(m) > max_size: + max_size = len(m) + content = [[str(messages[key][i]) if len(messages[key]) > i else " " for key in keys] for i in range(max_size)] + content.insert(0, [f'Device {key}' for key in keys]) + content[0].insert(0, "Message #") + for i in range(max_size): + content[i+1].insert(0, str(i)) + table = Table(content, "Pick a message to be transmitted next") + layout.addWidget(table) + footer_content = {"Device: ": QLineEdit(), "Message: ": QLineEdit()} + for entry in footer_content.items(): + frame = QHBoxLayout() + frame.addWidget(QLabel(entry[0])) + frame.addWidget(entry[1]) + layout.addLayout(frame) + + button = QPushButton('Confirm') + button.clicked.connect(execute) + layout.addWidget(button) + + window.setLayout(layout) + window.show() + self.windows.append(window) + + + def stop_stepper(self): self.emulator.listener.stop() self.emulator.listener.join() @@ -178,9 +248,14 @@ def main(self, num_devices, restart_function): button.clicked.connect(self.show_device_data(i)) self.buttons[i] = button - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data} + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Show queue': self.show_queue, 'Switch emulator': self.emulator.swap_emulator, 'Pick': self.pick} inner_layout = QHBoxLayout() + index = 0 for action in button_actions.items(): + index+=1 + if index == 4: + layout.addLayout(inner_layout) + inner_layout = QHBoxLayout() button = QPushButton(action[0]) button.clicked.connect(action[1]) inner_layout.addWidget(button) @@ -190,17 +265,28 @@ def main(self, num_devices, restart_function): def controls(self): controls_tab = QWidget() - content = {'Space': 'Step a single time through messages', 'f': 'Fast forward through messages', 'Enter': 'Kill stepper daemon and run as an async emulator'} + content = { + 'Space': 'Step a single time through messages', + 'f': 'Fast forward through messages', + 'Enter': 'Kill stepper daemon and run as an async emulator', + 'tab': 'Show all messages currently waiting to be transmitted', + 's': 'Pick the next message waiting to be transmitted to transmit next', + 'e': 'Toggle between sync and async emulation' + } main = QVBoxLayout() main.setAlignment(Qt.AlignmentFlag.AlignTop) controls = QLabel("Controls") controls.setAlignment(Qt.AlignmentFlag.AlignCenter) main.addWidget(controls) - for item in content.items(): - inner = QHBoxLayout() - inner.addWidget(QLabel(item[0])) - inner.addWidget(QLabel(item[1])) - main.addLayout(inner) + top = QHBoxLayout() + key_layout = QVBoxLayout() + value_layout = QVBoxLayout() + for key in content.keys(): + key_layout.addWidget(QLabel(key)) + value_layout.addWidget(QLabel(content[key])) + top.addLayout(key_layout) + top.addLayout(value_layout) + main.addLayout(top) controls_tab.setLayout(main) return controls_tab From 96eabe30602d2c51fea0bfde9a9c475ff1eb3e14 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Tue, 19 Jul 2022 14:26:56 +0200 Subject: [PATCH 042/106] fixed the bug, it wasn't a bug really, it was some misplaced printing --- emulators/SteppingEmulator.py | 17 +++++++++-------- emulators/exercise_overlay.py | 7 +++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index e9ba379..cb6d868 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -18,7 +18,6 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self.last_action = "" self._list_messages_received:list[MessageStub] = list() self._list_messages_sent:list[MessageStub] = list() - self._last_message:tuple[str, MessageStub] = tuple() #type(received or sent), message self._keyheld = False self._pick = False self.parent = AsyncEmulator @@ -38,23 +37,25 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here def dequeue(self, index: int) -> Optional[MessageStub]: self._progress.acquire() result = self.parent.dequeue(self, index, True) - if result != None and self._stepping and self._stepper.is_alive(): - self.step() + if result != None: self._list_messages_received.append(result) self.last_action = "receive" - self._last_message = ("received", result) + if self._stepping and self._stepper.is_alive(): + self.step() + self._progress.release() return result def queue(self, message: MessageStub): self._progress.acquire() + self.parent.queue(self, message, True) + self.last_action = "send" + self._list_messages_sent.append(message) + if self._stepping and self._stepper.is_alive(): self.step() - self.last_action = "send" - self._list_messages_sent.append(message) - self._last_message = ("sent", message) - self.parent.queue(self, message, True) + self._progress.release() #the main program to stop execution diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 3e2b9dd..63f5b65 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -8,6 +8,7 @@ from math import cos, sin, pi from emulators.AsyncEmulator import AsyncEmulator from emulators.MessageStub import MessageStub +from emulators.SyncEmulator import SyncEmulator from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator @@ -55,6 +56,7 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): self.setLayout(layout) self.setWindowTitle("Stepping Emulator") self.setWindowIcon(QIcon("icon.ico")) + self.set_device_color() def coordinates(self, center, r, i, n): x = sin((i*2*pi)/n) @@ -121,7 +123,7 @@ def show_queue(self): content = [["Source", "Destination", "Message"]] if self.emulator.parent is AsyncEmulator: queue = self.emulator._messages.values() - elif self.emulator: + else: queue = self.emulator._last_round_messages.values() for messages in queue: for message in messages: @@ -179,6 +181,7 @@ def execute(): button = QPushButton('Confirm') button.clicked.connect(execute) layout.addWidget(button) + window.setFixedSize(150*len(self.emulator._devices)+1, 400) window.setLayout(layout) window.show() @@ -248,7 +251,7 @@ def main(self, num_devices, restart_function): button.clicked.connect(self.show_device_data(i)) self.buttons[i] = button - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Show queue': self.show_queue, 'Switch emulator': self.emulator.swap_emulator, 'Pick': self.pick} + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.emulator.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} inner_layout = QHBoxLayout() index = 0 for action in button_actions.items(): From 8c472de6d63572f5fddb8bb02a85f73c600f6312 Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Mon, 25 Jul 2022 10:40:08 +0200 Subject: [PATCH 043/106] Update README.md --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 743262a..17ab619 100644 --- a/README.md +++ b/README.md @@ -282,3 +282,21 @@ NOTICE: To execute the code, issue for example: ```bash python3.10 exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 ``` + + +## Frequently Asked Questions + +# How do I install it under Ubuntu + +Since this software needs python 3.10, it is necessary to install it, at least in a virtual environment. + +I would suggest to perform the following commands, prior to the commands under the [Install](#Install) section: + +```bash +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt-get install python3.10-dev +sudo apt-get install python3.10-venv +python3.10 -m venv ds +source ds/bin/activate +``` + From 83b6f83c155c9dcf32cadeb88daac9cc300cfa88 Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Mon, 25 Jul 2022 10:41:40 +0200 Subject: [PATCH 044/106] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17ab619..5b33a81 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ pip3.10 install --user cryptography ``` Installation steps may vary depending on the operating system, for windows, installing tkinter through `pip` should be enough. -The framework is tested in both `python3.9` and `python3.10`, both versions should work. +The framework is tested under `python3.10`. ## General Exercises will be described later in this document. From efe14615c3e8ca393f7a7ae9def99309555be029 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sun, 31 Jul 2022 21:46:48 +0200 Subject: [PATCH 045/106] added requirements.txt and updated readme --- README.md | 27 ++++----------------------- requirements.txt | 3 +++ 2 files changed, 7 insertions(+), 23 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index 5b33a81..89dfb5f 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,13 @@ The stepping emulator requires the following packages to run These packages can be installed using `pip` as shown below: ```bash -pip3.10 install --user pyqt6 -pip3.10 install --user pynput -pip3.10 install --user cryptography +pip install --user -r requirements.txt ``` -Installation steps may vary depending on the operating system, for windows, installing tkinter through `pip` should be enough. - -The framework is tested under `python3.10`. +The framework is tested under `python3.10` in Arch Linux, Ubuntu and Windows. ## General +A FAQ can be found [here](https://github.com/DEIS-Tools/DistributedExercisesAAU/wiki) + Exercises will be described later in this document. In general avoid changing any of the files in the `emulators` subdirectory. @@ -283,20 +281,3 @@ NOTICE: To execute the code, issue for example: python3.10 exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 ``` - -## Frequently Asked Questions - -# How do I install it under Ubuntu - -Since this software needs python 3.10, it is necessary to install it, at least in a virtual environment. - -I would suggest to perform the following commands, prior to the commands under the [Install](#Install) section: - -```bash -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get install python3.10-dev -sudo apt-get install python3.10-venv -python3.10 -m venv ds -source ds/bin/activate -``` - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..689b6f7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +cryptography >= 37.0.4 +PyQt6 >= 6.3.1 +pynput >= 1.7.6 \ No newline at end of file From 9e27726db704ed0f277f1f499d7468cfc364a4f1 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Thu, 4 Aug 2022 12:50:25 +0200 Subject: [PATCH 046/106] added a combo box --- exercise_runner_overlay.py | 54 +++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index d44b312..75d6469 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,5 +1,5 @@ from exercise_runner import run_exercise -from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QPushButton, QHBoxLayout, QLabel +from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QComboBox from PyQt6.QtGui import QIcon from sys import argv app = QApplication(argv) @@ -12,26 +12,68 @@ window.setWindowTitle("Distributed Exercises AAU") main = QVBoxLayout() window.setFixedSize(600, 100) -start_button = QPushButton("start") -main.addWidget(start_button) +button_layout = QHBoxLayout() +start_button = QPushButton("Start") +button_layout.addWidget(start_button) +advanced_button = QPushButton("Advanced") +button_layout.addWidget(advanced_button) +main.addLayout(button_layout) input_area_labels = QHBoxLayout() input_area_areas = QHBoxLayout() -actions = {'Lecture':[print, 0], 'Algorithm': [print, 'PingPong'], 'Type': [print, 'stepping'], 'Devices': [print, 3]} +actions:dict[str, list[QLineEdit, str]] = {'Algorithm': [QLineEdit, 'PingPong'], 'Type': [QLineEdit, 'stepping'], 'Devices': [QLineEdit, 3]} +input_area_labels.addWidget(QLabel('Lecture')) +combobox = QComboBox() +combobox.addItems([str(i) for i in range(13) if i != 3]) +input_area_areas.addWidget(combobox) for action in actions.items(): input_area_labels.addWidget(QLabel(action[0])) field = QLineEdit() input_area_areas.addWidget(field) field.setText(str(action[1][1])) - actions[action[0]][0] = field.text + actions[action[0]][0] = field main.addLayout(input_area_labels) main.addLayout(input_area_areas) +actions['Algorithm'][0].setDisabled(True) +is_disabled = True +actions['Lecture'] = [combobox] + + +def text_changed(text): + exercises = { + 0:'PingPong', + 1:'Gossip', + 2:'RipCommunication', + 4:'TokenRing', + 5:'TOSEQMulticast', + 6:'PAXOS', + 7:'Bully', + 8:'GfsNetwork', + 9:'MapReduceNetwork', + 10:'BlockchainNetwork', + 11:'ChordClient', + 12:'AodvNode'} + lecture = int(text) + + actions['Algorithm'][0].setText(exercises[lecture]) + +combobox.currentTextChanged.connect(text_changed) def start_exercise(): - windows.append(run_exercise(int(actions['Lecture'][0]()), actions['Algorithm'][0](), actions['Type'][0](), int(actions['Devices'][0]()))) + windows.append(run_exercise(int(actions['Lecture'][0].currentText()), actions['Algorithm'][0].text(), actions['Type'][0].text(), int(actions['Devices'][0].text()))) + +def advanced(): + global is_disabled + if is_disabled: + actions['Algorithm'][0].setDisabled(False) + is_disabled = False + else: + actions['Algorithm'][0].setDisabled(True) + is_disabled = True start_button.clicked.connect(start_exercise) +advanced_button.clicked.connect(advanced) window.setLayout(main) window.show() app.exec() From bcddc92103f7468a94f4b91beaed8b4f03579d61 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 6 Aug 2022 12:50:29 +0200 Subject: [PATCH 047/106] made the pick command dequeue the chosen message correctly --- emulators/SteppingEmulator.py | 41 ++++++++++++++++++++++-- emulators/exercise_overlay.py | 59 ++++++++++++++++++++++++++++++++++- emulators/table.py | 12 ++++--- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index cb6d868..964250d 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -4,7 +4,7 @@ from emulators.MessageStub import MessageStub from pynput import keyboard from getpass import getpass #getpass to hide input, cleaner terminal -from threading import Thread #run getpass in seperate thread +from threading import Lock, Thread #run getpass in seperate thread @@ -20,6 +20,8 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._list_messages_sent:list[MessageStub] = list() self._keyheld = False self._pick = False + self.next_message:MessageStub = None + self.wait_lock = Lock() self.parent = AsyncEmulator self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() @@ -36,7 +38,30 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here def dequeue(self, index: int) -> Optional[MessageStub]: self._progress.acquire() - result = self.parent.dequeue(self, index, True) + init = False + while not self.next_message == None and not index == self.next_message.destination: + if not init: + #print(f'Device #{index} entered dequeue loop') + init = True + self._progress.release() + self.wait_lock.acquire() + + if init: + #print(f'Device #{index} exited dequeue loop') + self.wait_lock.release() + self._progress.acquire() + + if not self.next_message == None: + if self.parent == AsyncEmulator: + result = self._messages[index].pop(self._messages[index].index(self.next_message)) + else: + result = self._last_round_messages[index].pop(self._last_round_messages[index].index(self.next_message)) + print(f'\tRecieve {result}') + + self.next_message = None + else: + result = self.parent.dequeue(self, index, True) + if result != None: self._list_messages_received.append(result) self.last_action = "receive" @@ -48,6 +73,18 @@ def dequeue(self, index: int) -> Optional[MessageStub]: def queue(self, message: MessageStub): self._progress.acquire() + init = False + while not self.next_message == None and not message.source == self.next_message.destination: + if not init: + #print(f'Device #{message.source} entered queue loop') + init = True + self._progress.release() + self.wait_lock.acquire() + if init: + #print(f'Device #{message.source} exited queue loop') + self.wait_lock.release() + self._progress.acquire() + self.parent.queue(self, message, True) self.last_action = "send" self._list_messages_sent.append(message) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 63f5b65..436fd62 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -135,7 +135,7 @@ def show_queue(self): table.show() return table - def pick(self): + def old_pick(self): def execute(): device = int(footer_content['Device: '].text()) index = int(footer_content['Message: '].text()) @@ -143,6 +143,8 @@ def execute(): self.emulator._messages[device].append(self.emulator._messages[device].pop(index)) else: self.emulator._last_round_messages[device].append(self.emulator._last_round_messages[device].pop(index)) + self.emulator.is_pick = True + self.emulator.pick_device = device window.destroy(True, True) self.emulator.print_transit() @@ -186,6 +188,61 @@ def execute(): window.setLayout(layout) window.show() self.windows.append(window) + + def pick(self): + def execute(device, index): + def inner_execute(): + if self.emulator.parent is AsyncEmulator: + message = self.emulator._messages[device][index] + else: + message = self.emulator._last_round_messages[device][index] + + self.emulator.next_message = message + window.destroy(True, True) + while not self.emulator.last_action == "receive" and not self.last_message == message: + self.step() + + return inner_execute + + self.emulator.print_transit() + keys = [] + if self.emulator.parent is AsyncEmulator: + messages = self.emulator._messages + else: + messages = self.emulator._last_round_messages + if len(messages) == 0: + return + for item in messages.items(): + keys.append(item[0]) + keys.sort() + max_size = 0 + for m in messages.values(): + if len(m) > max_size: + max_size = len(m) + + window = QWidget() + layout = QVBoxLayout() + + content = [] + for i in range(max_size): + for key in keys: + content.append([]) + if len(messages[key]) > i: + button = QPushButton(str(messages[key][i])) + function_reference = execute(key, i) + button.clicked.connect(function_reference) + content[i].append(button) + content.insert(0, [f'Device {key}' for key in keys]) + content[0].insert(0, "Message #") + for i in range(max_size): + content[i+1].insert(0, str(i)) + table = Table(content, "Pick a message to be transmitted next to a device") + layout.addWidget(table) + window.setLayout(layout) + window.show() + self.windows.append(window) + + diff --git a/emulators/table.py b/emulators/table.py index ed63fbb..dd0d561 100644 --- a/emulators/table.py +++ b/emulators/table.py @@ -1,9 +1,10 @@ -from PyQt6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QScrollArea +from typing import Any +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QScrollArea, QPushButton from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt class Table(QScrollArea): - def __init__(self, content:list[list[str]], title="table"): + def __init__(self, content:list[list[str | QWidget]], title="table"): super().__init__() widget = QWidget() self.setWindowIcon(QIcon('icon.ico')) @@ -13,8 +14,11 @@ def __init__(self, content:list[list[str]], title="table"): for row in content: column = QHBoxLayout() for element in row: - label = QLabel(element) - label.setAlignment(Qt.AlignmentFlag.AlignCenter) + if type(element) is str: + label = QLabel(element) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + else: + label = element column.addWidget(label) columns.addLayout(column) widget.setLayout(columns) From 8404be40534add9540be4c686788399b784ce6b8 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 6 Aug 2022 13:09:07 +0200 Subject: [PATCH 048/106] finished pick gui --- emulators/exercise_overlay.py | 73 +++++------------------------------ 1 file changed, 9 insertions(+), 64 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 436fd62..3e2c0bf 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -135,60 +135,6 @@ def show_queue(self): table.show() return table - def old_pick(self): - def execute(): - device = int(footer_content['Device: '].text()) - index = int(footer_content['Message: '].text()) - if self.emulator.parent is AsyncEmulator: - self.emulator._messages[device].append(self.emulator._messages[device].pop(index)) - else: - self.emulator._last_round_messages[device].append(self.emulator._last_round_messages[device].pop(index)) - self.emulator.is_pick = True - self.emulator.pick_device = device - window.destroy(True, True) - - self.emulator.print_transit() - keys = [] - if self.emulator.parent is AsyncEmulator: - messages = self.emulator._messages - else: - messages = self.emulator._last_round_messages - if len(messages) == 0: - return - for item in messages.items(): - keys.append(item[0]) - keys.sort() - window = QWidget() - layout = QVBoxLayout() - header = QLabel("Pick a message to be transmitted next") - layout.addWidget(header) - max_size = 0 - for m in messages.values(): - if len(m) > max_size: - max_size = len(m) - content = [[str(messages[key][i]) if len(messages[key]) > i else " " for key in keys] for i in range(max_size)] - content.insert(0, [f'Device {key}' for key in keys]) - content[0].insert(0, "Message #") - for i in range(max_size): - content[i+1].insert(0, str(i)) - table = Table(content, "Pick a message to be transmitted next") - layout.addWidget(table) - footer_content = {"Device: ": QLineEdit(), "Message: ": QLineEdit()} - for entry in footer_content.items(): - frame = QHBoxLayout() - frame.addWidget(QLabel(entry[0])) - frame.addWidget(entry[1]) - layout.addLayout(frame) - - button = QPushButton('Confirm') - button.clicked.connect(execute) - layout.addWidget(button) - window.setFixedSize(150*len(self.emulator._devices)+1, 400) - - window.setLayout(layout) - window.show() - self.windows.append(window) - def pick(self): def execute(device, index): def inner_execute(): @@ -196,9 +142,9 @@ def inner_execute(): message = self.emulator._messages[device][index] else: message = self.emulator._last_round_messages[device][index] - + self.emulator.next_message = message - window.destroy(True, True) + table.destroy(True, True) while not self.emulator.last_action == "receive" and not self.last_message == message: self.step() @@ -220,27 +166,26 @@ def inner_execute(): if len(m) > max_size: max_size = len(m) - window = QWidget() - layout = QVBoxLayout() - content = [] for i in range(max_size): + content.append([]) for key in keys: - content.append([]) if len(messages[key]) > i: button = QPushButton(str(messages[key][i])) function_reference = execute(key, i) button.clicked.connect(function_reference) content[i].append(button) + else: + content[i].append("") content.insert(0, [f'Device {key}' for key in keys]) content[0].insert(0, "Message #") for i in range(max_size): content[i+1].insert(0, str(i)) + table = Table(content, "Pick a message to be transmitted next to a device") - layout.addWidget(table) - window.setLayout(layout) - window.show() - self.windows.append(window) + table.setFixedSize(150*len(self.emulator._devices)+1, 400) + table.show() + self.windows.append(table) From 4c44cf07f78a38b86ecac5367f73de272a0357db Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 6 Aug 2022 13:10:10 +0200 Subject: [PATCH 049/106] made improvements to pick command --- emulators/exercise_overlay.py | 1 - 1 file changed, 1 deletion(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 3e2c0bf..c84b96e 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -150,7 +150,6 @@ def inner_execute(): return inner_execute - self.emulator.print_transit() keys = [] if self.emulator.parent is AsyncEmulator: messages = self.emulator._messages From 220feb5777cee137296b50753b0b3979fc4c3420 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Tue, 9 Aug 2022 14:06:24 +0200 Subject: [PATCH 050/106] fixed a bug where the wrong devices would try to receive the ping packet --- emulators/SteppingEmulator.py | 46 ++++++++++++----------------------- emulators/exercise_overlay.py | 16 ++++++++---- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 964250d..bd9fe5f 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -22,6 +22,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self._pick = False self.next_message:MessageStub = None self.wait_lock = Lock() + self.pick_device = -1 self.parent = AsyncEmulator self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() @@ -37,28 +38,20 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - self._progress.acquire() - init = False - while not self.next_message == None and not index == self.next_message.destination: - if not init: - #print(f'Device #{index} entered dequeue loop') - init = True - self._progress.release() - self.wait_lock.acquire() - - if init: - #print(f'Device #{index} exited dequeue loop') - self.wait_lock.release() - self._progress.acquire() - if not self.next_message == None: + if self.next_message == None or not index == self.pick_device: + while not self.next_message == None: + pass + self._progress.acquire() + if not self.next_message == None and index == self.pick_device: + print(f"Device {index} is receiving through pick") if self.parent == AsyncEmulator: result = self._messages[index].pop(self._messages[index].index(self.next_message)) else: result = self._last_round_messages[index].pop(self._last_round_messages[index].index(self.next_message)) - print(f'\tRecieve {result}') - self.next_message = None + + print(f'\tRecieve {result}') else: result = self.parent.dequeue(self, index, True) @@ -67,22 +60,15 @@ def dequeue(self, index: int) -> Optional[MessageStub]: self.last_action = "receive" if self._stepping and self._stepper.is_alive(): self.step() - + self._progress.release() return result def queue(self, message: MessageStub): - self._progress.acquire() - init = False - while not self.next_message == None and not message.source == self.next_message.destination: - if not init: - #print(f'Device #{message.source} entered queue loop') - init = True - self._progress.release() - self.wait_lock.acquire() - if init: - #print(f'Device #{message.source} exited queue loop') - self.wait_lock.release() + + if self.next_message == None or not message.source == self.pick_device: + while not self.next_message == None: + pass self._progress.acquire() self.parent.queue(self, message, True) @@ -92,8 +78,8 @@ def queue(self, message: MessageStub): if self._stepping and self._stepper.is_alive(): self.step() - - self._progress.release() + if self.next_message == None or not self.next_message == self.pick_device: + self._progress.release() #the main program to stop execution def step(self): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index c84b96e..865f9f7 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -14,7 +14,7 @@ from emulators.SteppingEmulator import SteppingEmulator def circle_button_style(size, color = "black"): - return f''' + return f''' QPushButton {{ background-color: transparent; border-style: solid; @@ -129,10 +129,14 @@ def show_queue(self): for message in messages: message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") content.append([str(message.source), str(message.destination), message_stripped]) + window = QWidget() + layout = QVBoxLayout() table = Table(content, "Message queue") - self.windows.append(table) - table.setFixedSize(500, 500) - table.show() + layout.addWidget(table) + window.setLayout(layout) + self.windows.append(window) + window.setFixedSize(500, 500) + window.show() return table def pick(self): @@ -143,10 +147,12 @@ def inner_execute(): else: message = self.emulator._last_round_messages[device][index] + self.emulator.pick_device = device self.emulator.next_message = message table.destroy(True, True) - while not self.emulator.last_action == "receive" and not self.last_message == message: + while not self.emulator.next_message == None: self.step() + sleep(.1) return inner_execute From 18437998f1f42b41a4e7c30876b9260d6bebe82d Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Wed, 17 Aug 2022 16:02:15 +0200 Subject: [PATCH 051/106] cleaned up the stepper and fixed the last few bugs --- emulators/#exercise_overlay.py# | 311 ++++++++++++++++++++++++++++++++ emulators/SteppingEmulator.py | 58 +++--- emulators/exercise_overlay.py | 47 ++++- exercise_runner_overlay.py | 7 +- 4 files changed, 392 insertions(+), 31 deletions(-) create mode 100644 emulators/#exercise_overlay.py# diff --git a/emulators/#exercise_overlay.py# b/emulators/#exercise_overlay.py# new file mode 100644 index 0000000..8c56ab9 --- /dev/null +++ b/emulators/#exercise_overlay.py# @@ -0,0 +1,311 @@ +from random import randint +from threading import Thread +from time import sleep +from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel, QLineEdit +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt +from sys import argv +from math import cos, sin, pi +from emulators.AsyncEmulator import AsyncEmulator +from emulators.MessageStub import MessageStub +from emulators.SyncEmulator import SyncEmulator + +from emulators.table import Table +from emulators.SteppingEmulator import SteppingEmulator + +def circle_button_style(size, color = "black"): + return f''' + QPushButton {{ + background-color: transparent; + border-style: solid; + border-width: 2px; + border-radius: {int(size/2)}px; + border-color: {color}; + max-width: {size}px; + max-height: {size}px; + min-width: {size}px; + min-height: {size}px; + }} + QPushButton:hover {{ + background-color: gray; + border-width: 2px; + }} + QPushButton:pressed {{ + background-color: transparent; + border-width: 1px + }} + ''' + +class Window(QWidget): + h = 640 + w = 600 + device_size = 80 + last_message = None + buttons:dict[int, QPushButton] = {} + windows = list() + + def __init__(self, elements, restart_function, emulator:SteppingEmulator): + super().__init__() + self.emulator = emulator + self.setFixedSize(self.w, self.h) + layout = QVBoxLayout() + tabs = QTabWidget() + tabs.addTab(self.main(elements, restart_function), 'Main') + tabs.addTab(self.controls(), 'controls') + layout.addWidget(tabs) + self.setLayout(layout) + self.setWindowTitle("Stepping Emulator") + self.setWindowIcon(QIcon("icon.ico")) + self.set_device_color() + + def coordinates(self, center, r, i, n): + x = sin((i*2*pi)/n) + y = cos((i*2*pi)/n) + if x < pi: + return int(center[0] - (r*x)), int(center[1] - (r*y)) + else: + return int(center[0] - (r*-x)), int(center[1] - (r*y)) + + def show_device_data(self, device_id): + def show(): + received:list[MessageStub] = list() + sent:list[MessageStub] = list() + for message in self.emulator._list_messages_received: + if message.destination == device_id: + received.append(message) + if message.source == device_id: + sent.append(message) + if len(received) > len(sent): + for _ in range(len(received)-len(sent)): + sent.append("") + elif len(sent) > len(received): + for _ in range(len(sent)-len(received)): + received.append("") + content = list() + for i in range(len(received)): + if received[i] == "": + msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + content.append(["", received[i], str(sent[i].destination), msg]) + elif sent[i] == "": + msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([str(received[i].source), msg, "", sent[i]]) + else: + sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") + received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") + content.append([str(received[i].source), received_msg, str(sent[i].destination), sent_msg]) + content.insert(0, ['Source', 'Message', 'Destination', 'Message']) + table = Table(content, title=f'Device #{device_id}') + self.windows.append(table) + table.setFixedSize(300, 500) + table.show() + return table + return show + + def show_all_data(self): + content = [] + messages = self.emulator._list_messages_sent + message_content = [] + for message in messages: + temp = str(message) + temp = temp.replace(f'{message.source} -> {message.destination} : ', "") + temp = temp.replace(f'{message.source}->{message.destination} : ', "") + message_content.append(temp) + + content = [[str(messages[i].source), str(messages[i].destination), message_content[i], str(i)] for i in range(len(messages))] + content.insert(0, ['Source', 'Destination', 'Message', 'Sequence number']) + table = Table(content, title=f'All data') + self.windows.append(table) + table.setFixedSize(500, 500) + table.show() + return table + + def show_queue(self): + content = [["Source", "Destination", "Message"]] + if self.emulator.parent is AsyncEmulator: + queue = self.emulator._messages.values() + else: + queue = self.emulator._last_round_messages.values() + for messages in queue: + for message in messages: + message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") + content.append([str(message.source), str(message.destination), message_stripped]) + window = QWidget() + layout = QVBoxLayout() + table = Table(content, "Message queue") + layout.addWidget(table) + window.setLayout(layout) + self.windows.append(window) + window.setFixedSize(500, 500) + window.show() + return table + + def pick(self): + def execute(device, index): + def inner_execute(): + if self.emulator.parent is AsyncEmulator: + message = self.emulator._messages[device][index] + else: + message = self.emulator._last_round_messages[device][index] + + self.emulator.pick_device = device + self.emulator.next_message = message + table.destroy(True, True) + while not self.emulator.next_message == None: + self.step() + #sleep(.1) + + return inner_execute + + keys = [] + if self.emulator.parent is AsyncEmulator: + messages = self.emulator._messages + else: + messages = self.emulator._last_round_messages + if len(messages) == 0: + return + for item in messages.items(): + keys.append(item[0]) + keys.sort() + max_size = 0 + for m in messages.values(): + if len(m) > max_size: + max_size = len(m) + + content = [] + for i in range(max_size): + content.append([]) + for key in keys: + if len(messages[key]) > i: + button = QPushButton(str(messages[key][i])) + function_reference = execute(key, i) + button.clicked.connect(function_reference) + content[i].append(button) + else: + content[i].append("") + content.insert(0, [f'Device {key}' for key in keys]) + content[0].insert(0, "Message #") + for i in range(max_size): + content[i+1].insert(0, str(i)) + + table = Table(content, "Pick a message to be transmitted next to a device") + table.setFixedSize(150*len(self.emulator._devices)+1, 400) + table.show() + self.windows.append(table) + + + + + + def stop_stepper(self): + self.emulator.listener.stop() + self.emulator.listener.join() + + def end(self): + self.emulator._stepping = False + while not self.emulator.all_terminated(): + self.set_device_color() + Thread(target=self.stop_stepper, daemon=True).start() + + def set_device_color(self): + sleep(.1) + messages = self.emulator._list_messages_sent if self.emulator.last_action == "send" else self.emulator._list_messages_received + if len(messages) != 0: + last_message = messages[len(messages)-1] + if not last_message == self.last_message: + for button in self.buttons.values(): + button.setStyleSheet(circle_button_style(self.device_size)) + if last_message.source == last_message.destination: + self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'yellow')) + else: + self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'green')) + self.buttons[last_message.destination].setStyleSheet(circle_button_style(self.device_size, 'red')) + self.last_message = last_message + + def step(self): + self.emulator._single = True + if self.emulator.all_terminated(): + Thread(target=self.stop_stepper, daemon=True).start() + self.set_device_color() + + def restart_algorithm(self, function): + self.windows.append(function()) + + def main(self, num_devices, restart_function): + main_tab = QWidget() + green = QLabel("green: source", main_tab) + green.setStyleSheet("color: green;") + green.move(5, 0) + green.show() + red = QLabel("red: destination", main_tab) + red.setStyleSheet("color: red;") + red.move(5, 20) + red.show() + yellow = QLabel("yellow: same device", main_tab) + yellow.setStyleSheet("color: yellow;") + yellow.move(5, 40) + yellow.show() + layout = QVBoxLayout() + device_area = QWidget() + device_area.setFixedSize(500, 500) + layout.addWidget(device_area) + main_tab.setLayout(layout) + for i in range(num_devices): + x, y = self.coordinates((device_area.width()/2, device_area.height()/2), (device_area.height()/2) - (self.device_size/2), i, num_devices) + button = QPushButton(f'Device #{i}', main_tab) + button.resize(self.device_size, self.device_size) + button.setStyleSheet(circle_button_style(self.device_size)) + button.move(x, int(y - (self.device_size/2))) + button.clicked.connect(self.show_device_data(i)) + self.buttons[i] = button + + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.emulator.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} + inner_layout = QHBoxLayout() + index = 0 + for action in button_actions.items(): + index+=1 + if index == 4: + layout.addLayout(inner_layout) + inner_layout = QHBoxLayout() + button = QPushButton(action[0]) + button.clicked.connect(action[1]) + inner_layout.addWidget(button) + layout.addLayout(inner_layout) + + return main_tab + + def controls(self): + controls_tab = QWidget() + content = { + 'Space': 'Step a single time through messages', + 'f': 'Fast forward through messages', + 'Enter': 'Kill stepper daemon and run as an async emulator', + 'tab': 'Show all messages currently waiting to be transmitted', + 's': 'Pick the next message waiting to be transmitted to transmit next', + 'e': 'Toggle between sync and async emulation' + } + main = QVBoxLayout() + main.setAlignment(Qt.AlignmentFlag.AlignTop) + controls = QLabel("Controls") + controls.setAlignment(Qt.AlignmentFlag.AlignCenter) + main.addWidget(controls) + top = QHBoxLayout() + key_layout = QVBoxLayout() + value_layout = QVBoxLayout() + for key in content.keys(): + key_layout.addWidget(QLabel(key)) + value_layout.addWidget(QLabel(content[key])) + top.addLayout(key_layout) + top.addLayout(value_layout) + main.addLayout(top) + controls_tab.setLayout(main) + return controls_tab + + def closeEvent(self, event): + Thread(target=self.end).start() + event.accept() + +if __name__ == "__main__": + app = QApplication(argv) + window = Window(argv[1] if len(argv) > 1 else 10, lambda: print("Restart function")) + + app.exec() diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index bd9fe5f..5712fd8 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -1,5 +1,6 @@ from typing import Optional from emulators.AsyncEmulator import AsyncEmulator +from .EmulatorStub import EmulatorStub from emulators.SyncEmulator import SyncEmulator from emulators.MessageStub import MessageStub from pynput import keyboard @@ -9,21 +10,22 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): + _stepping = True + _single = False + last_action = "" + messages_received:list[MessageStub] = [] + messages_sent:list[MessageStub] = [] + keyheld = False + pick = False + next_message = None + pick_device = -1 + parent:EmulatorStub = AsyncEmulator + def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object super().__init__(number_of_devices, kind) self._stepper = Thread(target=lambda: getpass(""), daemon=True) self._stepper.start() - self._stepping = True - self._single = False - self.last_action = "" - self._list_messages_received:list[MessageStub] = list() - self._list_messages_sent:list[MessageStub] = list() - self._keyheld = False - self._pick = False - self.next_message:MessageStub = None self.wait_lock = Lock() - self.pick_device = -1 - self.parent = AsyncEmulator self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() msg = """ @@ -39,10 +41,19 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here def dequeue(self, index: int) -> Optional[MessageStub]: - if self.next_message == None or not index == self.pick_device: + #if self.next_message == None or not index == self.pick_device: + # while not self.next_message == None: + # pass + #self._progress.acquire() + + self._progress.acquire() + if not self.next_message == None and not index == self.pick_device: + self._progress.release() while not self.next_message == None: pass - self._progress.acquire() + return self.dequeue(index) + #print(f'Device {index} has the lock') + if not self.next_message == None and index == self.pick_device: print(f"Device {index} is receiving through pick") if self.parent == AsyncEmulator: @@ -56,11 +67,12 @@ def dequeue(self, index: int) -> Optional[MessageStub]: result = self.parent.dequeue(self, index, True) if result != None: - self._list_messages_received.append(result) + self.messages_received.append(result) self.last_action = "receive" if self._stepping and self._stepper.is_alive(): self.step() - + + #print(f'Device {index} is releasing the lock') self._progress.release() return result @@ -69,16 +81,18 @@ def queue(self, message: MessageStub): if self.next_message == None or not message.source == self.pick_device: while not self.next_message == None: pass - self._progress.acquire() - + self._progress.acquire() + #print(f'Device {message.source} has the lock') + self.parent.queue(self, message, True) self.last_action = "send" - self._list_messages_sent.append(message) + self.messages_sent.append(message) if self._stepping and self._stepper.is_alive(): self.step() if self.next_message == None or not self.next_message == self.pick_device: + #print(f'Device {message.source} is releasing the lock') self._progress.release() #the main program to stop execution @@ -89,9 +103,9 @@ def step(self): if self._single: #break while if the desired action is a single message self._single = False break - elif self._pick: - self._pick = False - self.pick() + elif self.pick: + self.pick = False + self.pick_function() #listen for pressed keys def _on_press(self, key:keyboard.KeyCode | keyboard.Key): @@ -108,7 +122,7 @@ def _on_press(self, key:keyboard.KeyCode | keyboard.Key): elif key == "tab": self.print_transit() elif key == "s": - self._pick = True + self.pick = True elif key == "e": self.swap_emulator() self._keyheld = True @@ -158,7 +172,7 @@ def swap_emulator(self): print(f'Changed emulator to {self.parent.__name__}') #Pick command function, this lets the user alter the queue for a specific device - def pick(self): + def pick_function(self): try: print("Press return to proceed") #prompt the user to kill the stepper daemon while self._stepper.is_alive(): #wait for the stepper to be killed diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 865f9f7..99eca05 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -43,6 +43,9 @@ class Window(QWidget): last_message = None buttons:dict[int, QPushButton] = {} windows = list() + pick_window = False + queue_window = False + all_data_window = False def __init__(self, elements, restart_function, emulator:SteppingEmulator): super().__init__() @@ -70,7 +73,7 @@ def show_device_data(self, device_id): def show(): received:list[MessageStub] = list() sent:list[MessageStub] = list() - for message in self.emulator._list_messages_received: + for message in self.emulator.messages_received: if message.destination == device_id: received.append(message) if message.source == device_id: @@ -102,8 +105,11 @@ def show(): return show def show_all_data(self): + if self.all_data_window: + return + self.all_data_window = True content = [] - messages = self.emulator._list_messages_sent + messages = self.emulator.messages_sent message_content = [] for message in messages: temp = str(message) @@ -113,13 +119,21 @@ def show_all_data(self): content = [[str(messages[i].source), str(messages[i].destination), message_content[i], str(i)] for i in range(len(messages))] content.insert(0, ['Source', 'Destination', 'Message', 'Sequence number']) - table = Table(content, title=f'All data') + parent = self + class MyTable(Table): + def closeEvent(self, event): + parent.all_data_window = False + return super().closeEvent(event) + table = MyTable(content, title=f'All data') self.windows.append(table) table.setFixedSize(500, 500) table.show() return table def show_queue(self): + if self.queue_window: + return + self.queue_window = True content = [["Source", "Destination", "Message"]] if self.emulator.parent is AsyncEmulator: queue = self.emulator._messages.values() @@ -129,7 +143,12 @@ def show_queue(self): for message in messages: message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") content.append([str(message.source), str(message.destination), message_stripped]) - window = QWidget() + parent = self + class MyWidget(QWidget): + def closeEvent(self, event): + parent.queue_window = False + return super().closeEvent(event) + window = MyWidget() layout = QVBoxLayout() table = Table(content, "Message queue") layout.addWidget(table) @@ -137,22 +156,29 @@ def show_queue(self): self.windows.append(window) window.setFixedSize(500, 500) window.show() - return table def pick(self): + if self.pick_window: + return + self.pick_window = True def execute(device, index): def inner_execute(): if self.emulator.parent is AsyncEmulator: message = self.emulator._messages[device][index] else: message = self.emulator._last_round_messages[device][index] + self.emulator.pick_device = device self.emulator.next_message = message table.destroy(True, True) + self.pick_window = False + size = len(self.emulator.messages_received) while not self.emulator.next_message == None: self.step() - sleep(.1) + #sleep(.1) + + assert len(self.emulator.messages_received) == size+1 return inner_execute @@ -187,7 +213,12 @@ def inner_execute(): for i in range(max_size): content[i+1].insert(0, str(i)) - table = Table(content, "Pick a message to be transmitted next to a device") + parent = self + class MyTable(Table): + def closeEvent(self, event): + parent.pick_window = False + return super().closeEvent(event) + table = MyTable(content, "Pick a message to be transmitted next to a device") table.setFixedSize(150*len(self.emulator._devices)+1, 400) table.show() self.windows.append(table) @@ -208,7 +239,7 @@ def end(self): def set_device_color(self): sleep(.1) - messages = self.emulator._list_messages_sent if self.emulator.last_action == "send" else self.emulator._list_messages_received + messages = self.emulator.messages_sent if self.emulator.last_action == "send" else self.emulator.messages_received if len(messages) != 0: last_message = messages[len(messages)-1] if not last_message == self.last_message: diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 75d6469..d640177 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -25,6 +25,7 @@ combobox = QComboBox() combobox.addItems([str(i) for i in range(13) if i != 3]) input_area_areas.addWidget(combobox) +starting_exercise = False for action in actions.items(): input_area_labels.addWidget(QLabel(action[0])) @@ -61,7 +62,11 @@ def text_changed(text): combobox.currentTextChanged.connect(text_changed) def start_exercise(): - windows.append(run_exercise(int(actions['Lecture'][0].currentText()), actions['Algorithm'][0].text(), actions['Type'][0].text(), int(actions['Devices'][0].text()))) + global starting_exercise + if not starting_exercise: + starting_exercise = True + windows.append(run_exercise(int(actions['Lecture'][0].currentText()), actions['Algorithm'][0].text(), actions['Type'][0].text(), int(actions['Devices'][0].text()))) + starting_exercise = False def advanced(): global is_disabled From b5b2b160adcdb12eb1f6874f5fe1e18b9c76512c Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Wed, 17 Aug 2022 16:59:03 +0200 Subject: [PATCH 052/106] cleaned up the stepping emulator and fixed the last few bugs, its been tested with 10 devices and works --- emulators/#exercise_overlay.py# | 311 -------------------------------- emulators/SteppingEmulator.py | 46 +++-- emulators/exercise_overlay.py | 5 +- exercise_runner_overlay.py | 1 + 4 files changed, 26 insertions(+), 337 deletions(-) delete mode 100644 emulators/#exercise_overlay.py# diff --git a/emulators/#exercise_overlay.py# b/emulators/#exercise_overlay.py# deleted file mode 100644 index 8c56ab9..0000000 --- a/emulators/#exercise_overlay.py# +++ /dev/null @@ -1,311 +0,0 @@ -from random import randint -from threading import Thread -from time import sleep -from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel, QLineEdit -from PyQt6.QtGui import QIcon -from PyQt6.QtCore import Qt -from sys import argv -from math import cos, sin, pi -from emulators.AsyncEmulator import AsyncEmulator -from emulators.MessageStub import MessageStub -from emulators.SyncEmulator import SyncEmulator - -from emulators.table import Table -from emulators.SteppingEmulator import SteppingEmulator - -def circle_button_style(size, color = "black"): - return f''' - QPushButton {{ - background-color: transparent; - border-style: solid; - border-width: 2px; - border-radius: {int(size/2)}px; - border-color: {color}; - max-width: {size}px; - max-height: {size}px; - min-width: {size}px; - min-height: {size}px; - }} - QPushButton:hover {{ - background-color: gray; - border-width: 2px; - }} - QPushButton:pressed {{ - background-color: transparent; - border-width: 1px - }} - ''' - -class Window(QWidget): - h = 640 - w = 600 - device_size = 80 - last_message = None - buttons:dict[int, QPushButton] = {} - windows = list() - - def __init__(self, elements, restart_function, emulator:SteppingEmulator): - super().__init__() - self.emulator = emulator - self.setFixedSize(self.w, self.h) - layout = QVBoxLayout() - tabs = QTabWidget() - tabs.addTab(self.main(elements, restart_function), 'Main') - tabs.addTab(self.controls(), 'controls') - layout.addWidget(tabs) - self.setLayout(layout) - self.setWindowTitle("Stepping Emulator") - self.setWindowIcon(QIcon("icon.ico")) - self.set_device_color() - - def coordinates(self, center, r, i, n): - x = sin((i*2*pi)/n) - y = cos((i*2*pi)/n) - if x < pi: - return int(center[0] - (r*x)), int(center[1] - (r*y)) - else: - return int(center[0] - (r*-x)), int(center[1] - (r*y)) - - def show_device_data(self, device_id): - def show(): - received:list[MessageStub] = list() - sent:list[MessageStub] = list() - for message in self.emulator._list_messages_received: - if message.destination == device_id: - received.append(message) - if message.source == device_id: - sent.append(message) - if len(received) > len(sent): - for _ in range(len(received)-len(sent)): - sent.append("") - elif len(sent) > len(received): - for _ in range(len(sent)-len(received)): - received.append("") - content = list() - for i in range(len(received)): - if received[i] == "": - msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - content.append(["", received[i], str(sent[i].destination), msg]) - elif sent[i] == "": - msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([str(received[i].source), msg, "", sent[i]]) - else: - sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([str(received[i].source), received_msg, str(sent[i].destination), sent_msg]) - content.insert(0, ['Source', 'Message', 'Destination', 'Message']) - table = Table(content, title=f'Device #{device_id}') - self.windows.append(table) - table.setFixedSize(300, 500) - table.show() - return table - return show - - def show_all_data(self): - content = [] - messages = self.emulator._list_messages_sent - message_content = [] - for message in messages: - temp = str(message) - temp = temp.replace(f'{message.source} -> {message.destination} : ', "") - temp = temp.replace(f'{message.source}->{message.destination} : ', "") - message_content.append(temp) - - content = [[str(messages[i].source), str(messages[i].destination), message_content[i], str(i)] for i in range(len(messages))] - content.insert(0, ['Source', 'Destination', 'Message', 'Sequence number']) - table = Table(content, title=f'All data') - self.windows.append(table) - table.setFixedSize(500, 500) - table.show() - return table - - def show_queue(self): - content = [["Source", "Destination", "Message"]] - if self.emulator.parent is AsyncEmulator: - queue = self.emulator._messages.values() - else: - queue = self.emulator._last_round_messages.values() - for messages in queue: - for message in messages: - message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") - content.append([str(message.source), str(message.destination), message_stripped]) - window = QWidget() - layout = QVBoxLayout() - table = Table(content, "Message queue") - layout.addWidget(table) - window.setLayout(layout) - self.windows.append(window) - window.setFixedSize(500, 500) - window.show() - return table - - def pick(self): - def execute(device, index): - def inner_execute(): - if self.emulator.parent is AsyncEmulator: - message = self.emulator._messages[device][index] - else: - message = self.emulator._last_round_messages[device][index] - - self.emulator.pick_device = device - self.emulator.next_message = message - table.destroy(True, True) - while not self.emulator.next_message == None: - self.step() - #sleep(.1) - - return inner_execute - - keys = [] - if self.emulator.parent is AsyncEmulator: - messages = self.emulator._messages - else: - messages = self.emulator._last_round_messages - if len(messages) == 0: - return - for item in messages.items(): - keys.append(item[0]) - keys.sort() - max_size = 0 - for m in messages.values(): - if len(m) > max_size: - max_size = len(m) - - content = [] - for i in range(max_size): - content.append([]) - for key in keys: - if len(messages[key]) > i: - button = QPushButton(str(messages[key][i])) - function_reference = execute(key, i) - button.clicked.connect(function_reference) - content[i].append(button) - else: - content[i].append("") - content.insert(0, [f'Device {key}' for key in keys]) - content[0].insert(0, "Message #") - for i in range(max_size): - content[i+1].insert(0, str(i)) - - table = Table(content, "Pick a message to be transmitted next to a device") - table.setFixedSize(150*len(self.emulator._devices)+1, 400) - table.show() - self.windows.append(table) - - - - - - def stop_stepper(self): - self.emulator.listener.stop() - self.emulator.listener.join() - - def end(self): - self.emulator._stepping = False - while not self.emulator.all_terminated(): - self.set_device_color() - Thread(target=self.stop_stepper, daemon=True).start() - - def set_device_color(self): - sleep(.1) - messages = self.emulator._list_messages_sent if self.emulator.last_action == "send" else self.emulator._list_messages_received - if len(messages) != 0: - last_message = messages[len(messages)-1] - if not last_message == self.last_message: - for button in self.buttons.values(): - button.setStyleSheet(circle_button_style(self.device_size)) - if last_message.source == last_message.destination: - self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'yellow')) - else: - self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'green')) - self.buttons[last_message.destination].setStyleSheet(circle_button_style(self.device_size, 'red')) - self.last_message = last_message - - def step(self): - self.emulator._single = True - if self.emulator.all_terminated(): - Thread(target=self.stop_stepper, daemon=True).start() - self.set_device_color() - - def restart_algorithm(self, function): - self.windows.append(function()) - - def main(self, num_devices, restart_function): - main_tab = QWidget() - green = QLabel("green: source", main_tab) - green.setStyleSheet("color: green;") - green.move(5, 0) - green.show() - red = QLabel("red: destination", main_tab) - red.setStyleSheet("color: red;") - red.move(5, 20) - red.show() - yellow = QLabel("yellow: same device", main_tab) - yellow.setStyleSheet("color: yellow;") - yellow.move(5, 40) - yellow.show() - layout = QVBoxLayout() - device_area = QWidget() - device_area.setFixedSize(500, 500) - layout.addWidget(device_area) - main_tab.setLayout(layout) - for i in range(num_devices): - x, y = self.coordinates((device_area.width()/2, device_area.height()/2), (device_area.height()/2) - (self.device_size/2), i, num_devices) - button = QPushButton(f'Device #{i}', main_tab) - button.resize(self.device_size, self.device_size) - button.setStyleSheet(circle_button_style(self.device_size)) - button.move(x, int(y - (self.device_size/2))) - button.clicked.connect(self.show_device_data(i)) - self.buttons[i] = button - - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.emulator.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} - inner_layout = QHBoxLayout() - index = 0 - for action in button_actions.items(): - index+=1 - if index == 4: - layout.addLayout(inner_layout) - inner_layout = QHBoxLayout() - button = QPushButton(action[0]) - button.clicked.connect(action[1]) - inner_layout.addWidget(button) - layout.addLayout(inner_layout) - - return main_tab - - def controls(self): - controls_tab = QWidget() - content = { - 'Space': 'Step a single time through messages', - 'f': 'Fast forward through messages', - 'Enter': 'Kill stepper daemon and run as an async emulator', - 'tab': 'Show all messages currently waiting to be transmitted', - 's': 'Pick the next message waiting to be transmitted to transmit next', - 'e': 'Toggle between sync and async emulation' - } - main = QVBoxLayout() - main.setAlignment(Qt.AlignmentFlag.AlignTop) - controls = QLabel("Controls") - controls.setAlignment(Qt.AlignmentFlag.AlignCenter) - main.addWidget(controls) - top = QHBoxLayout() - key_layout = QVBoxLayout() - value_layout = QVBoxLayout() - for key in content.keys(): - key_layout.addWidget(QLabel(key)) - value_layout.addWidget(QLabel(content[key])) - top.addLayout(key_layout) - top.addLayout(value_layout) - main.addLayout(top) - controls_tab.setLayout(main) - return controls_tab - - def closeEvent(self, event): - Thread(target=self.end).start() - event.accept() - -if __name__ == "__main__": - app = QApplication(argv) - window = Window(argv[1] if len(argv) > 1 else 10, lambda: print("Restart function")) - - app.exec() diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 5712fd8..ed2d8e8 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -17,8 +17,9 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): messages_sent:list[MessageStub] = [] keyheld = False pick = False - next_message = None pick_device = -1 + pick_running = False + next_message = None parent:EmulatorStub = AsyncEmulator def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object @@ -30,7 +31,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self.listener.start() msg = """ keyboard input: - space: Step a single time through messages + shift: Step a single time through messages f: Fast-forward through messages enter: Kill stepper daemon finish algorithm tab: Show all messages currently waiting to be transmitted @@ -40,20 +41,16 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - - #if self.next_message == None or not index == self.pick_device: - # while not self.next_message == None: - # pass - #self._progress.acquire() - + self._progress.acquire() + #release the lock if there has been specified which message to be delivered next if not self.next_message == None and not index == self.pick_device: self._progress.release() - while not self.next_message == None: - pass + self.pick_running = False + #restart this function to make sure all devices are at the correct position in the execution return self.dequeue(index) - #print(f'Device {index} has the lock') + #if the current thread is meant to receive a specific message, dequeue here instead of through the parent function if not self.next_message == None and index == self.pick_device: print(f"Device {index} is receiving through pick") if self.parent == AsyncEmulator: @@ -66,36 +63,35 @@ def dequeue(self, index: int) -> Optional[MessageStub]: else: result = self.parent.dequeue(self, index, True) + if self.pick_running: + self.pick_running = False if result != None: self.messages_received.append(result) self.last_action = "receive" if self._stepping and self._stepper.is_alive(): self.step() - #print(f'Device {index} is releasing the lock') self._progress.release() return result def queue(self, message: MessageStub): - - if self.next_message == None or not message.source == self.pick_device: - while not self.next_message == None: - pass self._progress.acquire() - #print(f'Device {message.source} has the lock') + if not self.next_message == None and not message.source == self.pick_device: + self._progress.release() + self.pick_running = False + return self.queue(message) self.parent.queue(self, message, True) self.last_action = "send" self.messages_sent.append(message) + if self.pick_running: + self.pick_running = False if self._stepping and self._stepper.is_alive(): self.step() - - if self.next_message == None or not self.next_message == self.pick_device: - #print(f'Device {message.source} is releasing the lock') - self._progress.release() + self._progress.release() - #the main program to stop execution + #the main function to stop execution def step(self): if not self._single: print(f'\t{self._messages_sent}: Step?') @@ -117,7 +113,7 @@ def _on_press(self, key:keyboard.KeyCode | keyboard.Key): key = key.name if key == "f" or key == "enter": self._stepping = False - elif key == "space" and not self._keyheld: + elif key == "shift" and not self.keyheld: self._single = True elif key == "tab": self.print_transit() @@ -125,7 +121,7 @@ def _on_press(self, key:keyboard.KeyCode | keyboard.Key): self.pick = True elif key == "e": self.swap_emulator() - self._keyheld = True + self.keyheld = True #listen for released keys def _on_release(self, key:keyboard.KeyCode | keyboard.Key): @@ -137,7 +133,7 @@ def _on_release(self, key:keyboard.KeyCode | keyboard.Key): key = key.name if key == "f": self._stepping = True - self._keyheld = False + self.keyheld = False #print all messages in transit def print_transit(self): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 99eca05..9bed9eb 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -175,7 +175,10 @@ def inner_execute(): self.pick_window = False size = len(self.emulator.messages_received) while not self.emulator.next_message == None: + self.emulator.pick_running = True self.step() + while self.emulator.pick_running: + pass #sleep(.1) assert len(self.emulator.messages_received) == size+1 @@ -307,7 +310,7 @@ def main(self, num_devices, restart_function): def controls(self): controls_tab = QWidget() content = { - 'Space': 'Step a single time through messages', + 'shift': 'Step a single time through messages', 'f': 'Fast forward through messages', 'Enter': 'Kill stepper daemon and run as an async emulator', 'tab': 'Show all messages currently waiting to be transmitted', diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index d640177..ed21ac8 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -16,6 +16,7 @@ start_button = QPushButton("Start") button_layout.addWidget(start_button) advanced_button = QPushButton("Advanced") +advanced_button.setFixedWidth(80) button_layout.addWidget(advanced_button) main.addLayout(button_layout) input_area_labels = QHBoxLayout() From 3df5df0060c41fd11d23dfafab0789835d410ee8 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Thu, 15 Sep 2022 19:45:47 +0200 Subject: [PATCH 053/106] fixed the pick bug, added coloring to the terminal output and implemented the run method for the stepper --- emulators/AsyncEmulator.py | 7 +- emulators/Device.py | 1 + emulators/SteppingEmulator.py | 140 +++++++++++++++++++++++++--------- emulators/SyncEmulator.py | 8 +- emulators/exercise_overlay.py | 15 +++- 5 files changed, 128 insertions(+), 43 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 52e31f2..28d06af 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -8,6 +8,8 @@ from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub +RESET = "\u001B[0m" +GREEN = "\u001B[32m" class AsyncEmulator(EmulatorStub): @@ -22,7 +24,6 @@ def run(self): self._start_threads() self._progress.release() - # make sure the round_lock is locked initially while True: time.sleep(0.1) self._progress.acquire() @@ -38,7 +39,7 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\tSend {message}') + print(f'\t{GREEN}Send{RESET} {message}') if message.destination not in self._messages: self._messages[message.destination] = [] self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing @@ -60,7 +61,7 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._messages[index].pop() - print(f'\tRecieve {m}') + print(f'\t{GREEN}Recieve{RESET} {m}') if not stepper: self._progress.release() return m diff --git a/emulators/Device.py b/emulators/Device.py index 841e27e..8051689 100644 --- a/emulators/Device.py +++ b/emulators/Device.py @@ -9,6 +9,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._id = index self._medium = medium self._number_of_devices = number_of_devices + self._finished = False def run(self): raise NotImplementedError("You have to implement a run-method!") diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index ed2d8e8..d08c0a3 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -1,3 +1,6 @@ +import copy +import random +from time import sleep from typing import Optional from emulators.AsyncEmulator import AsyncEmulator from .EmulatorStub import EmulatorStub @@ -5,8 +8,11 @@ from emulators.MessageStub import MessageStub from pynput import keyboard from getpass import getpass #getpass to hide input, cleaner terminal -from threading import Lock, Thread #run getpass in seperate thread +from threading import Barrier, Lock, Thread #run getpass in seperate thread +RESET = "\u001B[0m" +CYAN = "\u001B[36m" +GREEN = "\u001B[32m" class SteppingEmulator(SyncEmulator, AsyncEmulator): @@ -26,45 +32,44 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here super().__init__(number_of_devices, kind) self._stepper = Thread(target=lambda: getpass(""), daemon=True) self._stepper.start() + self.barrier = Barrier(parties=number_of_devices) self.wait_lock = Lock() self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() - msg = """ - keyboard input: - shift: Step a single time through messages - f: Fast-forward through messages - enter: Kill stepper daemon finish algorithm - tab: Show all messages currently waiting to be transmitted - s: Pick the next message waiting to be transmitted to transmit next - e: Toggle between sync and async emulation + msg = f""" +{CYAN}keyboard input{RESET}: + {CYAN}shift{RESET}: Step a single time through messages + {CYAN}f{RESET}: Fast-forward through messages + {CYAN}enter{RESET}: Kill stepper daemon finish algorithm + {CYAN}tab{RESET}: Show all messages currently waiting to be transmitted + {CYAN}s{RESET}: Pick the next message waiting to be transmitted to transmit next + {CYAN}e{RESET}: Toggle between sync and async emulation """ print(msg) def dequeue(self, index: int) -> Optional[MessageStub]: - self._progress.acquire() - #release the lock if there has been specified which message to be delivered next - if not self.next_message == None and not index == self.pick_device: - self._progress.release() - self.pick_running = False - #restart this function to make sure all devices are at the correct position in the execution - return self.dequeue(index) - - #if the current thread is meant to receive a specific message, dequeue here instead of through the parent function - if not self.next_message == None and index == self.pick_device: - print(f"Device {index} is receiving through pick") - if self.parent == AsyncEmulator: - result = self._messages[index].pop(self._messages[index].index(self.next_message)) + #print(f'thread {index} is in dequeue') + if not self.next_message == None: + if not index == self.pick_device: + self.collectThread() + return self.dequeue(index) else: - result = self._last_round_messages[index].pop(self._last_round_messages[index].index(self.next_message)) - self.next_message = None - - print(f'\tRecieve {result}') + if self.parent == AsyncEmulator: + messages = self._messages + else: + messages = self._last_round_messages + result = messages[index].pop(messages[index].index(self.next_message)) + self.next_message = None + self.pick_device = -1 + self.barrier.reset() + print(f'\t{GREEN}Receive{RESET} {result}') + else: result = self.parent.dequeue(self, index, True) - if self.pick_running: - self.pick_running = False + self.pick_running = False + if result != None: self.messages_received.append(result) self.last_action = "receive" @@ -76,17 +81,17 @@ def dequeue(self, index: int) -> Optional[MessageStub]: def queue(self, message: MessageStub): self._progress.acquire() + #print(f'thread {message.source} is in queue') if not self.next_message == None and not message.source == self.pick_device: - self._progress.release() - self.pick_running = False + self.collectThread() return self.queue(message) - + self.parent.queue(self, message, True) self.last_action = "send" self.messages_sent.append(message) - if self.pick_running: - self.pick_running = False + self.pick_running = False + if self._stepping and self._stepper.is_alive(): self.step() self._progress.release() @@ -96,6 +101,7 @@ def step(self): if not self._single: print(f'\t{self._messages_sent}: Step?') while self._stepping: #run while waiting for input + sleep(.1) if self._single: #break while if the desired action is a single message self._single = False break @@ -133,7 +139,7 @@ def _on_release(self, key:keyboard.KeyCode | keyboard.Key): key = key.name if key == "f": self._stepping = True - self.keyheld = False + self.keyheld = False #print all messages in transit def print_transit(self): @@ -196,3 +202,69 @@ def pick_function(self): self._stepper.start() self._stepping = True print(f'\t{self._messages_sent}: Step?') + + def run(self): + self._progress.acquire() + for index in self.ids(): + self._awaits[index].acquire() + self._start_threads() + self._progress.release() + + self._round_lock.acquire() + while True: + if self.parent is AsyncEmulator: + sleep(.1) + self._progress.acquire() + # check if everyone terminated + if self.all_terminated(): + break + self._progress.release() + else: + self._round_lock.acquire() + # check if everyone terminated + self._progress.acquire() + print(f'## {GREEN}ROUND {self._rounds}{RESET} ##') + if self.all_terminated(): + self._progress.release() + break + # send messages + for index in self.ids(): + # intentionally change the order + if index in self._current_round_messages: + nxt = copy.deepcopy(self._current_round_messages[index]) + random.shuffle(nxt) + if index in self._last_round_messages: + self._last_round_messages[index] += nxt + else: + self._last_round_messages[index] = nxt + self._current_round_messages = {} + self.reset_done() + self._rounds += 1 + ids = [x for x in self.ids()] # convert to list to make it shuffleable + random.shuffle(ids) + for index in ids: + if self._awaits[index].locked(): + self._awaits[index].release() + self._progress.release() + + for t in self._threads: + t.join() + + def done(self, id): + return self.parent.done(self, id) + + def _run_thread(self, index: int): + super()._run_thread(index) + self._devices[index]._finished = True + + + + def collectThread(self): + #print("collecting a thread") + self.pick_running = False + self._progress.release() + try: + self.barrier.wait() + except: + pass + \ No newline at end of file diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 4783855..50eeaa0 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -6,6 +6,8 @@ from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub +RESET = "\u001B[0m" +GREEN = "\u001B[32m" class SyncEmulator(EmulatorStub): @@ -35,7 +37,7 @@ def run(self): self._round_lock.acquire() # check if everyone terminated self._progress.acquire() - print(f'## ROUND {self._rounds} ##') + print(f'## {GREEN}ROUND {self._rounds}{RESET} ##') if self.all_terminated(): self._progress.release() break @@ -66,7 +68,7 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\tSend {message}') + print(f'\t{GREEN}Send{RESET} {message}') if message.destination not in self._current_round_messages: self._current_round_messages[message.destination] = [] self._current_round_messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing @@ -86,7 +88,7 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._last_round_messages[index].pop() - print(f'\tReceive {m}') + print(f'\t{GREEN}Receive{RESET} {m}') if not stepper: self._progress.release() return m diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 9bed9eb..23de919 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -13,6 +13,10 @@ from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator +RESET = "\u001B[0m" +CYAN = "\u001B[36m" +RED = "\u001B[31m" + def circle_button_style(size, color = "black"): return f''' QPushButton {{ @@ -163,11 +167,16 @@ def pick(self): self.pick_window = True def execute(device, index): def inner_execute(): + if self.emulator._devices[device]._finished == True: + table.destroy(True, True) + print(f'{RED}The selected device has already finished execution!{RESET}') + return if self.emulator.parent is AsyncEmulator: message = self.emulator._messages[device][index] else: message = self.emulator._last_round_messages[device][index] - + + print(f'{CYAN}Choice from pick command{RESET}: {message}') self.emulator.pick_device = device self.emulator.next_message = message @@ -177,9 +186,9 @@ def inner_execute(): while not self.emulator.next_message == None: self.emulator.pick_running = True self.step() - while self.emulator.pick_running: + while self.emulator.pick_running and not self.emulator.all_terminated(): pass - #sleep(.1) + sleep(.1) assert len(self.emulator.messages_received) == size+1 From 3b33e5fc126335085178abff096f260d4f69a20f Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Fri, 16 Sep 2022 13:23:18 +0200 Subject: [PATCH 054/106] fixed windows printing --- emulators/AsyncEmulator.py | 11 +++++++++-- emulators/SteppingEmulator.py | 12 +++++++++--- emulators/SyncEmulator.py | 11 +++++++++-- emulators/exercise_overlay.py | 16 +++++++++++----- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 28d06af..adcda58 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -4,12 +4,19 @@ import time from threading import Lock from typing import Optional +from os import name from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub -RESET = "\u001B[0m" -GREEN = "\u001B[32m" +if name == "posix": + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" +else: + RESET = "" + CYAN = "" + GREEN = "" class AsyncEmulator(EmulatorStub): diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index d08c0a3..5150a49 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -9,10 +9,16 @@ from pynput import keyboard from getpass import getpass #getpass to hide input, cleaner terminal from threading import Barrier, Lock, Thread #run getpass in seperate thread +from os import name -RESET = "\u001B[0m" -CYAN = "\u001B[36m" -GREEN = "\u001B[32m" +if name == "posix": + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" +else: + RESET = "" + CYAN = "" + GREEN = "" class SteppingEmulator(SyncEmulator, AsyncEmulator): diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 50eeaa0..002eca3 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -1,13 +1,20 @@ import copy import random import threading +from os import name from typing import Optional from emulators.EmulatorStub import EmulatorStub from emulators.MessageStub import MessageStub -RESET = "\u001B[0m" -GREEN = "\u001B[32m" +if name == "posix": + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" +else: + RESET = "" + CYAN = "" + GREEN = "" class SyncEmulator(EmulatorStub): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 23de919..e19e960 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -5,6 +5,7 @@ from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt from sys import argv +from os import name from math import cos, sin, pi from emulators.AsyncEmulator import AsyncEmulator from emulators.MessageStub import MessageStub @@ -13,9 +14,16 @@ from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator -RESET = "\u001B[0m" -CYAN = "\u001B[36m" -RED = "\u001B[31m" +if name == "posix": + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" + RED = "" +else: + RESET = "" + CYAN = "" + GREEN = "" + RED = "\u001B[31m" def circle_button_style(size, color = "black"): return f''' @@ -199,8 +207,6 @@ def inner_execute(): messages = self.emulator._messages else: messages = self.emulator._last_round_messages - if len(messages) == 0: - return for item in messages.items(): keys.append(item[0]) keys.sort() From a3dacfe908b42a36029f07a55206830bb7b070be Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Fri, 16 Sep 2022 15:24:45 +0200 Subject: [PATCH 055/106] new printing and new runner layout --- emulators/AsyncEmulator.py | 4 +- emulators/SteppingEmulator.py | 10 +++-- emulators/SyncEmulator.py | 6 +-- emulators/exercise_overlay.py | 3 +- exercise_runner.py | 14 ++++++- exercise_runner_overlay.py | 73 ++++++++++++++++------------------- 6 files changed, 59 insertions(+), 51 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index adcda58..b8125f7 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -79,8 +79,8 @@ def done(self, index: int): def print_statistics(self): - print(f'\tTotal {self._messages_sent} messages') - print(f'\tAverage {self._messages_sent/len(self._devices)} messages/device') + print(f'\t{GREEN}Total{RESET} {self._messages_sent} messages') + print(f'\t{GREEN}Average{RESET} {self._messages_sent/len(self._devices)} messages/device') def terminated(self, index:int): self._progress.acquire() diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 5150a49..841a632 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -32,6 +32,7 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): pick_device = -1 pick_running = False next_message = None + log = None parent:EmulatorStub = AsyncEmulator def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object @@ -42,6 +43,8 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self.wait_lock = Lock() self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) self.listener.start() + self.messages_received:list[MessageStub] = [] + self.messages_sent:list[MessageStub] = [] msg = f""" {CYAN}keyboard input{RESET}: {CYAN}shift{RESET}: Step a single time through messages @@ -105,7 +108,7 @@ def queue(self, message: MessageStub): #the main function to stop execution def step(self): if not self._single: - print(f'\t{self._messages_sent}: Step?') + print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}]') while self._stepping: #run while waiting for input sleep(.1) if self._single: #break while if the desired action is a single message @@ -207,7 +210,7 @@ def pick_function(self): self._stepper = Thread(target=lambda: getpass(""), daemon=True) #restart stepper daemon self._stepper.start() self._stepping = True - print(f'\t{self._messages_sent}: Step?') + print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}]') def run(self): self._progress.acquire() @@ -263,7 +266,8 @@ def _run_thread(self, index: int): super()._run_thread(index) self._devices[index]._finished = True - + def print_statistics(self): + return self.parent.print_statistics(self) def collectThread(self): #print("collecting a thread") diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 002eca3..c921d6e 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -116,9 +116,9 @@ def done(self, index: int): def print_statistics(self): - print(f'\tTotal {self._messages_sent} messages') - print(f'\tAverage {self._messages_sent/len(self._devices)} messages/device') - print(f'\tTotal {self._rounds} rounds') + print(f'\t{GREEN}Total:{RESET} {self._messages_sent} messages') + print(f'\t{GREEN}Average:{RESET} {self._messages_sent/len(self._devices)} messages/device') + print(f'\t{GREEN}Total:{RESET} {self._rounds} rounds') def terminated(self, index:int): self._progress.acquire() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index e19e960..89f7b0e 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -23,7 +23,7 @@ RESET = "" CYAN = "" GREEN = "" - RED = "\u001B[31m" + RED = "" def circle_button_style(size, color = "black"): return f''' @@ -65,6 +65,7 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): self.setFixedSize(self.w, self.h) layout = QVBoxLayout() tabs = QTabWidget() + tabs.setFixedSize(self.w-20, self.h-20) tabs.addTab(self.main(elements, restart_function), 'Main') tabs.addTab(self.controls(), 'controls') layout.addWidget(tabs) diff --git a/exercise_runner.py b/exercise_runner.py index ae33102..90aa62a 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -1,5 +1,6 @@ import argparse import inspect +from os import name from threading import Thread from emulators.exercise_overlay import Window @@ -19,6 +20,15 @@ from emulators.SyncEmulator import SyncEmulator from emulators.SteppingEmulator import SteppingEmulator +if name == "posix": + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" +else: + RESET = "" + CYAN = "" + GREEN = "" + def fetch_alg(lecture: str, algorithm: str): if '.' in algorithm or ';' in algorithm: raise ValueError(f'"." and ";" are not allowed as names of solutions.') @@ -53,9 +63,9 @@ def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_d def run_instance(): if instance is not None: instance.run() - print(f'Execution Complete') + print(f'{CYAN}Execution Complete{RESET}') instance.print_result() - print('Statistics') + print(f'{CYAN}Statistics{RESET}') instance.print_statistics() else: raise NotImplementedError( diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index ed21ac8..5a7b835 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,46 +1,48 @@ from exercise_runner import run_exercise from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QComboBox from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt from sys import argv app = QApplication(argv) windows = list() - +#new window = QWidget() window.setWindowIcon(QIcon('icon.ico')) window.setWindowTitle("Distributed Exercises AAU") main = QVBoxLayout() window.setFixedSize(600, 100) -button_layout = QHBoxLayout() start_button = QPushButton("Start") -button_layout.addWidget(start_button) -advanced_button = QPushButton("Advanced") -advanced_button.setFixedWidth(80) -button_layout.addWidget(advanced_button) -main.addLayout(button_layout) -input_area_labels = QHBoxLayout() -input_area_areas = QHBoxLayout() -actions:dict[str, list[QLineEdit, str]] = {'Algorithm': [QLineEdit, 'PingPong'], 'Type': [QLineEdit, 'stepping'], 'Devices': [QLineEdit, 3]} -input_area_labels.addWidget(QLabel('Lecture')) -combobox = QComboBox() -combobox.addItems([str(i) for i in range(13) if i != 3]) -input_area_areas.addWidget(combobox) +main.addWidget(start_button) +input_area = QHBoxLayout() +lecture_layout = QVBoxLayout() +lecture_layout.addWidget(QLabel("Lecture"), alignment=Qt.AlignmentFlag.AlignCenter) +lecture_combobox = QComboBox() +lecture_combobox.addItems([str(i) for i in range(13) if i != 3]) +lecture_layout.addWidget(lecture_combobox) +input_area.addLayout(lecture_layout) +type_layout = QVBoxLayout() +type_layout.addWidget(QLabel("Type"), alignment=Qt.AlignmentFlag.AlignCenter) +type_combobox = QComboBox() +type_combobox.addItems(["stepping", "async", "sync"]) +type_layout.addWidget(type_combobox) +input_area.addLayout(type_layout) +algorithm_layout = QVBoxLayout() +algorithm_layout.addWidget(QLabel("Algorithm"), alignment=Qt.AlignmentFlag.AlignCenter) +algorithm_input = QLineEdit() +algorithm_input.setText("PingPong") +algorithm_layout.addWidget(algorithm_input) +input_area.addLayout(algorithm_layout) +devices_layout = QVBoxLayout() +devices_layout.addWidget(QLabel("Devices"), alignment=Qt.AlignmentFlag.AlignCenter) +devices_input = QLineEdit() +devices_input.setText("3") +devices_layout.addWidget(devices_input) +input_area.addLayout(devices_layout) +main.addLayout(input_area) starting_exercise = False - -for action in actions.items(): - input_area_labels.addWidget(QLabel(action[0])) - field = QLineEdit() - input_area_areas.addWidget(field) - field.setText(str(action[1][1])) - actions[action[0]][0] = field -main.addLayout(input_area_labels) -main.addLayout(input_area_areas) -actions['Algorithm'][0].setDisabled(True) -is_disabled = True - -actions['Lecture'] = [combobox] - +actions:dict[str, QLineEdit | QComboBox] = {"Lecture":lecture_combobox, "Type":type_combobox, "Algorithm":algorithm_input, "Devices":devices_input} def text_changed(text): exercises = { @@ -58,28 +60,19 @@ def text_changed(text): 12:'AodvNode'} lecture = int(text) - actions['Algorithm'][0].setText(exercises[lecture]) + actions['Algorithm'].setText(exercises[lecture]) -combobox.currentTextChanged.connect(text_changed) +lecture_combobox.currentTextChanged.connect(text_changed) def start_exercise(): global starting_exercise if not starting_exercise: starting_exercise = True - windows.append(run_exercise(int(actions['Lecture'][0].currentText()), actions['Algorithm'][0].text(), actions['Type'][0].text(), int(actions['Devices'][0].text()))) + windows.append(run_exercise(int(actions['Lecture'].currentText()), actions['Algorithm'].text(), actions['Type'].currentText(), int(actions['Devices'].text()))) starting_exercise = False -def advanced(): - global is_disabled - if is_disabled: - actions['Algorithm'][0].setDisabled(False) - is_disabled = False - else: - actions['Algorithm'][0].setDisabled(True) - is_disabled = True start_button.clicked.connect(start_exercise) -advanced_button.clicked.connect(advanced) window.setLayout(main) window.show() app.exec() From 5394b1cc9e5d3413b2937a5b476a627039e3d503 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Fri, 16 Sep 2022 20:31:05 +0200 Subject: [PATCH 056/106] updated terminal interactivity to not trigger in the same way as the gui --- emulators/AsyncEmulator.py | 4 +- emulators/SteppingEmulator.py | 154 ++++++++++++++++------------------ emulators/exercise_overlay.py | 36 ++++---- 3 files changed, 91 insertions(+), 103 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index b8125f7..5420009 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -46,7 +46,7 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\t{GREEN}Send{RESET} {message}') + print(f'\r\t{GREEN}Send{RESET} {message}') if message.destination not in self._messages: self._messages[message.destination] = [] self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing @@ -68,7 +68,7 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._messages[index].pop() - print(f'\t{GREEN}Recieve{RESET} {m}') + print(f'\r\t{GREEN}Recieve{RESET} {m}') if not stepper: self._progress.release() return m diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 841a632..8efdcf1 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -22,13 +22,11 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): - _stepping = True _single = False last_action = "" messages_received:list[MessageStub] = [] messages_sent:list[MessageStub] = [] keyheld = False - pick = False pick_device = -1 pick_running = False next_message = None @@ -37,22 +35,25 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object super().__init__(number_of_devices, kind) - self._stepper = Thread(target=lambda: getpass(""), daemon=True) - self._stepper.start() + #self._stepper = Thread(target=lambda: getpass(""), daemon=True) + #self._stepper.start() self.barrier = Barrier(parties=number_of_devices) + self.is_stepping = True self.wait_lock = Lock() - self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) - self.listener.start() + #self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) + #self.listener.start() + self.shell = Thread(target=self.prompt, daemon=True) + self.shell.start() self.messages_received:list[MessageStub] = [] self.messages_sent:list[MessageStub] = [] msg = f""" -{CYAN}keyboard input{RESET}: - {CYAN}shift{RESET}: Step a single time through messages - {CYAN}f{RESET}: Fast-forward through messages - {CYAN}enter{RESET}: Kill stepper daemon finish algorithm - {CYAN}tab{RESET}: Show all messages currently waiting to be transmitted - {CYAN}s{RESET}: Pick the next message waiting to be transmitted to transmit next - {CYAN}e{RESET}: Toggle between sync and async emulation +{CYAN}Shell input:{RESET}: + {CYAN}step(press return){RESET}: Step a single time through messages + {CYAN}exit{RESET}: Finish the execution of the algorithm + {CYAN}queue{RESET}: Show all messages currently waiting to be transmitted + {CYAN}queue {RESET}: Show all messages currently waiting to be transmitted to a specific device + {CYAN}pick{RESET}: Pick the next message waiting to be transmitted to transmit next + {CYAN}swap{RESET}: Toggle between sync and async emulation """ print(msg) @@ -72,7 +73,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: self.next_message = None self.pick_device = -1 self.barrier.reset() - print(f'\t{GREEN}Receive{RESET} {result}') + print(f'\r\t{GREEN}Receive{RESET} {result}') else: result = self.parent.dequeue(self, index, True) @@ -82,7 +83,7 @@ def dequeue(self, index: int) -> Optional[MessageStub]: if result != None: self.messages_received.append(result) self.last_action = "receive" - if self._stepping and self._stepper.is_alive(): + if self.is_stepping: self.step() self._progress.release() @@ -101,54 +102,71 @@ def queue(self, message: MessageStub): self.pick_running = False - if self._stepping and self._stepper.is_alive(): + if self.is_stepping: self.step() self._progress.release() #the main function to stop execution def step(self): - if not self._single: - print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}]') - while self._stepping: #run while waiting for input + while self.is_stepping: #run while waiting for input sleep(.1) if self._single: #break while if the desired action is a single message self._single = False break - elif self.pick: - self.pick = False - self.pick_function() - #listen for pressed keys - def _on_press(self, key:keyboard.KeyCode | keyboard.Key): - try: - #for keycode class - key = key.char - except: - #for key class - key = key.name - if key == "f" or key == "enter": - self._stepping = False - elif key == "shift" and not self.keyheld: + def pick(self): + self.print_transit() + if self.parent is AsyncEmulator: + messages = self._messages + else: + messages = self._last_round_messages + keys = [] + for key in messages.keys(): + if len(messages[key]) > 0: + keys.append(key) + print(f'{GREEN}Available devices:{RESET} {keys}') + device = int(input(f'Specify device: ')) + self.print_transit_for_device(device) + index = int(input(f'Specify index of the next message: ')) + self.pick_device = device + self.next_message = messages[device][index] + while not self.next_message == None: + self.pick_running = True self._single = True - elif key == "tab": - self.print_transit() - elif key == "s": - self.pick = True - elif key == "e": - self.swap_emulator() - self.keyheld = True + while self.pick_running and not self.all_terminated(): + pass + sleep(.1) + + + def prompt(self): + self.prompt_active = True + line = "" + while not line == "exit": + sleep(.1) + line = input(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ') + args = line.split(" ") + match args[0]: + case "": + self._single = True + case "queue": + if len(args) == 1: + self.print_transit() + else: + self.print_transit_for_device(int(args[1])) + case "exit": + self.is_stepping = False + case "swap": + self.swap_emulator() + case "pick": + try: + self.pick() + except ValueError: + pass + self.prompt_active = False + + def print_prompt(self): + print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ', end="", flush=True) - #listen for released keys - def _on_release(self, key:keyboard.KeyCode | keyboard.Key): - try: - #for key class - key = key.char - except: - #for keycode class - key = key.name - if key == "f": - self._stepping = True - self.keyheld = False #print all messages in transit def print_transit(self): @@ -164,14 +182,14 @@ def print_transit(self): #print all messages in transit to specified device def print_transit_for_device(self, device): - print(f'Messages in transit to device #{device}') + print(f'{GREEN}Messages in transit to device #{device}{RESET}') index = 0 if self.parent is AsyncEmulator: messages:list[MessageStub] = self._messages.get(device) elif self.parent is SyncEmulator: messages:list[MessageStub] = self._last_round_messages.get(device) for message in messages: - print(f'{index}: {message}') + print(f'{CYAN}{index}{RESET}: {message}') index+=1 #swap between which parent class the program will run in between deliveries @@ -181,36 +199,6 @@ def swap_emulator(self): elif self.parent is SyncEmulator: self.parent = AsyncEmulator print(f'Changed emulator to {self.parent.__name__}') - - #Pick command function, this lets the user alter the queue for a specific device - def pick_function(self): - try: - print("Press return to proceed") #prompt the user to kill the stepper daemon - while self._stepper.is_alive(): #wait for the stepper to be killed - pass - self.print_transit() - keys = [] - if self.parent is AsyncEmulator: - for key in self._messages.keys(): - keys.append(key) - elif self.parent is SyncEmulator: - for key in self._last_round_messages.keys(): - keys.append(key) - print(f'Available devices: {keys}') - device = int(input(f'Specify device to send to: ')) #ask for user input to specify which device queue to alter - self.print_transit_for_device(device) - index = int(input(f'Specify index of the next element to send: ')) #ask for user input to specify a message to send - if self.parent is AsyncEmulator: - self._messages[device].append(self._messages[device].pop(index)) #pop index from input and append to the end of the list - elif self.parent is SyncEmulator: - self._last_round_messages[device].append(self._last_round_messages[device].pop(index)) #pop index from input and append to the end of the list - except Exception as e: - print(e) - if not self._stepper.is_alive(): - self._stepper = Thread(target=lambda: getpass(""), daemon=True) #restart stepper daemon - self._stepper.start() - self._stepping = True - print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}]') def run(self): self._progress.acquire() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 89f7b0e..5a8fc18 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -18,7 +18,7 @@ RESET = "\u001B[0m" CYAN = "\u001B[36m" GREEN = "\u001B[32m" - RED = "" + RED = "\u001B[31m" else: RESET = "" CYAN = "" @@ -185,7 +185,7 @@ def inner_execute(): else: message = self.emulator._last_round_messages[device][index] - print(f'{CYAN}Choice from pick command{RESET}: {message}') + print(f'\r{CYAN}Choice from pick command{RESET}: {message}') self.emulator.pick_device = device self.emulator.next_message = message @@ -241,20 +241,15 @@ def closeEvent(self, event): table.setFixedSize(150*len(self.emulator._devices)+1, 400) table.show() self.windows.append(table) - - - - - - def stop_stepper(self): - self.emulator.listener.stop() - self.emulator.listener.join() def end(self): - self.emulator._stepping = False + if self.emulator.all_terminated(): + return + self.emulator.is_stepping = False while not self.emulator.all_terminated(): self.set_device_color() - Thread(target=self.stop_stepper, daemon=True).start() + sleep(.1) + self.emulator.print_prompt() def set_device_color(self): sleep(.1) @@ -276,8 +271,13 @@ def step(self): if self.emulator.all_terminated(): Thread(target=self.stop_stepper, daemon=True).start() self.set_device_color() + self.emulator.print_prompt() def restart_algorithm(self, function): + if self.emulator.prompt_active: + print(f'\r{RED}Please type "exit" in the prompt below{RESET}') + self.emulator.print_prompt() + return self.windows.append(function()) def main(self, num_devices, restart_function): @@ -326,12 +326,12 @@ def main(self, num_devices, restart_function): def controls(self): controls_tab = QWidget() content = { - 'shift': 'Step a single time through messages', - 'f': 'Fast forward through messages', - 'Enter': 'Kill stepper daemon and run as an async emulator', - 'tab': 'Show all messages currently waiting to be transmitted', - 's': 'Pick the next message waiting to be transmitted to transmit next', - 'e': 'Toggle between sync and async emulation' + 'step(press return)': 'Step a single time through messages', + 'exit': 'Finish the execution of the algorithm', + 'queue': 'Show all messages currently waiting to be transmitted', + 'queue ': 'Show all messages currently waiting to be transmitted to a specific device', + 'pick': 'Pick the next message waiting to be transmitted to transmit next', + 'swap': 'Toggle between sync and async emulation' } main = QVBoxLayout() main.setAlignment(Qt.AlignmentFlag.AlignTop) From e72d2cb880ec3943b394265d1cda5c4edeacbb78 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Fri, 16 Sep 2022 20:42:15 +0200 Subject: [PATCH 057/106] changed stepper to a barrier, no idea why i hadn't considered doing that before --- emulators/SteppingEmulator.py | 14 +++++++------- emulators/exercise_overlay.py | 9 ++++++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 8efdcf1..6a3106a 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -38,6 +38,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here #self._stepper = Thread(target=lambda: getpass(""), daemon=True) #self._stepper.start() self.barrier = Barrier(parties=number_of_devices) + self.step_barrier = Barrier(parties=2) self.is_stepping = True self.wait_lock = Lock() #self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) @@ -108,11 +109,8 @@ def queue(self, message: MessageStub): #the main function to stop execution def step(self): - while self.is_stepping: #run while waiting for input - sleep(.1) - if self._single: #break while if the desired action is a single message - self._single = False - break + if self.is_stepping: + self.step_barrier.wait() def pick(self): self.print_transit() @@ -142,12 +140,13 @@ def prompt(self): self.prompt_active = True line = "" while not line == "exit": - sleep(.1) + sleep(1) line = input(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ') args = line.split(" ") match args[0]: case "": - self._single = True + if not self.all_terminated(): + self.step_barrier.wait() case "queue": if len(args) == 1: self.print_transit() @@ -155,6 +154,7 @@ def prompt(self): self.print_transit_for_device(int(args[1])) case "exit": self.is_stepping = False + self.step_barrier.wait() case "swap": self.swap_emulator() case "pick": diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 5a8fc18..f1e63ac 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -246,6 +246,7 @@ def end(self): if self.emulator.all_terminated(): return self.emulator.is_stepping = False + self.emulator.step_barrier.wait() while not self.emulator.all_terminated(): self.set_device_color() sleep(.1) @@ -267,9 +268,11 @@ def set_device_color(self): self.last_message = last_message def step(self): - self.emulator._single = True - if self.emulator.all_terminated(): - Thread(target=self.stop_stepper, daemon=True).start() + if not self.emulator.all_terminated(): + self.emulator.step_barrier.wait() + + while self.emulator.step_barrier.n_waiting == 0: + pass self.set_device_color() self.emulator.print_prompt() From 3999af352aca6838318fc6e0235fe42bfca3ba70 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Sat, 17 Sep 2022 18:26:41 +0200 Subject: [PATCH 058/106] forgot to change shell to use the barrier, added a lock to avoid commands being run simultaneously --- emulators/SteppingEmulator.py | 6 ++++-- emulators/exercise_overlay.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 6a3106a..a8077e2 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -40,7 +40,7 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here self.barrier = Barrier(parties=number_of_devices) self.step_barrier = Barrier(parties=2) self.is_stepping = True - self.wait_lock = Lock() + self.input_lock = Lock() #self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) #self.listener.start() self.shell = Thread(target=self.prompt, daemon=True) @@ -130,7 +130,7 @@ def pick(self): self.next_message = messages[device][index] while not self.next_message == None: self.pick_running = True - self._single = True + self.step_barrier.wait() while self.pick_running and not self.all_terminated(): pass sleep(.1) @@ -142,6 +142,7 @@ def prompt(self): while not line == "exit": sleep(1) line = input(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ') + self.input_lock.acquire() args = line.split(" ") match args[0]: case "": @@ -162,6 +163,7 @@ def prompt(self): self.pick() except ValueError: pass + self.input_lock.release() self.prompt_active = False def print_prompt(self): diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index f1e63ac..7b08d95 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -268,8 +268,10 @@ def set_device_color(self): self.last_message = last_message def step(self): + self.emulator.input_lock.acquire() if not self.emulator.all_terminated(): self.emulator.step_barrier.wait() + self.emulator.input_lock.release() while self.emulator.step_barrier.n_waiting == 0: pass From 19db8bad9c9cfe9803fb227c6f2da19ba8939411 Mon Sep 17 00:00:00 2001 From: Sean <37299188+sdfg610@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:32:53 +0200 Subject: [PATCH 059/106] Fixed few typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89dfb5f..5e14412 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return False return True ``` - Does it work? Each routing table should believe it is completed just one. How many times the routing tables appear to be completed? + Does it work? Each routing table should believe it is completed just one row short. How many times the routing tables appear to be completed? 4. Try this other approach, which works better: 1. ```python def routing_table_complete(self): @@ -122,7 +122,7 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return True ``` Is it realistic for a real network? -3. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right after receiving the `RoutingMessage` for itself? What happens to the rest of the nodes? +3. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right after receiving the `RoutableMessage` for itself? What happens to the rest of the nodes? 4. What happens, if a link has a negative cost? How many messages are sent before the `routing_tables` converge? # Exercise 3 From b8f44f27db0aa218e00542a78dc602d6b8f22bae Mon Sep 17 00:00:00 2001 From: Sean <37299188+sdfg610@users.noreply.github.com> Date: Fri, 23 Sep 2022 08:36:37 +0200 Subject: [PATCH 060/106] Added a notice to "Exercise 2, Task 1" Some groups struggled with debugging the `RipCommunication` Algorithm in Task 1 since they needed to first complete Task 2 to get the routers/nodes to communicate. I added a notice to inform future semesters of this. Also added a word that seemed to be missing. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e14412..e1deb74 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn ``` # Exercise 2 -1. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. +1. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. _(NOTICE: To run/debug the protocol, you must first implement the network topology described in "task 2.0" below.)_ 2. In the `__init__` of `RipCommunication`, create a ring topology (that is, set up who are the neighbors of each device). Consider a ring size of 10 devices. 1. How many messages are sent in total before the routing_tables of all nodes are synchronized? 2. How can you "know" that the routing tables are complete and you can start using the network to route packets? Consider the general case of internet, and the specific case of our toy ring network. @@ -109,7 +109,7 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return False return True ``` - Does it work? Each routing table should believe it is completed just one row short. How many times the routing tables appear to be completed? + Does it work? Each routing table should believe it is completed just one row short. How many times do the routing tables appear to be completed? 4. Try this other approach, which works better: 1. ```python def routing_table_complete(self): From 56d4c9af2332dedff1a51523f545ba56a466f3c0 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Tue, 27 Sep 2022 09:03:56 +0200 Subject: [PATCH 061/106] added a switch for cli and gui instead of running both simultaneously --- emulators/SteppingEmulator.py | 20 ++++++++++++-------- emulators/SyncEmulator.py | 6 +++--- emulators/exercise_overlay.py | 22 ++++++++++++++-------- exercise_runner.py | 20 +++++++++++++++----- exercise_runner_overlay.py | 2 +- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index a8077e2..1323240 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -44,7 +44,6 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here #self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) #self.listener.start() self.shell = Thread(target=self.prompt, daemon=True) - self.shell.start() self.messages_received:list[MessageStub] = [] self.messages_sent:list[MessageStub] = [] msg = f""" @@ -172,26 +171,31 @@ def print_prompt(self): #print all messages in transit def print_transit(self): - print("Messages in transit:") + print(f"{CYAN}Messages in transit:{RESET}") if self.parent is AsyncEmulator: for messages in self._messages.values(): for message in messages: - print(f'{message}') + print(f'\t{message}') elif self.parent is SyncEmulator: for messages in self._last_round_messages.values(): for message in messages: - print(f'{message}') + print(f'\t{message}') #print all messages in transit to specified device def print_transit_for_device(self, device): - print(f'{GREEN}Messages in transit to device #{device}{RESET}') + print(f'{CYAN}Messages in transit to device #{device}{RESET}') + print(f'\t{CYAN}index{RESET}: ') index = 0 if self.parent is AsyncEmulator: + if not device in self._messages.keys(): + return messages:list[MessageStub] = self._messages.get(device) elif self.parent is SyncEmulator: + if not device in self._last_round_messages.keys(): + return messages:list[MessageStub] = self._last_round_messages.get(device) for message in messages: - print(f'{CYAN}{index}{RESET}: {message}') + print(f'\t{CYAN}{index}{RESET}: {message}') index+=1 #swap between which parent class the program will run in between deliveries @@ -200,7 +204,7 @@ def swap_emulator(self): self.parent = SyncEmulator elif self.parent is SyncEmulator: self.parent = AsyncEmulator - print(f'Changed emulator to {self.parent.__name__}') + print(f'Changed emulator to {GREEN}{self.parent.__name__}{RESET}') def run(self): self._progress.acquire() @@ -222,7 +226,7 @@ def run(self): self._round_lock.acquire() # check if everyone terminated self._progress.acquire() - print(f'## {GREEN}ROUND {self._rounds}{RESET} ##') + print(f'\r\t## {GREEN}ROUND {self._rounds}{RESET} ##') if self.all_terminated(): self._progress.release() break diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index c921d6e..99d0b6c 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -44,7 +44,7 @@ def run(self): self._round_lock.acquire() # check if everyone terminated self._progress.acquire() - print(f'## {GREEN}ROUND {self._rounds}{RESET} ##') + print(f'\r\t## {GREEN}ROUND {self._rounds}{RESET} ##') if self.all_terminated(): self._progress.release() break @@ -75,7 +75,7 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\t{GREEN}Send{RESET} {message}') + print(f'\r\t{GREEN}Send{RESET} {message}') if message.destination not in self._current_round_messages: self._current_round_messages[message.destination] = [] self._current_round_messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing @@ -95,7 +95,7 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._last_round_messages[index].pop() - print(f'\t{GREEN}Receive{RESET} {m}') + print(f'\r\t{GREEN}Receive{RESET} {m}') if not stepper: self._progress.release() return m diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 7b08d95..bec23ca 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -240,7 +240,6 @@ def closeEvent(self, event): table = MyTable(content, "Pick a message to be transmitted next to a device") table.setFixedSize(150*len(self.emulator._devices)+1, 400) table.show() - self.windows.append(table) def end(self): if self.emulator.all_terminated(): @@ -250,7 +249,7 @@ def end(self): while not self.emulator.all_terminated(): self.set_device_color() sleep(.1) - self.emulator.print_prompt() + #self.emulator.print_prompt() def set_device_color(self): sleep(.1) @@ -276,13 +275,13 @@ def step(self): while self.emulator.step_barrier.n_waiting == 0: pass self.set_device_color() - self.emulator.print_prompt() + #self.emulator.print_prompt() def restart_algorithm(self, function): - if self.emulator.prompt_active: - print(f'\r{RED}Please type "exit" in the prompt below{RESET}') - self.emulator.print_prompt() - return + #if self.emulator.prompt_active: + #print(f'\r{RED}Please type "exit" in the prompt below{RESET}') + #self.emulator.print_prompt() + #return self.windows.append(function()) def main(self, num_devices, restart_function): @@ -313,7 +312,7 @@ def main(self, num_devices, restart_function): button.clicked.connect(self.show_device_data(i)) self.buttons[i] = button - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.emulator.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} + button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} inner_layout = QHBoxLayout() index = 0 for action in button_actions.items(): @@ -327,6 +326,13 @@ def main(self, num_devices, restart_function): layout.addLayout(inner_layout) return main_tab + + def swap_emulator(self): + self.emulator.input_lock.acquire() + print() + self.emulator.swap_emulator() + #self.emulator.print_prompt() + self.emulator.input_lock.release() def controls(self): controls_tab = QWidget() diff --git a/exercise_runner.py b/exercise_runner.py index 90aa62a..0cbcc44 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -41,7 +41,7 @@ def fetch_alg(lecture: str, algorithm: str): return alg -def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_devices: int): +def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_devices: int, gui:bool): print( f'Running Lecture {lecture_no} Algorithm {algorithm} in a network of type [{network_type}] using {number_of_devices} devices') if number_of_devices < 2: @@ -71,10 +71,12 @@ def run_instance(): raise NotImplementedError( f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') Thread(target=run_instance).start() - if isinstance(instance, SteppingEmulator): - window = Window(number_of_devices, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices), instance) + if isinstance(instance, SteppingEmulator) and gui: + window = Window(number_of_devices, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices, True), instance) window.show() return window + if not gui: + instance.shell.start() if __name__ == "__main__": parser = argparse.ArgumentParser(description='For exercises in Distributed Systems.') @@ -86,6 +88,14 @@ def run_instance(): help='whether to use [async] or [sync] network', required=True, choices=['async', 'sync', 'stepping']) parser.add_argument('--devices', metavar='N', type=int, nargs=1, help='Number of devices to run', required=True) + parser.add_argument("--gui", action="store_true", help="Toggle the gui or cli", required=False) args = parser.parse_args() - - run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0]) \ No newline at end of file + import sys + if args.gui: + from PyQt6.QtWidgets import QApplication + app = QApplication(sys.argv) + run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], True) + app.exec() + else: + run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], False) + \ No newline at end of file diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 5a7b835..eb7d0fd 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -68,7 +68,7 @@ def start_exercise(): global starting_exercise if not starting_exercise: starting_exercise = True - windows.append(run_exercise(int(actions['Lecture'].currentText()), actions['Algorithm'].text(), actions['Type'].currentText(), int(actions['Devices'].text()))) + windows.append(run_exercise(int(actions['Lecture'].currentText()), actions['Algorithm'].text(), actions['Type'].currentText(), int(actions['Devices'].text()), True)) starting_exercise = False From 5b2effd759c9576f8f5f9ea00f8ddf8342318b62 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Tue, 27 Sep 2022 09:29:16 +0200 Subject: [PATCH 062/106] changed some static variables to not be static, caused the gui to not function properly on a restart --- emulators/exercise_overlay.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index bec23ca..b98c5b2 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -53,11 +53,7 @@ class Window(QWidget): w = 600 device_size = 80 last_message = None - buttons:dict[int, QPushButton] = {} windows = list() - pick_window = False - queue_window = False - all_data_window = False def __init__(self, elements, restart_function, emulator:SteppingEmulator): super().__init__() @@ -73,6 +69,10 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): self.setWindowTitle("Stepping Emulator") self.setWindowIcon(QIcon("icon.ico")) self.set_device_color() + self.pick_window = False + self.queue_window = False + self.all_data_window = False + self.buttons:dict[int, QPushButton] = {} def coordinates(self, center, r, i, n): x = sin((i*2*pi)/n) From fb10260865e6828c1e9d11f705cfce96d093f041 Mon Sep 17 00:00:00 2001 From: mast3r_waf1z Date: Tue, 27 Sep 2022 10:33:58 +0200 Subject: [PATCH 063/106] placed the buttons list in exercise overlay wrong when moving static attributes --- emulators/exercise_overlay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index b98c5b2..0ff7675 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -62,6 +62,7 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): layout = QVBoxLayout() tabs = QTabWidget() tabs.setFixedSize(self.w-20, self.h-20) + self.buttons:dict[int, QPushButton] = {} tabs.addTab(self.main(elements, restart_function), 'Main') tabs.addTab(self.controls(), 'controls') layout.addWidget(tabs) @@ -72,7 +73,6 @@ def __init__(self, elements, restart_function, emulator:SteppingEmulator): self.pick_window = False self.queue_window = False self.all_data_window = False - self.buttons:dict[int, QPushButton] = {} def coordinates(self, center, r, i, n): x = sin((i*2*pi)/n) From e338a91f9cff36ce1470c22679fce0f9882c9210 Mon Sep 17 00:00:00 2001 From: Mast3r_waf1z Date: Thu, 29 Sep 2022 09:35:57 +0200 Subject: [PATCH 064/106] fixed an issue where the program hangs if run with --gui flag and not stepper type --- exercise_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise_runner.py b/exercise_runner.py index 0cbcc44..b461134 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -91,7 +91,7 @@ def run_instance(): parser.add_argument("--gui", action="store_true", help="Toggle the gui or cli", required=False) args = parser.parse_args() import sys - if args.gui: + if args.gui and args.type[0] == 'stepping': from PyQt6.QtWidgets import QApplication app = QApplication(sys.argv) run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], True) From 30c6219a28f3135f312c633cc5b49555a9d2c36c Mon Sep 17 00:00:00 2001 From: Sean <37299188+sdfg610@users.noreply.github.com> Date: Tue, 11 Oct 2022 21:14:19 +0200 Subject: [PATCH 065/106] Updated Exercise 5 Based on the slides, I believe "IP-multicast" in Task 1.x should have been "Reliable Multicast over IP" --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e1deb74..f92cec4 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,9 @@ For all exercises today, you can use the `sync` network type - but most algorith 3. Submit a pull-request! # Exercise 5 -1. Identify two problems with IP-multicast - 1. What is a practical problem for IP-multicast? - 2. What is a theoretical problem for IP-multicast? +1. Identify two problems with Reliable Multicast over IP + 1. What is a practical problem for Reliable Multicast over IP? + 2. What is a theoretical problem for Reliable Multicast over IP? 2. Identify all the events in the following picture 1. Compute the lamport clocks for each event From 7b0aa4effa726c48e13761c27590d9e3f645d46c Mon Sep 17 00:00:00 2001 From: Sean <37299188+sdfg610@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:17:38 +0200 Subject: [PATCH 066/106] Update README.md Some updates to Exercise 6 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f92cec4..fc0209b 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,16 @@ For all exercises today, you can use the `sync` network type - but most algorith 1. Hint: how (and when) do you identify a tie? # Exercise 6 -1. Study the pseudo-code in the slides (on moodle) and complete the implement of the `King` Algorithm in `exercise6.py` +1. Study the pseudo-code in the slides (on Moodle) and complete the implementation of the `King` Algorithm in `exercise6.py` 1. How does the algorithm deal with a Byzantine king (try f=1, the first king is byzantine)? 2. Why does the algorithm satisfy Byzantine integrity? - 3. Sketch/discuss a modification your implementation s.t. the algorithm works in an `async` network, but looses its termination guarantee + 3. Sketch/discuss a modification of your implementation such that the algorithm works in an `async` network, but looses its termination guarantee 1. What would happen with a Byzantine king? 2. What would happen with a slow king? 3. What about the combination of the above? -2. Bonus Exercise: Implement the Paxos algorithm in `exercise6.py`, see the pseudo-code on moodle (use the video for reference when in doubt) for the two interesting roles (proposer and acceptor). - 1. Identify messages send/received by each role +2. Bonus Exercise: Implement the Paxos algorithm in `exercise6.py`. See the pseudo-code on Moodle (use the video for reference when in doubt) for the two interesting roles (proposer and acceptor). + 1. Identify messages sent/received by each role 1. Investigate `PAXOSNetwork` 2. Implement each role but the learner 1. Assume that each device is both a `Proposer` and an `Acceptor` (the `Learner` is provided) From b66e52839638b59e2ce3e02f9f43c0936eae61f4 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Oct 2022 21:17:11 +0200 Subject: [PATCH 067/106] Updates on exercise 7 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc0209b..d352f13 100644 --- a/README.md +++ b/README.md @@ -204,14 +204,14 @@ For all exercises today, you can use the `sync` network type - but most algorith 4. Discuss how you can use Paxos in "continued consensus" where you have to agree on the order of entries in a log-file # Exercise 7 -1. DS5ed 18.5, 18.13 +1. DS5ed exercises 18.5 and 18.13 2. Sketch an architecture for the following three systems: A bulletin board (simple reddit), a bank, a version control system (e.g. GIT) - 1. Identify the system types - 2. Which replication type is suitable, and for which parts of the system + 1. Identify the system types (with respect to CAP). + 2. Which replication type is suitable, and for which parts of the system? 3. If you go for a gossip solution, what is a suitable update frequency? 3. BONUS Exercise: Implement the Bully algorithm (DS 5ed, page 660) in `exercise7.py` 1. In which replication scheme is it useful? - 2. What is the "extra cost" of a new leader in replication? + 2. What is the "extra cost" of electing a new leader in replication? # Exercise 8 1. Compare GFS and Chubby, and identify use cases that are better for one or the other solution. From e0bd9c4c6815f84b247096d8c872803f30b659f5 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 28 Oct 2022 15:51:43 +0200 Subject: [PATCH 068/106] Updated exercise descriptions. Updated exercise 11 code --- README.md | 16 ++++++++-------- exercises/exercise11.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d352f13..555ece9 100644 --- a/README.md +++ b/README.md @@ -242,11 +242,11 @@ python10 exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type asyn ``` # Exercise 10 -1. There are "exercises" (actually, questions) on the moodle. I suggest to start with them. +1. There are "exercises" (actually, questions) on the Moodle page. I suggest to start with them. 2. Consider the code in `exercise10.py`, which sketches a blockchain similar to bitcoin. Consider that transactions are just strings and we will do no check on the transactions. I had to add a very "random" termination condition. I associate a miner to each client, the code will never stop if I have an odd number of devices. - 1. Take a look at the Block and the Blockchain (they are NOT devices) and consider how the blockchain is supposed to grow. - 2. Design the logic for when a miner sends a blockchain (with its new block) to another miner. What do you do when you receive a new block? What if a fork? Can it happen? How do you manage it to preserve the "longest chain" rule? - 3. Look for the TODOs, and implement your solution + 1. Take a look at the Block and the Blockchain classes (they are NOT devices) and consider how the blockchain is supposed to grow. + 2. Design the logic for when a miner sends a blockchain (with its new block) to another miner. What do you do when you receive a new block? What if there is a fork? Can it happen? How do you manage it to preserve the "longest chain" rule? + 3. Look for the TODOs, and implement your solution. 4. Try the code for both sync and async devices. Does it work in both cases? NOTICE: To execute the code, issue for example: @@ -256,21 +256,21 @@ python3.10 exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type # Exercise 11 -1. There are "exercises" on the moodle. I suggest to start with them. +1. There are "exercises" on the Moodle page. I suggest to start with them. 2. Consider the code in `exercise11.py`, which sets up the finger tables for chord nodes. I have a client, connected always to the same node, which issues some PUTs. 1. Take a look at how the finger tables are populated, but please use the slides, since the code can be quite cryptic. 2. Design the logic for the routing process, thus: when do I end the routing process? Who should I send the message to, if I am not the destination? - 3. Look for the TODOs, and implement your solution + 3. Look for the TODOs, and implement your solution. 4. If you have time, implement the JOIN process for device 1. NOTICE: To execute the code, issue for example: ```bash -python3.10 exercise_runner.py --lecture 11 --algorithm BlockchainNetwork --type async --devices 10 +python3.10 exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async --devices 10 ``` # Exercise 12 -1. There are "exercises" on the moodle. I suggest to start with them. +1. There are "exercises" on the Moodle page. I suggest to start with them. 2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to implement AODV. 1. Please note that you can self.medium().send() messages only in the nodes in self.neighbors. This simulates a wireless network with limited range. 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply, which should be much easier. diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 2ffe227..1f8ae91 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -153,7 +153,7 @@ def print_result(self): class ChordNetwork: - def init_routing_tables(number_of_devices): + def init_routing_tables(number_of_devices: int): N = number_of_devices-2 # routing_data 0 will be for device 2, etc while len(all_nodes) < N: new_chord_id = random.randint(0, pow(2,address_size)-1) @@ -220,7 +220,7 @@ def __init__(self, sender: int, destination: int, guid: int): def __str__(self): return f'GET REQUEST MESSAGE {self.source} -> {self.destination}: ({self.guid})' -class GetReqMessage(MessageStub): +class GetResMessage(MessageStub): def __init__(self, sender: int, destination: int, guid: int, data: str): super().__init__(sender, destination) self.guid = guid From 80fe7dfdfbf1fb5dd1ea1d98d253aa98a8cb1e1f Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 28 Oct 2022 16:00:05 +0200 Subject: [PATCH 069/106] Re-fixed my fix in exercise 11 --- exercises/exercise11.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 1f8ae91..1eda619 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -220,7 +220,7 @@ def __init__(self, sender: int, destination: int, guid: int): def __str__(self): return f'GET REQUEST MESSAGE {self.source} -> {self.destination}: ({self.guid})' -class GetResMessage(MessageStub): +class GetRspMessage(MessageStub): def __init__(self, sender: int, destination: int, guid: int, data: str): super().__init__(sender, destination) self.guid = guid From cd2deba0f6f6de2607821f713f8c0ebc60975280 Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 30 Oct 2022 11:36:48 +0100 Subject: [PATCH 070/106] Small cleanup in exercise 10 code --- exercises/exercise10.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exercises/exercise10.py b/exercises/exercise10.py index eeda387..e4cd392 100644 --- a/exercises/exercise10.py +++ b/exercises/exercise10.py @@ -38,9 +38,8 @@ def hash_binary(self): -class Blockchain(): +class Blockchain: def __init__(self): - super().__init__() self.unconfirmed_transactions = [] self.chain = [] From 1667de5492ec7b33c5864cecd039c46a48dedf5b Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 30 Oct 2022 16:26:48 +0100 Subject: [PATCH 071/106] Added code on miner to receive transactions from client --- exercises/exercise10.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exercises/exercise10.py b/exercises/exercise10.py index e4cd392..7579569 100644 --- a/exercises/exercise10.py +++ b/exercises/exercise10.py @@ -165,6 +165,8 @@ def handle_ingoing(self, ingoing: MessageStub): # this is used to send the blockchain data to a client requesting them message = BlockchainMessage(self.index(), ingoing.source, self.blockchain.chain) self.medium().send(message) + elif isinstance(ingoing, TransactionMessage): + self.blockchain.add_new_transaction(ingoing.transaction) elif isinstance(ingoing, QuitMessage): return False return True From 87b90f5574f2234c0e6a1e4f29c7b3f8a7b4db18 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 1 Nov 2022 12:00:48 +0100 Subject: [PATCH 072/106] Many small, assorted updates to exercise 8 code --- exercises/exercise8.py | 97 +++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/exercises/exercise8.py b/exercises/exercise8.py index 374ba75..5f23b7c 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -12,15 +12,16 @@ # if you need repetition: # random.seed(100) + class GfsMaster(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - self._metadata = {} - self.chunks_being_allocated = [] + self._metadata: dict[tuple[str, int], tuple[int, list[int]]] = {} # (filename, chunk_index) -> (chunkhandle, [chunkservers]) + self.chunks_being_allocated: list[tuple[int, int]] = [] # [(chunkhandle, requester_index)] GfsNetwork.gfsmaster.append(index) def run(self): - # since this is a server, its job is to answer for requests (messages), then do something + # since this is a server, its job is to wait for requests (messages), then do something while True: for ingoing in self.medium().receive_all(): if not self.handle_ingoing(ingoing): @@ -32,69 +33,61 @@ def handle_ingoing(self, ingoing: MessageStub): key = (ingoing.filename, ingoing.chunkindex) chunk = self._metadata.get(key) if chunk is not None: - for c in self.chunks_being_allocated: if c[0] == chunk[0]: self.chunks_being_allocated.append((chunk[0], ingoing.source)) return True - - - anwser = File2ChunkRspMessage( + answer = File2ChunkRspMessage( self.index(), ingoing.source, chunk[0], chunk[1] ) - self.medium().send(anwser) + self.medium().send(answer) else: if ingoing.createIfNotExists: self.do_allocate_request(ingoing.filename, ingoing.chunkindex, ingoing.source) else: - anwser = File2ChunkRspMessage( + answer = File2ChunkRspMessage( self.index(), ingoing.source, 0, [] ) - self.medium().send(anwser) + self.medium().send(answer) elif isinstance(ingoing, QuitMessage): - print("I am Master " + str(self.index()) + " and I am quitting") + print(f"I am Master {self.index()} and I am quitting") return False elif isinstance(ingoing, AllocateChunkRspMessage): - if not ingoing.result == "ok": - print("allocation failed! I am quitting!!") + if ingoing.result != "ok": + print("Allocation failed! I am quitting!!") return False for chunk in self._metadata.values(): if chunk[0] == ingoing.chunkhandle: self.add_chunk_to_metadata(chunk, ingoing.source) return True - def add_chunk_to_metadata(self, chunk, chunkserver): + def add_chunk_to_metadata(self, chunk: tuple[int, list[int]], chunkserver: int): chunk[1].append(chunkserver) if len(chunk[1]) == NUMBER_OF_REPLICAS: - for request in self.chunks_being_allocated: - if request[0] == chunk[0]: - anwser = File2ChunkRspMessage( - self.index(), - request[1], - chunk[0], - chunk[1] - ) - self.medium().send(anwser) - + requests = [request for request in self.chunks_being_allocated if request[0] == chunk[0]] + for request in requests: + answer = File2ChunkRspMessage( + self.index(), + request[1], + chunk[0], + chunk[1] + ) + self.medium().send(answer) + self.chunks_being_allocated.remove(request) - def do_allocate_request(self, filename, chunkindex, requester): + def do_allocate_request(self, filename, chunkindex: int, requester: int): chunkhandle = random.randint(0, 999999) self.chunks_being_allocated.append((chunkhandle, requester)) self._metadata[(filename, chunkindex)] = (chunkhandle, []) - chunkservers = [] - startnumber = random.randint(0, 9999) - for i in range(NUMBER_OF_REPLICAS): - numChunkServer = len(GfsNetwork.gfschunkserver) - chosen = GfsNetwork.gfschunkserver[(startnumber + i ) % numChunkServer] - chunkservers.append(chosen) - + # Allocate the new chunk on "NUMBER_OF_REPLICAS" random chunkservers + chunkservers = random.sample(GfsNetwork.gfschunkserver, NUMBER_OF_REPLICAS) for i in chunkservers: message = AllocateChunkReqMessage(self.index(), i, chunkhandle, chunkservers) self.medium().send(message) @@ -107,9 +100,9 @@ class GfsChunkserver(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) GfsNetwork.gfschunkserver.append(index) - self.localchunks = {} + self.localchunks: dict[int, str] = {} # chunkhandle -> contents # the first server in chunkservers is the primary - self.chunkservers = {} + self.chunkservers: dict[int, list[int]] = {} # chunkhandle -> [chunkservers] def run(self): # since this is a server, its job is to answer for requests (messages), then do something @@ -121,7 +114,7 @@ def run(self): def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, QuitMessage): - print("I am Chunk Server " + str(self.index()) + " and I am quitting") + print(f"I am Chunk Server {self.index()} and I am quitting") return False elif isinstance(ingoing, AllocateChunkReqMessage): self.do_allocate_chunk(ingoing.chunkhandle, ingoing.chunkservers) @@ -129,32 +122,29 @@ def handle_ingoing(self, ingoing: MessageStub): self.medium().send(message) elif isinstance(ingoing, RecordAppendReqMessage): # - # TODO: need to implement the storate operation - # do not forget the passive replication discipline + # TODO: need to implement the storage operation + # do not forget the passive replication discipline # pass return True - def do_allocate_chunk(self, chunkhandle, servers): + def do_allocate_chunk(self, chunkhandle: int, servers: list[int]): self.localchunks[chunkhandle] = "" self.chunkservers[chunkhandle] = servers def print_result(self): print("chunk server quit. Currently, my saved chunks are as follows:") - for c in self.localchunks: - print("chunk " + str(c) + " : " + str(self.localchunks[c])) + for (chunkhandle, contents) in self.localchunks: + print(f"chunk {chunkhandle} : {contents}") class GfsClient(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - if index == 0: - for i in range(5): - pass def run(self): - # being a client, it listens to incoming messags, but it also does something to put the ball rolling - print("i am client " + str(self.index())) + # being a client, it listens to incoming messages, but it also does something to put the ball rolling + print(f"I am Client {self.index()}") master = GfsNetwork.gfsmaster[0] message = File2ChunkReqMessage(self.index(), master, "myfile.txt", 0, True) self.medium().send(message) @@ -167,13 +157,12 @@ def run(self): def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, File2ChunkRspMessage): - print("I found out where my chunk is: " + str(ingoing.chunkhandle) + " locations: " + str(ingoing.locations)) + print(f"I found out where my chunk is: {ingoing.chunkhandle}, locations: {ingoing.locations}") # I select a random chunk server, and I send the append request # I do not necessarily select the primary - - randomserver = ingoing.locations[random.randint(0,999)%len(ingoing.locations)] - data = "hello from client number " + str(self.index()) + "\n" + randomserver = random.choice(ingoing.locations) + data = f"hello from client number {self.index()}\n" self.medium().send(RecordAppendReqMessage(self.index(), randomserver, ingoing.chunkhandle, data)) elif isinstance(ingoing, RecordAppendRspMessage): # project completed, time to quit @@ -181,7 +170,6 @@ def handle_ingoing(self, ingoing: MessageStub): self.medium().send(QuitMessage(self.index(), i)) for i in GfsNetwork.gfschunkserver: self.medium().send(QuitMessage(self.index(), i)) - return False return True @@ -189,7 +177,6 @@ def print_result(self): pass - class GfsNetwork: def __new__(cls, index: int, number_of_devices: int, medium: Medium): if index < NUMBER_OF_MASTERS: @@ -203,7 +190,6 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): - class QuitMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) @@ -211,8 +197,6 @@ def __init__(self, sender: int, destination: int): def __str__(self): return f'QUIT REQUEST {self.source} -> {self.destination}' - - class File2ChunkReqMessage(MessageStub): def __init__(self, sender: int, destination: int, filename: str, chunkindex: int, createIfNotExists = False): super().__init__(sender, destination) @@ -232,9 +216,8 @@ def __init__(self, sender: int, destination: int, chunkhandle: int, locations: l def __str__(self): return f'FILE2CHUNK RESPONSE {self.source} -> {self.destination}: ({self.chunkhandle}, {self.locations})' - class AllocateChunkReqMessage(MessageStub): - def __init__(self, sender: int, destination: int, chunkhandle: int, chunkservers: list): + def __init__(self, sender: int, destination: int, chunkhandle: int, chunkservers: list[int]): super().__init__(sender, destination) self.chunkhandle = chunkhandle self.chunkservers = chunkservers @@ -267,5 +250,5 @@ def __init__(self, sender: int, destination: int, result: str): # TODO: possibly, complete this message with the fields you need def __str__(self): - return f'RECORD APPEND REQUEST {self.source} -> {self.destination}: ({self.result})' + return f'RECORD APPEND RESPONSE {self.source} -> {self.destination}: ({self.result})' From 37ace7607da5f802e7abe94ad541963a01852d73 Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Tue, 1 Nov 2022 12:58:11 +0100 Subject: [PATCH 073/106] Update README.md improved exercise 8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 555ece9..c9db3f6 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ For all exercises today, you can use the `sync` network type - but most algorith 2. Sketch a design for the "passive replication" solution of the GFS. Consider how many types of messages you need, when they should be sent, etc 3. Implement your solution, starting from the handler of RecordAppendReqMessage of the GfsChunkserver class 4. Try out your solution with a larger number of clients, to have more concurrent changes to the "file" -3. BONUS Exercise: Consider how to add shadow masters to the system. Clients and chunk servers will still interact with the first master to change the file system, but the shadow master can always work as read-only. +3. BONUS Exercise: Add shadow masters to the system. Clients and chunk servers will still interact with the first master to change the file system, but the shadow master can take over if the master shuts down. NOTICE: To execute the code, issue for example: ```bash From 9c7e24f3716f24b03d4c860b68285b02261ad5bc Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 3 Nov 2022 15:02:11 +0100 Subject: [PATCH 074/106] Small fix in exercise 8 --- exercises/exercise8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/exercise8.py b/exercises/exercise8.py index 5f23b7c..a74102a 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -134,7 +134,7 @@ def do_allocate_chunk(self, chunkhandle: int, servers: list[int]): def print_result(self): print("chunk server quit. Currently, my saved chunks are as follows:") - for (chunkhandle, contents) in self.localchunks: + for chunkhandle, contents in self.localchunks.items(): print(f"chunk {chunkhandle} : {contents}") From 58ad2d520af2178a72a8b8d50d62942b56d69e65 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 7 Nov 2022 11:56:18 +0100 Subject: [PATCH 075/106] Updates to exercise 9 descriptions and code --- README.md | 10 ++-- exercises/exercise9.py | 107 ++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index c9db3f6..6478010 100644 --- a/README.md +++ b/README.md @@ -229,12 +229,12 @@ python3.10 exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async -- # Exercise 9 1. Consider the code in `exercise9.py`, which sketches MapReduce, and complete it. - 1. Unzip the file books.zip in ex9data/books - 2. The Master is pretty much complete, the same can be said for the client. Take a look at how the Master is supposed to interact with Mappers and Reducers - 3. Consider how to implement the Reducers. Take into account that we are simulating "local storage in the mappers" using memory - 4. Look for the TODOs, and implement your solution + 1. Unzip the file books.zip in ex9data/books. + 2. The Master is pretty much complete. The same can be said for the client. Take a look at how the Master is supposed to interact with Mappers and Reducers. + 3. Consider how to implement the Reducers. Take into account that we are simulating "local storage in the mappers" using memory. + 4. Look for the TODOs, and implement your solution. 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many rounds are needed to complete the job with the "sync" simulator. -2. Compare MapReduce and Spark RDDs, and consider what it would change in terms of architecture, especially to supprt RDDs +2. Compare MapReduce and Spark RDDs, and consider what it would change in terms of architecture, especially to support RDDs. NOTICE: To execute the code, issue for example: ```bash diff --git a/exercises/exercise9.py b/exercises/exercise9.py index 62e9260..2768c23 100644 --- a/exercises/exercise9.py +++ b/exercises/exercise9.py @@ -9,16 +9,19 @@ from emulators.MessageStub import MessageStub - class Role(Enum): # is the Worker a Mapper, a Reducer, or in Idle state? IDLE = 1 MAPPER = 2 REDUCER = 3 + class MapReduceMaster(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) + self.number_partitions: int = -1 + self.result_files: list[str] = [] + self.number_finished_reducers = 0 def run(self): # since this is a server, its job is to answer for requests (messages), then do something @@ -39,7 +42,7 @@ def handle_ingoing(self, ingoing: MessageStub): self.medium().send(message) for i in range(0, number_of_mappers): length = len(ingoing.filenames) - length = 5 # TODO: comment out this line to process all files, once you think your code is ready + length = 5 # TODO: comment out this line to process all files, once you think your code is ready first = int(length * i / number_of_mappers) last = int(length * (i+1) / number_of_mappers) message = MapTaskMessage(self.index(), self.number_partitions + 2 + i, ingoing.filenames[first:last], self.number_partitions) @@ -50,18 +53,20 @@ def handle_ingoing(self, ingoing: MessageStub): self.medium().send(QuitMessage(self.index(), w)) return False elif isinstance(ingoing, MappingDoneMessage): - # TODO: - # contact all reducers, telling them that a mapper has completed its job - # hint: you need to define a new message type, for example ReducerVisitMapperMessage + # TODO: contact all reducers, telling them that a mapper has completed its job + # hint: you need to define a new message type, for example ReducerVisitMapperMessage (see MapReduceWorker) pass elif isinstance(ingoing, ReducingDoneMessage): - # I can tell the client that the job is done - message = ClientJobCompletedMessage(1, 0) - self.medium().send(message) + self.number_finished_reducers += 1 + self.result_files.append(ingoing.result_filename) + if self.number_finished_reducers == self.number_partitions: + # I can tell the client that the job is done + message = ClientJobCompletedMessage(1, 0, self.result_files) + self.medium().send(message) return True def print_result(self): - print("Master " + str(self.index()) + " quits") + print(f"Master {self.index()} quits") class MapReduceWorker(Device): @@ -72,28 +77,27 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): # number of partitions (equals to number of reducers) self.number_partitions = 0 # variables if it is a mapper - self.M_files_to_process = {} # list of files to process - self.M_cached_results = {} # in-memory cache - self.M_stored_results = {} # "R" files containing results + self.M_files_to_process: list[str] = [] # list of files to process + self.M_cached_results: dict[str, int] = {} # in-memory cache + self.M_stored_results: dict[int, dict[str, int]] = {} # "R" files containing results. partition -> word -> count # variables if it is a reducer - self.R_my_partition = 0 # the partition I am managing - self.R_number_mappers = 0 # how many mappers there are. I need to know it to decide when I can tell the master I am done with the reduce task + self.R_my_partition = 0 # the partition I am managing + self.R_number_mappers = 0 # how many mappers there are. I need to know it to decide when I can tell the master I am done with the reduce task - - def mapper_process_file(self, filename): + def mapper_process_file(self, filename: str) -> dict[str, int]: # goal: return the occurrences of words in the file words = [] with open("ex9data/books/"+filename) as file: for line in file: - words+=line.split() + words += line.split() result = {} for word in words: result[word.lower()] = 1 + result.get(word.lower(), 0) return result - def mapper_partition_function(self, key): - # compute the partition based on the key (see the lecture material) - # this function should be supplied by the client We stick to a fixed function for sake of clarity + def mapper_partition_function(self, key: str) -> int: + # Compute the partition based on the key (see the lecture material) + # This function should be supplied by the client, but we stick to a fixed function for sake of clarity char = ord(key[0]) if char < ord('a'): char = ord('a') @@ -102,14 +106,13 @@ def mapper_partition_function(self, key): partition = (char - ord('a')) * self.number_partitions / (1+ord('z')-ord('a')) return int(partition) - def mapper_shuffle(self): # goal: merge all the data I have in the cache to the stored results WITH SHUFFLE, then flush the cache for word in self.M_cached_results: p = self.mapper_partition_function(word) old_value = self.M_stored_results[p].get(word, 0) self.M_stored_results[p][word] = self.M_cached_results[word] + old_value - self.M_cached_results = [] # flushing the cache + self.M_cached_results = [] # flushing the cache def do_some_work(self): if self.role == Role.IDLE: @@ -121,10 +124,10 @@ def do_some_work(self): # if I have no more files, I "store" it locally into partitions and tell the master that I am done if self.M_files_to_process != []: filename = self.M_files_to_process.pop() - print(f"mapper {self.index()} file {filename} processed") map_result = self.mapper_process_file(filename) - for k in map_result: - self.M_cached_results[k] = self.M_cached_results.get(k, 0) + map_result[k] + for word in map_result: + self.M_cached_results[word] = self.M_cached_results.get(word, 0) + map_result[word] + print(f"Mapper {self.index()}: file '{filename}' processed") if self.M_files_to_process == []: self.mapper_shuffle() message = MappingDoneMessage(self.index(), 0) @@ -133,8 +136,6 @@ def do_some_work(self): # not much to do: everything is done when the master tells us about a mapper that completed its task pass - - def run(self): # since this is a worker, it looks for incoming requests (messages), then it works a little while True: @@ -146,7 +147,7 @@ def run(self): def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, QuitMessage): - print("I am Mapper " + str(self.index()) + " and I am quitting") + print(f"I am Worker {self.index()} and I am quitting") return False elif isinstance(ingoing, MapTaskMessage): # I was assigned to be a mapper, thus I: @@ -166,40 +167,38 @@ def handle_ingoing(self, ingoing: MessageStub): self.R_number_mappers = ingoing.number_mappers # nothing to do until the Master tells us to contact Mappers pass - elif isinstance(ingoing, ReducerVisitMapperMessage): + elif isinstance(ingoing, ReducerVisitMapperMessage): # 'ReducerVisitMapperMessage' does not exist by default # the master is saying that a mapper is done # thus this reducer will: - # get the "stored" results for the mapper, for the correct partition + # get the "stored" results from the mapper, for the correct partition (new message type) # if it is the last mapper I have to contact, I will: # merge the data - # store it somewhere + # store resulting data in "ex9data/results/{my_partition_file_name}" # tell the master I am done # TODO: write the code pass return True def print_result(self): - print(f"worker quits. It was a {self.Role}") + print(f"Worker {self.index()} quits. It was a {self.role}") class MapReduceClient(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) + self.result_files: list[str] = [] def scan_for_books(self): - books = [] with os.scandir('ex9data/books/') as entries: - for entry in entries: - if entry.is_file() and entry.name.endswith(".txt"): - books.append(entry.name) - return books + return [entry.name for entry in entries + if entry.is_file() and entry.name.endswith(".txt")] def run(self): # being a client, it listens to incoming messages, but it also does something to put the ball rolling - print("i am client " + str(self.index())) + print(f"I am client {self.index()}") books = self.scan_for_books() - - message = ClientJobStartMessage(self.index(), 1, books, 3) # TODO: experiment with different number of reducers + + message = ClientJobStartMessage(self.index(), 1, books, 3) # TODO: experiment with different number of reducers self.medium().send(message) while True: @@ -213,32 +212,30 @@ def handle_ingoing(self, ingoing: MessageStub): # I can tell the master to quit # I will print the result later, with the print_result function self.medium().send(QuitMessage(self.index(), 1)) + self.result_files = ingoing.result_files return False return True def print_result(self): for filename in self.result_files: - print("results from file: {self.filename}") - with open("ex9data/results/" + filename) as file: + print(f"Results from file: {filename}") + with open(f"ex9data/results/{filename}") as file: for line in file: print("\t" + line.rstrip()) - class MapReduceNetwork: def __new__(cls, index: int, number_of_devices: int, medium: Medium): # client has index 0 # master has index 1 # workers have index 2+ - cls.workers = [] if index == 0: return MapReduceClient(index, number_of_devices, medium) elif index == 1: return MapReduceMaster(index, number_of_devices, medium) else: return MapReduceWorker(index, number_of_devices, medium) - - + workers: list[int] = [] class QuitMessage(MessageStub): @@ -249,7 +246,6 @@ def __str__(self): return f'QUIT REQUEST {self.source} -> {self.destination}' - class ClientJobStartMessage(MessageStub): def __init__(self, sender: int, destination: int, filenames: list, number_partitions: int): super().__init__(sender, destination) @@ -259,6 +255,7 @@ def __init__(self, sender: int, destination: int, filenames: list, number_partit def __str__(self): return f'CLIENT START JOB REQUEST {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)' + class ClientJobCompletedMessage(MessageStub): def __init__(self, sender: int, destination: int, result_files: list): super().__init__(sender, destination) @@ -268,7 +265,6 @@ def __str__(self): return f'CLIENT JOB COMPLETED {self.source} -> {self.destination} ({self.result_files})' - class MapTaskMessage(MessageStub): def __init__(self, sender: int, destination: int, filenames: list, number_partitions: int): super().__init__(sender, destination) @@ -277,14 +273,14 @@ def __init__(self, sender: int, destination: int, filenames: list, number_partit def __str__(self): return f'MAP TASK ASSIGNMENT {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)' - + + class MappingDoneMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'MAP TASK COMKPLETED {self.source} -> {self.destination}' - + return f'MAP TASK COMPLETED {self.source} -> {self.destination}' class ReduceTaskMessage(MessageStub): @@ -297,12 +293,11 @@ def __init__(self, sender: int, destination: int, my_partition: int, number_part def __str__(self): return f'REDUCE TASK ASSIGNMENT {self.source} -> {self.destination}: (partition is {self.my_partition}, {self.number_partitions} partitions, {self.number_mappers} mappers)' + class ReducingDoneMessage(MessageStub): - def __init__(self, sender: int, destination: int): + def __init__(self, sender: int, destination: int, result_filename: str): super().__init__(sender, destination) + self.result_filename = result_filename def __str__(self): - return f'REDUCE TASK COMPLETED {self.source} -> {self.destination}: ()' - - - + return f'REDUCE TASK COMPLETED {self.source} -> {self.destination}: result_filename = {self.result_filename}' From 43dd888818657caa7f572960ef5775d6b1f76269 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 7 Nov 2022 14:41:23 +0100 Subject: [PATCH 076/106] Send "Done" to master, not client --- exercises/exercise9.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exercises/exercise9.py b/exercises/exercise9.py index 2768c23..a914545 100644 --- a/exercises/exercise9.py +++ b/exercises/exercise9.py @@ -130,7 +130,7 @@ def do_some_work(self): print(f"Mapper {self.index()}: file '{filename}' processed") if self.M_files_to_process == []: self.mapper_shuffle() - message = MappingDoneMessage(self.index(), 0) + message = MappingDoneMessage(self.index(), 1) self.medium().send(message) if self.role == Role.REDUCER: # not much to do: everything is done when the master tells us about a mapper that completed its task @@ -166,7 +166,6 @@ def handle_ingoing(self, ingoing: MessageStub): self.R_my_partition = ingoing.my_partition self.R_number_mappers = ingoing.number_mappers # nothing to do until the Master tells us to contact Mappers - pass elif isinstance(ingoing, ReducerVisitMapperMessage): # 'ReducerVisitMapperMessage' does not exist by default # the master is saying that a mapper is done # thus this reducer will: From 142d1ebb3b3c64e3722f940520267b2f21a9189e Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 7 Nov 2022 14:55:51 +0100 Subject: [PATCH 077/106] Added constants for client_index and master_index for readability --- exercises/exercise9.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/exercises/exercise9.py b/exercises/exercise9.py index a914545..3fc86cd 100644 --- a/exercises/exercise9.py +++ b/exercises/exercise9.py @@ -61,7 +61,7 @@ def handle_ingoing(self, ingoing: MessageStub): self.result_files.append(ingoing.result_filename) if self.number_finished_reducers == self.number_partitions: # I can tell the client that the job is done - message = ClientJobCompletedMessage(1, 0, self.result_files) + message = ClientJobCompletedMessage(1, MapReduceNetwork.client_index, self.result_files) self.medium().send(message) return True @@ -130,7 +130,7 @@ def do_some_work(self): print(f"Mapper {self.index()}: file '{filename}' processed") if self.M_files_to_process == []: self.mapper_shuffle() - message = MappingDoneMessage(self.index(), 1) + message = MappingDoneMessage(self.index(), MapReduceNetwork.master_index) self.medium().send(message) if self.role == Role.REDUCER: # not much to do: everything is done when the master tells us about a mapper that completed its task @@ -197,7 +197,7 @@ def run(self): print(f"I am client {self.index()}") books = self.scan_for_books() - message = ClientJobStartMessage(self.index(), 1, books, 3) # TODO: experiment with different number of reducers + message = ClientJobStartMessage(self.index(), MapReduceNetwork.master_index, books, 3) # TODO: experiment with different number of reducers self.medium().send(message) while True: @@ -210,7 +210,7 @@ def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, ClientJobCompletedMessage): # I can tell the master to quit # I will print the result later, with the print_result function - self.medium().send(QuitMessage(self.index(), 1)) + self.medium().send(QuitMessage(self.index(), MapReduceNetwork.master_index)) self.result_files = ingoing.result_files return False return True @@ -228,12 +228,14 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): # client has index 0 # master has index 1 # workers have index 2+ - if index == 0: + if index == MapReduceNetwork.client_index: return MapReduceClient(index, number_of_devices, medium) - elif index == 1: + elif index == MapReduceNetwork.master_index: return MapReduceMaster(index, number_of_devices, medium) else: return MapReduceWorker(index, number_of_devices, medium) + client_index = 0 + master_index = 1 workers: list[int] = [] From 97f164a4011eac3dbf66a6c74bb87b73c3007ea0 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 14 Nov 2022 17:58:13 +0100 Subject: [PATCH 078/106] Few minor updates to exercise 11 --- exercises/exercise11.py | 42 ++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 1eda619..a1cc286 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -1,6 +1,7 @@ import math import random import sys +from typing import Optional from emulators.Device import Device from emulators.Medium import Medium @@ -17,18 +18,20 @@ # size for the chord addresses in bits address_size = 6 # only for initialization: -all_nodes = [] -all_routing_data = [] +all_nodes: list[int] = [] +all_routing_data: list["RoutingData"] = [] + + class RoutingData: # all tuples ("prev" and the ones in the finger_table) are (index, chord_id) - def __init__(self, index: int, chord_id: int, prev: tuple, finger_table: list): + def __init__(self, index: int, chord_id: int, prev: tuple[int, int], finger_table: list[tuple[int, int]]): self.index = index self.chord_id = chord_id self.prev = prev self.finger_table = finger_table def to_string(self): - ret = f"node ({self.index}, {self.chord_id}) prev {self.prev} finger_table {self.finger_table}" + ret = f"Node ({self.index}, {self.chord_id}) prev {self.prev} finger_table {self.finger_table}" return ret @@ -44,11 +47,11 @@ def in_between(candidate, low, high): class ChordNode(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, connected: bool, routing_data: RoutingData): + def __init__(self, index: int, number_of_devices: int, medium: Medium, connected: bool, routing_data: Optional[RoutingData]): super().__init__(index, number_of_devices, medium) self.connected = connected self.routing_data = routing_data - self.saved_data = [] + self.saved_data: list[str] = [] def run(self): # a chord node acts like a server @@ -109,7 +112,6 @@ def print_result(self): print(f"Chord node {self.index()} quits, it was still disconnected") - class ChordClient(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) @@ -137,11 +139,11 @@ def run(self): return # currently, I do not manage incoming messages - while True: - for ingoing in self.medium().receive_all(): - if not self.handle_ingoing(ingoing): - return - self.medium().wait_for_next_round() + # while True: + # for ingoing in self.medium().receive_all(): + # if not self.handle_ingoing(ingoing): + # return + # self.medium().wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, QuitMessage): @@ -157,7 +159,7 @@ def init_routing_tables(number_of_devices: int): N = number_of_devices-2 # routing_data 0 will be for device 2, etc while len(all_nodes) < N: new_chord_id = random.randint(0, pow(2,address_size)-1) - if not new_chord_id in all_nodes: + if new_chord_id not in all_nodes: all_nodes.append(new_chord_id) all_nodes.sort() @@ -195,7 +197,6 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): return ChordNode(index, number_of_devices, medium, True, all_routing_data[index-2]) - class QuitMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) @@ -203,6 +204,7 @@ def __init__(self, sender: int, destination: int): def __str__(self): return f'QUIT REQUEST {self.source} -> {self.destination}' + class PutMessage(MessageStub): def __init__(self, sender: int, destination: int, guid: int, data: str): super().__init__(sender, destination) @@ -212,6 +214,7 @@ def __init__(self, sender: int, destination: int, guid: int, data: str): def __str__(self): return f'PUT MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})' + class GetReqMessage(MessageStub): def __init__(self, sender: int, destination: int, guid: int): super().__init__(sender, destination) @@ -220,6 +223,7 @@ def __init__(self, sender: int, destination: int, guid: int): def __str__(self): return f'GET REQUEST MESSAGE {self.source} -> {self.destination}: ({self.guid})' + class GetRspMessage(MessageStub): def __init__(self, sender: int, destination: int, guid: int, data: str): super().__init__(sender, destination) @@ -229,6 +233,7 @@ def __init__(self, sender: int, destination: int, guid: int, data: str): def __str__(self): return f'GET RESPONSE MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})' + class StartJoinMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) @@ -236,27 +241,34 @@ def __init__(self, sender: int, destination: int): def __str__(self): return f'StartJoinMessage MESSAGE {self.source} -> {self.destination}' + class JoinReqMessage(MessageStub): def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) + def __str__(self): return f'JoinReqMessage MESSAGE {self.source} -> {self.destination}' + class JoinRspMessage(MessageStub): def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) + def __str__(self): return f'JoinRspMessage MESSAGE {self.source} -> {self.destination}' + class NotifyMessage(MessageStub): def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) + def __str__(self): return f'NotifyMessage MESSAGE {self.source} -> {self.destination}' + class StabilizeMessage(MessageStub): def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) + def __str__(self): return f'StabilizeMessage MESSAGE {self.source} -> {self.destination}' - From 90018c1f830d65366b05ddd9054a67e8d30b0e67 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 14 Nov 2022 20:45:06 +0100 Subject: [PATCH 079/106] Updates in ex. 11 code. Fixed default algorithm for ex. 11 --- exercise_runner_overlay.py | 2 +- exercises/exercise11.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index eb7d0fd..c6251de 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -56,7 +56,7 @@ def text_changed(text): 8:'GfsNetwork', 9:'MapReduceNetwork', 10:'BlockchainNetwork', - 11:'ChordClient', + 11:'ChordNetwork', 12:'AodvNode'} lecture = int(text) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index a1cc286..6a4e312 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -35,7 +35,7 @@ def to_string(self): return ret -def in_between(candidate, low, high): +def in_between(candidate: int, low: int, high: int): # the function returns False when candidate == low or candidate == high # take care of those cases in the calling function if low == high: @@ -164,16 +164,14 @@ def init_routing_tables(number_of_devices: int): all_nodes.sort() for id in range(N): - prev_id = id-1 - if prev_id < 0: - prev_id += N + prev_id = (id-1) % N prev = (prev_id, all_nodes[prev_id]) new_finger_table = [] for i in range(address_size): at_least = (all_nodes[id] + pow(2, i)) % pow(2, address_size) - candidate = (id+1)%N + candidate = (id+1) % N while in_between(all_nodes[candidate], all_nodes[id], at_least): - candidate = (candidate+1)%N + candidate = (candidate+1) % N new_finger_table.append((candidate+2, all_nodes[candidate])) # I added 2 to candidate since routing_data 0 is for device 2, and so on all_routing_data.append(RoutingData(id+2, all_nodes[id], prev, new_finger_table)) print(RoutingData(id+2, all_nodes[id], prev, new_finger_table).to_string()) From 81b804cd8c2eeaf5bb25e7d455510506cc195f56 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 14 Nov 2022 20:52:18 +0100 Subject: [PATCH 080/106] Additional guiding/helping type-hints --- exercises/exercise11.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 6a4e312..35cf938 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -61,11 +61,11 @@ def run(self): return self.medium().wait_for_next_round() - def is_request_for_me(self, guid): + def is_request_for_me(self, guid: int) -> bool: # TODO: implement this function that checks if the routing process is over pass - def next_hop(self, guid): + def next_hop(self, guid: int) -> int: # TODO: implement this function with the routing logic pass @@ -78,7 +78,7 @@ def handle_ingoing(self, ingoing: MessageStub): # TODO: route the message # you can fill up the next_hop function for this next_hop = self.next_hop(ingoing.guid) - message = PutMessage(self.index(), next_hop[0], ingoing.guid, ingoing.data) + message = PutMessage(self.index(), next_hop, ingoing.guid, ingoing.data) self.medium().send(message) if isinstance(ingoing, GetReqMessage): # maybe TODO, but the GET is not very interesting From 968818204d1c4288b51762c692f92243b75e9a3b Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 17 Nov 2022 15:20:50 +0100 Subject: [PATCH 081/106] Small additional update to Exercise 11 code --- exercises/exercise11.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 35cf938..2a7ea1c 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -165,7 +165,7 @@ def init_routing_tables(number_of_devices: int): for id in range(N): prev_id = (id-1) % N - prev = (prev_id, all_nodes[prev_id]) + prev = (prev_id+2, all_nodes[prev_id]) # Add 2 to get "message-able" device index new_finger_table = [] for i in range(address_size): at_least = (all_nodes[id] + pow(2, i)) % pow(2, address_size) @@ -176,8 +176,6 @@ def init_routing_tables(number_of_devices: int): all_routing_data.append(RoutingData(id+2, all_nodes[id], prev, new_finger_table)) print(RoutingData(id+2, all_nodes[id], prev, new_finger_table).to_string()) - - def __new__(cls, index: int, number_of_devices: int, medium: Medium): # device #0 is the client # device #1 is a disconnected node From 4abd1bfeaa692f91f8666ff917b47b999119c6b6 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 21 Nov 2022 13:42:14 +0100 Subject: [PATCH 082/106] Updates to Exercise 12 description and code --- README.md | 4 ++-- exercises/exercise12.py | 51 +++++++++++++++++------------------------ 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 6478010..7e3abd1 100644 --- a/README.md +++ b/README.md @@ -272,8 +272,8 @@ python3.10 exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async # Exercise 12 1. There are "exercises" on the Moodle page. I suggest to start with them. 2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to implement AODV. - 1. Please note that you can self.medium().send() messages only in the nodes in self.neighbors. This simulates a wireless network with limited range. - 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply, which should be much easier. + 1. Please note that you can `self.medium().send()` messages only to the nodes in `self.neighbors`. This simulates a wireless network with limited range. + 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply process, which should be much easier. 3. Look for the TODOs, and implement your solution. NOTICE: To execute the code, issue for example: diff --git a/exercises/exercise12.py b/exercises/exercise12.py index d024cdc..ddc3a71 100644 --- a/exercises/exercise12.py +++ b/exercises/exercise12.py @@ -2,6 +2,7 @@ import random import sys import threading +from typing import Optional from emulators.Device import Device from emulators.Medium import Medium @@ -10,7 +11,6 @@ import json import time - # if you need controlled repetitions: # random.seed(100) @@ -31,17 +31,17 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): # I get the topology from the singleton self.neighbors = TopologyCreator.get_topology(number_of_devices, probability_arc)[index] # I initialize the "routing tables". Feel free to create your own structure if you prefer - self.reverse_path = {} - self.forward_path = {} - self.bcast_ids = [] + self.forward_path: dict[int, int] = {} # "Destination index" --> "Next-hop index" + self.reverse_path: dict[int, int] = {} # "Source index" --> "Next-hop index" + self.bcast_ids = [] # Type hint left out on purpose due to tasks below # data structures to cache outgoing messages, and save received data - self.saved_data = [] - self.outgoing_message_cache = [] + self.saved_data: list[str] = [] + self.outgoing_message_cache: list[DataMessage] = [] def run(self): last = random.randint(0, self.number_of_devices() - 1) # I send the message to myself, so it gets routed - message = DataMessage(self.index(), self.index(), last, f"hi I am {self.index()}") + message = DataMessage(self.index(), self.index(), last, f"Hi. I am {self.index()}.") self.medium().send(message) while True: for ingoing in self.medium().receive_all(): @@ -49,11 +49,8 @@ def run(self): return self.medium().wait_for_next_round() - def next_hop(self, last): - for destination in self.forward_path: - if destination == last: - return self.forward_path[destination] - return None + def next_hop(self, last: int) -> Optional[int]: + return self.forward_path.get(last) # Returns "None" if key does not exist def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, DataMessage): @@ -115,22 +112,18 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print(f"AODV node {self.index()} quits, neighbours {self.neighbors}, forward paths {self.forward_path}, reverse paths {self.reverse_path}, saved data {self.saved_data} length of message cache (should be 0) {len(self.outgoing_message_cache)}") - - + print(f"AODV node {self.index()} quits: neighbours = {self.neighbors}, forward paths = {self.forward_path}, reverse paths = {self.reverse_path}, saved data = {self.saved_data}, length of message cache (should be 0) = {len(self.outgoing_message_cache)}") class TopologyCreator: # singleton design pattern - __topology = None + __topology: dict[int, list[int]] = None # "Node index" --> [neighbor node indices] - def __check_connected(topology): + def __check_connected(topology: dict[int, list[int]]) -> Optional[tuple[int, int]]: # if the network is connected, it returns None; # if not, it returns two nodes belonging to two different partitions - queue = [] - visited = [] - visited.append(0) - queue.append(0) + queue = [0] + visited = [0] while queue: s = queue.pop(0) for neigh in topology.get(s): @@ -142,8 +135,8 @@ def __check_connected(topology): return (visited[-1], n) return None - def __create_topology(number_of_devices, probability): - topology = {} + def __create_topology(number_of_devices: int, probability: float): + topology: dict[int, list[int]] = {} for i in range(0, number_of_devices): topology[i] = [] for i in range(0, number_of_devices): @@ -158,16 +151,12 @@ def __create_topology(number_of_devices, probability): return topology @classmethod - def get_topology(cls, number_of_devices, probability): + def get_topology(cls, number_of_devices: int, probability: float): if cls.__topology is not None: - return cls.__topology - - cls.__topology = TopologyCreator.__create_topology(number_of_devices, probability) + cls.__topology = TopologyCreator.__create_topology(number_of_devices, probability) return cls.__topology - - class QuitMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) @@ -175,6 +164,7 @@ def __init__(self, sender: int, destination: int): def __str__(self): return f'QUIT REQUEST {self.source} -> {self.destination}' + class AodvRreqMessage(MessageStub): def __init__(self, sender: int, destination: int, first: int, last: int): super().__init__(sender, destination) @@ -184,6 +174,7 @@ def __init__(self, sender: int, destination: int, first: int, last: int): def __str__(self): return f'RREQ MESSAGE {self.source} -> {self.destination}: ({self.first} -> {self.last})' + class AodvRrepMessage(MessageStub): def __init__(self, sender: int, destination: int, first: int, last: int): super().__init__(sender, destination) @@ -201,4 +192,4 @@ def __init__(self, sender: int, destination: int, last: int, data: str): self.data = data def __str__(self): - return f'DATA MESSAGE {self.source} -> {self.destination}: (final target {self.last} data {self.data})' + return f'DATA MESSAGE {self.source} -> {self.destination}: (final target = {self.last}, data = "{self.data}")' From d73c4db855ebf309a34c1046b390206e775819ac Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 21 Nov 2022 14:17:04 +0100 Subject: [PATCH 083/106] Small fix to my updates --- exercises/exercise12.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/exercise12.py b/exercises/exercise12.py index ddc3a71..7459683 100644 --- a/exercises/exercise12.py +++ b/exercises/exercise12.py @@ -152,7 +152,7 @@ def __create_topology(number_of_devices: int, probability: float): @classmethod def get_topology(cls, number_of_devices: int, probability: float): - if cls.__topology is not None: + if cls.__topology is None: cls.__topology = TopologyCreator.__create_topology(number_of_devices, probability) return cls.__topology From c5a4f1b28e87dfeead909cae3ccf71f2a58f7122 Mon Sep 17 00:00:00 2001 From: Thomas Lohse <49527735+t-lohse@users.noreply.github.com> Date: Thu, 7 Sep 2023 08:34:13 +0200 Subject: [PATCH 084/106] Update README.md Spelling error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e3abd1..8d817f3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Exercises will be described later in this document. In general avoid changing any of the files in the `emulators` subdirectory. Instead, restrict your implementation to extending `emulators.Device` and `emulators.MessageStub`. -Your implementation/solution for, for instance, exersice 1 should go into the `exercises/exercise1.py` document. +Your implementation/solution for, for instance, exercise 1 should go into the `exercises/exercise1.py` document. I will provide new templates as the course progresses. You should be able to execute your solution to exercise 1 using the following lines: From 1fd2d9f5e69d297b20fe95b11fa46b958d4716d7 Mon Sep 17 00:00:00 2001 From: Christoffer Lind Andersen <57995582+Chri692u@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:31:05 +0200 Subject: [PATCH 085/106] micheles fix for shell.start() bug --- exercise_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise_runner.py b/exercise_runner.py index b461134..869faf1 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -75,7 +75,7 @@ def run_instance(): window = Window(number_of_devices, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices, True), instance) window.show() return window - if not gui: + if not gui and isinstance(instance, SteppingEmulator): instance.shell.start() if __name__ == "__main__": From e708239372969091cce2a27a2d3555dd13585e79 Mon Sep 17 00:00:00 2001 From: Christoffer Lind Andersen <57995582+Chri692u@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:33:58 +0200 Subject: [PATCH 086/106] Docs setup --- conf.py | 15 +++++++++++++++ emulators/Device.py | 40 ++++++++++++++++++++++++++++++++++++++++ emulators/Medium.py | 40 ++++++++++++++++++++++++++++++++++++++++ emulators/MessageStub.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 conf.py diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..7533632 --- /dev/null +++ b/conf.py @@ -0,0 +1,15 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = [ + 'sphinx.ext.autodoc', +] + +autodoc_modules = { + 'Device': './emulators/Device.py', + 'Medium': './emulators/Medium.py', + 'MessageStub': './emulators/MessageStub.py', +} + diff --git a/emulators/Device.py b/emulators/Device.py index 8051689..cd241ab 100644 --- a/emulators/Device.py +++ b/emulators/Device.py @@ -5,6 +5,21 @@ class Device: + """ + Base class representing a device in a simulation. + + Args: + index (int): The unique identifier for the device. + number_of_devices (int): The total number of devices in the simulation. + medium (Medium): The communication medium used by the devices. + + Attributes: + _id (int): The unique identifier for the device. + _medium (Medium): The communication medium used by the device. + _number_of_devices (int): The total number of devices in the simulation. + _finished (bool): A flag indicating if the device has finished its task. + """ + def __init__(self, index: int, number_of_devices: int, medium: Medium): self._id = index self._medium = medium @@ -12,18 +27,43 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._finished = False def run(self): + """ + Abstract method representing the main functionality of the device. + """ + raise NotImplementedError("You have to implement a run-method!") def print_result(self): + """ + Abstract method for the result printer + """ raise NotImplementedError("You have to implement a result printer!") def index(self): + """ + The unique identifier for the device. + + Returns: + int: The unique identifier of the device. + """ return self._id def number_of_devices(self): + """ + Get the total number of devices in the simulation. + + Returns: + int: The total number of devices. + """ return self._number_of_devices def medium(self): + """ + Get the communication medium used by the device. + + Returns: + Medium: The communication medium. + """ return self._medium diff --git a/emulators/Medium.py b/emulators/Medium.py index c86f764..018d9eb 100644 --- a/emulators/Medium.py +++ b/emulators/Medium.py @@ -4,6 +4,17 @@ class Medium: + """ + Represents a communication medium in a simulation. + + Args: + index (int): The unique identifier for the medium. + emulator: The emulator object responsible for managing message queues. + + Attributes: + _id (int): The unique identifier for the medium. + _emulator: The emulator object associated with the medium. + """ _id: int def __init__(self, index: int, emulator): @@ -11,12 +22,30 @@ def __init__(self, index: int, emulator): self._emulator = emulator def send(self, message: MessageStub): + """ + Send a message through the medium. + + Args: + message (MessageStub): The message to be sent. + """ self._emulator.queue(message) def receive(self) -> MessageStub: + """ + Receive a message from the medium. + + Returns: + MessageStub: The received message. + """ return self._emulator.dequeue(self._id) def receive_all(self) -> list[MessageStub]: + """ + Receive all available messages from the medium. + + Returns: + list[MessageStub]: A list of received messages. + """ messages = [] while True: message = self._emulator.dequeue(self._id) @@ -25,7 +54,18 @@ def receive_all(self) -> list[MessageStub]: messages.append(message) def wait_for_next_round(self): + """ + Wait for the next communication round. + + This method signals that the device is waiting for the next communication round to begin. + """ self._emulator.done(self._id) def ids(self): + """ + Get the unique identifier of the medium. + + Returns: + int: The unique identifier of the medium. + """ return self._emulator.ids() diff --git a/emulators/MessageStub.py b/emulators/MessageStub.py index 20108ed..d8eb489 100644 --- a/emulators/MessageStub.py +++ b/emulators/MessageStub.py @@ -1,23 +1,61 @@ class MessageStub: + """ + Represents a message used in communication within a simulation. + + Attributes: + _source (int): The identifier of the message sender. + _destination (int): The identifier of the message receiver. + """ _source: int _destination: int def __init__(self, sender_id: int, destination_id: int): + """ + Initialize a MessageStub instance. + + Args: + sender_id (int): The identifier of the message sender. + destination_id (int): The identifier of the message receiver. + """ self._source = sender_id self._destination = destination_id @property def destination(self) -> int: + """ + Get the identifier of the message's destination. + + Returns: + int: The identifier of the destination device. + """ return self._destination @property def source(self) -> int: + """ + Get the identifier of the message's source. + + Returns: + int: The identifier of the source device. + """ return self._source @destination.setter def destination(self, value): + """ + Set the identifier of the message's destination. + + Args: + value (int): The new identifier for the destination device. + """ self._destination = value @source.setter def source(self, value): + """ + Set the identifier of the message's source. + + Args: + value (int): The new identifier for the source device. + """ self._source = value From 754cafe4f1cd851cf561b707c8a2e4c2a935b3c6 Mon Sep 17 00:00:00 2001 From: Christoffer Lind Andersen <57995582+Chri692u@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:34:43 +0200 Subject: [PATCH 087/106] Documentation generation + index file --- docs/source/index.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/source/index.rst diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..9cc0533 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,27 @@ +DistributedExercisesAAU docs +============================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Device +------ + +.. automodule:: emulators.Device + :members: + :undoc-members: + +Medium +------ + +.. automodule:: emulators.Medium + :members: + :undoc-members: + +MessageStub +----------- + +.. automodule:: emulators.MessageStub + :members: + :undoc-members: From 1fad675051b866ba61915a085d70a0caed8102c2 Mon Sep 17 00:00:00 2001 From: Christoffer Lind Andersen <57995582+Chri692u@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:59:05 +0200 Subject: [PATCH 088/106] Update .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9527e8d..04004cb 100644 --- a/.gitignore +++ b/.gitignore @@ -69,9 +69,6 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ - # PyBuilder .pybuilder/ target/ From f93511e2b1ea6e4e053a1fb0f246bef44e4a5613 Mon Sep 17 00:00:00 2001 From: Andreas Knudsen Alstrp Date: Fri, 8 Sep 2023 00:43:11 +0200 Subject: [PATCH 089/106] Create conda environment --- environment.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..b650447 --- /dev/null +++ b/environment.yml @@ -0,0 +1,7 @@ +name: ds +dependencies: + - cryptography >= 37.0.4 + - pip + - pip: + - PyQt6 >= 6.3.1 + - pynput >= 1.7.6 \ No newline at end of file From 0063594062054144142d19791945e1c64ea8fb0d Mon Sep 17 00:00:00 2001 From: Andreas Knudsen Alstrp Date: Fri, 8 Sep 2023 00:45:12 +0200 Subject: [PATCH 090/106] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8d817f3..52b023d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ These packages can be installed using `pip` as shown below: pip install --user -r requirements.txt ``` +These packages can be installed using `conda` as shown below: +```bash +conda env create -f environment.yml + +conda activate ds +``` + The framework is tested under `python3.10` in Arch Linux, Ubuntu and Windows. ## General A FAQ can be found [here](https://github.com/DEIS-Tools/DistributedExercisesAAU/wiki) From 886318682b4bda334122a125a1984c7361aa2df9 Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Thu, 14 Sep 2023 15:47:09 +0200 Subject: [PATCH 091/106] Update README.md inverted sub-exercises 1 and 2 of lecture 2 --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8d817f3..7984715 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,9 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn ``` # Exercise 2 -1. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. _(NOTICE: To run/debug the protocol, you must first implement the network topology described in "task 2.0" below.)_ -2. In the `__init__` of `RipCommunication`, create a ring topology (that is, set up who are the neighbors of each device). Consider a ring size of 10 devices. +1. Similarly to the first lecture, in the `__init__` of `RipCommunication`, create a ring topology (that is, set up who are the neighbors of each device). +2. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. _(NOTICE: To run/debug the protocol, you must first implement the network topology described in "task 2.0" below.)_ +3. Now that you have a ring topology, consider a ring size of 10 devices. 1. How many messages are sent in total before the routing_tables of all nodes are synchronized? 2. How can you "know" that the routing tables are complete and you can start using the network to route packets? Consider the general case of internet, and the specific case of our toy ring network. 3. For the ring network, consider an approach similar to @@ -122,8 +123,8 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return True ``` Is it realistic for a real network? -3. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right after receiving the `RoutableMessage` for itself? What happens to the rest of the nodes? -4. What happens, if a link has a negative cost? How many messages are sent before the `routing_tables` converge? +4. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right after receiving the `RoutableMessage` for itself? What happens to the rest of the nodes? +5. What happens, if a link has a negative cost? How many messages are sent before the `routing_tables` converge? # Exercise 3 Please consult the moodle page, this exercise is not via this framework. From 284caa44b618aa9a0e8fef8c1a1c15dfa3b7aa65 Mon Sep 17 00:00:00 2001 From: Simon Deleuran Laursen <69715502+Laursen79@users.noreply.github.com> Date: Thu, 2 Nov 2023 08:58:16 +0100 Subject: [PATCH 092/106] Formatted with black and fixed ruff linting errors. --- emulators/AsyncEmulator.py | 37 +- emulators/Device.py | 11 +- emulators/EmulatorStub.py | 22 +- emulators/Medium.py | 2 - emulators/SteppingEmulator.py | 115 ++--- emulators/SyncEmulator.py | 34 +- emulators/exercise_overlay.py | 794 +++++++++++++++++++--------------- emulators/table.py | 65 +-- exercise_runner.py | 127 ++++-- exercise_runner_overlay.py | 63 ++- exercises/demo.py | 16 +- exercises/exercise1.py | 6 +- exercises/exercise10.py | 57 ++- exercises/exercise11.py | 95 ++-- exercises/exercise12.py | 38 +- exercises/exercise2.py | 45 +- exercises/exercise4.py | 113 ++--- exercises/exercise5.py | 105 +++-- exercises/exercise6.py | 95 +++- exercises/exercise7.py | 11 +- exercises/exercise8.py | 99 +++-- exercises/exercise9.py | 95 ++-- 22 files changed, 1212 insertions(+), 833 deletions(-) diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 5420009..18bdcc4 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -1,8 +1,6 @@ import copy import random -import threading import time -from threading import Lock from typing import Optional from os import name @@ -18,12 +16,12 @@ CYAN = "" GREEN = "" -class AsyncEmulator(EmulatorStub): +class AsyncEmulator(EmulatorStub): def __init__(self, number_of_devices: int, kind): super().__init__(number_of_devices, kind) self._terminated = 0 - self._messages:dict[int, list[MessageStub]] = {} + self._messages: dict[int, list[MessageStub]] = {} self._messages_sent = 0 def run(self): @@ -46,12 +44,18 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\r\t{GREEN}Send{RESET} {message}') + print(f"\r\t{GREEN}Send{RESET} {message}") if message.destination not in self._messages: self._messages[message.destination] = [] - self._messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing - random.shuffle(self._messages[message.destination]) # shuffle to emulate changes in order - time.sleep(random.uniform(0.01, 0.1)) # try to obfuscate delays and emulate network delays + self._messages[message.destination].append( + copy.deepcopy(message) + ) # avoid accidental memory sharing + random.shuffle( + self._messages[message.destination] + ) # shuffle to emulate changes in order + time.sleep( + random.uniform(0.01, 0.1) + ) # try to obfuscate delays and emulate network delays if not stepper: self._progress.release() @@ -68,21 +72,24 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._messages[index].pop() - print(f'\r\t{GREEN}Recieve{RESET} {m}') + print(f"\r\t{GREEN}Recieve{RESET} {m}") if not stepper: self._progress.release() return m def done(self, index: int): - time.sleep(random.uniform(0.01, 0.1)) # try to obfuscate delays and emulate network delays + time.sleep( + random.uniform(0.01, 0.1) + ) # try to obfuscate delays and emulate network delays return - def print_statistics(self): - print(f'\t{GREEN}Total{RESET} {self._messages_sent} messages') - print(f'\t{GREEN}Average{RESET} {self._messages_sent/len(self._devices)} messages/device') + print(f"\t{GREEN}Total{RESET} {self._messages_sent} messages") + print( + f"\t{GREEN}Average{RESET} {self._messages_sent/len(self._devices)} messages/device" + ) - def terminated(self, index:int): + def terminated(self, index: int): self._progress.acquire() self._terminated += 1 - self._progress.release() \ No newline at end of file + self._progress.release() diff --git a/emulators/Device.py b/emulators/Device.py index cd241ab..bb241e2 100644 --- a/emulators/Device.py +++ b/emulators/Device.py @@ -77,7 +77,10 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def has_work(self) -> bool: # The random call emulates that a concurrent process asked for the - self._has_work = self._has_work or random.randint(0, self.number_of_devices()) == self.index() + self._has_work = ( + self._has_work + or random.randint(0, self.number_of_devices()) == self.index() + ) return self._has_work def do_work(self): @@ -86,19 +89,19 @@ def do_work(self): # might require continued interaction. The "working" thread would then notify our Requester class back # when the mutex is done being used. self._lock.acquire() - print(f'Device {self.index()} has started working') + print(f"Device {self.index()} has started working") self._concurrent_workers += 1 if self._concurrent_workers > 1: self._lock.release() raise Exception("More than one concurrent worker!") self._lock.release() - assert (self.has_work()) + assert self.has_work() amount_of_work = random.randint(1, 4) for i in range(0, amount_of_work): self.medium().wait_for_next_round() self._lock.acquire() - print(f'Device {self.index()} has ended working') + print(f"Device {self.index()} has ended working") self._concurrent_workers -= 1 self._lock.release() self._has_work = False diff --git a/emulators/EmulatorStub.py b/emulators/EmulatorStub.py index 2da68c5..0a9e670 100644 --- a/emulators/EmulatorStub.py +++ b/emulators/EmulatorStub.py @@ -6,7 +6,6 @@ class EmulatorStub: - def __init__(self, number_of_devices: int, kind): self._nids = number_of_devices self._devices = [] @@ -17,7 +16,9 @@ def __init__(self, number_of_devices: int, kind): for index in self.ids(): self._media.append(Medium(index, self)) self._devices.append(kind(index, number_of_devices, self._media[-1])) - self._threads.append(threading.Thread(target=self._run_thread, args=[index])) + self._threads.append( + threading.Thread(target=self._run_thread, args=[index]) + ) def _run_thread(self, index: int): self._devices[index].run() @@ -30,13 +31,12 @@ def _run_thread(self, index: int): def _start_threads(self): cpy = self._threads.copy() random.shuffle(cpy) - print('Starting Threads') + print("Starting Threads") for thread in cpy: thread.start() def all_terminated(self) -> bool: - return all([not self._threads[x].is_alive() - for x in self.ids()]) + return all([not self._threads[x].is_alive() for x in self.ids()]) def ids(self): return range(0, self._nids) @@ -46,19 +46,19 @@ def print_result(self): d.print_result() def run(self): - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") def queue(self, message: MessageStub): - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") def dequeue(self, id) -> MessageStub: - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") def done(self, id): - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") def print_statistics(self): - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") def terminated(self, index: int): - raise NotImplementedError(f'Please contact the instructor') + raise NotImplementedError("Please contact the instructor") diff --git a/emulators/Medium.py b/emulators/Medium.py index 018d9eb..2b48be1 100644 --- a/emulators/Medium.py +++ b/emulators/Medium.py @@ -1,5 +1,3 @@ -import threading - from emulators.MessageStub import MessageStub diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 1323240..cb08cf0 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -6,9 +6,7 @@ from .EmulatorStub import EmulatorStub from emulators.SyncEmulator import SyncEmulator from emulators.MessageStub import MessageStub -from pynput import keyboard -from getpass import getpass #getpass to hide input, cleaner terminal -from threading import Barrier, Lock, Thread #run getpass in seperate thread +from threading import Barrier, Lock, Thread, BrokenBarrierError # run getpass in seperate thread from os import name if name == "posix": @@ -24,28 +22,30 @@ class SteppingEmulator(SyncEmulator, AsyncEmulator): _single = False last_action = "" - messages_received:list[MessageStub] = [] - messages_sent:list[MessageStub] = [] + messages_received: list[MessageStub] = [] + messages_sent: list[MessageStub] = [] keyheld = False pick_device = -1 pick_running = False next_message = None log = None - parent:EmulatorStub = AsyncEmulator + parent: EmulatorStub = AsyncEmulator - def __init__(self, number_of_devices: int, kind): #default init, add stuff here to run when creating object + def __init__( + self, number_of_devices: int, kind + ): # default init, add stuff here to run when creating object super().__init__(number_of_devices, kind) - #self._stepper = Thread(target=lambda: getpass(""), daemon=True) - #self._stepper.start() + # self._stepper = Thread(target=lambda: getpass(""), daemon=True) + # self._stepper.start() self.barrier = Barrier(parties=number_of_devices) self.step_barrier = Barrier(parties=2) self.is_stepping = True self.input_lock = Lock() - #self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) - #self.listener.start() + # self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) + # self.listener.start() self.shell = Thread(target=self.prompt, daemon=True) - self.messages_received:list[MessageStub] = [] - self.messages_sent:list[MessageStub] = [] + self.messages_received: list[MessageStub] = [] + self.messages_sent: list[MessageStub] = [] msg = f""" {CYAN}Shell input:{RESET}: {CYAN}step(press return){RESET}: Step a single time through messages @@ -56,11 +56,11 @@ def __init__(self, number_of_devices: int, kind): #default init, add stuff here {CYAN}swap{RESET}: Toggle between sync and async emulation """ print(msg) - + def dequeue(self, index: int) -> Optional[MessageStub]: self._progress.acquire() - #print(f'thread {index} is in dequeue') - if not self.next_message == None: + # print(f'thread {index} is in dequeue') + if self.next_message: if not index == self.pick_device: self.collectThread() return self.dequeue(index) @@ -73,14 +73,14 @@ def dequeue(self, index: int) -> Optional[MessageStub]: self.next_message = None self.pick_device = -1 self.barrier.reset() - print(f'\r\t{GREEN}Receive{RESET} {result}') - + print(f"\r\t{GREEN}Receive{RESET} {result}") + else: result = self.parent.dequeue(self, index, True) self.pick_running = False - if result != None: + if result: self.messages_received.append(result) self.last_action = "receive" if self.is_stepping: @@ -88,11 +88,11 @@ def dequeue(self, index: int) -> Optional[MessageStub]: self._progress.release() return result - + def queue(self, message: MessageStub): self._progress.acquire() - #print(f'thread {message.source} is in queue') - if not self.next_message == None and not message.source == self.pick_device: + # print(f'thread {message.source} is in queue') + if self.next_message and not message.source == self.pick_device: self.collectThread() return self.queue(message) @@ -101,12 +101,12 @@ def queue(self, message: MessageStub): self.messages_sent.append(message) self.pick_running = False - + if self.is_stepping: self.step() self._progress.release() - #the main function to stop execution + # the main function to stop execution def step(self): if self.is_stepping: self.step_barrier.wait() @@ -121,26 +121,27 @@ def pick(self): for key in messages.keys(): if len(messages[key]) > 0: keys.append(key) - print(f'{GREEN}Available devices:{RESET} {keys}') - device = int(input(f'Specify device: ')) + print(f"{GREEN}Available devices:{RESET} {keys}") + device = int(input("Specify device: ")) self.print_transit_for_device(device) - index = int(input(f'Specify index of the next message: ')) + index = int(input("Specify index of the next message: ")) self.pick_device = device self.next_message = messages[device][index] - while not self.next_message == None: + while self.next_message: self.pick_running = True self.step_barrier.wait() while self.pick_running and not self.all_terminated(): pass - sleep(.1) + sleep(0.1) - def prompt(self): self.prompt_active = True line = "" while not line == "exit": sleep(1) - line = input(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ') + line = input( + f"\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > " + ) self.input_lock.acquire() args = line.split(" ") match args[0]: @@ -166,45 +167,48 @@ def prompt(self): self.prompt_active = False def print_prompt(self): - print(f'\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ', end="", flush=True) + print( + f"\t[{CYAN}{len(self.messages_sent)} {RESET}->{CYAN} {len(self.messages_received)}{RESET}] > ", + end="", + flush=True, + ) - - #print all messages in transit + # print all messages in transit def print_transit(self): print(f"{CYAN}Messages in transit:{RESET}") if self.parent is AsyncEmulator: for messages in self._messages.values(): for message in messages: - print(f'\t{message}') + print(f"\t{message}") elif self.parent is SyncEmulator: for messages in self._last_round_messages.values(): for message in messages: - print(f'\t{message}') - - #print all messages in transit to specified device + print(f"\t{message}") + + # print all messages in transit to specified device def print_transit_for_device(self, device): - print(f'{CYAN}Messages in transit to device #{device}{RESET}') - print(f'\t{CYAN}index{RESET}: ') + print(f"{CYAN}Messages in transit to device #{device}{RESET}") + print(f"\t{CYAN}index{RESET}: ") index = 0 if self.parent is AsyncEmulator: - if not device in self._messages.keys(): + if device not in self._messages.keys(): return - messages:list[MessageStub] = self._messages.get(device) + messages: list[MessageStub] = self._messages.get(device) elif self.parent is SyncEmulator: - if not device in self._last_round_messages.keys(): + if device not in self._last_round_messages.keys(): return - messages:list[MessageStub] = self._last_round_messages.get(device) + messages: list[MessageStub] = self._last_round_messages.get(device) for message in messages: - print(f'\t{CYAN}{index}{RESET}: {message}') - index+=1 - - #swap between which parent class the program will run in between deliveries + print(f"\t{CYAN}{index}{RESET}: {message}") + index += 1 + + # swap between which parent class the program will run in between deliveries def swap_emulator(self): if self.parent is AsyncEmulator: self.parent = SyncEmulator elif self.parent is SyncEmulator: self.parent = AsyncEmulator - print(f'Changed emulator to {GREEN}{self.parent.__name__}{RESET}') + print(f"Changed emulator to {GREEN}{self.parent.__name__}{RESET}") def run(self): self._progress.acquire() @@ -216,7 +220,7 @@ def run(self): self._round_lock.acquire() while True: if self.parent is AsyncEmulator: - sleep(.1) + sleep(0.1) self._progress.acquire() # check if everyone terminated if self.all_terminated(): @@ -226,7 +230,7 @@ def run(self): self._round_lock.acquire() # check if everyone terminated self._progress.acquire() - print(f'\r\t## {GREEN}ROUND {self._rounds}{RESET} ##') + print(f"\r\t## {GREEN}ROUND {self._rounds}{RESET} ##") if self.all_terminated(): self._progress.release() break @@ -243,13 +247,13 @@ def run(self): self._current_round_messages = {} self.reset_done() self._rounds += 1 - ids = [x for x in self.ids()] # convert to list to make it shuffleable + ids = [x for x in self.ids()] # convert to list to make it shuffleable random.shuffle(ids) for index in ids: if self._awaits[index].locked(): self._awaits[index].release() self._progress.release() - + for t in self._threads: t.join() @@ -264,11 +268,10 @@ def print_statistics(self): return self.parent.print_statistics(self) def collectThread(self): - #print("collecting a thread") + # print("collecting a thread") self.pick_running = False self._progress.release() try: self.barrier.wait() - except: + except BrokenBarrierError: pass - \ No newline at end of file diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 99d0b6c..8b8a644 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -16,14 +16,14 @@ CYAN = "" GREEN = "" -class SyncEmulator(EmulatorStub): +class SyncEmulator(EmulatorStub): def __init__(self, number_of_devices: int, kind): super().__init__(number_of_devices, kind) self._round_lock = threading.Lock() self._done = [False for _ in self.ids()] self._awaits = [threading.Lock() for _ in self.ids()] - self._last_round_messages:dict[int, list[MessageStub]] = {} + self._last_round_messages: dict[int, list[MessageStub]] = {} self._current_round_messages = {} self._messages_sent = 0 self._rounds = 0 @@ -44,7 +44,7 @@ def run(self): self._round_lock.acquire() # check if everyone terminated self._progress.acquire() - print(f'\r\t## {GREEN}ROUND {self._rounds}{RESET} ##') + print(f"\r\t## {GREEN}ROUND {self._rounds}{RESET} ##") if self.all_terminated(): self._progress.release() break @@ -61,7 +61,7 @@ def run(self): self._current_round_messages = {} self.reset_done() self._rounds += 1 - ids = [x for x in self.ids()] # convert to list to make it shuffleable + ids = [x for x in self.ids()] # convert to list to make it shuffleable random.shuffle(ids) for index in ids: if self._awaits[index].locked(): @@ -75,10 +75,12 @@ def queue(self, message: MessageStub, stepper=False): if not stepper: self._progress.acquire() self._messages_sent += 1 - print(f'\r\t{GREEN}Send{RESET} {message}') + print(f"\r\t{GREEN}Send{RESET} {message}") if message.destination not in self._current_round_messages: self._current_round_messages[message.destination] = [] - self._current_round_messages[message.destination].append(copy.deepcopy(message)) # avoid accidental memory sharing + self._current_round_messages[message.destination].append( + copy.deepcopy(message) + ) # avoid accidental memory sharing if not stepper: self._progress.release() @@ -95,7 +97,7 @@ def dequeue(self, index: int, stepper=False) -> Optional[MessageStub]: return None else: m = self._last_round_messages[index].pop() - print(f'\r\t{GREEN}Receive{RESET} {m}') + print(f"\r\t{GREEN}Receive{RESET} {m}") if not stepper: self._progress.release() return m @@ -105,7 +107,9 @@ def done(self, index: int): if self._done[index]: # marked as done twice! self._progress.release() - raise RuntimeError(f'Device {index} called wait_for_next_round() twice in the same round!') + raise RuntimeError( + f"Device {index} called wait_for_next_round() twice in the same round!" + ) self._done[index] = True # check if the thread have marked their round as done OR have ended @@ -114,17 +118,17 @@ def done(self, index: int): self._progress.release() self._awaits[index].acquire() - def print_statistics(self): - print(f'\t{GREEN}Total:{RESET} {self._messages_sent} messages') - print(f'\t{GREEN}Average:{RESET} {self._messages_sent/len(self._devices)} messages/device') - print(f'\t{GREEN}Total:{RESET} {self._rounds} rounds') + print(f"\t{GREEN}Total:{RESET} {self._messages_sent} messages") + print( + f"\t{GREEN}Average:{RESET} {self._messages_sent/len(self._devices)} messages/device" + ) + print(f"\t{GREEN}Total:{RESET} {self._rounds} rounds") - def terminated(self, index:int): + def terminated(self, index: int): self._progress.acquire() self._done[index] = True - if all([self._done[x] or not self._threads[x].is_alive() - for x in self.ids()]): + if all([self._done[x] or not self._threads[x].is_alive() for x in self.ids()]): if self._round_lock.locked(): self._round_lock.release() self._progress.release() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 0ff7675..709aad6 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -1,7 +1,14 @@ -from random import randint from threading import Thread from time import sleep -from PyQt6.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QTabWidget, QLabel, QLineEdit +from PyQt6.QtWidgets import ( + QWidget, + QApplication, + QHBoxLayout, + QVBoxLayout, + QPushButton, + QTabWidget, + QLabel, +) from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt from sys import argv @@ -9,364 +16,447 @@ from math import cos, sin, pi from emulators.AsyncEmulator import AsyncEmulator from emulators.MessageStub import MessageStub -from emulators.SyncEmulator import SyncEmulator from emulators.table import Table from emulators.SteppingEmulator import SteppingEmulator if name == "posix": - RESET = "\u001B[0m" - CYAN = "\u001B[36m" - GREEN = "\u001B[32m" - RED = "\u001B[31m" + RESET = "\u001B[0m" + CYAN = "\u001B[36m" + GREEN = "\u001B[32m" + RED = "\u001B[31m" else: - RESET = "" - CYAN = "" - GREEN = "" - RED = "" - -def circle_button_style(size, color = "black"): - return f''' - QPushButton {{ - background-color: transparent; - border-style: solid; - border-width: 2px; - border-radius: {int(size/2)}px; - border-color: {color}; - max-width: {size}px; - max-height: {size}px; - min-width: {size}px; - min-height: {size}px; - }} - QPushButton:hover {{ - background-color: gray; - border-width: 2px; - }} - QPushButton:pressed {{ - background-color: transparent; - border-width: 1px - }} - ''' + RESET = "" + CYAN = "" + GREEN = "" + RED = "" + + +def circle_button_style(size, color="black"): + return f""" + QPushButton {{ + background-color: transparent; + border-style: solid; + border-width: 2px; + border-radius: {int(size / 2)}px; + border-color: {color}; + max-width: {size}px; + max-height: {size}px; + min-width: {size}px; + min-height: {size}px; + }} + QPushButton:hover {{ + background-color: gray; + border-width: 2px; + }} + QPushButton:pressed {{ + background-color: transparent; + border-width: 1px + }} + """ + class Window(QWidget): - h = 640 - w = 600 - device_size = 80 - last_message = None - windows = list() - - def __init__(self, elements, restart_function, emulator:SteppingEmulator): - super().__init__() - self.emulator = emulator - self.setFixedSize(self.w, self.h) - layout = QVBoxLayout() - tabs = QTabWidget() - tabs.setFixedSize(self.w-20, self.h-20) - self.buttons:dict[int, QPushButton] = {} - tabs.addTab(self.main(elements, restart_function), 'Main') - tabs.addTab(self.controls(), 'controls') - layout.addWidget(tabs) - self.setLayout(layout) - self.setWindowTitle("Stepping Emulator") - self.setWindowIcon(QIcon("icon.ico")) - self.set_device_color() - self.pick_window = False - self.queue_window = False - self.all_data_window = False - - def coordinates(self, center, r, i, n): - x = sin((i*2*pi)/n) - y = cos((i*2*pi)/n) - if x < pi: - return int(center[0] - (r*x)), int(center[1] - (r*y)) - else: - return int(center[0] - (r*-x)), int(center[1] - (r*y)) - - def show_device_data(self, device_id): - def show(): - received:list[MessageStub] = list() - sent:list[MessageStub] = list() - for message in self.emulator.messages_received: - if message.destination == device_id: - received.append(message) - if message.source == device_id: - sent.append(message) - if len(received) > len(sent): - for _ in range(len(received)-len(sent)): - sent.append("") - elif len(sent) > len(received): - for _ in range(len(sent)-len(received)): - received.append("") - content = list() - for i in range(len(received)): - if received[i] == "": - msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - content.append(["", received[i], str(sent[i].destination), msg]) - elif sent[i] == "": - msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([str(received[i].source), msg, "", sent[i]]) - else: - sent_msg = str(sent[i]).replace(f'{sent[i].source} -> {sent[i].destination} : ', "").replace(f'{sent[i].source}->{sent[i].destination} : ', "") - received_msg = str(received[i]).replace(f'{received[i].source} -> {received[i].destination} : ', "").replace(f'{received[i].source}->{received[i].destination} : ', "") - content.append([str(received[i].source), received_msg, str(sent[i].destination), sent_msg]) - content.insert(0, ['Source', 'Message', 'Destination', 'Message']) - table = Table(content, title=f'Device #{device_id}') - self.windows.append(table) - table.setFixedSize(300, 500) - table.show() - return table - return show - - def show_all_data(self): - if self.all_data_window: - return - self.all_data_window = True - content = [] - messages = self.emulator.messages_sent - message_content = [] - for message in messages: - temp = str(message) - temp = temp.replace(f'{message.source} -> {message.destination} : ', "") - temp = temp.replace(f'{message.source}->{message.destination} : ', "") - message_content.append(temp) - - content = [[str(messages[i].source), str(messages[i].destination), message_content[i], str(i)] for i in range(len(messages))] - content.insert(0, ['Source', 'Destination', 'Message', 'Sequence number']) - parent = self - class MyTable(Table): - def closeEvent(self, event): - parent.all_data_window = False - return super().closeEvent(event) - table = MyTable(content, title=f'All data') - self.windows.append(table) - table.setFixedSize(500, 500) - table.show() - return table - - def show_queue(self): - if self.queue_window: - return - self.queue_window = True - content = [["Source", "Destination", "Message"]] - if self.emulator.parent is AsyncEmulator: - queue = self.emulator._messages.values() - else: - queue = self.emulator._last_round_messages.values() - for messages in queue: - for message in messages: - message_stripped = str(message).replace(f'{message.source} -> {message.destination} : ', "").replace(f'{message.source}->{message.destination} : ', "") - content.append([str(message.source), str(message.destination), message_stripped]) - parent = self - class MyWidget(QWidget): - def closeEvent(self, event): - parent.queue_window = False - return super().closeEvent(event) - window = MyWidget() - layout = QVBoxLayout() - table = Table(content, "Message queue") - layout.addWidget(table) - window.setLayout(layout) - self.windows.append(window) - window.setFixedSize(500, 500) - window.show() - - def pick(self): - if self.pick_window: - return - self.pick_window = True - def execute(device, index): - def inner_execute(): - if self.emulator._devices[device]._finished == True: - table.destroy(True, True) - print(f'{RED}The selected device has already finished execution!{RESET}') - return - if self.emulator.parent is AsyncEmulator: - message = self.emulator._messages[device][index] - else: - message = self.emulator._last_round_messages[device][index] - - print(f'\r{CYAN}Choice from pick command{RESET}: {message}') - - self.emulator.pick_device = device - self.emulator.next_message = message - table.destroy(True, True) - self.pick_window = False - size = len(self.emulator.messages_received) - while not self.emulator.next_message == None: - self.emulator.pick_running = True - self.step() - while self.emulator.pick_running and not self.emulator.all_terminated(): - pass - sleep(.1) - - assert len(self.emulator.messages_received) == size+1 - - return inner_execute - - keys = [] - if self.emulator.parent is AsyncEmulator: - messages = self.emulator._messages - else: - messages = self.emulator._last_round_messages - for item in messages.items(): - keys.append(item[0]) - keys.sort() - max_size = 0 - for m in messages.values(): - if len(m) > max_size: - max_size = len(m) - - content = [] - for i in range(max_size): - content.append([]) - for key in keys: - if len(messages[key]) > i: - button = QPushButton(str(messages[key][i])) - function_reference = execute(key, i) - button.clicked.connect(function_reference) - content[i].append(button) - else: - content[i].append("") - content.insert(0, [f'Device {key}' for key in keys]) - content[0].insert(0, "Message #") - for i in range(max_size): - content[i+1].insert(0, str(i)) - - parent = self - class MyTable(Table): - def closeEvent(self, event): - parent.pick_window = False - return super().closeEvent(event) - table = MyTable(content, "Pick a message to be transmitted next to a device") - table.setFixedSize(150*len(self.emulator._devices)+1, 400) - table.show() - - def end(self): - if self.emulator.all_terminated(): - return - self.emulator.is_stepping = False - self.emulator.step_barrier.wait() - while not self.emulator.all_terminated(): - self.set_device_color() - sleep(.1) - #self.emulator.print_prompt() - - def set_device_color(self): - sleep(.1) - messages = self.emulator.messages_sent if self.emulator.last_action == "send" else self.emulator.messages_received - if len(messages) != 0: - last_message = messages[len(messages)-1] - if not last_message == self.last_message: - for button in self.buttons.values(): - button.setStyleSheet(circle_button_style(self.device_size)) - if last_message.source == last_message.destination: - self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'yellow')) - else: - self.buttons[last_message.source].setStyleSheet(circle_button_style(self.device_size, 'green')) - self.buttons[last_message.destination].setStyleSheet(circle_button_style(self.device_size, 'red')) - self.last_message = last_message - - def step(self): - self.emulator.input_lock.acquire() - if not self.emulator.all_terminated(): - self.emulator.step_barrier.wait() - self.emulator.input_lock.release() - - while self.emulator.step_barrier.n_waiting == 0: - pass - self.set_device_color() - #self.emulator.print_prompt() - - def restart_algorithm(self, function): - #if self.emulator.prompt_active: - #print(f'\r{RED}Please type "exit" in the prompt below{RESET}') - #self.emulator.print_prompt() - #return - self.windows.append(function()) - - def main(self, num_devices, restart_function): - main_tab = QWidget() - green = QLabel("green: source", main_tab) - green.setStyleSheet("color: green;") - green.move(5, 0) - green.show() - red = QLabel("red: destination", main_tab) - red.setStyleSheet("color: red;") - red.move(5, 20) - red.show() - yellow = QLabel("yellow: same device", main_tab) - yellow.setStyleSheet("color: yellow;") - yellow.move(5, 40) - yellow.show() - layout = QVBoxLayout() - device_area = QWidget() - device_area.setFixedSize(500, 500) - layout.addWidget(device_area) - main_tab.setLayout(layout) - for i in range(num_devices): - x, y = self.coordinates((device_area.width()/2, device_area.height()/2), (device_area.height()/2) - (self.device_size/2), i, num_devices) - button = QPushButton(f'Device #{i}', main_tab) - button.resize(self.device_size, self.device_size) - button.setStyleSheet(circle_button_style(self.device_size)) - button.move(x, int(y - (self.device_size/2))) - button.clicked.connect(self.show_device_data(i)) - self.buttons[i] = button - - button_actions = {'Step': self.step, 'End': self.end, 'Restart algorithm': lambda: self.restart_algorithm(restart_function), 'Show all messages': self.show_all_data, 'Switch emulator': self.swap_emulator, 'Show queue': self.show_queue, 'Pick': self.pick} - inner_layout = QHBoxLayout() - index = 0 - for action in button_actions.items(): - index+=1 - if index == 4: - layout.addLayout(inner_layout) - inner_layout = QHBoxLayout() - button = QPushButton(action[0]) - button.clicked.connect(action[1]) - inner_layout.addWidget(button) - layout.addLayout(inner_layout) - - return main_tab - - def swap_emulator(self): - self.emulator.input_lock.acquire() - print() - self.emulator.swap_emulator() - #self.emulator.print_prompt() - self.emulator.input_lock.release() - - def controls(self): - controls_tab = QWidget() - content = { - 'step(press return)': 'Step a single time through messages', - 'exit': 'Finish the execution of the algorithm', - 'queue': 'Show all messages currently waiting to be transmitted', - 'queue ': 'Show all messages currently waiting to be transmitted to a specific device', - 'pick': 'Pick the next message waiting to be transmitted to transmit next', - 'swap': 'Toggle between sync and async emulation' - } - main = QVBoxLayout() - main.setAlignment(Qt.AlignmentFlag.AlignTop) - controls = QLabel("Controls") - controls.setAlignment(Qt.AlignmentFlag.AlignCenter) - main.addWidget(controls) - top = QHBoxLayout() - key_layout = QVBoxLayout() - value_layout = QVBoxLayout() - for key in content.keys(): - key_layout.addWidget(QLabel(key)) - value_layout.addWidget(QLabel(content[key])) - top.addLayout(key_layout) - top.addLayout(value_layout) - main.addLayout(top) - controls_tab.setLayout(main) - return controls_tab - - def closeEvent(self, event): - Thread(target=self.end).start() - event.accept() + h = 640 + w = 600 + device_size = 80 + last_message = None + windows = list() + + def __init__(self, elements, restart_function, emulator: SteppingEmulator): + super().__init__() + self.emulator = emulator + self.setFixedSize(self.w, self.h) + layout = QVBoxLayout() + tabs = QTabWidget() + tabs.setFixedSize(self.w - 20, self.h - 20) + self.buttons: dict[int, QPushButton] = {} + tabs.addTab(self.main(elements, restart_function), "Main") + tabs.addTab(self.controls(), "controls") + layout.addWidget(tabs) + self.setLayout(layout) + self.setWindowTitle("Stepping Emulator") + self.setWindowIcon(QIcon("icon.ico")) + self.set_device_color() + self.pick_window = False + self.queue_window = False + self.all_data_window = False + + def coordinates(self, center, r, i, n): + x = sin((i * 2 * pi) / n) + y = cos((i * 2 * pi) / n) + if x < pi: + return int(center[0] - (r * x)), int(center[1] - (r * y)) + else: + return int(center[0] - (r * -x)), int(center[1] - (r * y)) + + def show_device_data(self, device_id): + def show(): + received: list[MessageStub] = list() + sent: list[MessageStub] = list() + for message in self.emulator.messages_received: + if message.destination == device_id: + received.append(message) + if message.source == device_id: + sent.append(message) + if len(received) > len(sent): + for _ in range(len(received) - len(sent)): + sent.append("") + elif len(sent) > len(received): + for _ in range(len(sent) - len(received)): + received.append("") + content = list() + for i in range(len(received)): + if received[i] == "": + msg = ( + str(sent[i]) + .replace(f"{sent[i].source} -> {sent[i].destination} : ", "") + .replace(f"{sent[i].source}->{sent[i].destination} : ", "") + ) + content.append(["", received[i], str(sent[i].destination), msg]) + elif sent[i] == "": + msg = ( + str(received[i]) + .replace( + f"{received[i].source} -> {received[i].destination} : ", "" + ) + .replace( + f"{received[i].source}->{received[i].destination} : ", "" + ) + ) + content.append([str(received[i].source), msg, "", sent[i]]) + else: + sent_msg = ( + str(sent[i]) + .replace(f"{sent[i].source} -> {sent[i].destination} : ", "") + .replace(f"{sent[i].source}->{sent[i].destination} : ", "") + ) + received_msg = ( + str(received[i]) + .replace( + f"{received[i].source} -> {received[i].destination} : ", "" + ) + .replace( + f"{received[i].source}->{received[i].destination} : ", "" + ) + ) + content.append( + [ + str(received[i].source), + received_msg, + str(sent[i].destination), + sent_msg, + ] + ) + content.insert(0, ["Source", "Message", "Destination", "Message"]) + table = Table(content, title=f"Device #{device_id}") + self.windows.append(table) + table.setFixedSize(300, 500) + table.show() + return table + + return show + + def show_all_data(self): + if self.all_data_window: + return + self.all_data_window = True + content = [] + messages = self.emulator.messages_sent + message_content = [] + for message in messages: + temp = str(message) + temp = temp.replace(f"{message.source} -> {message.destination} : ", "") + temp = temp.replace(f"{message.source}->{message.destination} : ", "") + message_content.append(temp) + + content = [ + [ + str(messages[i].source), + str(messages[i].destination), + message_content[i], + str(i), + ] + for i in range(len(messages)) + ] + content.insert(0, ["Source", "Destination", "Message", "Sequence number"]) + parent = self + + class MyTable(Table): + def closeEvent(self, event): + parent.all_data_window = False + return super().closeEvent(event) + + table = MyTable(content, title="All data") + self.windows.append(table) + table.setFixedSize(500, 500) + table.show() + return table + + def show_queue(self): + if self.queue_window: + return + self.queue_window = True + content = [["Source", "Destination", "Message"]] + if self.emulator.parent is AsyncEmulator: + queue = self.emulator._messages.values() + else: + queue = self.emulator._last_round_messages.values() + for messages in queue: + for message in messages: + message_stripped = ( + str(message) + .replace(f"{message.source} -> {message.destination} : ", "") + .replace(f"{message.source}->{message.destination} : ", "") + ) + content.append( + [str(message.source), str(message.destination), message_stripped] + ) + parent = self + + class MyWidget(QWidget): + def closeEvent(self, event): + parent.queue_window = False + return super().closeEvent(event) + + window = MyWidget() + layout = QVBoxLayout() + table = Table(content, "Message queue") + layout.addWidget(table) + window.setLayout(layout) + self.windows.append(window) + window.setFixedSize(500, 500) + window.show() + + def pick(self): + if self.pick_window: + return + self.pick_window = True + + def execute(device, index): + def inner_execute(): + if self.emulator._devices[device]._finished: + table.destroy(True, True) + print( + f"{RED}The selected device has already finished execution!{RESET}" + ) + return + if self.emulator.parent is AsyncEmulator: + message = self.emulator._messages[device][index] + else: + message = self.emulator._last_round_messages[device][index] + + print(f"\r{CYAN}Choice from pick command{RESET}: {message}") + + self.emulator.pick_device = device + self.emulator.next_message = message + table.destroy(True, True) + self.pick_window = False + size = len(self.emulator.messages_received) + while self.emulator.next_message: + self.emulator.pick_running = True + self.step() + while ( + self.emulator.pick_running + and not self.emulator.all_terminated() + ): + pass + sleep(0.1) + + assert len(self.emulator.messages_received) == size + 1 + + return inner_execute + + keys = [] + if self.emulator.parent is AsyncEmulator: + messages = self.emulator._messages + else: + messages = self.emulator._last_round_messages + for item in messages.items(): + keys.append(item[0]) + keys.sort() + max_size = 0 + for m in messages.values(): + if len(m) > max_size: + max_size = len(m) + + content = [] + for i in range(max_size): + content.append([]) + for key in keys: + if len(messages[key]) > i: + button = QPushButton(str(messages[key][i])) + function_reference = execute(key, i) + button.clicked.connect(function_reference) + content[i].append(button) + else: + content[i].append("") + content.insert(0, [f"Device {key}" for key in keys]) + content[0].insert(0, "Message #") + for i in range(max_size): + content[i + 1].insert(0, str(i)) + + parent = self + + class MyTable(Table): + def closeEvent(self, event): + parent.pick_window = False + return super().closeEvent(event) + + table = MyTable(content, "Pick a message to be transmitted next to a device") + table.setFixedSize(150 * len(self.emulator._devices) + 1, 400) + table.show() + + def end(self): + if self.emulator.all_terminated(): + return + self.emulator.is_stepping = False + self.emulator.step_barrier.wait() + while not self.emulator.all_terminated(): + self.set_device_color() + sleep(0.1) + # self.emulator.print_prompt() + + def set_device_color(self): + sleep(0.1) + messages = ( + self.emulator.messages_sent + if self.emulator.last_action == "send" + else self.emulator.messages_received + ) + if len(messages) != 0: + last_message = messages[len(messages) - 1] + if not last_message == self.last_message: + for button in self.buttons.values(): + button.setStyleSheet(circle_button_style(self.device_size)) + if last_message.source == last_message.destination: + self.buttons[last_message.source].setStyleSheet( + circle_button_style(self.device_size, "yellow") + ) + else: + self.buttons[last_message.source].setStyleSheet( + circle_button_style(self.device_size, "green") + ) + self.buttons[last_message.destination].setStyleSheet( + circle_button_style(self.device_size, "red") + ) + self.last_message = last_message + + def step(self): + self.emulator.input_lock.acquire() + if not self.emulator.all_terminated(): + self.emulator.step_barrier.wait() + self.emulator.input_lock.release() + + while self.emulator.step_barrier.n_waiting == 0: + pass + self.set_device_color() + # self.emulator.print_prompt() + + def restart_algorithm(self, function): + # if self.emulator.prompt_active: + # print(f'\r{RED}Please type "exit" in the prompt below{RESET}') + # self.emulator.print_prompt() + # return + self.windows.append(function()) + + def main(self, num_devices, restart_function): + main_tab = QWidget() + green = QLabel("green: source", main_tab) + green.setStyleSheet("color: green;") + green.move(5, 0) + green.show() + red = QLabel("red: destination", main_tab) + red.setStyleSheet("color: red;") + red.move(5, 20) + red.show() + yellow = QLabel("yellow: same device", main_tab) + yellow.setStyleSheet("color: yellow;") + yellow.move(5, 40) + yellow.show() + layout = QVBoxLayout() + device_area = QWidget() + device_area.setFixedSize(500, 500) + layout.addWidget(device_area) + main_tab.setLayout(layout) + for i in range(num_devices): + x, y = self.coordinates( + (device_area.width() / 2, device_area.height() / 2), + (device_area.height() / 2) - (self.device_size / 2), + i, + num_devices, + ) + button = QPushButton(f"Device #{i}", main_tab) + button.resize(self.device_size, self.device_size) + button.setStyleSheet(circle_button_style(self.device_size)) + button.move(x, int(y - (self.device_size / 2))) + button.clicked.connect(self.show_device_data(i)) + self.buttons[i] = button + + button_actions = { + "Step": self.step, + "End": self.end, + "Restart algorithm": lambda: self.restart_algorithm(restart_function), + "Show all messages": self.show_all_data, + "Switch emulator": self.swap_emulator, + "Show queue": self.show_queue, + "Pick": self.pick, + } + inner_layout = QHBoxLayout() + index = 0 + for action in button_actions.items(): + index += 1 + if index == 4: + layout.addLayout(inner_layout) + inner_layout = QHBoxLayout() + button = QPushButton(action[0]) + button.clicked.connect(action[1]) + inner_layout.addWidget(button) + layout.addLayout(inner_layout) + + return main_tab + + def swap_emulator(self): + self.emulator.input_lock.acquire() + print() + self.emulator.swap_emulator() + # self.emulator.print_prompt() + self.emulator.input_lock.release() + + def controls(self): + controls_tab = QWidget() + content = { + "step(press return)": "Step a single time through messages", + "exit": "Finish the execution of the algorithm", + "queue": "Show all messages currently waiting to be transmitted", + "queue ": "Show all messages currently waiting to be transmitted to a specific device", + "pick": "Pick the next message waiting to be transmitted to transmit next", + "swap": "Toggle between sync and async emulation", + } + main = QVBoxLayout() + main.setAlignment(Qt.AlignmentFlag.AlignTop) + controls = QLabel("Controls") + controls.setAlignment(Qt.AlignmentFlag.AlignCenter) + main.addWidget(controls) + top = QHBoxLayout() + key_layout = QVBoxLayout() + value_layout = QVBoxLayout() + for key in content.keys(): + key_layout.addWidget(QLabel(key)) + value_layout.addWidget(QLabel(content[key])) + top.addLayout(key_layout) + top.addLayout(value_layout) + main.addLayout(top) + controls_tab.setLayout(main) + return controls_tab + + def closeEvent(self, event): + Thread(target=self.end).start() + event.accept() + if __name__ == "__main__": - app = QApplication(argv) - window = Window(argv[1] if len(argv) > 1 else 10, lambda: print("Restart function")) + app = QApplication(argv) + window = Window(argv[1] if len(argv) > 1 else 10, lambda: print("Restart function")) - app.exec() + app.exec() diff --git a/emulators/table.py b/emulators/table.py index dd0d561..460073b 100644 --- a/emulators/table.py +++ b/emulators/table.py @@ -1,37 +1,42 @@ -from typing import Any -from PyQt6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QScrollArea, QPushButton +from PyQt6.QtWidgets import ( + QWidget, + QHBoxLayout, + QVBoxLayout, + QLabel, + QScrollArea, +) from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt -class Table(QScrollArea): - def __init__(self, content:list[list[str | QWidget]], title="table"): - super().__init__() - widget = QWidget() - self.setWindowIcon(QIcon('icon.ico')) - self.setWindowTitle(title) - columns = QVBoxLayout() - columns.setAlignment(Qt.AlignmentFlag.AlignTop) - for row in content: - column = QHBoxLayout() - for element in row: - if type(element) is str: - label = QLabel(element) - label.setAlignment(Qt.AlignmentFlag.AlignCenter) - else: - label = element - column.addWidget(label) - columns.addLayout(column) - widget.setLayout(columns) - self.setWidgetResizable(True) - self.setWidget(widget) +class Table(QScrollArea): + def __init__(self, content: list[list[str | QWidget]], title="table"): + super().__init__() + widget = QWidget() + self.setWindowIcon(QIcon("icon.ico")) + self.setWindowTitle(title) + columns = QVBoxLayout() + columns.setAlignment(Qt.AlignmentFlag.AlignTop) + for row in content: + column = QHBoxLayout() + for element in row: + if isinstance(element, str): + label = QLabel(element) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + else: + label = element + column.addWidget(label) + columns.addLayout(column) + widget.setLayout(columns) + self.setWidgetResizable(True) + self.setWidget(widget) if __name__ == "__main__": - from PyQt6.QtWidgets import QApplication - from sys import argv - app = QApplication(argv) - table = Table([[str(i+j) for i in range(5)] for j in range(5)]) - table.show() - app.exec() - \ No newline at end of file + from PyQt6.QtWidgets import QApplication + from sys import argv + + app = QApplication(argv) + table = Table([[str(i + j) for i in range(5)] for j in range(5)]) + table.show() + app.exec() diff --git a/exercise_runner.py b/exercise_runner.py index 869faf1..663e021 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -4,18 +4,6 @@ from threading import Thread from emulators.exercise_overlay import Window -import exercises.exercise1 -import exercises.exercise2 -import exercises.exercise4 -import exercises.exercise5 -import exercises.exercise6 -import exercises.exercise7 -import exercises.exercise8 -import exercises.exercise9 -import exercises.exercise10 -import exercises.exercise11 -import exercises.exercise12 -import exercises.demo from emulators.AsyncEmulator import AsyncEmulator from emulators.SyncEmulator import SyncEmulator from emulators.SteppingEmulator import SteppingEmulator @@ -29,73 +17,128 @@ CYAN = "" GREEN = "" + def fetch_alg(lecture: str, algorithm: str): - if '.' in algorithm or ';' in algorithm: - raise ValueError(f'"." and ";" are not allowed as names of solutions.') + if "." in algorithm or ";" in algorithm: + raise ValueError('"." and ";" are not allowed as names of solutions.') try: - alg = eval(f'exercises.{lecture}.{algorithm}') + alg = eval(f"exercises.{lecture}.{algorithm}") if not inspect.isclass(alg): raise TypeError(f'Could not find "exercises.{lecture}.{algorithm} class') - except: + except NameError: raise TypeError(f'Could not find "exercises.{lecture}.{algorithm} class') return alg -def run_exercise(lecture_no: int, algorithm: str, network_type: str, number_of_devices: int, gui:bool): +def run_exercise( + lecture_no: int, + algorithm: str, + network_type: str, + number_of_devices: int, + gui: bool, +): print( - f'Running Lecture {lecture_no} Algorithm {algorithm} in a network of type [{network_type}] using {number_of_devices} devices') + f"Running Lecture {lecture_no} Algorithm {algorithm} in a network of type [{network_type}] using {number_of_devices} devices" + ) if number_of_devices < 2: - raise IndexError(f'At least two devices are needed as an input argument, got {number_of_devices}') + raise IndexError( + f"At least two devices are needed as an input argument, got {number_of_devices}" + ) emulator = None - if network_type == 'async': + if network_type == "async": emulator = AsyncEmulator - elif network_type == 'sync': + elif network_type == "sync": emulator = SyncEmulator - elif network_type == 'stepping': + elif network_type == "stepping": emulator = SteppingEmulator instance = None if lecture_no == 0: - alg = fetch_alg('demo', 'PingPong') + alg = fetch_alg("demo", "PingPong") instance = emulator(number_of_devices, alg) else: - alg = fetch_alg(f'exercise{lecture_no}', algorithm) + alg = fetch_alg(f"exercise{lecture_no}", algorithm) instance = emulator(number_of_devices, alg) + def run_instance(): if instance is not None: instance.run() - print(f'{CYAN}Execution Complete{RESET}') + print(f"{CYAN}Execution Complete{RESET}") instance.print_result() - print(f'{CYAN}Statistics{RESET}') + print(f"{CYAN}Statistics{RESET}") instance.print_statistics() else: raise NotImplementedError( - f'You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released') + f"You are trying to run an exercise ({algorithm}) of a lecture ({lecture_no}) which has not yet been released" + ) + Thread(target=run_instance).start() if isinstance(instance, SteppingEmulator) and gui: - window = Window(number_of_devices, lambda: run_exercise(lecture_no, algorithm, network_type, number_of_devices, True), instance) + window = Window( + number_of_devices, + lambda: run_exercise( + lecture_no, algorithm, network_type, number_of_devices, True + ), + instance, + ) window.show() return window if not gui and isinstance(instance, SteppingEmulator): instance.shell.start() + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='For exercises in Distributed Systems.') - parser.add_argument('--lecture', metavar='N', type=int, nargs=1, - help='Lecture number', required=True, choices=[0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12]) - parser.add_argument('--algorithm', metavar='alg', type=str, nargs=1, - help='Which algorithm from the exercise to run', required=True) - parser.add_argument('--type', metavar='nw', type=str, nargs=1, - help='whether to use [async] or [sync] network', required=True, choices=['async', 'sync', 'stepping']) - parser.add_argument('--devices', metavar='N', type=int, nargs=1, - help='Number of devices to run', required=True) - parser.add_argument("--gui", action="store_true", help="Toggle the gui or cli", required=False) + parser = argparse.ArgumentParser( + description="For exercises in Distributed Systems." + ) + parser.add_argument( + "--lecture", + metavar="N", + type=int, + nargs=1, + help="Lecture number", + required=True, + choices=[0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ) + parser.add_argument( + "--algorithm", + metavar="alg", + type=str, + nargs=1, + help="Which algorithm from the exercise to run", + required=True, + ) + parser.add_argument( + "--type", + metavar="nw", + type=str, + nargs=1, + help="whether to use [async] or [sync] network", + required=True, + choices=["async", "sync", "stepping"], + ) + parser.add_argument( + "--devices", + metavar="N", + type=int, + nargs=1, + help="Number of devices to run", + required=True, + ) + parser.add_argument( + "--gui", action="store_true", help="Toggle the gui or cli", required=False + ) args = parser.parse_args() import sys - if args.gui and args.type[0] == 'stepping': + + if args.gui and args.type[0] == "stepping": from PyQt6.QtWidgets import QApplication + app = QApplication(sys.argv) - run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], True) + run_exercise( + args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], True + ) app.exec() else: - run_exercise(args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], False) - \ No newline at end of file + run_exercise( + args.lecture[0], args.algorithm[0], args.type[0], args.devices[0], False + ) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index c6251de..86eaf78 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,15 +1,25 @@ from exercise_runner import run_exercise -from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QComboBox +from PyQt6.QtWidgets import ( + QApplication, + QWidget, + QLineEdit, + QVBoxLayout, + QPushButton, + QHBoxLayout, + QLabel, + QComboBox, +) from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt from sys import argv + app = QApplication(argv) windows = list() -#new +# new window = QWidget() -window.setWindowIcon(QIcon('icon.ico')) +window.setWindowIcon(QIcon("icon.ico")) window.setWindowTitle("Distributed Exercises AAU") main = QVBoxLayout() window.setFixedSize(600, 100) @@ -42,33 +52,50 @@ input_area.addLayout(devices_layout) main.addLayout(input_area) starting_exercise = False -actions:dict[str, QLineEdit | QComboBox] = {"Lecture":lecture_combobox, "Type":type_combobox, "Algorithm":algorithm_input, "Devices":devices_input} +actions: dict[str, QLineEdit | QComboBox] = { + "Lecture": lecture_combobox, + "Type": type_combobox, + "Algorithm": algorithm_input, + "Devices": devices_input, +} + def text_changed(text): exercises = { - 0:'PingPong', - 1:'Gossip', - 2:'RipCommunication', - 4:'TokenRing', - 5:'TOSEQMulticast', - 6:'PAXOS', - 7:'Bully', - 8:'GfsNetwork', - 9:'MapReduceNetwork', - 10:'BlockchainNetwork', - 11:'ChordNetwork', - 12:'AodvNode'} + 0: "PingPong", + 1: "Gossip", + 2: "RipCommunication", + 4: "TokenRing", + 5: "TOSEQMulticast", + 6: "PAXOS", + 7: "Bully", + 8: "GfsNetwork", + 9: "MapReduceNetwork", + 10: "BlockchainNetwork", + 11: "ChordNetwork", + 12: "AodvNode", + } lecture = int(text) - actions['Algorithm'].setText(exercises[lecture]) + actions["Algorithm"].setText(exercises[lecture]) + lecture_combobox.currentTextChanged.connect(text_changed) + def start_exercise(): global starting_exercise if not starting_exercise: starting_exercise = True - windows.append(run_exercise(int(actions['Lecture'].currentText()), actions['Algorithm'].text(), actions['Type'].currentText(), int(actions['Devices'].text()), True)) + windows.append( + run_exercise( + int(actions["Lecture"].currentText()), + actions["Algorithm"].text(), + actions["Type"].currentText(), + int(actions["Devices"].text()), + True, + ) + ) starting_exercise = False diff --git a/exercises/demo.py b/exercises/demo.py index 85e526d..853748e 100644 --- a/exercises/demo.py +++ b/exercises/demo.py @@ -7,7 +7,6 @@ # We extend the MessageStub here for the message-types we wish to communicate class PingMessage(MessageStub): - # the constructor-function takes the source and destination as arguments. These are used for "routing" but also # for pretty-printing. Here we also take the specific flag of "is_ping" def __init__(self, sender: int, destination: int, is_ping: bool): @@ -19,14 +18,13 @@ def __init__(self, sender: int, destination: int, is_ping: bool): # remember to implement the __str__ method such that the debug of the framework works! def __str__(self): if self.is_ping: - return f'{self.source} -> {self.destination} : Ping' + return f"{self.source} -> {self.destination} : Ping" else: - return f'{self.source} -> {self.destination} : Pong' + return f"{self.source} -> {self.destination} : Pong" # This class extends on the basic Device class. We will implement the protocol in the run method class PingPong(Device): - # The constructor must have exactly this form. def __init__(self, index: int, number_of_devices: int, medium: Medium): # forward the constructor arguments to the super-constructor @@ -41,7 +39,11 @@ def run(self): # for this algorithm, we will repeat the protocol 10 times and then stop for repetetions in range(0, 10): # in each repetition, let us send the ping to one random other device - message = PingMessage(self.index(), random.randrange(0, self.number_of_devices()), self._is_ping) + message = PingMessage( + self.index(), + random.randrange(0, self.number_of_devices()), + self._is_ping, + ) # we send the message via a "medium" self.medium().send(message) # in this instance, we also try to receive some messages, there can be multiple, but @@ -69,4 +71,6 @@ def run(self): # for pretty-printing and debugging, implement this function def print_result(self): - print(f'\tDevice {self.index()} got pings: {self._rec_ping} and pongs: {self._rec_pong}') + print( + f"\tDevice {self.index()} got pings: {self._rec_ping} and pongs: {self._rec_pong}" + ) diff --git a/exercises/exercise1.py b/exercises/exercise1.py index 32b83ba..7067fb4 100644 --- a/exercises/exercise1.py +++ b/exercises/exercise1.py @@ -4,18 +4,16 @@ class GossipMessage(MessageStub): - def __init__(self, sender: int, destination: int, secrets): super().__init__(sender, destination) # we use a set to keep the "secrets" here self.secrets = secrets def __str__(self): - return f'{self.source} -> {self.destination} : {self.secrets}' + return f"{self.source} -> {self.destination} : {self.secrets}" class Gossip(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) # for this exercise we use the index as the "secret", but it could have been a new routing-table (for instance) @@ -29,4 +27,4 @@ def run(self): return def print_result(self): - print(f'\tDevice {self.index()} got secrets: {self._secrets}') + print(f"\tDevice {self.index()} got secrets: {self._secrets}") diff --git a/exercises/exercise10.py b/exercises/exercise10.py index 7579569..fcf899c 100644 --- a/exercises/exercise10.py +++ b/exercises/exercise10.py @@ -1,21 +1,12 @@ -import math -import random -import sys -import os - from emulators.Device import Device from emulators.Medium import Medium from emulators.MessageStub import MessageStub -from cryptography.hazmat.primitives import hashes from hashlib import sha256 import json import time - - - class Block: def __init__(self, index, transactions, timestamp, previous_hash, nonce=0): self.index = index @@ -37,7 +28,6 @@ def hash_binary(self): return "{0:0256b}".format(int(self.hash, 16)) - class Blockchain: def __init__(self): self.unconfirmed_transactions = [] @@ -57,11 +47,12 @@ def last_block(self): return self.chain[-1] difficulty = 4 + # TODO: set difficulty to 2, to have many more forks. # TODO: understand why having lower difficulty leads to more forks. def proof_of_work(self, block): computed_hash_binary = block.hash_binary - if not computed_hash_binary.startswith('0' * Blockchain.difficulty): + if not computed_hash_binary.startswith("0" * Blockchain.difficulty): return False return True @@ -76,7 +67,7 @@ def add_block(self, block): return False self.chain.append(block) return True - + def add_new_transaction(self, transaction): self.unconfirmed_transactions.append(transaction) @@ -87,7 +78,6 @@ def to_string(self): return msg - class BlockchainMiner(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) @@ -100,11 +90,13 @@ def try_mining(self): last_block = self.blockchain.last_block # I create a block using current timestamp, unconfirmed transactions and nonce - new_block = Block(index=last_block.index + 1, - transactions=self.blockchain.unconfirmed_transactions, - timestamp=time.time(), - previous_hash=last_block.hash, - nonce=self.next_nonce) + new_block = Block( + index=last_block.index + 1, + transactions=self.blockchain.unconfirmed_transactions, + timestamp=time.time(), + previous_hash=last_block.hash, + nonce=self.next_nonce, + ) # I check if the block passes the proof_of_work test proof = self.blockchain.proof_of_work(new_block) @@ -163,7 +155,9 @@ def handle_ingoing(self, ingoing: MessageStub): pass elif isinstance(ingoing, BlockchainRequestMessage): # this is used to send the blockchain data to a client requesting them - message = BlockchainMessage(self.index(), ingoing.source, self.blockchain.chain) + message = BlockchainMessage( + self.index(), ingoing.source, self.blockchain.chain + ) self.medium().send(message) elif isinstance(ingoing, TransactionMessage): self.blockchain.add_new_transaction(ingoing.transaction) @@ -175,9 +169,10 @@ def print_result(self): print("Miner " + str(self.index()) + " quits") - class BlockchainClient(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, my_miner: int): + def __init__( + self, index: int, number_of_devices: int, medium: Medium, my_miner: int + ): super().__init__(index, number_of_devices, medium) self.my_miner = my_miner @@ -191,7 +186,9 @@ def run(self): self.medium().wait_for_next_round() def send_transaction(self): - message = TransactionMessage(self.index(), self.my_miner, f"(transaction by client {self.index()})") + message = TransactionMessage( + self.index(), self.my_miner, f"(transaction by client {self.index()})" + ) self.medium().send(message) def request_blockchain(self): @@ -216,9 +213,9 @@ def print_result(self): print(f"client {self.index()} quits") - class BlockchainNetwork: miners = [] + def __new__(cls, index: int, number_of_devices: int, medium: Medium): # first miner MUST have index 0 @@ -226,8 +223,7 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): return BlockchainMiner(index, number_of_devices, medium) else: # I associate a miner to each client, in a very aleatory manner - return BlockchainClient(index, number_of_devices, medium, index-1) - + return BlockchainClient(index, number_of_devices, medium, index - 1) class QuitMessage(MessageStub): @@ -235,8 +231,7 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'QUIT REQUEST {self.source} -> {self.destination}' - + return f"QUIT REQUEST {self.source} -> {self.destination}" class BlockchainMessage(MessageStub): @@ -245,8 +240,7 @@ def __init__(self, sender: int, destination: int, chain: list): self.chain = chain def __str__(self): - return f'NEW BLOCK MESSAGE {self.source} -> {self.destination}: ({len(self.chain)} blocks)' - + return f"NEW BLOCK MESSAGE {self.source} -> {self.destination}: ({len(self.chain)} blocks)" class TransactionMessage(MessageStub): @@ -255,8 +249,7 @@ def __init__(self, sender: int, destination: int, transaction: str): self.transaction = transaction def __str__(self): - return f'TRANSACTION {self.source} -> {self.destination}: ({self.transaction})' - + return f"TRANSACTION {self.source} -> {self.destination}: ({self.transaction})" class BlockchainRequestMessage(MessageStub): @@ -264,4 +257,4 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'REQUEST FOR BLOCKCHAIN DATA {self.source} -> {self.destination}' + return f"REQUEST FOR BLOCKCHAIN DATA {self.source} -> {self.destination}" diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 2a7ea1c..7d3dd17 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -1,13 +1,10 @@ -import math import random -import sys from typing import Optional from emulators.Device import Device from emulators.Medium import Medium from emulators.MessageStub import MessageStub -import json import time @@ -24,7 +21,13 @@ class RoutingData: # all tuples ("prev" and the ones in the finger_table) are (index, chord_id) - def __init__(self, index: int, chord_id: int, prev: tuple[int, int], finger_table: list[tuple[int, int]]): + def __init__( + self, + index: int, + chord_id: int, + prev: tuple[int, int], + finger_table: list[tuple[int, int]], + ): self.index = index self.chord_id = chord_id self.prev = prev @@ -47,7 +50,14 @@ def in_between(candidate: int, low: int, high: int): class ChordNode(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, connected: bool, routing_data: Optional[RoutingData]): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + connected: bool, + routing_data: Optional[RoutingData], + ): super().__init__(index, number_of_devices, medium) self.connected = connected self.routing_data = routing_data @@ -107,7 +117,9 @@ def print_result(self): my_range = self.routing_data.chord_id - self.routing_data.prev[1] if my_range < 0: my_range += pow(2, address_size) - print(f"Chord node {self.index()} quits, it managed {my_range} addresses, it had {len(self.saved_data)} data blocks") + print( + f"Chord node {self.index()} quits, it managed {my_range} addresses, it had {len(self.saved_data)} data blocks" + ) else: print(f"Chord node {self.index()} quits, it was still disconnected") @@ -118,7 +130,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): for i in range(pow(2, address_size)): - guid = i # I send a message to each address, to check if each node stores one data for each address it manages + guid = i # I send a message to each address, to check if each node stores one data for each address it manages # if your chord address space gets too big, use the following code: # for i in range(pow(2, address_size)): # guid = random.randint(0, pow(2,address_size)-1) @@ -126,13 +138,15 @@ def run(self): self.medium().send(message) # TODO: uncomment this code to start the JOIN process - #new_chord_id = random.randint(0, pow(2,address_size)-1) - #while new_chord_id in all_nodes: + # new_chord_id = random.randint(0, pow(2,address_size)-1) + # while new_chord_id in all_nodes: # new_chord_id = random.randint(0, pow(2,address_size)-1) - #message = StartJoinMessage(self.index(), 1, new_chord_id) - #self.medium().send(message) + # message = StartJoinMessage(self.index(), 1, new_chord_id) + # self.medium().send(message) - time.sleep(10) # or use some smart trick to wait for the routing process to be completed before shutting down the distributed system + time.sleep( + 10 + ) # or use some smart trick to wait for the routing process to be completed before shutting down the distributed system for i in range(1, self.number_of_devices()): message = QuitMessage(self.index(), i) self.medium().send(message) @@ -156,25 +170,34 @@ def print_result(self): class ChordNetwork: def init_routing_tables(number_of_devices: int): - N = number_of_devices-2 # routing_data 0 will be for device 2, etc + N = number_of_devices - 2 # routing_data 0 will be for device 2, etc while len(all_nodes) < N: - new_chord_id = random.randint(0, pow(2,address_size)-1) + new_chord_id = random.randint(0, pow(2, address_size) - 1) if new_chord_id not in all_nodes: all_nodes.append(new_chord_id) all_nodes.sort() for id in range(N): - prev_id = (id-1) % N - prev = (prev_id+2, all_nodes[prev_id]) # Add 2 to get "message-able" device index + prev_id = (id - 1) % N + prev = ( + prev_id + 2, + all_nodes[prev_id], + ) # Add 2 to get "message-able" device index new_finger_table = [] for i in range(address_size): at_least = (all_nodes[id] + pow(2, i)) % pow(2, address_size) - candidate = (id+1) % N + candidate = (id + 1) % N while in_between(all_nodes[candidate], all_nodes[id], at_least): - candidate = (candidate+1) % N - new_finger_table.append((candidate+2, all_nodes[candidate])) # I added 2 to candidate since routing_data 0 is for device 2, and so on - all_routing_data.append(RoutingData(id+2, all_nodes[id], prev, new_finger_table)) - print(RoutingData(id+2, all_nodes[id], prev, new_finger_table).to_string()) + candidate = (candidate + 1) % N + new_finger_table.append( + (candidate + 2, all_nodes[candidate]) + ) # I added 2 to candidate since routing_data 0 is for device 2, and so on + all_routing_data.append( + RoutingData(id + 2, all_nodes[id], prev, new_finger_table) + ) + print( + RoutingData(id + 2, all_nodes[id], prev, new_finger_table).to_string() + ) def __new__(cls, index: int, number_of_devices: int, medium: Medium): # device #0 is the client @@ -190,7 +213,9 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): if index == 1: return ChordNode(index, number_of_devices, medium, False, None) if index > 1: - return ChordNode(index, number_of_devices, medium, True, all_routing_data[index-2]) + return ChordNode( + index, number_of_devices, medium, True, all_routing_data[index - 2] + ) class QuitMessage(MessageStub): @@ -198,7 +223,7 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'QUIT REQUEST {self.source} -> {self.destination}' + return f"QUIT REQUEST {self.source} -> {self.destination}" class PutMessage(MessageStub): @@ -208,7 +233,7 @@ def __init__(self, sender: int, destination: int, guid: int, data: str): self.data = data def __str__(self): - return f'PUT MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})' + return f"PUT MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})" class GetReqMessage(MessageStub): @@ -217,7 +242,7 @@ def __init__(self, sender: int, destination: int, guid: int): self.guid = guid def __str__(self): - return f'GET REQUEST MESSAGE {self.source} -> {self.destination}: ({self.guid})' + return f"GET REQUEST MESSAGE {self.source} -> {self.destination}: ({self.guid})" class GetRspMessage(MessageStub): @@ -227,7 +252,7 @@ def __init__(self, sender: int, destination: int, guid: int, data: str): self.data = data def __str__(self): - return f'GET RESPONSE MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})' + return f"GET RESPONSE MESSAGE {self.source} -> {self.destination}: ({self.guid}, {self.data})" class StartJoinMessage(MessageStub): @@ -235,36 +260,36 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'StartJoinMessage MESSAGE {self.source} -> {self.destination}' + return f"StartJoinMessage MESSAGE {self.source} -> {self.destination}" class JoinReqMessage(MessageStub): - def __init__(self, sender: int, destination: int): # etc + def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) def __str__(self): - return f'JoinReqMessage MESSAGE {self.source} -> {self.destination}' + return f"JoinReqMessage MESSAGE {self.source} -> {self.destination}" class JoinRspMessage(MessageStub): - def __init__(self, sender: int, destination: int): # etc + def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) def __str__(self): - return f'JoinRspMessage MESSAGE {self.source} -> {self.destination}' + return f"JoinRspMessage MESSAGE {self.source} -> {self.destination}" class NotifyMessage(MessageStub): - def __init__(self, sender: int, destination: int): # etc + def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) def __str__(self): - return f'NotifyMessage MESSAGE {self.source} -> {self.destination}' + return f"NotifyMessage MESSAGE {self.source} -> {self.destination}" class StabilizeMessage(MessageStub): - def __init__(self, sender: int, destination: int): # etc + def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) def __str__(self): - return f'StabilizeMessage MESSAGE {self.source} -> {self.destination}' + return f"StabilizeMessage MESSAGE {self.source} -> {self.destination}" diff --git a/exercises/exercise12.py b/exercises/exercise12.py index 7459683..7c8192e 100644 --- a/exercises/exercise12.py +++ b/exercises/exercise12.py @@ -1,6 +1,4 @@ -import math import random -import sys import threading from typing import Optional @@ -8,8 +6,6 @@ from emulators.Medium import Medium from emulators.MessageStub import MessageStub -import json -import time # if you need controlled repetitions: # random.seed(100) @@ -29,9 +25,13 @@ class AodvNode(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) # I get the topology from the singleton - self.neighbors = TopologyCreator.get_topology(number_of_devices, probability_arc)[index] + self.neighbors = TopologyCreator.get_topology( + number_of_devices, probability_arc + )[index] # I initialize the "routing tables". Feel free to create your own structure if you prefer - self.forward_path: dict[int, int] = {} # "Destination index" --> "Next-hop index" + self.forward_path: dict[ + int, int + ] = {} # "Destination index" --> "Next-hop index" self.reverse_path: dict[int, int] = {} # "Source index" --> "Next-hop index" self.bcast_ids = [] # Type hint left out on purpose due to tasks below # data structures to cache outgoing messages, and save received data @@ -41,7 +41,9 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): last = random.randint(0, self.number_of_devices() - 1) # I send the message to myself, so it gets routed - message = DataMessage(self.index(), self.index(), last, f"Hi. I am {self.index()}.") + message = DataMessage( + self.index(), self.index(), last, f"Hi. I am {self.index()}." + ) self.medium().send(message) while True: for ingoing in self.medium().receive_all(): @@ -66,10 +68,14 @@ def handle_ingoing(self, ingoing: MessageStub): self.medium().send(QuitMessage(self.index(), i)) # else: - next = self.next_hop(ingoing.last) # change self.next_hop if you implement a different data structure for the routing tables + next = self.next_hop( + ingoing.last + ) # change self.next_hop if you implement a different data structure for the routing tables if next is not None: # I know how to reach the destination - message = DataMessage(self.index(), next, ingoing.last, ingoing.data) + message = DataMessage( + self.index(), next, ingoing.last, ingoing.data + ) self.medium().send(message) return True # I don't have the route to the destination. @@ -112,7 +118,9 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print(f"AODV node {self.index()} quits: neighbours = {self.neighbors}, forward paths = {self.forward_path}, reverse paths = {self.reverse_path}, saved data = {self.saved_data}, length of message cache (should be 0) = {len(self.outgoing_message_cache)}") + print( + f"AODV node {self.index()} quits: neighbours = {self.neighbors}, forward paths = {self.forward_path}, reverse paths = {self.reverse_path}, saved data = {self.saved_data}, length of message cache (should be 0) = {len(self.outgoing_message_cache)}" + ) class TopologyCreator: @@ -153,7 +161,9 @@ def __create_topology(number_of_devices: int, probability: float): @classmethod def get_topology(cls, number_of_devices: int, probability: float): if cls.__topology is None: - cls.__topology = TopologyCreator.__create_topology(number_of_devices, probability) + cls.__topology = TopologyCreator.__create_topology( + number_of_devices, probability + ) return cls.__topology @@ -162,7 +172,7 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'QUIT REQUEST {self.source} -> {self.destination}' + return f"QUIT REQUEST {self.source} -> {self.destination}" class AodvRreqMessage(MessageStub): @@ -172,7 +182,7 @@ def __init__(self, sender: int, destination: int, first: int, last: int): self.last = last def __str__(self): - return f'RREQ MESSAGE {self.source} -> {self.destination}: ({self.first} -> {self.last})' + return f"RREQ MESSAGE {self.source} -> {self.destination}: ({self.first} -> {self.last})" class AodvRrepMessage(MessageStub): @@ -182,7 +192,7 @@ def __init__(self, sender: int, destination: int, first: int, last: int): self.last = last def __str__(self): - return f'RREP MESSAGE {self.source} -> {self.destination}: ({self.first} -> {self.last})' + return f"RREP MESSAGE {self.source} -> {self.destination}: ({self.first} -> {self.last})" class DataMessage(MessageStub): diff --git a/exercises/exercise2.py b/exercises/exercise2.py index 6595a0b..12fceae 100644 --- a/exercises/exercise2.py +++ b/exercises/exercise2.py @@ -9,27 +9,27 @@ def __init__(self, sender: int, destination: int, table): self.table = table def __str__(self): - return f'RipMessage: {self.source} -> {self.destination} : {self.table}' + return f"RipMessage: {self.source} -> {self.destination} : {self.table}" + class RoutableMessage(MessageStub): - def __init__(self, sender: int, destination: int, first_node: int, last_node: int, content): + def __init__( + self, sender: int, destination: int, first_node: int, last_node: int, content + ): super().__init__(sender, destination) self.content = content self.first_node = first_node self.last_node = last_node def __str__(self): - return f'RoutableMessage: {self.source} -> {self.destination} : {self.content}' - - + return f"RoutableMessage: {self.source} -> {self.destination} : {self.content}" class RipCommunication(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - - self.neighbors = [] # generate an appropriate list + + self.neighbors = [] # generate an appropriate list self.routing_table = dict() @@ -53,18 +53,34 @@ def run(self): if returned_table is not None: self.routing_table = returned_table for neigh in self.neighbors: - self.medium().send(RipMessage(self.index(), neigh, self.routing_table)) + self.medium().send( + RipMessage(self.index(), neigh, self.routing_table) + ) if type(ingoing) is RoutableMessage: - print(f"Device {self.index()}: Routing from {ingoing.first_node} to {ingoing.last_node} via #{self.index()}: [#{ingoing.content}]") + print( + f"Device {self.index()}: Routing from {ingoing.first_node} to {ingoing.last_node} via #{self.index()}: [#{ingoing.content}]" + ) if ingoing.last_node is self.index(): - print(f"\tDevice {self.index()}: delivered message from {ingoing.first_node} to {ingoing.last_node}: {ingoing.content}") + print( + f"\tDevice {self.index()}: delivered message from {ingoing.first_node} to {ingoing.last_node}: {ingoing.content}" + ) continue if self.routing_table[ingoing.last_node] is not None: (next_hop, distance) = self.routing_table[ingoing.last_node] - self.medium().send(RoutableMessage(self.index(), next_hop, ingoing.first_node, ingoing.last_node, ingoing.content)) + self.medium().send( + RoutableMessage( + self.index(), + next_hop, + ingoing.first_node, + ingoing.last_node, + ingoing.content, + ) + ) continue - print(f"\tDevice {self.index()}: DROP Unknown route #{ingoing.first_node} to #{ingoing.last_node} via #{self.index}, message #{ingoing.content}") + print( + f"\tDevice {self.index()}: DROP Unknown route #{ingoing.first_node} to #{ingoing.last_node} via #{self.index}, message #{ingoing.content}" + ) # this call is only used for synchronous networks self.medium().wait_for_next_round() @@ -73,6 +89,5 @@ def merge_tables(self, src, table): # return None if the table does not change pass - def print_result(self): - print(f'\tDevice {self.index()} has routing table: {self.routing_table}') \ No newline at end of file + print(f"\tDevice {self.index()} has routing table: {self.routing_table}") diff --git a/exercises/exercise4.py b/exercises/exercise4.py index 9c07302..52ec91a 100644 --- a/exercises/exercise4.py +++ b/exercises/exercise4.py @@ -1,7 +1,4 @@ import math -import random -import threading -import time from emulators.Medium import Medium from emulators.Device import Device, WorkerDevice @@ -14,11 +11,10 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'Ping: {self.source} -> {self.destination}' + return f"Ping: {self.source} -> {self.destination}" class Pinger(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._output_ping = False @@ -46,7 +42,6 @@ class Type(enum.Enum): class MutexMessage(MessageStub): - def __init__(self, sender: int, destination: int, message_type: Type): super().__init__(sender, destination) self._type = message_type @@ -62,11 +57,11 @@ def is_release(self): def __str__(self): if self._type == Type.REQUEST: - return f'Request: {self.source} -> {self.destination}' + return f"Request: {self.source} -> {self.destination}" if self._type == Type.RELEASE: - return f'Release: {self.source} -> {self.destination}' + return f"Release: {self.source} -> {self.destination}" if self._type == Type.GRANT: - return f'Grant: {self.source} -> {self.destination}' + return f"Grant: {self.source} -> {self.destination}" class Centralised: @@ -78,10 +73,9 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): class Coordinator(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - assert (self.index() == 0) # we assume that the coordinator is fixed at index 0. + assert self.index() == 0 # we assume that the coordinator is fixed at index 0. self._granted = None self._waiting = [] @@ -94,12 +88,14 @@ def run(self): if ingoing.is_request(): self._waiting.append(ingoing.source) elif ingoing.is_release(): - assert (self._granted == ingoing.source) + assert self._granted == ingoing.source self._granted = None if len(self._waiting) > 0 and self._granted is None: self._granted = self._waiting.pop(0) - self.medium().send(MutexMessage(self.index(), self._granted, Type.GRANT)) + self.medium().send( + MutexMessage(self.index(), self._granted, Type.GRANT) + ) self.medium().wait_for_next_round() def print_result(self): @@ -107,10 +103,9 @@ def print_result(self): class Requester(WorkerDevice): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - assert (self.index() != 0) # we assume that the coordinator is fixed at index 0. + assert self.index() != 0 # we assume that the coordinator is fixed at index 0. self._requested = False def run(self): @@ -118,16 +113,14 @@ def run(self): ingoing = self.medium().receive() if ingoing is not None: if ingoing.is_grant(): - assert (self._requested) + assert self._requested self.do_work() self._requested = False - self.medium().send( - MutexMessage(self.index(), 0, Type.RELEASE)) + self.medium().send(MutexMessage(self.index(), 0, Type.RELEASE)) if self.has_work() and not self._requested: self._requested = True - self.medium().send( - MutexMessage(self.index(), 0, Type.REQUEST)) + self.medium().send(MutexMessage(self.index(), 0, Type.REQUEST)) self.medium().wait_for_next_round() @@ -163,7 +156,6 @@ def print_result(self): class StampedMessage(MutexMessage): - def __init__(self, sender: int, destination: int, message_type: Type, time: int): super().__init__(sender, destination, message_type) self._stamp = time @@ -172,7 +164,7 @@ def stamp(self) -> int: return self._stamp def __str__(self): - return super().__str__() + f' [stamp={self._stamp}]' + return super().__str__() + f" [stamp={self._stamp}]" class State(enum.Enum): @@ -182,7 +174,6 @@ class State(enum.Enum): class RicartAgrawala(WorkerDevice): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._state = State.RELEASED @@ -207,13 +198,17 @@ def run(self): def handle_request(self, message: StampedMessage): new_time = max(self._time, message.stamp()) + 1 - if self._state == State.HELD or (self._state == State.WANTED and - (self._time, self.index()) < (message.stamp(), message.source)): + if self._state == State.HELD or ( + self._state == State.WANTED + and (self._time, self.index()) < (message.stamp(), message.source) + ): self._time = new_time self._waiting.append(message.source) else: new_time += 1 - self.medium().send(StampedMessage(self.index(), message.source, Type.GRANT, new_time)) + self.medium().send( + StampedMessage(self.index(), message.source, Type.GRANT, new_time) + ) self._time = new_time def handle_grant(self, message: StampedMessage): @@ -229,9 +224,7 @@ def release(self): self._state = State.RELEASED self._time += 1 for id in self._waiting: - self.medium().send( - StampedMessage(self.index(), id, Type.GRANT, self._time) - ) + self.medium().send(StampedMessage(self.index(), id, Type.GRANT, self._time)) self._waiting.clear() def acquire(self): @@ -242,15 +235,16 @@ def acquire(self): for id in self.medium().ids(): if id != self.index(): self.medium().send( - StampedMessage(self.index(), id, - Type.REQUEST, self._time)) + StampedMessage(self.index(), id, Type.REQUEST, self._time) + ) def print_result(self): - print(f"RA {self.index()} Terminated with request? {self._state == State.WANTED}") + print( + f"RA {self.index()} Terminated with request? {self._state == State.WANTED}" + ) class Maekawa(WorkerDevice): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._state = State.RELEASED @@ -320,7 +314,9 @@ def handle_release(self, message: MutexMessage): self._voted = False def print_result(self): - print(f"MA {self.index()} Terminated with request? {self._state == State.WANTED}") + print( + f"MA {self.index()} Terminated with request? {self._state == State.WANTED}" + ) class SKToken(MessageStub): @@ -336,13 +332,14 @@ def ln(self): return self._ln def __str__(self): - return f"Token: {self.source} -> {self.destination}, \n" \ - f"\t\tQueue {str(self._queue)}\n" \ - f"\t\tLN {str(self._ln)}" + return ( + f"Token: {self.source} -> {self.destination}, \n" + f"\t\tQueue {str(self._queue)}\n" + f"\t\tLN {str(self._ln)}" + ) class SuzukiKasami(WorkerDevice): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._rn = [0 for _ in range(0, number_of_devices)] @@ -413,13 +410,16 @@ def acquire(self): for id in self.medium().ids(): if id != self.index(): self.medium().send( - StampedMessage(self.index(), id, Type.REQUEST, self._rn[self.index()])) + StampedMessage( + self.index(), id, Type.REQUEST, self._rn[self.index()] + ) + ) # Election Algorithms -class Vote(MessageStub): +class Vote(MessageStub): def __init__(self, sender: int, destination: int, vote: int, decided: bool): super().__init__(sender, destination) self._vote = vote @@ -432,7 +432,7 @@ def decided(self): return self._decided def __str__(self): - return f'Vote: {self.source} -> {self.destination}, voted for {self._vote}, decided? {self._decided}' + return f"Vote: {self.source} -> {self.destination}, voted for {self._vote}, decided? {self._decided}" class ChangRoberts(Device): @@ -445,15 +445,13 @@ def run(self): while True: nxt = (self.index() + 1) % self.number_of_devices() if not self._participated: - self.medium().send( - Vote(self.index(), nxt, self.index(), False)) + self.medium().send(Vote(self.index(), nxt, self.index(), False)) self._participated = True ingoing = self.medium().receive() if ingoing is not None: if ingoing.vote() == self.index(): if not ingoing.decided(): - self.medium().send( - Vote(self.index(), nxt, self.index(), True)) + self.medium().send(Vote(self.index(), nxt, self.index(), True)) else: self._leader = self.index() return # this device is the new leader @@ -462,18 +460,18 @@ def run(self): elif ingoing.vote() > self.index(): # forward the message self.medium().send( - Vote(self.index(), nxt, ingoing.vote(), ingoing.decided())) + Vote(self.index(), nxt, ingoing.vote(), ingoing.decided()) + ) if ingoing.decided(): self._leader = ingoing.vote() return self.medium().wait_for_next_round() def print_result(self): - print(f'Leader seen from {self._id} is {self._leader}') + print(f"Leader seen from {self._id} is {self._leader}") class Bully(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._leader = None @@ -496,7 +494,14 @@ def run(self): if ingoing is not None: got_input = True if ingoing.vote() < self.index(): - self.medium().send(Vote(self.index(), ingoing.source, self.index(), self.largest())) + self.medium().send( + Vote( + self.index(), + ingoing.source, + self.index(), + self.largest(), + ) + ) new_election = True else: self._shut_up = True @@ -517,7 +522,9 @@ def run(self): # we are the new leader, we could declare everybody else dead for id in self.medium().ids(): if id != self.index(): - self.medium().send(Vote(self.index(), id, self.index(), True)) + self.medium().send( + Vote(self.index(), id, self.index(), True) + ) self._leader = self.index() return self.medium().wait_for_next_round() @@ -528,7 +535,9 @@ def start_election(self): self._election = True for id in self.medium().ids(): if id > self.index(): - self.medium().send(Vote(self.index(), id, self.index(), self.largest())) + self.medium().send( + Vote(self.index(), id, self.index(), self.largest()) + ) def print_result(self): - print(f'Leader seen from {self._id} is {self._leader}') + print(f"Leader seen from {self._id} is {self._leader}") diff --git a/exercises/exercise5.py b/exercises/exercise5.py index fef20f7..d9e3e01 100644 --- a/exercises/exercise5.py +++ b/exercises/exercise5.py @@ -2,13 +2,13 @@ import threading import time -import math import copy from emulators.Device import Device from emulators.Medium import Medium from emulators.MessageStub import MessageStub + class MulticastMessage(MessageStub): def __init__(self, sender: int, destination: int, content): super().__init__(sender, destination) @@ -18,7 +18,7 @@ def content(self): return self._content def __str__(self): - return f'Multicast: {self.source} -> {self.destination} [{self._content}]' + return f"Multicast: {self.source} -> {self.destination} [{self._content}]" class MulticastListener: @@ -56,8 +56,13 @@ def forward(self, message): class BasicMulticast(Device, MulticastService): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -94,15 +99,22 @@ def print_result(self): class ReliableMulticast(MulticastListener, MulticastService, Device): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application else: self._application = Multicaster(index, self) self._b_multicast = BasicMulticast(index, number_of_devices, medium, self) - self._seq_number = 0 # not strictly needed, but helps giving messages a unique ID + self._seq_number = ( + 0 # not strictly needed, but helps giving messages a unique ID + ) self._received = set() def send(self, content): @@ -134,7 +146,7 @@ def seq_number(self): return self._seq_number def __str__(self): - return f'NACK: {self.source} -> {self.destination}: {self._seq_number}' + return f"NACK: {self.source} -> {self.destination}: {self._seq_number}" class Resend(MessageStub): @@ -146,12 +158,17 @@ def message(self): return self._message def __str__(self): - return f'Resend: {self.source} -> {self.destination}: {self._message}' + return f"Resend: {self.source} -> {self.destination}: {self._message}" class ReliableIPMulticast(MulticastListener, MulticastService, Device): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -170,8 +187,7 @@ def deliver(self, message): self.nack_missing(seq_numbers) def send(self, content): - self._received[(self.index(), - self._seq_numbers[self.index()])] = content + self._received[(self.index(), self._seq_numbers[self.index()])] = content self._b_multicast.send((self.index(), self._seq_numbers, content)) self.try_deliver() @@ -180,9 +196,17 @@ def run(self): def forward(self, message): if isinstance(message, NACK): - self.medium().send(Resend(self.index(), message.source, - (self.index(), self._seq_numbers, - self._received[(self.index(), message.seq_number())]))) + self.medium().send( + Resend( + self.index(), + message.source, + ( + self.index(), + self._seq_numbers, + self._received[(self.index(), message.seq_number())], + ), + ) + ) elif isinstance(message, Resend): self.deliver(message.message()) else: @@ -199,8 +223,7 @@ def try_deliver(self): def nack_missing(self, n_seq: list[int]): for id in range(0, len(n_seq)): for mid in range(self._seq_numbers[id] + 1, n_seq[id]): - self.medium().send( - NACK(self.index(), id, mid)) + self.medium().send(NACK(self.index(), id, mid)) class Order: @@ -215,11 +238,17 @@ def message_id(self): return self._message_id def __str__(self): - return f'Order(<{self.message_id()}> = {self.order()})' + return f"Order(<{self.message_id()}> = {self.order()})" -class TOSEQMulticast(MulticastListener, MulticastService, Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): +class TOSEQMulticast(MulticastListener, MulticastService, Device): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -269,7 +298,9 @@ def forward(self, message): class Vote(MessageStub): - def __init__(self, sender: int, destination: int, order: (int, int), message_id: (int, int)): + def __init__( + self, sender: int, destination: int, order: (int, int), message_id: (int, int) + ): super().__init__(sender, destination) self._order = order self._message_id = message_id @@ -281,12 +312,17 @@ def message_id(self) -> (int, int): return self._message_id def __str__(self): - return f'Vote: {self.source} -> {self.destination}: <{self.message_id()}> = {self.order()}' + return f"Vote: {self.source} -> {self.destination}: <{self.message_id()}> = {self.order()}" class ISISMulticast(MulticastListener, MulticastService, Device): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -319,18 +355,14 @@ def deliver(self, message): self._hb_q[(sid, sseq)] = content self._p_seq = max(self._a_seq, self._p_seq) + 1 # We should technically send proposer ID for tie-breaks - self.medium().send( - Vote(self.index(), sid, self._p_seq, (sid, sseq)) - ) + self.medium().send(Vote(self.index(), sid, self._p_seq, (sid, sseq))) def forward(self, message): if isinstance(message, Vote): votes = self._votes[message.message_id()] votes.append(message.order()) if len(votes) == self.number_of_devices(): - self._b_multicast.send( - Order(message.message_id(), max(votes)) - ) + self._b_multicast.send(Order(message.message_id(), max(votes))) else: self._application.forward(message) @@ -344,8 +376,13 @@ def try_deliver(self): class COMulticast(MulticastListener, MulticastService, Device): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: MulticastListener = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: MulticastListener = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -367,7 +404,7 @@ def forward(self, message): self._application.forward(message) def try_deliver(self): - for (vec, index, content) in self._hb_q: + for vec, index, content in self._hb_q: if self.is_next(vec, index): self._application.deliver(content) self._n_vect[index] += 1 diff --git a/exercises/exercise6.py b/exercises/exercise6.py index 48233da..217bbb8 100644 --- a/exercises/exercise6.py +++ b/exercises/exercise6.py @@ -15,7 +15,6 @@ def initial_value(self): class SimpleRequester(ConsensusRequester): - def __init__(self): self._proposal = random.randint(0, 100) @@ -32,7 +31,8 @@ def consensus_reached(self, element): SimpleRequester._consensus = element if SimpleRequester._consensus != element: raise ValueError( - f"Disagreement in consensus, expected {element} but other process already got {SimpleRequester._consensus}") + f"Disagreement in consensus, expected {element} but other process already got {SimpleRequester._consensus}" + ) class Propose(MessageStub): @@ -44,11 +44,17 @@ def value(self): return self._value def __str__(self): - return f'Propose: {self.source} -> {self.destination}: {self._value}' + return f"Propose: {self.source} -> {self.destination}: {self._value}" class FResilientConsensus(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: ConsensusRequester = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -81,8 +87,13 @@ def print_result(self): class SingleByzantine(Device): - - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: ConsensusRequester = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application @@ -140,16 +151,51 @@ def find_majority(raw: [(int, int)]): best = None return best + class King(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: ConsensusRequester = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application else: self._application = SimpleRequester() + def b_multicast(self, message: MessageStub): + message.source = self.index() + for i in self.medium().ids(): + message.destination = i + self.medium().send(message) + def run(self): - pass + # Set own v to a preferred value // f+1 phases in total + # for each phase i ∈ 0 . . . f do + # // round 1: + # B-multicast(v ) + # Await vj from each process pj + # Set v to the most frequent element ∈ v0...vn−1 + # Set mult to the number of occurrences of v + # Set v to a default value if mult < n/2 + # // round 2: + # if k = i then + # B-multicast(v ) // king for phase k is pk , send tie breaker + # end + # Set vk to the value received from the king + # if mult ≤ (n/2) + f then + # Set v to the vk + # end + # end + v = random.randint(1, 100) + for i in range(0, self.number_of_devices()): + self.b_multicast(message=Propose(v)) + vs = self.medium().receive_all() + v = most_common() + mult = vs.count(v) def print_result(self): pass @@ -161,19 +207,22 @@ def __init__(self, sender: int, destination: int, uid: int): self.uid = uid def __str__(self): - return f'PREPARE {self.source} -> {self.destination}: {self.uid}' + return f"PREPARE {self.source} -> {self.destination}: {self.uid}" class PromiseMessage(MessageStub): - def __init__(self, sender: int, destination: int, uid: int, prev_uid: int, prev_value): + def __init__( + self, sender: int, destination: int, uid: int, prev_uid: int, prev_value + ): super().__init__(sender, destination) self.uid = uid self.prev_uid = prev_uid self.prev_value = prev_value def __str__(self): - return f'PROMISE {self.source} -> {self.destination}: {self.uid}' + \ - ('' if self.prev_uid == 0 else f'accepted {self.prev_uid}, {self.prev_value}') + return f"PROMISE {self.source} -> {self.destination}: {self.uid}" + ( + "" if self.prev_uid == 0 else f"accepted {self.prev_uid}, {self.prev_value}" + ) class RequestAcceptMessage(MessageStub): @@ -183,7 +232,7 @@ def __init__(self, sender: int, destination: int, uid: int, value): self.value = value def __str__(self): - return f'ACCEPT-REQUEST {self.source} -> {self.destination}: {self.uid}, {self.value}' + return f"ACCEPT-REQUEST {self.source} -> {self.destination}: {self.uid}, {self.value}" class AcceptMessage(MessageStub): @@ -193,11 +242,13 @@ def __init__(self, sender: int, destination: int, uid: int, value): self.value = value def __str__(self): - return f'ACCEPT {self.source} -> {self.destination}: {self.uid}, {self.value}' + return f"ACCEPT {self.source} -> {self.destination}: {self.uid}, {self.value}" class PAXOSNetwork: - def __init__(self, index: int, medium: Medium, acceptors: list[int], learners: list[int]): + def __init__( + self, index: int, medium: Medium, acceptors: list[int], learners: list[int] + ): self._acceptors = acceptors self._learners = learners self._medium = medium @@ -236,14 +287,22 @@ def index(self): class PAXOS(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium, application: ConsensusRequester = None): + def __init__( + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, + ): super().__init__(index, number_of_devices, medium) if application is not None: self._application = application else: self._application = SimpleRequester() # assumes everyone has every role - config = PAXOSNetwork(index, self.medium(), self.medium().ids(), self.medium().ids()) + config = PAXOSNetwork( + index, self.medium(), self.medium().ids(), self.medium().ids() + ) self._proposer = Proposer(config, self._application) self._acceptor = Acceptor(config) self._learner = Learner(config, self._application) @@ -337,7 +396,7 @@ def handle_accept(self, msg: AcceptMessage): if self._done: return self._done = True - print(f'CONSENSUS {self._network.index} LEARNER on {msg.value}') + print(f"CONSENSUS {self._network.index} LEARNER on {msg.value}") self._application.consensus_reached(msg.value) def done(self): diff --git a/exercises/exercise7.py b/exercises/exercise7.py index cabbf72..6e60c6f 100644 --- a/exercises/exercise7.py +++ b/exercises/exercise7.py @@ -1,15 +1,9 @@ -import math -import random -import threading -import time - from emulators.Medium import Medium from emulators.Device import Device from emulators.MessageStub import MessageStub class Vote(MessageStub): - def __init__(self, sender: int, destination: int, vote: int, decided: bool): super().__init__(sender, destination) self._vote = vote @@ -22,11 +16,10 @@ def decided(self): return self._decided def __str__(self): - return f'Vote: {self.source} -> {self.destination}, voted for {self._vote}, decided? {self._decided}' + return f"Vote: {self.source} -> {self.destination}, voted for {self._vote}, decided? {self._decided}" class Bully(Device): - def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._leader = None @@ -43,4 +36,4 @@ def start_election(self): """TODO""" def print_result(self): - print(f'Leader seen from {self._id} is {self._leader}') \ No newline at end of file + print(f"Leader seen from {self._id} is {self._leader}") diff --git a/exercises/exercise8.py b/exercises/exercise8.py index a74102a..e941f11 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -1,6 +1,4 @@ -import math import random -import sys from emulators.Device import Device from emulators.Medium import Medium @@ -16,8 +14,12 @@ class GfsMaster(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - self._metadata: dict[tuple[str, int], tuple[int, list[int]]] = {} # (filename, chunk_index) -> (chunkhandle, [chunkservers]) - self.chunks_being_allocated: list[tuple[int, int]] = [] # [(chunkhandle, requester_index)] + self._metadata: dict[ + tuple[str, int], tuple[int, list[int]] + ] = {} # (filename, chunk_index) -> (chunkhandle, [chunkservers]) + self.chunks_being_allocated: list[ + tuple[int, int] + ] = [] # [(chunkhandle, requester_index)] GfsNetwork.gfsmaster.append(index) def run(self): @@ -38,22 +40,16 @@ def handle_ingoing(self, ingoing: MessageStub): self.chunks_being_allocated.append((chunk[0], ingoing.source)) return True answer = File2ChunkRspMessage( - self.index(), - ingoing.source, - chunk[0], - chunk[1] - ) + self.index(), ingoing.source, chunk[0], chunk[1] + ) self.medium().send(answer) else: if ingoing.createIfNotExists: - self.do_allocate_request(ingoing.filename, ingoing.chunkindex, ingoing.source) + self.do_allocate_request( + ingoing.filename, ingoing.chunkindex, ingoing.source + ) else: - answer = File2ChunkRspMessage( - self.index(), - ingoing.source, - 0, - [] - ) + answer = File2ChunkRspMessage(self.index(), ingoing.source, 0, []) self.medium().send(answer) elif isinstance(ingoing, QuitMessage): print(f"I am Master {self.index()} and I am quitting") @@ -70,14 +66,15 @@ def handle_ingoing(self, ingoing: MessageStub): def add_chunk_to_metadata(self, chunk: tuple[int, list[int]], chunkserver: int): chunk[1].append(chunkserver) if len(chunk[1]) == NUMBER_OF_REPLICAS: - requests = [request for request in self.chunks_being_allocated if request[0] == chunk[0]] + requests = [ + request + for request in self.chunks_being_allocated + if request[0] == chunk[0] + ] for request in requests: answer = File2ChunkRspMessage( - self.index(), - request[1], - chunk[0], - chunk[1] - ) + self.index(), request[1], chunk[0], chunk[1] + ) self.medium().send(answer) self.chunks_being_allocated.remove(request) @@ -89,7 +86,9 @@ def do_allocate_request(self, filename, chunkindex: int, requester: int): # Allocate the new chunk on "NUMBER_OF_REPLICAS" random chunkservers chunkservers = random.sample(GfsNetwork.gfschunkserver, NUMBER_OF_REPLICAS) for i in chunkservers: - message = AllocateChunkReqMessage(self.index(), i, chunkhandle, chunkservers) + message = AllocateChunkReqMessage( + self.index(), i, chunkhandle, chunkservers + ) self.medium().send(message) def print_result(self): @@ -118,7 +117,9 @@ def handle_ingoing(self, ingoing: MessageStub): return False elif isinstance(ingoing, AllocateChunkReqMessage): self.do_allocate_chunk(ingoing.chunkhandle, ingoing.chunkservers) - message = AllocateChunkRspMessage(self.index(), ingoing.source, ingoing.chunkhandle, "ok") + message = AllocateChunkRspMessage( + self.index(), ingoing.source, ingoing.chunkhandle, "ok" + ) self.medium().send(message) elif isinstance(ingoing, RecordAppendReqMessage): # @@ -157,13 +158,19 @@ def run(self): def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, File2ChunkRspMessage): - print(f"I found out where my chunk is: {ingoing.chunkhandle}, locations: {ingoing.locations}") + print( + f"I found out where my chunk is: {ingoing.chunkhandle}, locations: {ingoing.locations}" + ) # I select a random chunk server, and I send the append request # I do not necessarily select the primary randomserver = random.choice(ingoing.locations) data = f"hello from client number {self.index()}\n" - self.medium().send(RecordAppendReqMessage(self.index(), randomserver, ingoing.chunkhandle, data)) + self.medium().send( + RecordAppendReqMessage( + self.index(), randomserver, ingoing.chunkhandle, data + ) + ) elif isinstance(ingoing, RecordAppendRspMessage): # project completed, time to quit for i in GfsNetwork.gfsmaster: @@ -185,45 +192,60 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): return GfsChunkserver(index, number_of_devices, medium) else: return GfsClient(index, number_of_devices, medium) + gfsmaster = [] gfschunkserver = [] - class QuitMessage(MessageStub): def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'QUIT REQUEST {self.source} -> {self.destination}' + return f"QUIT REQUEST {self.source} -> {self.destination}" + class File2ChunkReqMessage(MessageStub): - def __init__(self, sender: int, destination: int, filename: str, chunkindex: int, createIfNotExists = False): + def __init__( + self, + sender: int, + destination: int, + filename: str, + chunkindex: int, + createIfNotExists=False, + ): super().__init__(sender, destination) self.filename = filename self.chunkindex = chunkindex self.createIfNotExists = createIfNotExists def __str__(self): - return f'FILE2CHUNK REQUEST {self.source} -> {self.destination}: ({self.filename}, {self.chunkindex}, createIfNotExists = {self.createIfNotExists})' + return f"FILE2CHUNK REQUEST {self.source} -> {self.destination}: ({self.filename}, {self.chunkindex}, createIfNotExists = {self.createIfNotExists})" + class File2ChunkRspMessage(MessageStub): - def __init__(self, sender: int, destination: int, chunkhandle: int, locations: list): + def __init__( + self, sender: int, destination: int, chunkhandle: int, locations: list + ): super().__init__(sender, destination) self.chunkhandle = chunkhandle self.locations = locations def __str__(self): - return f'FILE2CHUNK RESPONSE {self.source} -> {self.destination}: ({self.chunkhandle}, {self.locations})' + return f"FILE2CHUNK RESPONSE {self.source} -> {self.destination}: ({self.chunkhandle}, {self.locations})" + class AllocateChunkReqMessage(MessageStub): - def __init__(self, sender: int, destination: int, chunkhandle: int, chunkservers: list[int]): + def __init__( + self, sender: int, destination: int, chunkhandle: int, chunkservers: list[int] + ): super().__init__(sender, destination) self.chunkhandle = chunkhandle self.chunkservers = chunkservers def __str__(self): - return f'ALLOCATE REQUEST {self.source} -> {self.destination}: ({self.chunkhandle})' + return f"ALLOCATE REQUEST {self.source} -> {self.destination}: ({self.chunkhandle})" + class AllocateChunkRspMessage(MessageStub): def __init__(self, sender: int, destination: int, chunkhandle: int, result: str): @@ -232,7 +254,8 @@ def __init__(self, sender: int, destination: int, chunkhandle: int, result: str) self.result = result def __str__(self): - return f'ALLOCATE RESPONSE {self.source} -> {self.destination}: ({self.chunkhandle, self.result})' + return f"ALLOCATE RESPONSE {self.source} -> {self.destination}: ({self.chunkhandle, self.result})" + class RecordAppendReqMessage(MessageStub): def __init__(self, sender: int, destination: int, chunkhandle: int, data: str): @@ -241,7 +264,8 @@ def __init__(self, sender: int, destination: int, chunkhandle: int, data: str): self.data = data def __str__(self): - return f'RECORD APPEND REQUEST {self.source} -> {self.destination}: ({self.chunkhandle}, {self.data})' + return f"RECORD APPEND REQUEST {self.source} -> {self.destination}: ({self.chunkhandle}, {self.data})" + class RecordAppendRspMessage(MessageStub): def __init__(self, sender: int, destination: int, result: str): @@ -250,5 +274,4 @@ def __init__(self, sender: int, destination: int, result: str): # TODO: possibly, complete this message with the fields you need def __str__(self): - return f'RECORD APPEND RESPONSE {self.source} -> {self.destination}: ({self.result})' - + return f"RECORD APPEND RESPONSE {self.source} -> {self.destination}: ({self.result})" diff --git a/exercises/exercise9.py b/exercises/exercise9.py index 3fc86cd..cd37c48 100644 --- a/exercises/exercise9.py +++ b/exercises/exercise9.py @@ -1,6 +1,3 @@ -import math -import random -import sys import os from enum import Enum @@ -38,14 +35,21 @@ def handle_ingoing(self, ingoing: MessageStub): self.number_partitions = ingoing.number_partitions number_of_mappers = self.number_of_devices() - self.number_partitions - 2 for i in range(2, self.number_partitions + 2): - message = ReduceTaskMessage(self.index(), i, i - 2, self.number_partitions, number_of_mappers) # the reducer needs to know how many mappers they are, to know when its task is completed + message = ReduceTaskMessage( + self.index(), i, i - 2, self.number_partitions, number_of_mappers + ) # the reducer needs to know how many mappers they are, to know when its task is completed self.medium().send(message) for i in range(0, number_of_mappers): length = len(ingoing.filenames) length = 5 # TODO: comment out this line to process all files, once you think your code is ready first = int(length * i / number_of_mappers) - last = int(length * (i+1) / number_of_mappers) - message = MapTaskMessage(self.index(), self.number_partitions + 2 + i, ingoing.filenames[first:last], self.number_partitions) + last = int(length * (i + 1) / number_of_mappers) + message = MapTaskMessage( + self.index(), + self.number_partitions + 2 + i, + ingoing.filenames[first:last], + self.number_partitions, + ) self.medium().send(message) elif isinstance(ingoing, QuitMessage): # if the client is satisfied with the work done, I can tell all workers to quit, then I can quit @@ -61,7 +65,9 @@ def handle_ingoing(self, ingoing: MessageStub): self.result_files.append(ingoing.result_filename) if self.number_finished_reducers == self.number_partitions: # I can tell the client that the job is done - message = ClientJobCompletedMessage(1, MapReduceNetwork.client_index, self.result_files) + message = ClientJobCompletedMessage( + 1, MapReduceNetwork.client_index, self.result_files + ) self.medium().send(message) return True @@ -79,7 +85,9 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): # variables if it is a mapper self.M_files_to_process: list[str] = [] # list of files to process self.M_cached_results: dict[str, int] = {} # in-memory cache - self.M_stored_results: dict[int, dict[str, int]] = {} # "R" files containing results. partition -> word -> count + self.M_stored_results: dict[ + int, dict[str, int] + ] = {} # "R" files containing results. partition -> word -> count # variables if it is a reducer self.R_my_partition = 0 # the partition I am managing self.R_number_mappers = 0 # how many mappers there are. I need to know it to decide when I can tell the master I am done with the reduce task @@ -87,7 +95,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def mapper_process_file(self, filename: str) -> dict[str, int]: # goal: return the occurrences of words in the file words = [] - with open("ex9data/books/"+filename) as file: + with open("ex9data/books/" + filename) as file: for line in file: words += line.split() result = {} @@ -99,11 +107,13 @@ def mapper_partition_function(self, key: str) -> int: # Compute the partition based on the key (see the lecture material) # This function should be supplied by the client, but we stick to a fixed function for sake of clarity char = ord(key[0]) - if char < ord('a'): - char = ord('a') - if char > ord('z'): - char = ord('z') - partition = (char - ord('a')) * self.number_partitions / (1+ord('z')-ord('a')) + if char < ord("a"): + char = ord("a") + if char > ord("z"): + char = ord("z") + partition = ( + (char - ord("a")) * self.number_partitions / (1 + ord("z") - ord("a")) + ) return int(partition) def mapper_shuffle(self): @@ -126,11 +136,15 @@ def do_some_work(self): filename = self.M_files_to_process.pop() map_result = self.mapper_process_file(filename) for word in map_result: - self.M_cached_results[word] = self.M_cached_results.get(word, 0) + map_result[word] + self.M_cached_results[word] = ( + self.M_cached_results.get(word, 0) + map_result[word] + ) print(f"Mapper {self.index()}: file '{filename}' processed") if self.M_files_to_process == []: self.mapper_shuffle() - message = MappingDoneMessage(self.index(), MapReduceNetwork.master_index) + message = MappingDoneMessage( + self.index(), MapReduceNetwork.master_index + ) self.medium().send(message) if self.role == Role.REDUCER: # not much to do: everything is done when the master tells us about a mapper that completed its task @@ -166,7 +180,9 @@ def handle_ingoing(self, ingoing: MessageStub): self.R_my_partition = ingoing.my_partition self.R_number_mappers = ingoing.number_mappers # nothing to do until the Master tells us to contact Mappers - elif isinstance(ingoing, ReducerVisitMapperMessage): # 'ReducerVisitMapperMessage' does not exist by default + elif isinstance( + ingoing, ReducerVisitMapperMessage + ): # 'ReducerVisitMapperMessage' does not exist by default # the master is saying that a mapper is done # thus this reducer will: # get the "stored" results from the mapper, for the correct partition (new message type) @@ -188,16 +204,21 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self.result_files: list[str] = [] def scan_for_books(self): - with os.scandir('ex9data/books/') as entries: - return [entry.name for entry in entries - if entry.is_file() and entry.name.endswith(".txt")] + with os.scandir("ex9data/books/") as entries: + return [ + entry.name + for entry in entries + if entry.is_file() and entry.name.endswith(".txt") + ] def run(self): # being a client, it listens to incoming messages, but it also does something to put the ball rolling print(f"I am client {self.index()}") books = self.scan_for_books() - message = ClientJobStartMessage(self.index(), MapReduceNetwork.master_index, books, 3) # TODO: experiment with different number of reducers + message = ClientJobStartMessage( + self.index(), MapReduceNetwork.master_index, books, 3 + ) # TODO: experiment with different number of reducers self.medium().send(message) while True: @@ -234,6 +255,7 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): return MapReduceMaster(index, number_of_devices, medium) else: return MapReduceWorker(index, number_of_devices, medium) + client_index = 0 master_index = 1 workers: list[int] = [] @@ -244,17 +266,19 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'QUIT REQUEST {self.source} -> {self.destination}' + return f"QUIT REQUEST {self.source} -> {self.destination}" class ClientJobStartMessage(MessageStub): - def __init__(self, sender: int, destination: int, filenames: list, number_partitions: int): + def __init__( + self, sender: int, destination: int, filenames: list, number_partitions: int + ): super().__init__(sender, destination) self.filenames = filenames self.number_partitions = number_partitions def __str__(self): - return f'CLIENT START JOB REQUEST {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)' + return f"CLIENT START JOB REQUEST {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)" class ClientJobCompletedMessage(MessageStub): @@ -263,17 +287,19 @@ def __init__(self, sender: int, destination: int, result_files: list): self.result_files = result_files def __str__(self): - return f'CLIENT JOB COMPLETED {self.source} -> {self.destination} ({self.result_files})' + return f"CLIENT JOB COMPLETED {self.source} -> {self.destination} ({self.result_files})" class MapTaskMessage(MessageStub): - def __init__(self, sender: int, destination: int, filenames: list, number_partitions: int): + def __init__( + self, sender: int, destination: int, filenames: list, number_partitions: int + ): super().__init__(sender, destination) self.filenames = filenames self.number_partitions = number_partitions def __str__(self): - return f'MAP TASK ASSIGNMENT {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)' + return f"MAP TASK ASSIGNMENT {self.source} -> {self.destination}: ({len(self.filenames)} files, {self.number_partitions} partitions)" class MappingDoneMessage(MessageStub): @@ -281,18 +307,25 @@ def __init__(self, sender: int, destination: int): super().__init__(sender, destination) def __str__(self): - return f'MAP TASK COMPLETED {self.source} -> {self.destination}' + return f"MAP TASK COMPLETED {self.source} -> {self.destination}" class ReduceTaskMessage(MessageStub): - def __init__(self, sender: int, destination: int, my_partition: int, number_partitions: int, number_mappers: int): + def __init__( + self, + sender: int, + destination: int, + my_partition: int, + number_partitions: int, + number_mappers: int, + ): super().__init__(sender, destination) self.my_partition = my_partition self.number_partitions = number_partitions self.number_mappers = number_mappers def __str__(self): - return f'REDUCE TASK ASSIGNMENT {self.source} -> {self.destination}: (partition is {self.my_partition}, {self.number_partitions} partitions, {self.number_mappers} mappers)' + return f"REDUCE TASK ASSIGNMENT {self.source} -> {self.destination}: (partition is {self.my_partition}, {self.number_partitions} partitions, {self.number_mappers} mappers)" class ReducingDoneMessage(MessageStub): @@ -301,4 +334,4 @@ def __init__(self, sender: int, destination: int, result_filename: str): self.result_filename = result_filename def __str__(self): - return f'REDUCE TASK COMPLETED {self.source} -> {self.destination}: result_filename = {self.result_filename}' + return f"REDUCE TASK COMPLETED {self.source} -> {self.destination}: result_filename = {self.result_filename}" From 62e844498ab5ba058a3864487980d121ef7b34cc Mon Sep 17 00:00:00 2001 From: Simon Deleuran Laursen <69715502+Laursen79@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:13:32 +0100 Subject: [PATCH 093/106] Now runs in python 3.12 --- emulators/SteppingEmulator.py | 2 +- exercise_runner.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index cb08cf0..1c265a5 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -43,7 +43,7 @@ def __init__( self.input_lock = Lock() # self.listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release) # self.listener.start() - self.shell = Thread(target=self.prompt, daemon=True) + self.shell = self.prompt self.messages_received: list[MessageStub] = [] self.messages_sent: list[MessageStub] = [] msg = f""" diff --git a/exercise_runner.py b/exercise_runner.py index 663e021..834d349 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -3,7 +3,18 @@ from os import name from threading import Thread from emulators.exercise_overlay import Window - +import exercises.exercise1 +import exercises.exercise2 +import exercises.exercise4 +import exercises.exercise5 +import exercises.exercise6 +import exercises.exercise7 +import exercises.exercise8 +import exercises.exercise9 +import exercises.exercise10 +import exercises.exercise11 +import exercises.exercise12 +import exercises.demo from emulators.AsyncEmulator import AsyncEmulator from emulators.SyncEmulator import SyncEmulator from emulators.SteppingEmulator import SteppingEmulator @@ -60,7 +71,7 @@ def run_exercise( instance = emulator(number_of_devices, alg) def run_instance(): - if instance is not None: + if instance: instance.run() print(f"{CYAN}Execution Complete{RESET}") instance.print_result() @@ -83,7 +94,7 @@ def run_instance(): window.show() return window if not gui and isinstance(instance, SteppingEmulator): - instance.shell.start() + instance.shell() if __name__ == "__main__": From 46fa108a545772ede8ad7fc7397596b2eabc935c Mon Sep 17 00:00:00 2001 From: Simon Deleuran Laursen <69715502+Laursen79@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:47:41 +0100 Subject: [PATCH 094/106] Converted getter methods to properties. Fixed ruff linting errors. --- README.md | 6 +- emulators/Device.py | 13 +-- emulators/SteppingEmulator.py | 2 +- exercise_runner.py | 18 ++-- exercises/demo.py | 12 +-- exercises/exercise1.py | 2 +- exercises/exercise10.py | 38 ++++---- exercises/exercise11.py | 24 ++--- exercises/exercise12.py | 28 +++--- exercises/exercise2.py | 30 +++---- exercises/exercise4.py | 162 +++++++++++++++++----------------- exercises/exercise5.py | 50 +++++------ exercises/exercise6.py | 98 ++++++++++---------- exercises/exercise7.py | 2 +- exercises/exercise8.py | 52 +++++------ exercises/exercise9.py | 50 ++++++----- 16 files changed, 295 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index 7984715..d6d7191 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn 3. For the ring network, consider an approach similar to 1. ```python def routing_table_complete(self): - if len(self.routing_table) < self.number_of_devices()-1: + if len(self.routing_table) < self.number_of_devices-1: return False return True ``` @@ -114,11 +114,11 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn 4. Try this other approach, which works better: 1. ```python def routing_table_complete(self): - if len(self.routing_table) < self.number_of_devices()-1: + if len(self.routing_table) < self.number_of_devices-1: return False for row in self.routing_table: (next_hop, distance) = self.routing_table[row] - if distance > (self.number_of_devices()/2): + if distance > (self.number_of_devices/2): return False return True ``` diff --git a/emulators/Device.py b/emulators/Device.py index bb241e2..15d4cd6 100644 --- a/emulators/Device.py +++ b/emulators/Device.py @@ -39,6 +39,7 @@ def print_result(self): """ raise NotImplementedError("You have to implement a result printer!") + @property def index(self): """ The unique identifier for the device. @@ -48,6 +49,7 @@ def index(self): """ return self._id + @property def number_of_devices(self): """ Get the total number of devices in the simulation. @@ -57,6 +59,7 @@ def number_of_devices(self): """ return self._number_of_devices + @property def medium(self): """ Get the communication medium used by the device. @@ -78,8 +81,8 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def has_work(self) -> bool: # The random call emulates that a concurrent process asked for the self._has_work = ( - self._has_work - or random.randint(0, self.number_of_devices()) == self.index() + self._has_work + or random.randint(0, self.number_of_devices) == self.index ) return self._has_work @@ -89,7 +92,7 @@ def do_work(self): # might require continued interaction. The "working" thread would then notify our Requester class back # when the mutex is done being used. self._lock.acquire() - print(f"Device {self.index()} has started working") + print(f"Device {self.index} has started working") self._concurrent_workers += 1 if self._concurrent_workers > 1: self._lock.release() @@ -99,9 +102,9 @@ def do_work(self): assert self.has_work() amount_of_work = random.randint(1, 4) for i in range(0, amount_of_work): - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() self._lock.acquire() - print(f"Device {self.index()} has ended working") + print(f"Device {self.index} has ended working") self._concurrent_workers -= 1 self._lock.release() self._has_work = False diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 1c265a5..35c79bb 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -6,7 +6,7 @@ from .EmulatorStub import EmulatorStub from emulators.SyncEmulator import SyncEmulator from emulators.MessageStub import MessageStub -from threading import Barrier, Lock, Thread, BrokenBarrierError # run getpass in seperate thread +from threading import Barrier, Lock, BrokenBarrierError # run getpass in seperate thread from os import name if name == "posix": diff --git a/exercise_runner.py b/exercise_runner.py index 834d349..0998bf2 100644 --- a/exercise_runner.py +++ b/exercise_runner.py @@ -1,24 +1,15 @@ import argparse +import importlib import inspect from os import name from threading import Thread + from emulators.exercise_overlay import Window -import exercises.exercise1 -import exercises.exercise2 -import exercises.exercise4 -import exercises.exercise5 -import exercises.exercise6 -import exercises.exercise7 -import exercises.exercise8 -import exercises.exercise9 -import exercises.exercise10 -import exercises.exercise11 -import exercises.exercise12 -import exercises.demo from emulators.AsyncEmulator import AsyncEmulator from emulators.SyncEmulator import SyncEmulator from emulators.SteppingEmulator import SteppingEmulator + if name == "posix": RESET = "\u001B[0m" CYAN = "\u001B[36m" @@ -33,7 +24,8 @@ def fetch_alg(lecture: str, algorithm: str): if "." in algorithm or ";" in algorithm: raise ValueError('"." and ";" are not allowed as names of solutions.') try: - alg = eval(f"exercises.{lecture}.{algorithm}") + module = importlib.import_module(f"exercises.{lecture}") + alg = getattr(module, algorithm) if not inspect.isclass(alg): raise TypeError(f'Could not find "exercises.{lecture}.{algorithm} class') except NameError: diff --git a/exercises/demo.py b/exercises/demo.py index 853748e..03a5109 100644 --- a/exercises/demo.py +++ b/exercises/demo.py @@ -40,16 +40,16 @@ def run(self): for repetetions in range(0, 10): # in each repetition, let us send the ping to one random other device message = PingMessage( - self.index(), - random.randrange(0, self.number_of_devices()), + self.index, + random.randrange(0, self.number_of_devices), self._is_ping, ) # we send the message via a "medium" - self.medium().send(message) + self.medium.send(message) # in this instance, we also try to receive some messages, there can be multiple, but # eventually the medium will return "None" while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is None: break @@ -67,10 +67,10 @@ def run(self): self._is_ping = ingoing.is_ping # this call is only used for synchronous networks - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() # for pretty-printing and debugging, implement this function def print_result(self): print( - f"\tDevice {self.index()} got pings: {self._rec_ping} and pongs: {self._rec_pong}" + f"\tDevice {self.index} got pings: {self._rec_ping} and pongs: {self._rec_pong}" ) diff --git a/exercises/exercise1.py b/exercises/exercise1.py index 7067fb4..de8b928 100644 --- a/exercises/exercise1.py +++ b/exercises/exercise1.py @@ -27,4 +27,4 @@ def run(self): return def print_result(self): - print(f"\tDevice {self.index()} got secrets: {self._secrets}") + print(f"\tDevice {self.index} got secrets: {self._secrets}") diff --git a/exercises/exercise10.py b/exercises/exercise10.py index fcf899c..bcbc949 100644 --- a/exercises/exercise10.py +++ b/exercises/exercise10.py @@ -117,17 +117,17 @@ def try_mining(self): def disseminate_chain(self): # I send the blockchain to everybody for m in BlockchainNetwork.miners: - if not m == self.index(): - message = BlockchainMessage(self.index(), m, self.blockchain.chain) - self.medium().send(message) + if not m == self.index: + message = BlockchainMessage(self.index, m, self.blockchain.chain) + self.medium.send(message) # Since I flushed the unconfirmed transactions, I assign the incentives to myself for next block, in case I will be the winner of the proof of work test - self.blockchain.add_new_transaction(f"(miner {self.index()} gets incentive)") + self.blockchain.add_new_transaction(f"(miner {self.index} gets incentive)") def do_some_work(self): # if the chain is empty and index == 0, create the genesis block and disseminate the blockchain # if index is not 0, do nothing if len(self.blockchain.chain) == 0: - if self.index() == 0: + if self.index == 0: self.blockchain.create_genesis_block() self.disseminate_chain() else: @@ -138,14 +138,14 @@ def do_some_work(self): def run(self): # I assign the incentives to myself, in case I add the next block - self.blockchain.add_new_transaction(f"(miner {self.index()} gets incentive)") + self.blockchain.add_new_transaction(f"(miner {self.index} gets incentive)") # since this is a miner, it tries to mine, then it looks for incoming requests (messages) to accumulate transactions etc while True: self.do_some_work() - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, BlockchainMessage): @@ -156,9 +156,9 @@ def handle_ingoing(self, ingoing: MessageStub): elif isinstance(ingoing, BlockchainRequestMessage): # this is used to send the blockchain data to a client requesting them message = BlockchainMessage( - self.index(), ingoing.source, self.blockchain.chain + self.index, ingoing.source, self.blockchain.chain ) - self.medium().send(message) + self.medium.send(message) elif isinstance(ingoing, TransactionMessage): self.blockchain.add_new_transaction(ingoing.transaction) elif isinstance(ingoing, QuitMessage): @@ -166,7 +166,7 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print("Miner " + str(self.index()) + " quits") + print("Miner " + str(self.index) + " quits") class BlockchainClient(Device): @@ -180,20 +180,20 @@ def run(self): # the client spends its time adding transactions (reasonable) and asking how long the blockchain is (unreasonable, but used for the termination) self.request_blockchain() while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def send_transaction(self): message = TransactionMessage( - self.index(), self.my_miner, f"(transaction by client {self.index()})" + self.index, self.my_miner, f"(transaction by client {self.index})" ) - self.medium().send(message) + self.medium.send(message) def request_blockchain(self): - message = BlockchainRequestMessage(self.index(), self.my_miner) - self.medium().send(message) + message = BlockchainRequestMessage(self.index, self.my_miner) + self.medium.send(message) def handle_ingoing(self, ingoing: MessageStub): # the termination clause is *very* random @@ -202,7 +202,7 @@ def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, BlockchainMessage): if target_len <= len(ingoing.chain): - self.medium().send(QuitMessage(self.index(), self.my_miner)) + self.medium.send(QuitMessage(self.index, self.my_miner)) return False # if I don't decide to quit, I add a transaction and request the blockchain data one more time self.send_transaction() @@ -210,7 +210,7 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print(f"client {self.index()} quits") + print(f"client {self.index} quits") class BlockchainNetwork: diff --git a/exercises/exercise11.py b/exercises/exercise11.py index 7d3dd17..ec0065f 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -66,10 +66,10 @@ def __init__( def run(self): # a chord node acts like a server while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def is_request_for_me(self, guid: int) -> bool: # TODO: implement this function that checks if the routing process is over @@ -88,8 +88,8 @@ def handle_ingoing(self, ingoing: MessageStub): # TODO: route the message # you can fill up the next_hop function for this next_hop = self.next_hop(ingoing.guid) - message = PutMessage(self.index(), next_hop, ingoing.guid, ingoing.data) - self.medium().send(message) + message = PutMessage(self.index, next_hop, ingoing.guid, ingoing.data) + self.medium.send(message) if isinstance(ingoing, GetReqMessage): # maybe TODO, but the GET is not very interesting pass @@ -118,10 +118,10 @@ def print_result(self): if my_range < 0: my_range += pow(2, address_size) print( - f"Chord node {self.index()} quits, it managed {my_range} addresses, it had {len(self.saved_data)} data blocks" + f"Chord node {self.index} quits, it managed {my_range} addresses, it had {len(self.saved_data)} data blocks" ) else: - print(f"Chord node {self.index()} quits, it was still disconnected") + print(f"Chord node {self.index} quits, it was still disconnected") class ChordClient(Device): @@ -134,8 +134,8 @@ def run(self): # if your chord address space gets too big, use the following code: # for i in range(pow(2, address_size)): # guid = random.randint(0, pow(2,address_size)-1) - message = PutMessage(self.index(), 2, guid, "hello") - self.medium().send(message) + message = PutMessage(self.index, 2, guid, "hello") + self.medium.send(message) # TODO: uncomment this code to start the JOIN process # new_chord_id = random.randint(0, pow(2,address_size)-1) @@ -147,9 +147,9 @@ def run(self): time.sleep( 10 ) # or use some smart trick to wait for the routing process to be completed before shutting down the distributed system - for i in range(1, self.number_of_devices()): - message = QuitMessage(self.index(), i) - self.medium().send(message) + for i in range(1, self.number_of_devices): + message = QuitMessage(self.index, i) + self.medium.send(message) return # currently, I do not manage incoming messages @@ -165,7 +165,7 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print(f"client {self.index()} quits") + print(f"client {self.index} quits") class ChordNetwork: diff --git a/exercises/exercise12.py b/exercises/exercise12.py index 7c8192e..c6b0a3b 100644 --- a/exercises/exercise12.py +++ b/exercises/exercise12.py @@ -39,33 +39,33 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self.outgoing_message_cache: list[DataMessage] = [] def run(self): - last = random.randint(0, self.number_of_devices() - 1) + last = random.randint(0, self.number_of_devices - 1) # I send the message to myself, so it gets routed message = DataMessage( - self.index(), self.index(), last, f"Hi. I am {self.index()}." + self.index, self.index, last, f"Hi. I am {self.index}." ) - self.medium().send(message) + self.medium.send(message) while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def next_hop(self, last: int) -> Optional[int]: return self.forward_path.get(last) # Returns "None" if key does not exist def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, DataMessage): - if self.index() == ingoing.last: + if self.index == ingoing.last: # the message is for me self.saved_data.append(ingoing.data) # AodvNode.messages_lock.acquire() AodvNode.data_messages_received += 1 AodvNode.messages_lock.release() - if AodvNode.data_messages_received == self.number_of_devices(): - for i in range(0, self.number_of_devices()): - self.medium().send(QuitMessage(self.index(), i)) + if AodvNode.data_messages_received == self.number_of_devices: + for i in range(0, self.number_of_devices): + self.medium.send(QuitMessage(self.index, i)) # else: next = self.next_hop( @@ -74,9 +74,9 @@ def handle_ingoing(self, ingoing: MessageStub): if next is not None: # I know how to reach the destination message = DataMessage( - self.index(), next, ingoing.last, ingoing.data + self.index, next, ingoing.last, ingoing.data ) - self.medium().send(message) + self.medium.send(message) return True # I don't have the route to the destination. # I need to save the outgoing message in a cache @@ -93,7 +93,7 @@ def handle_ingoing(self, ingoing: MessageStub): # TODO pass - if self.index() == ingoing.last: + if self.index == ingoing.last: # the message is for me. I can send back a Route Reply # TODO pass @@ -105,7 +105,7 @@ def handle_ingoing(self, ingoing: MessageStub): # If I don't have the forward_path in my routing table, I save it # TODO - if self.index() == ingoing.first: + if self.index == ingoing.first: # Finally, I can send all the messages I had saved "first -> last" from my cache # TODO pass @@ -119,7 +119,7 @@ def handle_ingoing(self, ingoing: MessageStub): def print_result(self): print( - f"AODV node {self.index()} quits: neighbours = {self.neighbors}, forward paths = {self.forward_path}, reverse paths = {self.reverse_path}, saved data = {self.saved_data}, length of message cache (should be 0) = {len(self.outgoing_message_cache)}" + f"AODV node {self.index} quits: neighbours = {self.neighbors}, forward paths = {self.forward_path}, reverse paths = {self.reverse_path}, saved data = {self.saved_data}, length of message cache (should be 0) = {len(self.outgoing_message_cache)}" ) diff --git a/exercises/exercise2.py b/exercises/exercise2.py index 12fceae..c025382 100644 --- a/exercises/exercise2.py +++ b/exercises/exercise2.py @@ -36,41 +36,41 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): for neigh in self.neighbors: self.routing_table[neigh] = (neigh, 1) - self.routing_table[self.index()] = (self.index(), 0) + self.routing_table[self.index] = (self.index, 0) for neigh in self.neighbors: - self.medium().send(RipMessage(self.index(), neigh, self.routing_table)) + self.medium.send(RipMessage(self.index, neigh, self.routing_table)) while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is None: # this call is only used for synchronous networks - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() continue if type(ingoing) is RipMessage: - print(f"Device {self.index()}: Got new table from {ingoing.source}") + print(f"Device {self.index}: Got new table from {ingoing.source}") returned_table = self.merge_tables(ingoing.source, ingoing.table) if returned_table is not None: self.routing_table = returned_table for neigh in self.neighbors: - self.medium().send( - RipMessage(self.index(), neigh, self.routing_table) + self.medium.send( + RipMessage(self.index, neigh, self.routing_table) ) if type(ingoing) is RoutableMessage: print( - f"Device {self.index()}: Routing from {ingoing.first_node} to {ingoing.last_node} via #{self.index()}: [#{ingoing.content}]" + f"Device {self.index}: Routing from {ingoing.first_node} to {ingoing.last_node} via #{self.index}: [#{ingoing.content}]" ) - if ingoing.last_node is self.index(): + if ingoing.last_node is self.index: print( - f"\tDevice {self.index()}: delivered message from {ingoing.first_node} to {ingoing.last_node}: {ingoing.content}" + f"\tDevice {self.index}: delivered message from {ingoing.first_node} to {ingoing.last_node}: {ingoing.content}" ) continue if self.routing_table[ingoing.last_node] is not None: (next_hop, distance) = self.routing_table[ingoing.last_node] - self.medium().send( + self.medium.send( RoutableMessage( - self.index(), + self.index, next_hop, ingoing.first_node, ingoing.last_node, @@ -79,15 +79,15 @@ def run(self): ) continue print( - f"\tDevice {self.index()}: DROP Unknown route #{ingoing.first_node} to #{ingoing.last_node} via #{self.index}, message #{ingoing.content}" + f"\tDevice {self.index}: DROP Unknown route #{ingoing.first_node} to #{ingoing.last_node} via #{self.index}, message #{ingoing.content}" ) # this call is only used for synchronous networks - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def merge_tables(self, src, table): # return None if the table does not change pass def print_result(self): - print(f"\tDevice {self.index()} has routing table: {self.routing_table}") + print(f"\tDevice {self.index} has routing table: {self.routing_table}") diff --git a/exercises/exercise4.py b/exercises/exercise4.py index 52ec91a..aea84b2 100644 --- a/exercises/exercise4.py +++ b/exercises/exercise4.py @@ -21,7 +21,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if isinstance(ingoing, Ping): if self._output_ping: print("Ping") @@ -29,7 +29,7 @@ def run(self): else: print("Pong") self._output_ping = True - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def print_result(self): print("Done!") @@ -75,14 +75,14 @@ def __new__(cls, index: int, number_of_devices: int, medium: Medium): class Coordinator(Device): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - assert self.index() == 0 # we assume that the coordinator is fixed at index 0. + assert self.index == 0 # we assume that the coordinator is fixed at index 0. self._granted = None self._waiting = [] def run(self): while True: while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is None: break if ingoing.is_request(): @@ -93,10 +93,10 @@ def run(self): if len(self._waiting) > 0 and self._granted is None: self._granted = self._waiting.pop(0) - self.medium().send( - MutexMessage(self.index(), self._granted, Type.GRANT) + self.medium.send( + MutexMessage(self.index, self._granted, Type.GRANT) ) - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def print_result(self): print("Coordinator Terminated") @@ -105,27 +105,27 @@ def print_result(self): class Requester(WorkerDevice): def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) - assert self.index() != 0 # we assume that the coordinator is fixed at index 0. + assert self.index != 0 # we assume that the coordinator is fixed at index 0. self._requested = False def run(self): while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is not None: if ingoing.is_grant(): assert self._requested self.do_work() self._requested = False - self.medium().send(MutexMessage(self.index(), 0, Type.RELEASE)) + self.medium.send(MutexMessage(self.index, 0, Type.RELEASE)) if self.has_work() and not self._requested: self._requested = True - self.medium().send(MutexMessage(self.index(), 0, Type.REQUEST)) + self.medium.send(MutexMessage(self.index, 0, Type.REQUEST)) - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def print_result(self): - print(f"Requester {self.index()} Terminated with request? {self._requested}") + print(f"Requester {self.index} Terminated with request? {self._requested}") class TokenRing(WorkerDevice): @@ -138,7 +138,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): while True: while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is None: break if ingoing.is_grant(): @@ -146,13 +146,13 @@ def run(self): if self._has_token: if self.has_work(): self.do_work() - nxt = (self.index() + 1) % self.number_of_devices() + nxt = (self.index + 1) % self.number_of_devices self._has_token = False - self.medium().send(MutexMessage(self.index(), nxt, Type.GRANT)) - self.medium().wait_for_next_round() + self.medium.send(MutexMessage(self.index, nxt, Type.GRANT)) + self.medium.wait_for_next_round() def print_result(self): - print(f"Token Ring {self.index()} Terminated with request? {self._requested}") + print(f"Token Ring {self.index} Terminated with request? {self._requested}") class StampedMessage(MutexMessage): @@ -186,7 +186,7 @@ def run(self): if self.has_work(): self.acquire() while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is not None: if ingoing.is_grant(): self.handle_grant(ingoing) @@ -194,27 +194,27 @@ def run(self): self.handle_request(ingoing) else: break - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_request(self, message: StampedMessage): new_time = max(self._time, message.stamp()) + 1 if self._state == State.HELD or ( - self._state == State.WANTED - and (self._time, self.index()) < (message.stamp(), message.source) + self._state == State.WANTED + and (self._time, self.index) < (message.stamp(), message.source) ): self._time = new_time self._waiting.append(message.source) else: new_time += 1 - self.medium().send( - StampedMessage(self.index(), message.source, Type.GRANT, new_time) + self.medium.send( + StampedMessage(self.index, message.source, Type.GRANT, new_time) ) self._time = new_time def handle_grant(self, message: StampedMessage): self._grants += 1 self._time = max(self._time, message.stamp()) + 1 - if self._grants == self.number_of_devices() - 1: + if self._grants == self.number_of_devices - 1: self._state = State.HELD self.do_work() self.release() @@ -224,7 +224,7 @@ def release(self): self._state = State.RELEASED self._time += 1 for id in self._waiting: - self.medium().send(StampedMessage(self.index(), id, Type.GRANT, self._time)) + self.medium.send(StampedMessage(self.index, id, Type.GRANT, self._time)) self._waiting.clear() def acquire(self): @@ -232,15 +232,15 @@ def acquire(self): return self._state = State.WANTED self._time += 1 - for id in self.medium().ids(): - if id != self.index(): - self.medium().send( - StampedMessage(self.index(), id, Type.REQUEST, self._time) + for id in self.medium.ids(): + if id != self.index: + self.medium.send( + StampedMessage(self.index, id, Type.REQUEST, self._time) ) def print_result(self): print( - f"RA {self.index()} Terminated with request? {self._state == State.WANTED}" + f"RA {self.index} Terminated with request? {self._state == State.WANTED}" ) @@ -253,20 +253,20 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._grants = 0 # Generate quorums/ voting set self._voting_set = set() - dimension = int(math.sqrt(self.number_of_devices())) - offset_x = self.index() % dimension - offset_y = int(self.index() / dimension) + dimension = int(math.sqrt(self.number_of_devices)) + offset_x = self.index % dimension + offset_y = int(self.index / dimension) for i in range(0, dimension): # same "column" - if i * dimension + offset_x < self.number_of_devices(): + if i * dimension + offset_x < self.number_of_devices: self._voting_set.add(i * dimension + offset_x) # same "row" - if offset_y * dimension + i < self.number_of_devices(): + if offset_y * dimension + i < self.number_of_devices: self._voting_set.add(offset_y * dimension + i) def run(self): while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is not None: if ingoing.is_grant(): self.handle_grant(ingoing) @@ -276,14 +276,14 @@ def run(self): self.handle_release(ingoing) if self.has_work(): self.acquire() - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def acquire(self): if self._state == State.WANTED: return self._state = State.WANTED for id in self._voting_set: - self.medium().send(MutexMessage(self.index(), id, Type.REQUEST)) + self.medium.send(MutexMessage(self.index, id, Type.REQUEST)) def handle_grant(self, message: MutexMessage): self._grants += 1 @@ -296,26 +296,26 @@ def release(self): self._grants = 0 self._state = State.RELEASED for id in self._voting_set: - self.medium().send(MutexMessage(self.index(), id, Type.RELEASE)) + self.medium.send(MutexMessage(self.index, id, Type.RELEASE)) def handle_request(self, message: MutexMessage): if self._state == State.HELD or self._voted: self._waiting.append(message.source) else: self._voted = True - self.medium().send(MutexMessage(self.index(), message.source, Type.GRANT)) + self.medium.send(MutexMessage(self.index, message.source, Type.GRANT)) def handle_release(self, message: MutexMessage): if len(self._waiting) > 0: nxt = self._waiting.pop(0) self._voted = True - self.medium().send(MutexMessage(self.index(), nxt, Type.GRANT)) + self.medium.send(MutexMessage(self.index, nxt, Type.GRANT)) else: self._voted = False def print_result(self): print( - f"MA {self.index()} Terminated with request? {self._state == State.WANTED}" + f"MA {self.index} Terminated with request? {self._state == State.WANTED}" ) @@ -365,11 +365,11 @@ def run(self): else: self.acquire() - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_messages(self): while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is None: break if isinstance(ingoing, SKToken): @@ -383,15 +383,15 @@ def handle_request(self, message: StampedMessage): (queue, ln) = self._token if self._rn[message.source] == ln[message.source] + 1: self._token = None - self.medium().send(SKToken(self.index(), message.source, queue, ln)) + self.medium.send(SKToken(self.index, message.source, queue, ln)) def release(self): self._working = False self._requested = False (queue, ln) = self._token - ln[self.index()] = self._rn[self.index()] + ln[self.index] = self._rn[self.index] # let's generate a new queue with all devices with outstanding requests - for id in self.medium().ids(): + for id in self.medium.ids(): if ln[id] + 1 == self._rn[id]: if id not in queue: queue.append(id) @@ -399,19 +399,19 @@ def release(self): if len(queue) > 0: nxt = queue.pop(0) self._token = None - self.medium().send(SKToken(self.index(), nxt, queue, ln)) + self.medium.send(SKToken(self.index, nxt, queue, ln)) def acquire(self): if self._requested: return # Tell everyone that we want the token! self._requested = True - self._rn[self.index()] += 1 - for id in self.medium().ids(): - if id != self.index(): - self.medium().send( + self._rn[self.index] += 1 + for id in self.medium.ids(): + if id != self.index: + self.medium.send( StampedMessage( - self.index(), id, Type.REQUEST, self._rn[self.index()] + self.index, id, Type.REQUEST, self._rn[self.index] ) ) @@ -443,29 +443,29 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): while True: - nxt = (self.index() + 1) % self.number_of_devices() + nxt = (self.index + 1) % self.number_of_devices if not self._participated: - self.medium().send(Vote(self.index(), nxt, self.index(), False)) + self.medium.send(Vote(self.index, nxt, self.index, False)) self._participated = True - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is not None: - if ingoing.vote() == self.index(): + if ingoing.vote() == self.index: if not ingoing.decided(): - self.medium().send(Vote(self.index(), nxt, self.index(), True)) + self.medium.send(Vote(self.index, nxt, self.index, True)) else: - self._leader = self.index() + self._leader = self.index return # this device is the new leader - elif ingoing.vote() < self.index(): + elif ingoing.vote() < self.index: continue - elif ingoing.vote() > self.index(): + elif ingoing.vote() > self.index: # forward the message - self.medium().send( - Vote(self.index(), nxt, ingoing.vote(), ingoing.decided()) + self.medium.send( + Vote(self.index, nxt, ingoing.vote(), ingoing.decided()) ) if ingoing.decided(): self._leader = ingoing.vote() return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def print_result(self): print(f"Leader seen from {self._id} is {self._leader}") @@ -479,7 +479,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._election = False def largest(self): - return self.index() == max(self.medium().ids()) + return self.index == max(self.medium.ids()) def run(self): first_round = True @@ -490,15 +490,15 @@ def run(self): new_election = False while True: - ingoing = self.medium().receive() + ingoing = self.medium.receive() if ingoing is not None: got_input = True - if ingoing.vote() < self.index(): - self.medium().send( + if ingoing.vote() < self.index: + self.medium.send( Vote( - self.index(), + self.index, ingoing.source, - self.index(), + self.index, self.largest(), ) ) @@ -520,23 +520,23 @@ def run(self): self.start_election() else: # we are the new leader, we could declare everybody else dead - for id in self.medium().ids(): - if id != self.index(): - self.medium().send( - Vote(self.index(), id, self.index(), True) + for id in self.medium.ids(): + if id != self.index: + self.medium.send( + Vote(self.index, id, self.index, True) ) - self._leader = self.index() + self._leader = self.index return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() first_round = False def start_election(self): if not self._election: self._election = True - for id in self.medium().ids(): - if id > self.index(): - self.medium().send( - Vote(self.index(), id, self.index(), self.largest()) + for id in self.medium.ids(): + if id > self.index: + self.medium.send( + Vote(self.index, id, self.index, self.largest()) ) def print_result(self): diff --git a/exercises/exercise5.py b/exercises/exercise5.py index d9e3e01..cc022c5 100644 --- a/exercises/exercise5.py +++ b/exercises/exercise5.py @@ -72,12 +72,12 @@ def __init__( def run(self): while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): self.handle_ingoing(ingoing) while len(self._outbox) > 0: msg = self._outbox.pop(0) self.send_to_all(msg) - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, MulticastMessage): @@ -86,10 +86,10 @@ def handle_ingoing(self, ingoing: MessageStub): self._application.forward(ingoing) def send_to_all(self, content): - for id in self.medium().ids(): + for id in self.medium.ids(): # we purposely send to ourselves also! - message = MulticastMessage(self.index(), id, content) - self.medium().send(message) + message = MulticastMessage(self.index, id, content) + self.medium.send(message) def send(self, message): self._outbox.append(copy.deepcopy(message)) @@ -118,14 +118,14 @@ def __init__( self._received = set() def send(self, content): - self._b_multicast.send((self.index(), self._seq_number, content)) + self._b_multicast.send((self.index, self._seq_number, content)) self._seq_number += 1 def deliver(self, message): (origin_index, seq_number, content) = message if message not in self._received: - if origin_index is not self.index(): + if origin_index is not self.index: self._b_multicast.send(message) self._received.add(message) self._application.deliver(content) @@ -187,8 +187,8 @@ def deliver(self, message): self.nack_missing(seq_numbers) def send(self, content): - self._received[(self.index(), self._seq_numbers[self.index()])] = content - self._b_multicast.send((self.index(), self._seq_numbers, content)) + self._received[(self.index, self._seq_numbers[self.index])] = content + self._b_multicast.send((self.index, self._seq_numbers, content)) self.try_deliver() def run(self): @@ -196,14 +196,14 @@ def run(self): def forward(self, message): if isinstance(message, NACK): - self.medium().send( + self.medium.send( Resend( - self.index(), + self.index, message.source, ( - self.index(), + self.index, self._seq_numbers, - self._received[(self.index(), message.seq_number())], + self._received[(self.index, message.seq_number())], ), ) ) @@ -223,7 +223,7 @@ def try_deliver(self): def nack_missing(self, n_seq: list[int]): for id in range(0, len(n_seq)): for mid in range(self._seq_numbers[id] + 1, n_seq[id]): - self.medium().send(NACK(self.index(), id, mid)) + self.medium.send(NACK(self.index, id, mid)) class Order: @@ -261,14 +261,14 @@ def __init__( self._received = {} def send(self, content): - self._b_multicast.send((self.index(), self._l_seq, content)) + self._b_multicast.send((self.index, self._l_seq, content)) self._l_seq += 1 def deliver(self, message): if not isinstance(message, Order): (sid, sseq, content) = message mid = (sid, sseq) - if self.index() == 0: + if self.index == 0: # index 0 is global sequencer self._order[mid] = self._g_seq self._b_multicast.send(Order(mid, self._g_seq)) @@ -277,7 +277,7 @@ def deliver(self, message): else: self._received[mid] = content self.try_deliver() - elif self.index() != 0: + elif self.index != 0: # index 0 is global sequencer self._order[message.message_id()] = message.order() self.try_deliver() @@ -341,8 +341,8 @@ def run(self): self._b_multicast.run() def send(self, content): - self._b_multicast.send((self.index(), self._l_seq, content)) - self._votes[(self.index(), self._l_seq)] = [] + self._b_multicast.send((self.index, self._l_seq, content)) + self._votes[(self.index, self._l_seq)] = [] self._l_seq += 1 def deliver(self, message): @@ -355,13 +355,13 @@ def deliver(self, message): self._hb_q[(sid, sseq)] = content self._p_seq = max(self._a_seq, self._p_seq) + 1 # We should technically send proposer ID for tie-breaks - self.medium().send(Vote(self.index(), sid, self._p_seq, (sid, sseq))) + self.medium.send(Vote(self.index, sid, self._p_seq, (sid, sseq))) def forward(self, message): if isinstance(message, Vote): votes = self._votes[message.message_id()] votes.append(message.order()) - if len(votes) == self.number_of_devices(): + if len(votes) == self.number_of_devices: self._b_multicast.send(Order(message.message_id(), max(votes))) else: self._application.forward(message) @@ -389,12 +389,12 @@ def __init__( else: self._application = Multicaster(index, self) self._b_multicast = BasicMulticast(index, number_of_devices, medium, self) - self._n_vect = [-1 for _ in self.medium().ids()] + self._n_vect = [-1 for _ in self.medium.ids()] self._hb_q = [] def send(self, content): - self._n_vect[self.index()] += 1 - self._b_multicast.send((self._n_vect, self.index(), content)) + self._n_vect[self.index] += 1 + self._b_multicast.send((self._n_vect, self.index, content)) def deliver(self, message): self._hb_q.append(message) @@ -413,7 +413,7 @@ def try_deliver(self): def is_next(self, vec, index): if vec[index] != self._n_vect[index] + 1: return False - for i in self.medium().ids(): + for i in self.medium.ids(): if i != index and vec[i] > self._n_vect[i]: return False return True diff --git a/exercises/exercise6.py b/exercises/exercise6.py index 217bbb8..074fc3b 100644 --- a/exercises/exercise6.py +++ b/exercises/exercise6.py @@ -49,11 +49,11 @@ def __str__(self): class FResilientConsensus(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -66,10 +66,10 @@ def __init__( def run(self): self.b_multicast(Propose({self._application.initial_value})) - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() for i in range(0, self._f): # f + 1 rounds v_p = self._v.copy() - for p in self.medium().receive_all(): + for p in self.medium.receive_all(): self._v.update(p.value()) if i != self._f - 1: self.b_multicast(Propose(v_p.difference(v_p))) @@ -77,22 +77,22 @@ def run(self): self._application.consensus_reached(min(self._v)) def b_multicast(self, message: MessageStub): - message.source = self.index() - for i in self.medium().ids(): + message.source = self.index + for i in self.medium.ids(): message.destination = i - self.medium().send(message) + self.medium.send(message) def print_result(self): - print(f"Device {self.index()} agrees on {min(self._v)}") + print(f"Device {self.index} agrees on {min(self._v)}") class SingleByzantine(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -102,7 +102,7 @@ def __init__( self._consensus = None def run(self): - if self.index() == 0: + if self.index == 0: self.run_commander() else: self.run_lieutenant() @@ -112,27 +112,27 @@ def run_commander(self): """Done!""" def run_lieutenant(self): - self.medium().wait_for_next_round() - from_commander = self.medium().receive_all() + self.medium.wait_for_next_round() + from_commander = self.medium.receive_all() assert len(from_commander) <= 1 v = None if from_commander is not None and len(from_commander) > 0: v = from_commander[0].value() - self.b_multicast(Propose((self.index(), v))) - self.medium().wait_for_next_round() - from_others = [m.value() for m in self.medium().receive_all()] + self.b_multicast(Propose((self.index, v))) + self.medium.wait_for_next_round() + from_others = [m.value() for m in self.medium.receive_all()] self._consensus = find_majority(from_others) self._application.consensus_reached(self._consensus) def b_multicast(self, message: MessageStub): - message.source = self.index() - for i in self.medium().ids(): + message.source = self.index + for i in self.medium.ids(): message.destination = i - self.medium().send(message) + self.medium.send(message) def print_result(self): - if self.index() != 0: - print(f"Device {self.index()} is done, consensus: {self._consensus}") + if self.index != 0: + print(f"Device {self.index} is done, consensus: {self._consensus}") else: print("Commander is done") @@ -152,13 +152,17 @@ def find_majority(raw: [(int, int)]): return best +def most_common(): + raise NotImplementedError() + + class King(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -167,10 +171,10 @@ def __init__( self._application = SimpleRequester() def b_multicast(self, message: MessageStub): - message.source = self.index() - for i in self.medium().ids(): + message.source = self.index + for i in self.medium.ids(): message.destination = i - self.medium().send(message) + self.medium.send(message) def run(self): # Set own v to a preferred value // f+1 phases in total @@ -191,9 +195,9 @@ def run(self): # end # end v = random.randint(1, 100) - for i in range(0, self.number_of_devices()): + for i in range(0, self.number_of_devices): self.b_multicast(message=Propose(v)) - vs = self.medium().receive_all() + vs = self.medium.receive_all() v = most_common() mult = vs.count(v) @@ -212,7 +216,7 @@ def __str__(self): class PromiseMessage(MessageStub): def __init__( - self, sender: int, destination: int, uid: int, prev_uid: int, prev_value + self, sender: int, destination: int, uid: int, prev_uid: int, prev_value ): super().__init__(sender, destination) self.uid = uid @@ -247,7 +251,7 @@ def __str__(self): class PAXOSNetwork: def __init__( - self, index: int, medium: Medium, acceptors: list[int], learners: list[int] + self, index: int, medium: Medium, acceptors: list[int], learners: list[int] ): self._acceptors = acceptors self._learners = learners @@ -288,11 +292,11 @@ def index(self): class PAXOS(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -301,7 +305,7 @@ def __init__( self._application = SimpleRequester() # assumes everyone has every role config = PAXOSNetwork( - index, self.medium(), self.medium().ids(), self.medium().ids() + index, self.medium, self.medium.ids(), self.medium.ids() ) self._proposer = Proposer(config, self._application) self._acceptor = Acceptor(config) @@ -312,9 +316,9 @@ def run(self): self._proposer.check_prepare() if self._proposer.done() and self._acceptor.done() and self._learner.done(): return - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): self.handle_ingoing(ingoing) - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, PrepareMessage): diff --git a/exercises/exercise7.py b/exercises/exercise7.py index 6e60c6f..45104ec 100644 --- a/exercises/exercise7.py +++ b/exercises/exercise7.py @@ -27,7 +27,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._election = False def largest(self): - return self.index() == max(self.medium().ids()) + return self.index == max(self.medium.ids()) def run(self): """TODO""" diff --git a/exercises/exercise8.py b/exercises/exercise8.py index e941f11..c83b76b 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -25,10 +25,10 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): # since this is a server, its job is to wait for requests (messages), then do something while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, File2ChunkReqMessage): @@ -40,19 +40,19 @@ def handle_ingoing(self, ingoing: MessageStub): self.chunks_being_allocated.append((chunk[0], ingoing.source)) return True answer = File2ChunkRspMessage( - self.index(), ingoing.source, chunk[0], chunk[1] + self.index, ingoing.source, chunk[0], chunk[1] ) - self.medium().send(answer) + self.medium.send(answer) else: if ingoing.createIfNotExists: self.do_allocate_request( ingoing.filename, ingoing.chunkindex, ingoing.source ) else: - answer = File2ChunkRspMessage(self.index(), ingoing.source, 0, []) - self.medium().send(answer) + answer = File2ChunkRspMessage(self.index, ingoing.source, 0, []) + self.medium.send(answer) elif isinstance(ingoing, QuitMessage): - print(f"I am Master {self.index()} and I am quitting") + print(f"I am Master {self.index} and I am quitting") return False elif isinstance(ingoing, AllocateChunkRspMessage): if ingoing.result != "ok": @@ -73,9 +73,9 @@ def add_chunk_to_metadata(self, chunk: tuple[int, list[int]], chunkserver: int): ] for request in requests: answer = File2ChunkRspMessage( - self.index(), request[1], chunk[0], chunk[1] + self.index, request[1], chunk[0], chunk[1] ) - self.medium().send(answer) + self.medium.send(answer) self.chunks_being_allocated.remove(request) def do_allocate_request(self, filename, chunkindex: int, requester: int): @@ -87,9 +87,9 @@ def do_allocate_request(self, filename, chunkindex: int, requester: int): chunkservers = random.sample(GfsNetwork.gfschunkserver, NUMBER_OF_REPLICAS) for i in chunkservers: message = AllocateChunkReqMessage( - self.index(), i, chunkhandle, chunkservers + self.index, i, chunkhandle, chunkservers ) - self.medium().send(message) + self.medium.send(message) def print_result(self): pass @@ -106,21 +106,21 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): # since this is a server, its job is to answer for requests (messages), then do something while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, QuitMessage): - print(f"I am Chunk Server {self.index()} and I am quitting") + print(f"I am Chunk Server {self.index} and I am quitting") return False elif isinstance(ingoing, AllocateChunkReqMessage): self.do_allocate_chunk(ingoing.chunkhandle, ingoing.chunkservers) message = AllocateChunkRspMessage( - self.index(), ingoing.source, ingoing.chunkhandle, "ok" + self.index, ingoing.source, ingoing.chunkhandle, "ok" ) - self.medium().send(message) + self.medium.send(message) elif isinstance(ingoing, RecordAppendReqMessage): # # TODO: need to implement the storage operation @@ -145,16 +145,16 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): # being a client, it listens to incoming messages, but it also does something to put the ball rolling - print(f"I am Client {self.index()}") + print(f"I am Client {self.index}") master = GfsNetwork.gfsmaster[0] - message = File2ChunkReqMessage(self.index(), master, "myfile.txt", 0, True) - self.medium().send(message) + message = File2ChunkReqMessage(self.index, master, "myfile.txt", 0, True) + self.medium.send(message) while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, File2ChunkRspMessage): @@ -165,18 +165,18 @@ def handle_ingoing(self, ingoing: MessageStub): # I select a random chunk server, and I send the append request # I do not necessarily select the primary randomserver = random.choice(ingoing.locations) - data = f"hello from client number {self.index()}\n" - self.medium().send( + data = f"hello from client number {self.index}\n" + self.medium.send( RecordAppendReqMessage( - self.index(), randomserver, ingoing.chunkhandle, data + self.index, randomserver, ingoing.chunkhandle, data ) ) elif isinstance(ingoing, RecordAppendRspMessage): # project completed, time to quit for i in GfsNetwork.gfsmaster: - self.medium().send(QuitMessage(self.index(), i)) + self.medium.send(QuitMessage(self.index, i)) for i in GfsNetwork.gfschunkserver: - self.medium().send(QuitMessage(self.index(), i)) + self.medium.send(QuitMessage(self.index, i)) return False return True diff --git a/exercises/exercise9.py b/exercises/exercise9.py index cd37c48..e156b13 100644 --- a/exercises/exercise9.py +++ b/exercises/exercise9.py @@ -23,38 +23,38 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): # since this is a server, its job is to answer for requests (messages), then do something while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, ClientJobStartMessage): # I assign ingoing.number_partitions workers as reducers, the rest as mappers # and I assign some files to each mapper self.number_partitions = ingoing.number_partitions - number_of_mappers = self.number_of_devices() - self.number_partitions - 2 + number_of_mappers = self.number_of_devices - self.number_partitions - 2 for i in range(2, self.number_partitions + 2): message = ReduceTaskMessage( - self.index(), i, i - 2, self.number_partitions, number_of_mappers + self.index, i, i - 2, self.number_partitions, number_of_mappers ) # the reducer needs to know how many mappers they are, to know when its task is completed - self.medium().send(message) + self.medium.send(message) for i in range(0, number_of_mappers): length = len(ingoing.filenames) length = 5 # TODO: comment out this line to process all files, once you think your code is ready first = int(length * i / number_of_mappers) last = int(length * (i + 1) / number_of_mappers) message = MapTaskMessage( - self.index(), + self.index, self.number_partitions + 2 + i, ingoing.filenames[first:last], self.number_partitions, ) - self.medium().send(message) + self.medium.send(message) elif isinstance(ingoing, QuitMessage): # if the client is satisfied with the work done, I can tell all workers to quit, then I can quit for w in MapReduceNetwork.workers: - self.medium().send(QuitMessage(self.index(), w)) + self.medium.send(QuitMessage(self.index, w)) return False elif isinstance(ingoing, MappingDoneMessage): # TODO: contact all reducers, telling them that a mapper has completed its job @@ -68,11 +68,15 @@ def handle_ingoing(self, ingoing: MessageStub): message = ClientJobCompletedMessage( 1, MapReduceNetwork.client_index, self.result_files ) - self.medium().send(message) + self.medium.send(message) return True def print_result(self): - print(f"Master {self.index()} quits") + print(f"Master {self.index} quits") + + +class ReducerVisitMapperMessage: + pass class MapReduceWorker(Device): @@ -139,13 +143,13 @@ def do_some_work(self): self.M_cached_results[word] = ( self.M_cached_results.get(word, 0) + map_result[word] ) - print(f"Mapper {self.index()}: file '{filename}' processed") + print(f"Mapper {self.index}: file '{filename}' processed") if self.M_files_to_process == []: self.mapper_shuffle() message = MappingDoneMessage( - self.index(), MapReduceNetwork.master_index + self.index, MapReduceNetwork.master_index ) - self.medium().send(message) + self.medium.send(message) if self.role == Role.REDUCER: # not much to do: everything is done when the master tells us about a mapper that completed its task pass @@ -153,15 +157,15 @@ def do_some_work(self): def run(self): # since this is a worker, it looks for incoming requests (messages), then it works a little while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return self.do_some_work() - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, QuitMessage): - print(f"I am Worker {self.index()} and I am quitting") + print(f"I am Worker {self.index} and I am quitting") return False elif isinstance(ingoing, MapTaskMessage): # I was assigned to be a mapper, thus I: @@ -195,7 +199,7 @@ def handle_ingoing(self, ingoing: MessageStub): return True def print_result(self): - print(f"Worker {self.index()} quits. It was a {self.role}") + print(f"Worker {self.index} quits. It was a {self.role}") class MapReduceClient(Device): @@ -213,25 +217,25 @@ def scan_for_books(self): def run(self): # being a client, it listens to incoming messages, but it also does something to put the ball rolling - print(f"I am client {self.index()}") + print(f"I am client {self.index}") books = self.scan_for_books() message = ClientJobStartMessage( - self.index(), MapReduceNetwork.master_index, books, 3 + self.index, MapReduceNetwork.master_index, books, 3 ) # TODO: experiment with different number of reducers - self.medium().send(message) + self.medium.send(message) while True: - for ingoing in self.medium().receive_all(): + for ingoing in self.medium.receive_all(): if not self.handle_ingoing(ingoing): return - self.medium().wait_for_next_round() + self.medium.wait_for_next_round() def handle_ingoing(self, ingoing: MessageStub): if isinstance(ingoing, ClientJobCompletedMessage): # I can tell the master to quit # I will print the result later, with the print_result function - self.medium().send(QuitMessage(self.index(), MapReduceNetwork.master_index)) + self.medium.send(QuitMessage(self.index, MapReduceNetwork.master_index)) self.result_files = ingoing.result_files return False return True From 8cfdfa381783edfe617a9740d3ac17da7c3853bd Mon Sep 17 00:00:00 2001 From: Simon Deleuran Laursen <69715502+Laursen79@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:56:56 +0100 Subject: [PATCH 095/106] Updated README.md --- README.md | 130 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index d6d7191..686ecb0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Exercises for Distributed Systems -This repository contains a small framework written in python for emulating *asynchronous* and *synchronous* distributed systems. +This repository contains a small framework written in python for emulating *asynchronous* and *synchronous* distributed +systems. ## Install The stepping emulator requires the following packages to run * PyQt6 @@ -11,7 +12,6 @@ These packages can be installed using `pip` as shown below: pip install --user -r requirements.txt ``` -The framework is tested under `python3.10` in Arch Linux, Ubuntu and Windows. ## General A FAQ can be found [here](https://github.com/DEIS-Tools/DistributedExercisesAAU/wiki) @@ -25,23 +25,25 @@ I will provide new templates as the course progresses. You should be able to execute your solution to exercise 1 using the following lines: ```bash -python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type sync --devices 3 -python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type async --devices 3 -python3.10 exercise_runner.py --lecture 1 --algorithm Gossip --type stepping --devices 3 +python exercise_runner.py --lecture 1 --algorithm Gossip --type sync --devices 3 +python exercise_runner.py --lecture 1 --algorithm Gossip --type async --devices 3 +python exercise_runner.py --lecture 1 --algorithm Gossip --type stepping --devices 3 ``` The first line will execute your implementation of the `Gossip` algorithm in a synchronous setting with three devices, while the second line will execute in an asynchronous setting. -The third line will execute your implementation in a synchronous setting, launching a GUI to visualize your implementation, the setting can be adjusted during execution. +The third line will execute your implementation in a synchronous setting, launching a GUI to visualize your +implementation, the setting can be adjusted during execution. For usage of the framework, see `exercises/demo.py` for a lightweight example. The example can be run with: ```bash -python3.10 exercise_runner.py --lecture 0 --algorithm PingPong --type async --devices 3 +python exercise_runner.py --lecture 0 --algorithm PingPong --type async --devices 3 ``` ## Stepping emulator -The stepping emulator can be used to run the algorithm in steps where one message is sent or received for each step in this emulator. The Stepping emulator can be controlled with the following keyboard input: +The stepping emulator can be used to run the algorithm in steps where one message is sent or received for each step in +this emulator. The Stepping emulator can be controlled with the following keyboard input: ``` space: Step a single time through messages f: Fast-forward through messages @@ -53,12 +55,13 @@ e: Toggle between sync and async emulation ## GUI The framework can also be launched with an interface by executing the following line: ``` -python3.10 exercise_runner_overlay.py +python exercise_runner_overlay.py ``` Where your solution can be executed through this GUI. ### Stepping emulator GUI -If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your algorithm, an example of the Stepping GUI is shown below: +If the stepping emulator is chosen, the framework will launch with a GUI visualising some different aspects of your +algorithm, an example of the Stepping GUI is shown below: ![](figures/stepping_gui.png) @@ -76,12 +79,15 @@ A number of persons initially know one distinct secret each. In each message, a person discloses all their secrets to the recipient. -These individuals can communicate only in pairs (no conference calls) but it is possible that different pairs of people talk concurrently. For all the tasks below you should consider the following two scenarios: +These individuals can communicate only in pairs (no conference calls) but it is possible that different pairs of people +talk concurrently. For all the tasks below you should consider the following two scenarios: - Scenario 1: a person may call any other person, thus the network is a total graph, - - Scenario 2: the persons are organized in a bi-directional circle, where the each person can only pass messages to the left and the right (use the modulo operator). + - Scenario 2: the persons are organized in a bi-directional circle, where each person can only pass messages to the + - left and the right (use the modulo operator). -In both scenarios you should use the `async` network, details of the differences between `sync` and `async` will be given in the third lecture. +In both scenarios you should use the `async` network, details of the differences between `sync` and `async` will be +given in the third lecture. Your tasks are as follows: @@ -91,18 +97,22 @@ Your tasks are as follows: - Is your solution optimal? And in what sense? ### NOTICE: -You can have several copies of the `Gossip` class, just give the class another name in the `exercise1.py` document, for instance `ImprovedGossip`. +You can have several copies of the `Gossip` class, just give the class another name in the `exercise1.py` document, for +instance `ImprovedGossip`. You should then be able to call the framework with your new class via: ```bash -python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type async --devices 3 +python exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type async --devices 3 ``` # Exercise 2 -1. Similarly to the first lecture, in the `__init__` of `RipCommunication`, create a ring topology (that is, set up who are the neighbors of each device). -2. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. _(NOTICE: To run/debug the protocol, you must first implement the network topology described in "task 2.0" below.)_ +1. Similarly to the first lecture, in the `__init__` of `RipCommunication`, create a ring topology (that is, set up who + are the neighbors of each device). +2. Implement the RIP protocol (fill in missing code in merge_tables), described in \[DS, fifth edition\] Page 115-118. + _(NOTICE: To run/debug the protocol, you must first implement the network topology described in "task 2.0" below.)_ 3. Now that you have a ring topology, consider a ring size of 10 devices. 1. How many messages are sent in total before the routing_tables of all nodes are synchronized? - 2. How can you "know" that the routing tables are complete and you can start using the network to route packets? Consider the general case of internet, and the specific case of our toy ring network. + 2. How can you "know" that the routing tables are complete and you can start using the network to route packets? + Consider the general case of internet, and the specific case of our toy ring network. 3. For the ring network, consider an approach similar to 1. ```python def routing_table_complete(self): @@ -110,7 +120,8 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return False return True ``` - Does it work? Each routing table should believe it is completed just one row short. How many times do the routing tables appear to be completed? + Does it work? Each routing table should believe it is completed just one row short. How many times do the + routing tables appear to be completed? 4. Try this other approach, which works better: 1. ```python def routing_table_complete(self): @@ -123,7 +134,8 @@ python3.10 exercise_runner.py --lecture 1 --algorithm ImprovedGossip --type asyn return True ``` Is it realistic for a real network? -4. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right after receiving the `RoutableMessage` for itself? What happens to the rest of the nodes? +4. Send a `RoutableMessage` after the routing tables are ready. Consider the termination problem. Can a node quit right + after receiving the `RoutableMessage` for itself? What happens to the rest of the nodes? 5. What happens, if a link has a negative cost? How many messages are sent before the `routing_tables` converge? # Exercise 3 @@ -136,10 +148,12 @@ implementing Suzuki-Kasami’s Mutex Algorithm. For all exercises today, you can use the `sync` network type - but most algorithms should work for `async` also. 1. Examine the algorithm - 1. Make a doodle on the blackboard/paper showing a few processes, their state, and messages exchanged. Use e.g. a sequence diagram. + 1. Make a doodle on the blackboard/paper showing a few processes, their state, and messages exchanged. Use e.g. a + sequence diagram. 2. Define the purpose of the vectors `_rn` and `_ln`. 2. Discuss the following situations - 1. Is it possible that a node receives a token request message after the corresponding request has been granted? Sketch a scenario. + 1. Is it possible that a node receives a token request message after the corresponding request has been granted? + Sketch a scenario. 2. How can a node know which nodes have ungranted requests? 3. How does the queue grow? @@ -154,7 +168,8 @@ For all exercises today, you can use the `sync` network type - but most algorith 3. Make it possible for new processes to join the ring. 5. Extracurricular exercise/challenge (only if you have nothing better to do over the weekend) - 1. Extend the mutex algorithm implementations s.t. the `do_work()` call starts an asynchronous process (e.g. a future) which later calls a `release()` method on the mutex classes. + 1. Extend the mutex algorithm implementations s.t. the `do_work()` call starts an asynchronous process (e.g. a + future) which later calls a `release()` method on the mutex classes. 2. Check that the algorithms still work, and modify where needed. 3. Submit a pull-request! @@ -185,15 +200,18 @@ For all exercises today, you can use the `sync` network type - but most algorith 1. Hint: how (and when) do you identify a tie? # Exercise 6 -1. Study the pseudo-code in the slides (on Moodle) and complete the implementation of the `King` Algorithm in `exercise6.py` +1. Study the pseudo-code in the slides (on Moodle) and complete the implementation of the `King` Algorithm in + `exercise6.py` 1. How does the algorithm deal with a Byzantine king (try f=1, the first king is byzantine)? 2. Why does the algorithm satisfy Byzantine integrity? - 3. Sketch/discuss a modification of your implementation such that the algorithm works in an `async` network, but looses its termination guarantee + 3. Sketch/discuss a modification of your implementation such that the algorithm works in an `async` network, but + looses its termination guarantee 1. What would happen with a Byzantine king? 2. What would happen with a slow king? 3. What about the combination of the above? -2. Bonus Exercise: Implement the Paxos algorithm in `exercise6.py`. See the pseudo-code on Moodle (use the video for reference when in doubt) for the two interesting roles (proposer and acceptor). +2. Bonus Exercise: Implement the Paxos algorithm in `exercise6.py`. See the pseudocode on Moodle (use the video for + reference when in doubt) for the two interesting roles (proposer and acceptor). 1. Identify messages sent/received by each role 1. Investigate `PAXOSNetwork` 2. Implement each role but the learner @@ -202,11 +220,13 @@ For all exercises today, you can use the `sync` network type - but most algorith 3. Your job is to implement the missing functionality in `Proposer` and `Acceptor`, search for "TODO" 3. Demonstrate that your code works in an `async` environment 1. Try with a large number of devices (for instance 20 or 30) - 4. Discuss how you can use Paxos in "continued consensus" where you have to agree on the order of entries in a log-file + 4. Discuss how you can use Paxos in "continued consensus" where you have to agree on the order of entries in a + log-file # Exercise 7 1. DS5ed exercises 18.5 and 18.13 -2. Sketch an architecture for the following three systems: A bulletin board (simple reddit), a bank, a version control system (e.g. GIT) +2. Sketch an architecture for the following three systems: A bulletin board (simple reddit), a bank, a version control + system (e.g. GIT) 1. Identify the system types (with respect to CAP). 2. Which replication type is suitable, and for which parts of the system? 3. If you go for a gossip solution, what is a suitable update frequency? @@ -218,67 +238,83 @@ For all exercises today, you can use the `sync` network type - but most algorith 1. Compare GFS and Chubby, and identify use cases that are better for one or the other solution. 2. Consider the code in `exercise8.py`, which sketches GFS, and complete the "append record" implementation. 1. Take a look at how the GFS master is implemented, and how it translates file + chunk index to chunk handle - 2. Sketch a design for the "passive replication" solution of the GFS. Consider how many types of messages you need, when they should be sent, etc + 2. Sketch a design for the "passive replication" solution of the GFS. Consider how many types of messages you need, + when they should be sent, etc 3. Implement your solution, starting from the handler of RecordAppendReqMessage of the GfsChunkserver class 4. Try out your solution with a larger number of clients, to have more concurrent changes to the "file" -3. BONUS Exercise: Add shadow masters to the system. Clients and chunk servers will still interact with the first master to change the file system, but the shadow master can take over if the master shuts down. +3. BONUS Exercise: Add shadow masters to the system. Clients and chunk servers will still interact with the first master + to change the file system, but the shadow master can take over if the master shuts down. NOTICE: To execute the code, issue for example: ```bash -python3.10 exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devices 7 +python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devices 7 ``` # Exercise 9 1. Consider the code in `exercise9.py`, which sketches MapReduce, and complete it. 1. Unzip the file books.zip in ex9data/books. - 2. The Master is pretty much complete. The same can be said for the client. Take a look at how the Master is supposed to interact with Mappers and Reducers. - 3. Consider how to implement the Reducers. Take into account that we are simulating "local storage in the mappers" using memory. + 2. The Master is pretty much complete. The same can be said for the client. Take a look at how the Master is supposed + to interact with Mappers and Reducers. + 3. Consider how to implement the Reducers. Take into account that we are simulating "local storage in the mappers" + using memory. 4. Look for the TODOs, and implement your solution. - 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many rounds are needed to complete the job with the "sync" simulator. -2. Compare MapReduce and Spark RDDs, and consider what it would change in terms of architecture, especially to support RDDs. + 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many + rounds are needed to complete the job with the "sync" simulator. +2. Compare MapReduce and Spark RDDs, and consider what it would change in terms of architecture, especially to support + RDDs. NOTICE: To execute the code, issue for example: ```bash -python10 exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async --devices 6 +python exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async --devices 6 ``` # Exercise 10 1. There are "exercises" (actually, questions) on the Moodle page. I suggest to start with them. -2. Consider the code in `exercise10.py`, which sketches a blockchain similar to bitcoin. Consider that transactions are just strings and we will do no check on the transactions. I had to add a very "random" termination condition. I associate a miner to each client, the code will never stop if I have an odd number of devices. - 1. Take a look at the Block and the Blockchain classes (they are NOT devices) and consider how the blockchain is supposed to grow. - 2. Design the logic for when a miner sends a blockchain (with its new block) to another miner. What do you do when you receive a new block? What if there is a fork? Can it happen? How do you manage it to preserve the "longest chain" rule? +2. Consider the code in `exercise10.py`, which sketches a blockchain similar to bitcoin. Consider that transactions are + just strings and we will do no check on the transactions. I had to add a very "random" termination condition. I + associate a miner to each client, the code will never stop if I have an odd number of devices. + 1. Take a look at the Block and the Blockchain classes (they are NOT devices) and consider how the blockchain is + supposed to grow. + 2. Design the logic for when a miner sends a blockchain (with its new block) to another miner. What do you do when + you receive a new block? What if there is a fork? Can it happen? How do you manage it to preserve the "longest + chain" rule? 3. Look for the TODOs, and implement your solution. 4. Try the code for both sync and async devices. Does it work in both cases? NOTICE: To execute the code, issue for example: ```bash -python3.10 exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type async --devices 4 +python exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type async --devices 4 ``` # Exercise 11 1. There are "exercises" on the Moodle page. I suggest to start with them. -2. Consider the code in `exercise11.py`, which sets up the finger tables for chord nodes. I have a client, connected always to the same node, which issues some PUTs. +2. Consider the code in `exercise11.py`, which sets up the finger tables for chord nodes. I have a client, connected + always to the same node, which issues some PUTs. 1. Take a look at how the finger tables are populated, but please use the slides, since the code can be quite cryptic. - 2. Design the logic for the routing process, thus: when do I end the routing process? Who should I send the message to, if I am not the destination? + 2. Design the logic for the routing process, thus: when do I end the routing process? Who should I send the message + to, if I am not the destination? 3. Look for the TODOs, and implement your solution. 4. If you have time, implement the JOIN process for device 1. NOTICE: To execute the code, issue for example: ```bash -python3.10 exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async --devices 10 +python exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async --devices 10 ``` # Exercise 12 1. There are "exercises" on the Moodle page. I suggest to start with them. -2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to implement AODV. - 1. Please note that you can `self.medium().send()` messages only to the nodes in `self.neighbors`. This simulates a wireless network with limited range. - 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply process, which should be much easier. +2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to + implement AODV. + 1. Please note that you can `self.medium().send()` messages only to the nodes in `self.neighbors`. This simulates a + wireless network with limited range. + 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply + process, which should be much easier. 3. Look for the TODOs, and implement your solution. NOTICE: To execute the code, issue for example: ```bash -python3.10 exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 +python exercise_runner.py --lecture 12 --algorithm AodvNode --type async --devices 10 ``` From 0bd974cf29a3d73b241b52bacea4bbc9719e0fee Mon Sep 17 00:00:00 2001 From: "Magnus J. Harder" <26723180+MJHC@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:44:10 +0100 Subject: [PATCH 096/106] refactored exercise_runner_overlay (#1) --- exercise_runner_overlay.py | 212 ++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 96 deletions(-) diff --git a/exercise_runner_overlay.py b/exercise_runner_overlay.py index 86eaf78..5a5b3e8 100644 --- a/exercise_runner_overlay.py +++ b/exercise_runner_overlay.py @@ -1,105 +1,125 @@ +import typing +from PyQt6 import QtCore from exercise_runner import run_exercise -from PyQt6.QtWidgets import ( - QApplication, - QWidget, - QLineEdit, - QVBoxLayout, - QPushButton, - QHBoxLayout, - QLabel, - QComboBox, -) -from PyQt6.QtGui import QIcon -from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import * +from PyQt6.QtGui import QIcon, QRegularExpressionValidator +from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal, QRegularExpression from sys import argv -app = QApplication(argv) +class Worker(QObject): + finished: pyqtSignal = pyqtSignal() + window: pyqtSignal = pyqtSignal(QObject) + def __init__(self) -> None: + super().__init__() + + def run(self, + lecture_no: int, + algorithm: str, + network_type: str, + number_of_devices: int, + ): + self.window.emit(run_exercise(lecture_no, algorithm, network_type, number_of_devices, True)) + self.finished.emit() + + +class InputArea(QFormLayout): + def __init__(self) -> None: + super().__init__() + self.__algs = [ + "PingPong", + "Gossip", + "RipCommunication", + "TokenRing", + "TOSEQMulticast", + "PAXOS", + "Bully", + "GfsNetwork", + "MapReduceNetwork", + "BlockchainNetwork", + "ChordNetwork", + "AodvNode", + ] + self.lecture = QComboBox() + self.lecture.addItems([str(i) for i in range(13) if i != 3]) + + self.type = QComboBox() + self.type.addItems(["stepping", "async", "sync"]) + + self.alg = QComboBox() + self.alg.addItems(self.__algs) + + self.device = QLineEdit(text="3") + + self.device.setValidator( + QRegularExpressionValidator( + QRegularExpression(r"[0-9]*") + )) + + self.addRow("Lecture", self.lecture) + self.addRow("Type", self.type) + self.addRow("Algorithm", self.alg) + self.addRow("Devices", self.device) + + self.lecture.currentTextChanged.connect(self.__lecture_handler) + self.__lecture_handler("0") -windows = list() - -# new -window = QWidget() -window.setWindowIcon(QIcon("icon.ico")) -window.setWindowTitle("Distributed Exercises AAU") -main = QVBoxLayout() -window.setFixedSize(600, 100) -start_button = QPushButton("Start") -main.addWidget(start_button) -input_area = QHBoxLayout() -lecture_layout = QVBoxLayout() -lecture_layout.addWidget(QLabel("Lecture"), alignment=Qt.AlignmentFlag.AlignCenter) -lecture_combobox = QComboBox() -lecture_combobox.addItems([str(i) for i in range(13) if i != 3]) -lecture_layout.addWidget(lecture_combobox) -input_area.addLayout(lecture_layout) -type_layout = QVBoxLayout() -type_layout.addWidget(QLabel("Type"), alignment=Qt.AlignmentFlag.AlignCenter) -type_combobox = QComboBox() -type_combobox.addItems(["stepping", "async", "sync"]) -type_layout.addWidget(type_combobox) -input_area.addLayout(type_layout) -algorithm_layout = QVBoxLayout() -algorithm_layout.addWidget(QLabel("Algorithm"), alignment=Qt.AlignmentFlag.AlignCenter) -algorithm_input = QLineEdit() -algorithm_input.setText("PingPong") -algorithm_layout.addWidget(algorithm_input) -input_area.addLayout(algorithm_layout) -devices_layout = QVBoxLayout() -devices_layout.addWidget(QLabel("Devices"), alignment=Qt.AlignmentFlag.AlignCenter) -devices_input = QLineEdit() -devices_input.setText("3") -devices_layout.addWidget(devices_input) -input_area.addLayout(devices_layout) -main.addLayout(input_area) -starting_exercise = False -actions: dict[str, QLineEdit | QComboBox] = { - "Lecture": lecture_combobox, - "Type": type_combobox, - "Algorithm": algorithm_input, - "Devices": devices_input, -} - - -def text_changed(text): - exercises = { - 0: "PingPong", - 1: "Gossip", - 2: "RipCommunication", - 4: "TokenRing", - 5: "TOSEQMulticast", - 6: "PAXOS", - 7: "Bully", - 8: "GfsNetwork", - 9: "MapReduceNetwork", - 10: "BlockchainNetwork", - 11: "ChordNetwork", - 12: "AodvNode", - } - lecture = int(text) - - actions["Algorithm"].setText(exercises[lecture]) - - -lecture_combobox.currentTextChanged.connect(text_changed) - - -def start_exercise(): - global starting_exercise - if not starting_exercise: - starting_exercise = True - windows.append( - run_exercise( - int(actions["Lecture"].currentText()), - actions["Algorithm"].text(), - actions["Type"].currentText(), - int(actions["Devices"].text()), - True, + def __lecture_handler(self, text: str): + self.alg.setCurrentText(self.__algs[int(text)]) + + def data(self): + return ( + int(self.lecture.currentText()), + self.alg.currentText(), + self.type.currentText(), + int(self.device.text()) + ) + +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__( + windowTitle="Distributed Exercises AAU", + windowIcon=QIcon("icon.ico") ) - ) - starting_exercise = False + self.__thread = None + self.__worker = None + self.__windows = [] + layout = QVBoxLayout() + self.__start_btn = QPushButton("Start") + + group = QGroupBox(title="Inputs") + self.__input_area = InputArea() + group.setLayout(self.__input_area) + + layout.addWidget(group) + layout.addWidget(self.__start_btn) + widget = QWidget() + widget.setLayout(layout) + self.setCentralWidget(widget) + self.__start_btn.clicked.connect(self.__start) -start_button.clicked.connect(start_exercise) -window.setLayout(main) + def __start(self): + if self.__worker is not None: + return + + self.__thread = QThread() + self.__worker = Worker() + self.__thread.started.connect(lambda: self.__worker.run(*self.__input_area.data())) + self.__worker.finished.connect(self.__thread.quit) + self.__worker.window.connect(self.__window_handler) + self.__worker.finished.connect(self.__worker.deleteLater) + self.__thread.finished.connect(self.__thread.deleteLater) + self.__thread.start() + + def __window_handler(self, window: QObject): + self.__windows.append(window) + + def show(self) -> None: + self.resize(600, 100) + return super().show() + + +app = QApplication(argv) +window = MainWindow() window.show() app.exec() From d7f445d2a44e1ea769bb10fc3b998d3be0f25a89 Mon Sep 17 00:00:00 2001 From: Simon Deleuran Laursen <69715502+Laursen79@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:00:08 +0100 Subject: [PATCH 097/106] Converted methods to properties, formatted using black and fixed ruff lint errors. --- conf.py | 11 +++---- emulators/AsyncEmulator.py | 2 +- emulators/Device.py | 6 ++-- emulators/EmulatorStub.py | 6 ++-- emulators/Medium.py | 4 ++- emulators/MessageStub.py | 1 + emulators/SteppingEmulator.py | 20 +++++++----- emulators/SyncEmulator.py | 18 +++++------ emulators/exercise_overlay.py | 9 +++--- exercises/exercise12.py | 8 ++--- exercises/exercise4.py | 46 +++++++++++---------------- exercises/exercise5.py | 8 ++--- exercises/exercise6.py | 59 +++++++++++++++++------------------ exercises/exercise7.py | 2 +- exercises/exercise8.py | 4 +-- 15 files changed, 96 insertions(+), 108 deletions(-) diff --git a/conf.py b/conf.py index 7533632..72b2fb5 100644 --- a/conf.py +++ b/conf.py @@ -1,15 +1,14 @@ import os import sys -sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(".")) extensions = [ - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", ] autodoc_modules = { - 'Device': './emulators/Device.py', - 'Medium': './emulators/Medium.py', - 'MessageStub': './emulators/MessageStub.py', + "Device": "./emulators/Device.py", + "Medium": "./emulators/Medium.py", + "MessageStub": "./emulators/MessageStub.py", } - diff --git a/emulators/AsyncEmulator.py b/emulators/AsyncEmulator.py index 18bdcc4..47c9187 100644 --- a/emulators/AsyncEmulator.py +++ b/emulators/AsyncEmulator.py @@ -33,7 +33,7 @@ def run(self): time.sleep(0.1) self._progress.acquire() # check if everyone terminated - if self.all_terminated(): + if self.all_terminated: break self._progress.release() for t in self._threads: diff --git a/emulators/Device.py b/emulators/Device.py index 15d4cd6..e93d559 100644 --- a/emulators/Device.py +++ b/emulators/Device.py @@ -78,11 +78,11 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): super().__init__(index, number_of_devices, medium) self._has_work = False + @property def has_work(self) -> bool: # The random call emulates that a concurrent process asked for the self._has_work = ( - self._has_work - or random.randint(0, self.number_of_devices) == self.index + self._has_work or random.randint(0, self.number_of_devices) == self.index ) return self._has_work @@ -99,7 +99,7 @@ def do_work(self): raise Exception("More than one concurrent worker!") self._lock.release() - assert self.has_work() + assert self.has_work amount_of_work = random.randint(1, 4) for i in range(0, amount_of_work): self.medium.wait_for_next_round() diff --git a/emulators/EmulatorStub.py b/emulators/EmulatorStub.py index 0a9e670..d68f043 100644 --- a/emulators/EmulatorStub.py +++ b/emulators/EmulatorStub.py @@ -13,7 +13,7 @@ def __init__(self, number_of_devices: int, kind): self._media = [] self._progress = threading.Lock() - for index in self.ids(): + for index in self.ids: self._media.append(Medium(index, self)) self._devices.append(kind(index, number_of_devices, self._media[-1])) self._threads.append( @@ -35,9 +35,11 @@ def _start_threads(self): for thread in cpy: thread.start() + @property def all_terminated(self) -> bool: - return all([not self._threads[x].is_alive() for x in self.ids()]) + return all([not self._threads[x].is_alive() for x in self.ids]) + @property def ids(self): return range(0, self._nids) diff --git a/emulators/Medium.py b/emulators/Medium.py index 2b48be1..4ec6d9a 100644 --- a/emulators/Medium.py +++ b/emulators/Medium.py @@ -13,6 +13,7 @@ class Medium: _id (int): The unique identifier for the medium. _emulator: The emulator object associated with the medium. """ + _id: int def __init__(self, index: int, emulator): @@ -59,6 +60,7 @@ def wait_for_next_round(self): """ self._emulator.done(self._id) + @property def ids(self): """ Get the unique identifier of the medium. @@ -66,4 +68,4 @@ def ids(self): Returns: int: The unique identifier of the medium. """ - return self._emulator.ids() + return self._emulator.ids diff --git a/emulators/MessageStub.py b/emulators/MessageStub.py index d8eb489..55d2ae9 100644 --- a/emulators/MessageStub.py +++ b/emulators/MessageStub.py @@ -6,6 +6,7 @@ class MessageStub: _source (int): The identifier of the message sender. _destination (int): The identifier of the message receiver. """ + _source: int _destination: int diff --git a/emulators/SteppingEmulator.py b/emulators/SteppingEmulator.py index 35c79bb..71e4d24 100644 --- a/emulators/SteppingEmulator.py +++ b/emulators/SteppingEmulator.py @@ -6,7 +6,11 @@ from .EmulatorStub import EmulatorStub from emulators.SyncEmulator import SyncEmulator from emulators.MessageStub import MessageStub -from threading import Barrier, Lock, BrokenBarrierError # run getpass in seperate thread +from threading import ( + Barrier, + Lock, + BrokenBarrierError, +) # run getpass in seperate thread from os import name if name == "posix": @@ -130,7 +134,7 @@ def pick(self): while self.next_message: self.pick_running = True self.step_barrier.wait() - while self.pick_running and not self.all_terminated(): + while self.pick_running and not self.all_terminated: pass sleep(0.1) @@ -146,7 +150,7 @@ def prompt(self): args = line.split(" ") match args[0]: case "": - if not self.all_terminated(): + if not self.all_terminated: self.step_barrier.wait() case "queue": if len(args) == 1: @@ -212,7 +216,7 @@ def swap_emulator(self): def run(self): self._progress.acquire() - for index in self.ids(): + for index in self.ids: self._awaits[index].acquire() self._start_threads() self._progress.release() @@ -223,7 +227,7 @@ def run(self): sleep(0.1) self._progress.acquire() # check if everyone terminated - if self.all_terminated(): + if self.all_terminated: break self._progress.release() else: @@ -231,11 +235,11 @@ def run(self): # check if everyone terminated self._progress.acquire() print(f"\r\t## {GREEN}ROUND {self._rounds}{RESET} ##") - if self.all_terminated(): + if self.all_terminated: self._progress.release() break # send messages - for index in self.ids(): + for index in self.ids: # intentionally change the order if index in self._current_round_messages: nxt = copy.deepcopy(self._current_round_messages[index]) @@ -247,7 +251,7 @@ def run(self): self._current_round_messages = {} self.reset_done() self._rounds += 1 - ids = [x for x in self.ids()] # convert to list to make it shuffleable + ids = [x for x in self.ids] # convert to list to make it shuffleable random.shuffle(ids) for index in ids: if self._awaits[index].locked(): diff --git a/emulators/SyncEmulator.py b/emulators/SyncEmulator.py index 8b8a644..caf08b7 100644 --- a/emulators/SyncEmulator.py +++ b/emulators/SyncEmulator.py @@ -21,19 +21,19 @@ class SyncEmulator(EmulatorStub): def __init__(self, number_of_devices: int, kind): super().__init__(number_of_devices, kind) self._round_lock = threading.Lock() - self._done = [False for _ in self.ids()] - self._awaits = [threading.Lock() for _ in self.ids()] + self._done = [False for _ in self.ids] + self._awaits = [threading.Lock() for _ in self.ids] self._last_round_messages: dict[int, list[MessageStub]] = {} self._current_round_messages = {} self._messages_sent = 0 self._rounds = 0 def reset_done(self): - self._done = [False for _ in self.ids()] + self._done = [False for _ in self.ids] def run(self): self._progress.acquire() - for index in self.ids(): + for index in self.ids: self._awaits[index].acquire() self._start_threads() self._progress.release() @@ -45,11 +45,11 @@ def run(self): # check if everyone terminated self._progress.acquire() print(f"\r\t## {GREEN}ROUND {self._rounds}{RESET} ##") - if self.all_terminated(): + if self.all_terminated: self._progress.release() break # send messages - for index in self.ids(): + for index in self.ids: # intentionally change the order if index in self._current_round_messages: nxt = copy.deepcopy(self._current_round_messages[index]) @@ -61,7 +61,7 @@ def run(self): self._current_round_messages = {} self.reset_done() self._rounds += 1 - ids = [x for x in self.ids()] # convert to list to make it shuffleable + ids = [x for x in self.ids] # convert to list to make it shuffleable random.shuffle(ids) for index in ids: if self._awaits[index].locked(): @@ -113,7 +113,7 @@ def done(self, index: int): self._done[index] = True # check if the thread have marked their round as done OR have ended - if all([self._done[x] or not self._threads[x].is_alive() for x in self.ids()]): + if all([self._done[x] or not self._threads[x].is_alive() for x in self.ids]): self._round_lock.release() self._progress.release() self._awaits[index].acquire() @@ -128,7 +128,7 @@ def print_statistics(self): def terminated(self, index: int): self._progress.acquire() self._done[index] = True - if all([self._done[x] or not self._threads[x].is_alive() for x in self.ids()]): + if all([self._done[x] or not self._threads[x].is_alive() for x in self.ids]): if self._round_lock.locked(): self._round_lock.release() self._progress.release() diff --git a/emulators/exercise_overlay.py b/emulators/exercise_overlay.py index 709aad6..163960e 100644 --- a/emulators/exercise_overlay.py +++ b/emulators/exercise_overlay.py @@ -257,8 +257,7 @@ def inner_execute(): self.emulator.pick_running = True self.step() while ( - self.emulator.pick_running - and not self.emulator.all_terminated() + self.emulator.pick_running and not self.emulator.all_terminated ): pass sleep(0.1) @@ -308,11 +307,11 @@ def closeEvent(self, event): table.show() def end(self): - if self.emulator.all_terminated(): + if self.emulator.all_terminated: return self.emulator.is_stepping = False self.emulator.step_barrier.wait() - while not self.emulator.all_terminated(): + while not self.emulator.all_terminated: self.set_device_color() sleep(0.1) # self.emulator.print_prompt() @@ -344,7 +343,7 @@ def set_device_color(self): def step(self): self.emulator.input_lock.acquire() - if not self.emulator.all_terminated(): + if not self.emulator.all_terminated: self.emulator.step_barrier.wait() self.emulator.input_lock.release() diff --git a/exercises/exercise12.py b/exercises/exercise12.py index c6b0a3b..8b68947 100644 --- a/exercises/exercise12.py +++ b/exercises/exercise12.py @@ -41,9 +41,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): last = random.randint(0, self.number_of_devices - 1) # I send the message to myself, so it gets routed - message = DataMessage( - self.index, self.index, last, f"Hi. I am {self.index}." - ) + message = DataMessage(self.index, self.index, last, f"Hi. I am {self.index}.") self.medium.send(message) while True: for ingoing in self.medium.receive_all(): @@ -73,9 +71,7 @@ def handle_ingoing(self, ingoing: MessageStub): ) # change self.next_hop if you implement a different data structure for the routing tables if next is not None: # I know how to reach the destination - message = DataMessage( - self.index, next, ingoing.last, ingoing.data - ) + message = DataMessage(self.index, next, ingoing.last, ingoing.data) self.medium.send(message) return True # I don't have the route to the destination. diff --git a/exercises/exercise4.py b/exercises/exercise4.py index aea84b2..04ceeed 100644 --- a/exercises/exercise4.py +++ b/exercises/exercise4.py @@ -118,7 +118,7 @@ def run(self): self._requested = False self.medium.send(MutexMessage(self.index, 0, Type.RELEASE)) - if self.has_work() and not self._requested: + if self.has_work and not self._requested: self._requested = True self.medium.send(MutexMessage(self.index, 0, Type.REQUEST)) @@ -144,7 +144,7 @@ def run(self): if ingoing.is_grant(): self._has_token = True if self._has_token: - if self.has_work(): + if self.has_work: self.do_work() nxt = (self.index + 1) % self.number_of_devices self._has_token = False @@ -183,7 +183,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): while True: - if self.has_work(): + if self.has_work: self.acquire() while True: ingoing = self.medium.receive() @@ -199,8 +199,8 @@ def run(self): def handle_request(self, message: StampedMessage): new_time = max(self._time, message.stamp()) + 1 if self._state == State.HELD or ( - self._state == State.WANTED - and (self._time, self.index) < (message.stamp(), message.source) + self._state == State.WANTED + and (self._time, self.index) < (message.stamp(), message.source) ): self._time = new_time self._waiting.append(message.source) @@ -232,16 +232,14 @@ def acquire(self): return self._state = State.WANTED self._time += 1 - for id in self.medium.ids(): + for id in self.medium.ids: if id != self.index: self.medium.send( StampedMessage(self.index, id, Type.REQUEST, self._time) ) def print_result(self): - print( - f"RA {self.index} Terminated with request? {self._state == State.WANTED}" - ) + print(f"RA {self.index} Terminated with request? {self._state == State.WANTED}") class Maekawa(WorkerDevice): @@ -274,7 +272,7 @@ def run(self): self.handle_request(ingoing) elif ingoing.is_release(): self.handle_release(ingoing) - if self.has_work(): + if self.has_work: self.acquire() self.medium.wait_for_next_round() @@ -314,9 +312,7 @@ def handle_release(self, message: MutexMessage): self._voted = False def print_result(self): - print( - f"MA {self.index} Terminated with request? {self._state == State.WANTED}" - ) + print(f"MA {self.index} Terminated with request? {self._state == State.WANTED}") class SKToken(MessageStub): @@ -355,7 +351,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): while True: self.handle_messages() - if self.has_work(): + if self.has_work: if self._token is not None: self._working = True self.do_work() @@ -391,7 +387,7 @@ def release(self): (queue, ln) = self._token ln[self.index] = self._rn[self.index] # let's generate a new queue with all devices with outstanding requests - for id in self.medium.ids(): + for id in self.medium.ids: if ln[id] + 1 == self._rn[id]: if id not in queue: queue.append(id) @@ -407,12 +403,10 @@ def acquire(self): # Tell everyone that we want the token! self._requested = True self._rn[self.index] += 1 - for id in self.medium.ids(): + for id in self.medium.ids: if id != self.index: self.medium.send( - StampedMessage( - self.index, id, Type.REQUEST, self._rn[self.index] - ) + StampedMessage(self.index, id, Type.REQUEST, self._rn[self.index]) ) @@ -479,7 +473,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._election = False def largest(self): - return self.index == max(self.medium.ids()) + return self.index == max(self.medium.ids) def run(self): first_round = True @@ -520,11 +514,9 @@ def run(self): self.start_election() else: # we are the new leader, we could declare everybody else dead - for id in self.medium.ids(): + for id in self.medium.ids: if id != self.index: - self.medium.send( - Vote(self.index, id, self.index, True) - ) + self.medium.send(Vote(self.index, id, self.index, True)) self._leader = self.index return self.medium.wait_for_next_round() @@ -533,11 +525,9 @@ def run(self): def start_election(self): if not self._election: self._election = True - for id in self.medium.ids(): + for id in self.medium.ids: if id > self.index: - self.medium.send( - Vote(self.index, id, self.index, self.largest()) - ) + self.medium.send(Vote(self.index, id, self.index, self.largest())) def print_result(self): print(f"Leader seen from {self._id} is {self._leader}") diff --git a/exercises/exercise5.py b/exercises/exercise5.py index cc022c5..c846a86 100644 --- a/exercises/exercise5.py +++ b/exercises/exercise5.py @@ -86,7 +86,7 @@ def handle_ingoing(self, ingoing: MessageStub): self._application.forward(ingoing) def send_to_all(self, content): - for id in self.medium.ids(): + for id in self.medium.ids: # we purposely send to ourselves also! message = MulticastMessage(self.index, id, content) self.medium.send(message) @@ -175,7 +175,7 @@ def __init__( else: self._application = Multicaster(index, self) self._b_multicast = BasicMulticast(index, number_of_devices, medium, self) - self._seq_numbers = [0 for _ in medium.ids()] + self._seq_numbers = [0 for _ in medium.ids] self._received = {} def deliver(self, message): @@ -389,7 +389,7 @@ def __init__( else: self._application = Multicaster(index, self) self._b_multicast = BasicMulticast(index, number_of_devices, medium, self) - self._n_vect = [-1 for _ in self.medium.ids()] + self._n_vect = [-1 for _ in self.medium.ids] self._hb_q = [] def send(self, content): @@ -413,7 +413,7 @@ def try_deliver(self): def is_next(self, vec, index): if vec[index] != self._n_vect[index] + 1: return False - for i in self.medium.ids(): + for i in self.medium.ids: if i != index and vec[i] > self._n_vect[i]: return False return True diff --git a/exercises/exercise6.py b/exercises/exercise6.py index 074fc3b..bdc373e 100644 --- a/exercises/exercise6.py +++ b/exercises/exercise6.py @@ -49,11 +49,11 @@ def __str__(self): class FResilientConsensus(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -78,7 +78,7 @@ def run(self): def b_multicast(self, message: MessageStub): message.source = self.index - for i in self.medium.ids(): + for i in self.medium.ids: message.destination = i self.medium.send(message) @@ -88,11 +88,11 @@ def print_result(self): class SingleByzantine(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -126,7 +126,7 @@ def run_lieutenant(self): def b_multicast(self, message: MessageStub): message.source = self.index - for i in self.medium.ids(): + for i in self.medium.ids: message.destination = i self.medium.send(message) @@ -158,11 +158,11 @@ def most_common(): class King(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -172,7 +172,7 @@ def __init__( def b_multicast(self, message: MessageStub): message.source = self.index - for i in self.medium.ids(): + for i in self.medium.ids: message.destination = i self.medium.send(message) @@ -197,9 +197,8 @@ def run(self): v = random.randint(1, 100) for i in range(0, self.number_of_devices): self.b_multicast(message=Propose(v)) - vs = self.medium.receive_all() - v = most_common() - mult = vs.count(v) + # vs = self.medium.receive_all() + # ... def print_result(self): pass @@ -216,7 +215,7 @@ def __str__(self): class PromiseMessage(MessageStub): def __init__( - self, sender: int, destination: int, uid: int, prev_uid: int, prev_value + self, sender: int, destination: int, uid: int, prev_uid: int, prev_value ): super().__init__(sender, destination) self.uid = uid @@ -251,7 +250,7 @@ def __str__(self): class PAXOSNetwork: def __init__( - self, index: int, medium: Medium, acceptors: list[int], learners: list[int] + self, index: int, medium: Medium, acceptors: list[int], learners: list[int] ): self._acceptors = acceptors self._learners = learners @@ -292,11 +291,11 @@ def index(self): class PAXOS(Device): def __init__( - self, - index: int, - number_of_devices: int, - medium: Medium, - application: ConsensusRequester = None, + self, + index: int, + number_of_devices: int, + medium: Medium, + application: ConsensusRequester = None, ): super().__init__(index, number_of_devices, medium) if application is not None: @@ -304,9 +303,7 @@ def __init__( else: self._application = SimpleRequester() # assumes everyone has every role - config = PAXOSNetwork( - index, self.medium, self.medium.ids(), self.medium.ids() - ) + config = PAXOSNetwork(index, self.medium, self.medium.ids, self.medium.ids) self._proposer = Proposer(config, self._application) self._acceptor = Acceptor(config) self._learner = Learner(config, self._application) diff --git a/exercises/exercise7.py b/exercises/exercise7.py index 45104ec..e722cd6 100644 --- a/exercises/exercise7.py +++ b/exercises/exercise7.py @@ -27,7 +27,7 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): self._election = False def largest(self): - return self.index == max(self.medium.ids()) + return self.index == max(self.medium.ids) def run(self): """TODO""" diff --git a/exercises/exercise8.py b/exercises/exercise8.py index c83b76b..d96e21d 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -86,9 +86,7 @@ def do_allocate_request(self, filename, chunkindex: int, requester: int): # Allocate the new chunk on "NUMBER_OF_REPLICAS" random chunkservers chunkservers = random.sample(GfsNetwork.gfschunkserver, NUMBER_OF_REPLICAS) for i in chunkservers: - message = AllocateChunkReqMessage( - self.index, i, chunkhandle, chunkservers - ) + message = AllocateChunkReqMessage(self.index, i, chunkhandle, chunkservers) self.medium.send(message) def print_result(self): From 95c4ad5f6ece266f37d0c95b36c8a941a054804f Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Thu, 24 Oct 2024 14:24:42 +0200 Subject: [PATCH 098/106] Update README.md one change in exercise 7.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dbe46d..cd6173a 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ For all exercises today, you can use the `sync` network type - but most algorith log-file # Exercise 7 -1. DS5ed exercises 18.5 and 18.13 +1. DS5ed exercises 18.10 and 18.13 2. Sketch an architecture for the following three systems: A bulletin board (simple reddit), a bank, a version control system (e.g. GIT) 1. Identify the system types (with respect to CAP). From 04c7334044f0fc3997285ad5d18da8fc0d6c03e8 Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Tue, 5 Nov 2024 15:29:19 +0100 Subject: [PATCH 099/106] Update README.md new exercise for the blockchain lecture --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index cd6173a..11c6b9f 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,17 @@ python exercise_runner.py --lecture 9 --algorithm MapReduceNetwork --type async 3. Look for the TODOs, and implement your solution. 4. Try the code for both sync and async devices. Does it work in both cases? +3. Consider the modified blockchain, we will simulate an attack that controls a percentage of the total computing power. The goal of the attacker is to create a fork of the blockchain that ignore the last 5 blocks, and eventually make the fork the dominant chain in the network. + 1. Think about how you can exploit a majority control of the network to alter the blockchain history by introducing a fork. + 1. How can the attackers ensure that their fork becomes the longest chain and eventually gets accepted by other miners? + 2. How do you deal with new transactions in both the original chain and the forked chain? + 2. Take a look at the existing BlockchainNetwork and BlockchainMiner classes. Modify these to implement your blockchain attack. + 1. Create the BlockchainAttacker, which misbehaves if _self.id() is lower than a fraction of the total number of nodes. + 2. Initiate a fork of the blockchain using the attackers. + 3. Make the attackers collude to ensure that the new fork surpasses the original. + 4. Observe how the other miners react to your fork and if they eventually switch to your chain. + 3. Play around with the number of attackers and discuss their impact on the network, can they create a new longest chain without majority control? + NOTICE: To execute the code, issue for example: ```bash python exercise_runner.py --lecture 10 --algorithm BlockchainNetwork --type async --devices 4 From 67d16cd7560026d744feb24d0c58be27957b5c64 Mon Sep 17 00:00:00 2001 From: Casper-NS Date: Fri, 8 Nov 2024 09:23:57 +0100 Subject: [PATCH 100/106] Added and improved comments for exercise 11 --- exercises/exercise11.py | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/exercises/exercise11.py b/exercises/exercise11.py index ec0065f..d738375 100644 --- a/exercises/exercise11.py +++ b/exercises/exercise11.py @@ -130,7 +130,8 @@ def __init__(self, index: int, number_of_devices: int, medium: Medium): def run(self): for i in range(pow(2, address_size)): - guid = i # I send a message to each address, to check if each node stores one data for each address it manages + guid = i + # I send a message to each address, to check if each node stores one data for each address it manages # if your chord address space gets too big, use the following code: # for i in range(pow(2, address_size)): # guid = random.randint(0, pow(2,address_size)-1) @@ -144,15 +145,14 @@ def run(self): # message = StartJoinMessage(self.index(), 1, new_chord_id) # self.medium().send(message) - time.sleep( - 10 - ) # or use some smart trick to wait for the routing process to be completed before shutting down the distributed system + time.sleep(10) + # or use some smart trick to wait for the routing process to be completed before shutting down the distributed system for i in range(1, self.number_of_devices): message = QuitMessage(self.index, i) self.medium.send(message) return - # currently, I do not manage incoming messages + # currently, I do not manage incoming messages since there are no incoming messages for the client. # while True: # for ingoing in self.medium().receive_all(): # if not self.handle_ingoing(ingoing): @@ -169,32 +169,47 @@ def print_result(self): class ChordNetwork: + # Initializes routing tables for all nodes, except for the client and disconnected node def init_routing_tables(number_of_devices: int): - N = number_of_devices - 2 # routing_data 0 will be for device 2, etc + # The first node is always the client, and the second is a disconnected node + # Therefore routing_data 0 will be for device 2, etc + N = number_of_devices - 2 + + # Populate the list of Chord IDs for the nodes while len(all_nodes) < N: new_chord_id = random.randint(0, pow(2, address_size) - 1) if new_chord_id not in all_nodes: all_nodes.append(new_chord_id) + + # Sort to determine the correct positions within the Chord ring all_nodes.sort() + # Initialize routing data for each node, including predecessor and finger table for id in range(N): + # Determine the previous node in the ring, using modulo for wrap-around prev_id = (id - 1) % N - prev = ( - prev_id + 2, - all_nodes[prev_id], - ) # Add 2 to get "message-able" device index + # Add 2 to get "message-able" device index + prev = (prev_id + 2, all_nodes[prev_id]) + + # Populate the finger table for each node new_finger_table = [] for i in range(address_size): + # Calculate the target ID for the current finger entry at_least = (all_nodes[id] + pow(2, i)) % pow(2, address_size) candidate = (id + 1) % N + + # Find the appropriate node that should be the entry for this finger while in_between(all_nodes[candidate], all_nodes[id], at_least): candidate = (candidate + 1) % N - new_finger_table.append( - (candidate + 2, all_nodes[candidate]) - ) # I added 2 to candidate since routing_data 0 is for device 2, and so on + + # I added 2 to candidate since routing_data 0 is for device 2, and so on + new_finger_table.append((candidate + 2, all_nodes[candidate])) + + # Store the routing data for the current node all_routing_data.append( RoutingData(id + 2, all_nodes[id], prev, new_finger_table) ) + print( RoutingData(id + 2, all_nodes[id], prev, new_finger_table).to_string() ) @@ -292,4 +307,4 @@ def __init__(self, sender: int, destination: int): # etc super().__init__(sender, destination) def __str__(self): - return f"StabilizeMessage MESSAGE {self.source} -> {self.destination}" + return f"StabilizeMessage MESSAGE {self.source} -> {self.destination}" \ No newline at end of file From 1386b80b1f4791d1b5e1985012ef92030bdb47e1 Mon Sep 17 00:00:00 2001 From: Casper-NS <37834568+Casper-NS@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:43:45 +0100 Subject: [PATCH 101/106] Proposed a new exercise for the 12. lecture --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11c6b9f..94beda8 100644 --- a/README.md +++ b/README.md @@ -323,13 +323,18 @@ python exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async --d # Exercise 12 1. There are "exercises" on the Moodle page. I suggest to start with them. -2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to - implement AODV. +2. Consider the code in `exercise12.py`, which creates the topology for your IoT wireless network. The goal is to implement AODV. 1. Please note that you can `self.medium().send()` messages only to the nodes in `self.neighbors`. This simulates a wireless network with limited range. 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply process, which should be much easier. 3. Look for the TODOs, and implement your solution. +3. Now consider a node going offline in the network. Simulate a temporary node failure in the AODV-based network and observe how the network handles reconnection. + 1. Discuss how you expect the network to handle the disconnection and reconnection of a node. + 2. Implement logic that simulates a temporary node failure for one of the nodes by disconnecting it from its neighbors. Let it reconnect after a short delay. + 3. When the node reconnects, it should send a Route Request message to re-establish its presence in the network. + 4. Observe the network's routing behavior during disconnection and reconnection. Does it match your expectations? + NOTICE: To execute the code, issue for example: ```bash From 055e741a3a2497c639fcdb758a3f3f5d1b7e8ff3 Mon Sep 17 00:00:00 2001 From: Casper-NS Date: Wed, 13 Nov 2024 09:34:35 +0100 Subject: [PATCH 102/106] simplified the exercise to only simulate a node failure --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 94beda8..c55c649 100644 --- a/README.md +++ b/README.md @@ -329,11 +329,10 @@ python exercise_runner.py --lecture 11 --algorithm ChordNetwork --type async --d 2. Design the logic for the Route Request process. What can you use as a broadcast id? Design also the Route Reply process, which should be much easier. 3. Look for the TODOs, and implement your solution. -3. Now consider a node going offline in the network. Simulate a temporary node failure in the AODV-based network and observe how the network handles reconnection. - 1. Discuss how you expect the network to handle the disconnection and reconnection of a node. - 2. Implement logic that simulates a temporary node failure for one of the nodes by disconnecting it from its neighbors. Let it reconnect after a short delay. - 3. When the node reconnects, it should send a Route Request message to re-establish its presence in the network. - 4. Observe the network's routing behavior during disconnection and reconnection. Does it match your expectations? +3. Now consider a node going offline in the network. Simulate a node failure in the AODV-based network and observe how the network handles it. + 1. Discuss how you expect the network to handle the disconnection of a node. + 2. Implement logic that simulates a node failure for one of the nodes by disconnecting it from its neighbours. + 3. Observe the network's routing behaviour after the node fails. Does it match your expectations? NOTICE: To execute the code, issue for example: From 86f2060544139a74690e3b6f4386c1847c9e2548 Mon Sep 17 00:00:00 2001 From: Casper-NS Date: Tue, 26 Nov 2024 12:21:45 +0100 Subject: [PATCH 103/106] Added a new exercise for lecture 8 --- README.md | 8 ++++++++ exercises/exercise8.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index c55c649..aa5821a 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,14 @@ For all exercises today, you can use the `sync` network type - but most algorith when they should be sent, etc 3. Implement your solution, starting from the handler of RecordAppendReqMessage of the GfsChunkserver class 4. Try out your solution with a larger number of clients, to have more concurrent changes to the "file" +3. Implement fault tolerance mechanisms(Heartbeat Detection and Failover Mechanism) in the system: + 1. Heartbeat Detection: The GFS master should periodically receive heartbeats from each chunkserver to monitor their health. + 1. Modify the GfsChunkserver class to send periodic heartbeat messages to the GfsMaster. + 2. Update the GfsMaster class to maintain a list of active chunkservers based on received heartbeats. + 2. Failover Mechanism: When a chunkserver fails (i.e., stops sending heartbeats), the requests should be redirected to available replicas. + 1. Introduce a simple mechanism to simulate a chunkserver failing. + 2. Update the GfsMaster to detect a missing heartbeat and mark the corresponding chunkserver as failed. + 3. Ensure the client (GfsClient) can reroute operations to other replicas if a chunkserver has failed. 3. BONUS Exercise: Add shadow masters to the system. Clients and chunk servers will still interact with the first master to change the file system, but the shadow master can take over if the master shuts down. diff --git a/exercises/exercise8.py b/exercises/exercise8.py index d96e21d..4e85ad2 100644 --- a/exercises/exercise8.py +++ b/exercises/exercise8.py @@ -273,3 +273,10 @@ def __init__(self, sender: int, destination: int, result: str): def __str__(self): return f"RECORD APPEND RESPONSE {self.source} -> {self.destination}: ({self.result})" + +class HeartbeatMessage(MessageStub): + def __init__(self, sender: int, destination: int): + super().__init__(sender, destination) + + def __str__(self): + return f"HEARTBEAT {self.source} -> {self.destination}" \ No newline at end of file From 8ffb74bb0d8ca52d5c193d3b7e8c9f12c41e06fb Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Thu, 28 Nov 2024 06:06:29 +0100 Subject: [PATCH 104/106] Update README.md ex.9 (mapreduce) modified --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa5821a..faf9823 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,11 @@ python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devi ``` # Exercise 9 -1. Consider the code in `exercise9.py`, which sketches MapReduce, and complete it. +1. How do MapReduce, Spark, and Pregel differ? Discuss the following: +   1. What are some major design and architectural differences in the three systems? +   2. What are the target use cases for the different system? When do they perform good or bad. +   3. What are trade-offs in terms of performance, scalability, and complexity for the three systems? +2. Consider the code in `exercise9.py`, which sketches MapReduce, and complete it. 1. Unzip the file books.zip in ex9data/books. 2. The Master is pretty much complete. The same can be said for the client. Take a look at how the Master is supposed to interact with Mappers and Reducers. @@ -275,8 +279,10 @@ python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devi 4. Look for the TODOs, and implement your solution. 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many rounds are needed to complete the job with the "sync" simulator. -2. Compare MapReduce and Spark RDDs, and consider what it would change in terms of architecture, especially to support - RDDs. +3. Add simulation for stragglers (slow workers) +   1. Modify the `MapReduceWorker` `run` or `do_some_work` method to occasionally add a random delay for specific workers, which can rarely be very large. +   2. Modify the `MapReduceMaster`to track the progress of each worker and reassign uncompleted tasks if a worker takes too long. +   3. Discuss the real-world relevance of stragglers in distributed systems (e.g., slow network nodes, overloaded servers) and how they affect system throughput and overall performance. NOTICE: To execute the code, issue for example: ```bash From d66806aa539bc38afa88ca4e821e7bb567d2458e Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Thu, 28 Nov 2024 06:09:40 +0100 Subject: [PATCH 105/106] Update README.md debugging ex.9 text --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index faf9823..2bb62a9 100644 --- a/README.md +++ b/README.md @@ -267,9 +267,9 @@ python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devi # Exercise 9 1. How do MapReduce, Spark, and Pregel differ? Discuss the following: -   1. What are some major design and architectural differences in the three systems? -   2. What are the target use cases for the different system? When do they perform good or bad. -   3. What are trade-offs in terms of performance, scalability, and complexity for the three systems? + 1. What are some major design and architectural differences in the three systems? + 2. What are the target use cases for the different system? When do they perform good or bad. + 3. What are trade-offs in terms of performance, scalability, and complexity for the three systems? 2. Consider the code in `exercise9.py`, which sketches MapReduce, and complete it. 1. Unzip the file books.zip in ex9data/books. 2. The Master is pretty much complete. The same can be said for the client. Take a look at how the Master is supposed @@ -279,7 +279,7 @@ python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devi 4. Look for the TODOs, and implement your solution. 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many rounds are needed to complete the job with the "sync" simulator. -3. Add simulation for stragglers (slow workers) +4. Add simulation for stragglers (slow workers)    1. Modify the `MapReduceWorker` `run` or `do_some_work` method to occasionally add a random delay for specific workers, which can rarely be very large.    2. Modify the `MapReduceMaster`to track the progress of each worker and reassign uncompleted tasks if a worker takes too long.    3. Discuss the real-world relevance of stragglers in distributed systems (e.g., slow network nodes, overloaded servers) and how they affect system throughput and overall performance. From a30cd884427d95b8bd23c2601e39353acb781b8e Mon Sep 17 00:00:00 2001 From: Michele Albano Date: Thu, 28 Nov 2024 06:12:13 +0100 Subject: [PATCH 106/106] Update README.md now ex.9 should be final --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2bb62a9..18f778b 100644 --- a/README.md +++ b/README.md @@ -277,12 +277,11 @@ python exercise_runner.py --lecture 8 --algorithm GfsNetwork --type async --devi 3. Consider how to implement the Reducers. Take into account that we are simulating "local storage in the mappers" using memory. 4. Look for the TODOs, and implement your solution. - 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many - rounds are needed to complete the job with the "sync" simulator. -4. Add simulation for stragglers (slow workers) -   1. Modify the `MapReduceWorker` `run` or `do_some_work` method to occasionally add a random delay for specific workers, which can rarely be very large. -   2. Modify the `MapReduceMaster`to track the progress of each worker and reassign uncompleted tasks if a worker takes too long. -   3. Discuss the real-world relevance of stragglers in distributed systems (e.g., slow network nodes, overloaded servers) and how they affect system throughput and overall performance. + 5. Try to change the number of mappers and reducers, and look at the "performance". In particular, look at how many rounds are needed to complete the job with the "sync" simulator. +3. Add simulation for stragglers (slow workers) + 1. Modify the `MapReduceWorker` `run` or `do_some_work` method to occasionally add a random delay for specific workers, which can rarely be very large. + 2. Modify the `MapReduceMaster`to track the progress of each worker and reassign uncompleted tasks if a worker takes too long. + 3. Discuss the real-world relevance of stragglers in distributed systems (e.g., slow network nodes, overloaded servers) and how they affect system throughput and overall performance. NOTICE: To execute the code, issue for example: ```bash