diff --git a/examples/lab2/.gitignore b/examples/lab2/.gitignore new file mode 100644 index 0000000..a81c8ee --- /dev/null +++ b/examples/lab2/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/examples/lab2/application.py b/examples/lab2/application.py new file mode 100644 index 0000000..73e2e6f --- /dev/null +++ b/examples/lab2/application.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import json +import socket +import threading +import messages +import model +import view + + +BUFFER_SIZE = 2 ** 10 + + +class Application(object): + + instance = None + + def __init__(self, args): + self.args = args + self.closing = False + self.host = None + self.port = None + self.receive_worker = None + self.sock = None + self.username = None + self.ui = view.EzChatUI(self) + Application.instance = self + + def execute(self): + if not self.ui.show(): + return + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.sock.connect((self.host, self.port)) + except (socket.error, OverflowError): + self.ui.alert(messages.ERROR, messages.CONNECTION_ERROR) + return + self.receive_worker = threading.Thread(target=self.receive) + self.receive_worker.start() + self.ui.loop() + + def receive(self): + while True: + try: + message = model.Message(**json.loads(self.receive_all())) + except (ConnectionAbortedError, ConnectionResetError): + if not self.closing: + self.ui.alert(messages.ERROR, messages.CONNECTION_ERROR) + return + self.ui.show_message(message) + + def receive_all(self): + buffer = "" + while not buffer.endswith(model.END_CHARACTER): + buffer += self.sock.recv(BUFFER_SIZE).decode(model.TARGET_ENCODING) + return buffer[:-1] + + def send(self, event=None): + message = self.ui.message.get() + if len(message) == 0: + return + self.ui.message.set("") + message = model.Message(username=self.username, message=message, quit=False) + try: + self.sock.sendall(message.marshal()) + except (ConnectionAbortedError, ConnectionResetError): + if not self.closing: + self.ui.alert(messages.ERROR, messages.CONNECTION_ERROR) + + def exit(self): + self.closing = True + try: + self.sock.sendall(model.Message(username=self.username, message="", quit=True).marshal()) + except (ConnectionResetError, ConnectionAbortedError, OSError): + print(messages.CONNECTION_ERROR) + finally: + self.sock.close() \ No newline at end of file diff --git a/examples/lab2/main.py b/examples/lab2/main.py new file mode 100644 index 0000000..4f6e325 --- /dev/null +++ b/examples/lab2/main.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +import sys +import application + + +def main(args): + app = application.Application(args) + app.execute() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/examples/lab2/messages.py b/examples/lab2/messages.py new file mode 100644 index 0000000..05b44e6 --- /dev/null +++ b/examples/lab2/messages.py @@ -0,0 +1,12 @@ + # -*- coding: utf-8 -*- + +CONNECTION_ERROR = "Could not connect to server" +ERROR = "Error" +INPUT_SERVER_HOST = "Input Server Host" +INPUT_SERVER_PORT = "Input Server Port" +INPUT_USERNAME = "Input your username" +SEND = "Send" +SERVER_HOST = "Server Host" +SERVER_PORT = "Server Port" +TITLE = "ezChat" +USERNAME = "Username" \ No newline at end of file diff --git a/examples/lab2/model.py b/examples/lab2/model.py new file mode 100644 index 0000000..e020c35 --- /dev/null +++ b/examples/lab2/model.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import json + +END_CHARACTER = "\0" +MESSAGE_PATTERN = "{username}>{message}" +TARGET_ENCODING = "utf-8" + + +class Message(object): + + def __init__(self, **kwargs): + self.username = None + self.message = None + self.quit = False + self.__dict__.update(kwargs) + + def __str__(self): + return MESSAGE_PATTERN.format(**self.__dict__) + + def marshal(self): + return (json.dumps(self.__dict__) + END_CHARACTER).encode(TARGET_ENCODING) \ No newline at end of file diff --git a/examples/lab2/server.py b/examples/lab2/server.py new file mode 100644 index 0000000..149cb7a --- /dev/null +++ b/examples/lab2/server.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +import json +import socket +import sys +import threading +import model + + +BUFFER_SIZE = 2 ** 10 +CLOSING = "Application closing..." +CONNECTION_ABORTED = "Connection aborted" +CONNECTED_PATTERN = "Client connected: {}:{}" +ERROR_ARGUMENTS = "Provide port number as the first command line argument" +ERROR_OCCURRED = "Error Occurred" +EXIT = "exit" +JOIN_PATTERN = "{username} has joined" +RUNNING = "Server is running..." +SERVER = "SERVER" +SHUTDOWN_MESSAGE = "shutdown" +TYPE_EXIT = "Type 'exit' to exit>" + + +class Server(object): + + def __init__(self, argv): + self.clients = set() + self.listen_thread = None + self.port = None + self.sock = None + self.parse_args(argv) + + def listen(self): + self.sock.listen(1) + while True: + try: + client, address = self.sock.accept() + except OSError: + print(CONNECTION_ABORTED) + return + print(CONNECTED_PATTERN.format(*address)) + self.clients.add(client) + threading.Thread(target=self.handle, args=(client,)).start() + + def handle(self, client): + while True: + try: + message = model.Message(**json.loads(self.receive(client))) + except (ConnectionAbortedError, ConnectionResetError): + print(CONNECTION_ABORTED) + return + if message.quit: + client.close() + self.clients.remove(client) + return + print(str(message)) + if SHUTDOWN_MESSAGE.lower() == message.message.lower(): + self.exit() + return + self.broadcast(message) + + def broadcast(self, message): + for client in self.clients: + client.sendall(message.marshal()) + + def receive(self, client): + buffer = "" + while not buffer.endswith(model.END_CHARACTER): + buffer += client.recv(BUFFER_SIZE).decode(model.TARGET_ENCODING) + return buffer[:-1] + + def run(self): + print(RUNNING) + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.bind(("", self.port)) + self.listen_thread = threading.Thread(target=self.listen) + self.listen_thread.start() + + def parse_args(self, argv): + if len(argv) != 2: + raise RuntimeError(ERROR_ARGUMENTS) + try: + self.port = int(argv[1]) + except ValueError: + raise RuntimeError(ERROR_ARGUMENTS) + + def exit(self): + self.sock.close() + for client in self.clients: + client.close() + print(CLOSING) + + +if __name__ == "__main__": + try: + Server(sys.argv).run() + except RuntimeError as error: + print(ERROR_OCCURRED) + print(str(error)) \ No newline at end of file diff --git a/examples/lab2/view.py b/examples/lab2/view.py new file mode 100644 index 0000000..0e6384f --- /dev/null +++ b/examples/lab2/view.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import tkinter +import messages + +from tkinter import messagebox, simpledialog + + +CLOSING_PROTOCOL = "WM_DELETE_WINDOW" +END_OF_LINE = "\n" +KEY_RETURN = "" +TEXT_STATE_DISABLED = "disabled" +TEXT_STATE_NORMAL = "normal" + + +class EzChatUI(object): + + def __init__(self, application): + self.application = application + self.gui = None + self.frame = None + self.input_field = None + self.message = None + self.message_list = None + self.scrollbar = None + self.send_button = None + + def show(self): + self.gui = tkinter.Tk() + self.gui.title(messages.TITLE) + self.fill_frame() + self.gui.protocol(CLOSING_PROTOCOL, self.on_closing) + return self.input_dialogs() + + def loop(self): + self.gui.mainloop() + + def fill_frame(self): + self.frame = tkinter.Frame(self.gui) + self.scrollbar = tkinter.Scrollbar(self.frame) + self.message_list = tkinter.Text(self.frame, state=TEXT_STATE_DISABLED) + self.scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) + self.message_list.pack(side=tkinter.LEFT, fill=tkinter.BOTH) + self.message = tkinter.StringVar() + self.frame.pack() + self.input_field = tkinter.Entry(self.gui, textvariable=self.message) + self.input_field.pack() + self.input_field.bind(KEY_RETURN, self.application.send) + self.send_button = tkinter.Button(self.gui, text=messages.SEND, command=self.application.send) + self.send_button.pack() + + def input_dialogs(self): + self.gui.lower() + self.application.username = simpledialog.askstring(messages.USERNAME, messages.INPUT_USERNAME, parent=self.gui) + if self.application.username is None: + return False + self.application.host = simpledialog.askstring(messages.SERVER_HOST, messages.INPUT_SERVER_HOST, + parent=self.gui) + if self.application.host is None: + return False + self.application.port = simpledialog.askinteger(messages.SERVER_PORT, messages.INPUT_SERVER_PORT, + parent=self.gui) + if self.application.port is None: + return False + return True + + def alert(self, title, message): + messagebox.showerror(title, message) + + def show_message(self, message): + self.message_list.configure(state=TEXT_STATE_NORMAL) + self.message_list.insert(tkinter.END, str(message) + END_OF_LINE) + self.message_list.configure(state=TEXT_STATE_DISABLED) + + def on_closing(self): + self.application.exit() + self.gui.destroy()