diff --git a/.gitmodules b/.gitmodules index 8739090..7f3769d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ [submodule "Shared/ws4py"] path = Shared/ws4py url = https://github.com/robot0nfire/ws4py.git -[submodule "Shared/bottle"] - path = Shared/bottle - url = https://github.com/bottlepy/bottle.git -[submodule "Shared/dashb0ard"] - path = Shared/dashb0ard - url = https://github.com/robot0nfire/dashb0ard.git diff --git a/.no-fl0w b/.no-fl0w new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 1f18318..a7231af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -fl0w logo +fl0w logo # fl0w fl0w is currently in the process of being refactored and revamped. diff --git a/Server/Server.py b/Server/Server.py index 5e36eb4..e058d29 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -1,35 +1,69 @@ -from Meh import Config, Option, ExceptionInConfigError -from Highway import Server, Route, DummyPipe import Logging +import Config +from .Broadcast import Broadcast + +import json import os import subprocess import re import pwd import platform +import struct from subprocess import Popen, PIPE -from _thread import start_new_thread from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication -from bottle.bottle import route, run, static_file +from Highway import Server, Route, DummyPipe -@route("/") -def index(): - return static_file("index.html", root="Shared/dashb0ard") - -@route("/static/") -def static(filepath): - return static_file(filepath, root="Shared/dashb0ard/static") - class Info(Route): def run(self, data, handler): handler.send({"routes" : list(handler.routes.keys())}, "info") +class Compile: + HAS_MAIN = re.compile(r"\w*\s*main\(\)\s*(\{|.*)$") + + @staticmethod + def is_valid_c_program(path): + for line in open(path, "r").read().split("\n"): + if Compile.HAS_MAIN.match(line): + return True + return False + + + def __init__(self, source_path, binary_path): + self.source_path = os.path.abspath(source_path) + "/" + self.binary_path = os.path.abspath(binary_path) + "/" + self.wallaby_library_avaliable = os.path.isfile("/usr/local/lib/libaurora.so") and os.path.isfile("/usr/local/lib/libdaylite.so") + if not self.wallaby_library_avaliable: + Logging.warning("Wallaby library not found. All Wallaby functions are unavaliable.") + if platform.machine() != "armv7l": + Logging.warning("Wrong processor architecture! Generated binaries will not run on Wallaby Controllers.") + + + def compile(self, path, relpath, handler=None): + if relpath.endswith(".c") and Compile.is_valid_c_program(path + relpath): + name = "-".join(relpath.split("/")).rstrip(".c") + full_path = self.binary_path + name + if not os.path.exists(full_path): + os.mkdir(full_path) + error = True + command = ["gcc", "-pipe", "-O0", "-lwallaby", "-I%s" % self.source_path, "-o", "%s" % full_path + "/botball_user_program", path + relpath] + if not self.wallaby_library_avaliable: + del command[command.index("-lwallaby")] + p = Popen(command, stdout=PIPE, stderr=PIPE) + error = False if p.wait() == 0 else True + result = "" + for line in p.communicate(): + result += line.decode() + if handler != None: + handler.send({"failed" : error, "returned" : result, "relpath" : relpath}, self.handler.reverse_routes[self]) + + class Subscribe(Route): EDITOR = 1 @@ -42,12 +76,13 @@ class Subscribe(Route): if "channel" in data: if data["channel"] in Subscribe.CHANNELS: handler.channel = data["channel"] + handler.broadcast.add(handler, handler.channel) if handler.debug: Logging.info("'%s:%i' has identified as a %s client." % (handler.address, handler.port, "Editor" if handler.channel == Subscribe.EDITOR else "Controller" if handler.channel == Subscribe.WALLABY else "Web" if handler.channel == Subscribe.WEB else - "Unknown")) + "Unknown (will not subscribe to broadcast)")) if "name" in data: handler.name = data["name"] handler.routes["peers"].push_changes(handler) @@ -55,8 +90,8 @@ class Subscribe(Route): class WhoAmI(Route): def run(self, data, handler): - handler.send({"id" : handler.id_, - "user" : pwd.getpwuid(os.getuid()).pw_name}, + handler.send({"id" : handler.id_, + "user" : pwd.getpwuid(os.getuid()).pw_name}, handler.reverse_routes[self]) @@ -84,7 +119,7 @@ class Peers(Route): for channel in channels: self.subscribe(handler, channel) # Send on channels and on subscribe - self.send_connected_peers(handler, channels) + self.send_connected_peers(handler, channels) def send_connected_peers(self, handler, channels): @@ -134,8 +169,9 @@ class Peers(Route): class Handler(Server): - def setup(self, routes, websockets, debug=False): + def setup(self, routes, broadcast, websockets, debug=False): super().setup(routes, websockets, debug=debug) + self.broadcast = broadcast self.channel = None self.name = "Unknown" @@ -143,9 +179,11 @@ class Handler(Server): def ready(self): if self.debug: Logging.info("Handler for '%s:%d' ready." % (self.address, self.port)) - + def closed(self, code, reason): + if self.channel != None: + self.broadcast.remove(self, self.channel) if self.debug: Logging.info("'%s:%d' disconnected." % (self.address, self.port)) self.routes["peers"].push_changes(self) @@ -162,31 +200,34 @@ def folder_validator(folder): CONFIG_PATH = "server.cfg" - -config = Config() -config.add(Option("fl0w_address", ("127.0.0.1", 3077))) -config.add(Option("behem0th_address", ("127.0.0.1", 3078))) -config.add(Option("dashb0ard_address", ("127.0.0.1", 8080))) -config.add(Option("debug", True, validator=lambda x: True if True or False else False)) -config.add(Option("path", "Content", validator=folder_validator)) +config = Config.Config() +config.add(Config.Option("server_address", ("127.0.0.1", 3077))) +config.add(Config.Option("debug", True, validator=lambda x: True if True or False else False)) +config.add(Config.Option("binary_path", "Binaries", validator=folder_validator)) +config.add(Config.Option("source_path", "Source", validator=folder_validator)) try: - config = config.load(CONFIG_PATH) -except (IOError, ExceptionInConfigError): - config.dump(CONFIG_PATH) - config = config.load(CONFIG_PATH) + config = config.read_from_file(CONFIG_PATH) +except FileNotFoundError: + config.write_to_file(CONFIG_PATH) + config = config.read_from_file(CONFIG_PATH) -#compile = Compile(config.source_path, config.binary_path) +broadcast = Broadcast() +# Populating broadcast channels with all channels defined in Subscribe.Channels +for channel in Subscribe.CHANNELS: + broadcast.add_channel(channel) + +compile = Compile(config.source_path, config.binary_path) -server = make_server(config.fl0w_address[0], config.fl0w_address[1], +server = make_server(config.server_address[0], config.server_address[1], server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=None) server.initialize_websockets_manager() server.set_app(WebSocketWSGIApplication(handler_cls=Handler, - handler_args={"debug" : config.debug, + handler_args={"debug" : config.debug, "broadcast" : broadcast, "websockets" : server.manager.websockets, "routes" : {"info" : Info(), "whoami" : WhoAmI(), @@ -196,21 +237,16 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "peers" : Peers(), "sensor" : DummyPipe(), "identify" : DummyPipe(), + "list_programs" : DummyPipe(), + "run_program" : DummyPipe(), "std_stream" : DummyPipe(), "stop_programs" : DummyPipe(), "shutdown" : DummyPipe(), - "reboot" : DummyPipe(), - "output" : DummyPipe()}})) + "reboot" : DummyPipe()}})) try: Logging.header("Server loop starting.") - start_new_thread(run, (), {"host" : config.dashb0ard_address[0], - "port" : config.dashb0ard_address[1], "quiet" : True}) - Logging.info("Starting dashb0ard on 'http://%s:%d'" % (config.dashb0ard_address[0], - config.dashb0ard_address[1])) - Logging.info("Starting fl0w on 'ws://%s:%d'" % (config.fl0w_address[0], - config.fl0w_address[1])) server.serve_forever() except KeyboardInterrupt: Logging.header("Gracefully shutting down server.") diff --git a/Shared/Disc0very.py b/Shared/Disc0very.py new file mode 100644 index 0000000..ed3aff9 --- /dev/null +++ b/Shared/Disc0very.py @@ -0,0 +1,97 @@ +from socket import * +from random import getrandbits +from random import choice +from time import time +from time import sleep +import Utils +import Logging +from _thread import start_new_thread + + +HEY = "HEY".encode() +NAY = "NAY".encode() +WHAT = "WHAT".encode() + + +class MissingInterfaceError(TypeError): + def __init__(self): + super(TypeError, self).__init__("platform requires interface parameter.") + + +class Disc0very: + def __init__(self, port, interface=None, max_peers=32): + self.port = port + self.sock = socket(AF_INET, SOCK_DGRAM) + self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + self.sock.bind(("", self.port)) + self.ip_address = Utils.get_ip_address(interface) + self.discovering = False + self.enlisting = False + + + + # Not thread-safe if ran in parallel with __enlist + def discover(self, time_out=1): + self.sock.setblocking(False) + end_time = time() + time_out + servers = [] + self.sock.sendto(HEY, ('255.255.255.255', self.port)) + while time() < end_time: + try: + data, address = self.sock.recvfrom(512) + address = address[0] + # If enlisted peer responds with ip address + if data not in (HEY, NAY) and address != self.ip_address and not data in servers: + servers.append(data) + # If another peer is currently discovering + elif data == HEY and address != self.ip_address: + self.sock.sendto(NAY, ('255.255.255.255', self.port)) + return self.discover(time_out=time_out) + # If another peer gave up + elif data == NAY and address != self.ip_address: + return None + except BlockingIOError: + sleep((choice(range(1, 10)) / 2) / 10) + if len(servers) == 0: + return None + elif len(servers) > 1: + self.sock.sendto(WHAT, ('255.255.255.255', self.port)) + else: + return servers[0] + + + def enlist(self, interface, blocking=False): + if blocking: + self.__enlist(interface) + else: + start_new_thread(self.__enlist, (interface, )) + + + # Not thread-safe if ran in parallel with discover + # Interface should always be provided when using a Wallaby + # because wlan0 and wlan1 have an IP address assigned + def __enlist(self, interface=None): + self.sock.setblocking(True) + data = "" + while True: + try: + data, address = self.sock.recvfrom(512) + except BlockingIOError: + sleep(0.1) + if data == HEY: + self.sock.sendto(self.ip_address.encode(), ('255.255.255.255', self.port)) + elif data == WHAT: + Logging.error("Apparently more than one server is running. " + "Investigating...") + # Discover and if other server is found shutdown + + +if __name__ == "__main__": + disc0very = Disc0very(3077) + server = disc0very.discover() + if not server: + print("enlisting") + disc0very.enlist(None, blocking=True) + else: + print(server) \ No newline at end of file diff --git a/Shared/Highway.py b/Shared/Highway.py index 90c679f..ffc8233 100644 --- a/Shared/Highway.py +++ b/Shared/Highway.py @@ -330,10 +330,10 @@ class Shared: data_repr = str(data).replace("\n", " ") if len(data_repr) > 80: data_repr = data_repr[:80] + "..." - Logging.info("Received '%s' on route '%s': %s (%s:%d)" % ( - type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict", - route, data_repr, self.address, - self.port)) + Logging.info("Received '%s' on route '%s': %s (%s:%d)" % ( + type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict", + route, data_repr, self.address, + self.port)) try: route = self.routes[route] except: diff --git a/Shared/Logging.py b/Shared/Logging.py index 52d0ab9..6bd0e4d 100644 --- a/Shared/Logging.py +++ b/Shared/Logging.py @@ -112,6 +112,7 @@ def success(message): if __name__ == "__main__": + import sys info("Hi!", color=UNDERLINE+BACKGROUND_GREEN+RED) header("This is a header") warning("This is a warning") # > stderr diff --git a/Shared/Meh.py b/Shared/Meh.py index 5d0611d..e704158 100644 --- a/Shared/Meh.py +++ b/Shared/Meh.py @@ -4,12 +4,12 @@ from sys import version class OptionDuplicateError(IndexError): def __init__(self, name): - super(IndexError, self).__init__("'%s' already exists" % name) + super(IndexError, self).__init__("'%s' already exists" % name) class OptionNotFoundError(IndexError): def __init__(self, name): - super(IndexError, self).__init__("'%s' does not exist" % name) + super(IndexError, self).__init__("'%s' does not exist" % name) class NameMustBeStringError(Exception): @@ -25,8 +25,8 @@ class ValidationError(Exception): class UnsupportedTypeError(TypeError): def __init__(self): super(TypeError, self).__init__("only list, tuple, dict, bytes, " - "str, float, complex, int and bool are supported (same " - "thing applies to list, dict and tuple contents)") + "str, float, complex, int and bool are supported (same " + "thing applies to list, dict and tuple contents)") class ExceptionInConfigError(Exception): @@ -37,7 +37,7 @@ class ExceptionInConfigError(Exception): """ def __init__(self, error): self.error = error - super(Exception, self).__init__("error occured during config import (%s)" % + super(Exception, self).__init__("error occured during config import (%s)" % error.__class__.__name__) def validate_value(value): @@ -69,9 +69,9 @@ class _EditableConfig: """ Automatically created proxy class. - HINTS: + HINTS: _values: All options with their respective values - _options: All Option instances that were originally added to + _options: All Option instances that were originally added to the Config instance _file: Path to the config file _validation_failed: Optional function that's called on a validation error @@ -115,7 +115,7 @@ class _EditableConfig: else: raise UnsupportedTypeError() if dump_required: - if self._debug: + if self._debug: print("Rewriting config because the value of '%s' changed." % name) open(self._file, "w").write(self._dumps()) @@ -124,7 +124,7 @@ class _EditableConfig: out = "" for option in self._options: value = make_value(self._values[option.name]) - out += "%s = %s%s\n" % (option.name, value, + out += "%s = %s%s\n" % (option.name, value, (" # %s" % option.comment) if option.comment else "") return out.rstrip("\n") @@ -134,7 +134,7 @@ class _EditableConfig: class Option: - + def __init__(self, name, default_value, validator=None, comment=""): if not type(name) is str: raise NameMustBeStringError() @@ -154,7 +154,7 @@ class Option: def name(self, value): if name.startswith("__"): raise InvalidOptionName() - self._name = value + self._name = value def __eq__(self, other): @@ -163,17 +163,17 @@ class Option: def __repr__(self): - return "%s = %s" % (self.name, str(self.default_value)) + return "%s = %s" % (self.name, str(self.default_value)) class Config: """ The central element of Meh (TM). - IN: + IN: options=[] (type: list, hint: a list of options) - validation_failed=None (type: function, hint: function accepting two + validation_failed=None (type: function, hint: function accepting two parameters that's called when a validation fails) - + Example usage: from Meh import Config, Option config = Config() @@ -235,7 +235,7 @@ class Config: # Retrieve the option value value = getattr(config, option.name) # Make sure validator passes - if option.validator != None: + if option.validator != None: # If validation doesn't pass if not option.validator(value): # Resort to default value @@ -252,7 +252,7 @@ class Config: values[option.name] = value if option_missing: self.dump(file) - return _EditableConfig(values, self.options, file, + return _EditableConfig(values, self.options, file, validation_failed=self.validation_failed, debug=self.debug) else: error = "'%s' not found" % file @@ -279,7 +279,7 @@ class Config: """ Adds an option to a Config instance IN: option (type: Option) - """ + """ if option.__class__ == Option: for _option in self.options: if option.name == _option.name: @@ -319,7 +319,7 @@ class Config: out = "" for option in self.options: value = make_value(option.default_value) - out += "%s = %s%s\n" % (option.name, value, + out += "%s = %s%s\n" % (option.name, value, (" # %s" % option.comment) if option.comment else "") return out.rstrip("\n") diff --git a/Shared/Utils.py b/Shared/Utils.py index 0972417..bae4483 100644 --- a/Shared/Utils.py +++ b/Shared/Utils.py @@ -6,8 +6,6 @@ import struct import socket import fcntl import subprocess -import urllib - class HostnameNotChangedError(PermissionError): def __init__(self): @@ -15,11 +13,11 @@ class HostnameNotChangedError(PermissionError): class NotSupportedOnPlatform(OSError): def __init__(self): - super(OSError, self).__init__("feature not avaliable on OS") + super(OSError, self).__init__("feature not avaliable on OS") class PlaybackFailure(OSError): def __init__(self): - super(OSError, self).__init__("audio playback failed") + super(OSError, self).__init__("audio playback failed") def capture_trace(): @@ -40,7 +38,7 @@ def is_darwin(): def is_windows(): - return platform.uname().system == "Windows" + return platform.uname().system == "Windows" def set_hostname(hostname): @@ -84,10 +82,6 @@ def get_ip_address(ifname=None): return ip_address -def get_ip_from_url(url): - return urllib.parse.urlsplit(url).netloc.split(':')[0] - - def play_sound(path): if is_linux() or is_darwin(): try: @@ -95,4 +89,4 @@ def play_sound(path): except subprocess.CalledProcessError as e: raise PlaybackFailure() else: - raise NotSupportedOnPlatform() + raise NotSupportedOnPlatform() \ No newline at end of file diff --git a/Shared/bottle b/Shared/bottle deleted file mode 160000 index 5a6fc77..0000000 --- a/Shared/bottle +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5a6fc77c9c57501b08e5b4f9161ae07d277effa9 diff --git a/Shared/dashb0ard b/Shared/dashb0ard deleted file mode 160000 index cca5173..0000000 --- a/Shared/dashb0ard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cca517355923990423a2c744bf2bc81080090ff3 diff --git a/Sublime/fl0w/CompileHighlight.sublime-syntax b/Sublime/fl0w/CompileHighlight.sublime-syntax new file mode 100644 index 0000000..3e997dc --- /dev/null +++ b/Sublime/fl0w/CompileHighlight.sublime-syntax @@ -0,0 +1,11 @@ +%YAML 1.2 +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: Compile Highlight +file_extensions: [] +hidden: true +scope: source.inspect +contexts: + main: + - match: \b(warning|error)\b + scope: keyword.control.c \ No newline at end of file diff --git a/Sublime/fl0w/Default (Linux).sublime-keymap b/Sublime/fl0w/Default (Linux).sublime-keymap new file mode 100644 index 0000000..00b52dc --- /dev/null +++ b/Sublime/fl0w/Default (Linux).sublime-keymap @@ -0,0 +1,14 @@ +[ + { + "keys" : ["f8"], + "command" : "run" + }, + { + "keys" : ["f9"], + "command" : "stop" + }, + { + "keys" : ["f10"], + "command" : "sensor" + } +] \ No newline at end of file diff --git a/Sublime/fl0w/Default (OSX).sublime-keymap b/Sublime/fl0w/Default (OSX).sublime-keymap new file mode 100644 index 0000000..00b52dc --- /dev/null +++ b/Sublime/fl0w/Default (OSX).sublime-keymap @@ -0,0 +1,14 @@ +[ + { + "keys" : ["f8"], + "command" : "run" + }, + { + "keys" : ["f9"], + "command" : "stop" + }, + { + "keys" : ["f10"], + "command" : "sensor" + } +] \ No newline at end of file diff --git a/Sublime/fl0w/Default (Windows).sublime-keymap b/Sublime/fl0w/Default (Windows).sublime-keymap new file mode 100644 index 0000000..00b52dc --- /dev/null +++ b/Sublime/fl0w/Default (Windows).sublime-keymap @@ -0,0 +1,14 @@ +[ + { + "keys" : ["f8"], + "command" : "run" + }, + { + "keys" : ["f9"], + "command" : "stop" + }, + { + "keys" : ["f10"], + "command" : "sensor" + } +] \ No newline at end of file diff --git a/Sublime/fl0w/Shared b/Sublime/fl0w/Shared new file mode 120000 index 0000000..8c1ab6a --- /dev/null +++ b/Sublime/fl0w/Shared @@ -0,0 +1 @@ +../../Shared \ No newline at end of file diff --git a/Sublime/fl0w/SublimeMenu.py b/Sublime/fl0w/SublimeMenu.py new file mode 100644 index 0000000..8ff2c59 --- /dev/null +++ b/Sublime/fl0w/SublimeMenu.py @@ -0,0 +1,140 @@ +FUNCTION = type(lambda: 1) + +class Input: + def __init__(self, caption, initial_text="", on_done=None, on_change=None, + on_cancel=None, kwargs={}): + self.caption = caption + self.initial_text = initial_text + self.on_done = on_done + self.on_change = on_change + self.on_cancel = on_cancel + self.kwargs = kwargs + + + def wrapped_on_done(self, input_): + if not self.on_done == None: + self.on_done(input_, **self.kwargs) + + + def wrapped_on_change(self, input_): + if not self.on_change == None: + self.on_change(input_, **self.kwargs) + + + def wrapped_on_cancel(self): + if not self.on_cancel == None: + self.on_cancel(**self.kwargs) + + + def invoke(self, window): + window.show_input_panel(self.caption, self.initial_text, + self.wrapped_on_done, self.wrapped_on_change, + self.wrapped_on_cancel) + + +class Entry: + def __init__(self, name, description="", action=None, kwargs={}, + sub_menu=None, input=None): + self.name = name + self.description = description + self.action = action + self.kwargs = kwargs + self.sub_menu = sub_menu + self.input = input + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class Menu: + def __init__(self, selected_index=-1, on_highlight=None, subtitles=True): + self.selected_index = selected_index + self.on_highlight = on_highlight + self.subtitles = subtitles + self.entries = {} + self.window = None + self.back = None + + def invoke(self, window, back=None): + self.window = window + self.back = back + entries = self.menu_entries + if back: + entries.insert(0, ["Back", + "Back to previous menu"] if self.subtitles else ["Back"]) + window.show_quick_panel(entries, self._action, + flags=0, selected_index=self.selected_index, + on_highlight=self.on_highlight) + + def _action(self, entry_id): + if entry_id != -1: + if self.back: + if entry_id != 0: + entry = self.entries[entry_id - 1] + else: + self.back.invoke(self.window, back=self.back.back) + return + else: + entry = self.entries[entry_id] + if entry.action != None: + entry.action(**entry.kwargs) + if entry.input != None: + entry.input.invoke(self.window) + if type(entry.sub_menu) is FUNCTION: + entry.sub_menu(entry).invoke(self.window, back=self) + elif entry.sub_menu != None: + entry.sub_menu.invoke(self.window, back=self) + + + @property + def menu_entries(self): + entries = [] + for entry_id in self.entries: + if self.subtitles: + entries.append([self.entries[entry_id].name, self.entries[entry_id].description]) + else: + entries.append([self.entries[entry_id].name]) + return entries + + + def __add__(self, other): + try: + self.add(other) + except TypeError: + return NotImplemented + return self + + + def __sub__(self, other): + try: + self.remove(other) + except TypeError: + return NotImplemented + return self + + + def add(self, entry): + if entry.__class__ == Entry: + if len(self.entries) > 0: + entry_id = tuple(self.entries.keys())[-1] + 1 + else: + entry_id = 0 + self.entries[entry_id] = entry + else: + raise TypeError("invalid type supplied") + + + def remove(self, entry): + if entry.__class__ == Entry: + if entry in self.entries.values(): + found_entry_id = None + for entry_id in self.entries: + if self.entries[entry_id] == entry: + found_entry_id = entry_id + if found_entry_id != None: + del self.entries[entry_id] + else: + raise TypeError("invalid type supplied") + + def clear(self): + self.entries = {} diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py new file mode 100644 index 0000000..fa2ea18 --- /dev/null +++ b/Sublime/fl0w/fl0w.py @@ -0,0 +1,749 @@ +from sys import path +import os +from time import strftime +from functools import partial +import re + +fl0w_path = os.path.dirname(os.path.realpath(__file__)) +shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/" +if fl0w_path not in path: + path.append(fl0w_path) +if shared_path not in path: + path.append(shared_path) + + +import sublime +import sublime_plugin + +from Highway import Client, Route, Pipe, DummyPipe +from Utils import get_hostname + +from SublimeMenu import * +import Logging + +import webbrowser +import threading +from time import sleep +import os + +CHANNEL = 1 +FL0W_STATUS = "fl0w" + +def plugin_unloaded(): + for window in windows: + if hasattr(window, "fl0w") and window.fl0w.connected: + window.fl0w.invoke_disconnect() + for sensor_type in ("analog", "digital"): + window.active_view().erase_phantoms(sensor_type) + + +PARENTHESES_REGEX = re.compile("\((.*?)\)") +STYLE_OPEN = "" +STYLE_CLOSE = "" + +ERROR_OPEN = "" +ERROR_CLOSE = "" + +windows = [] +views = [] +sensor_phantoms = [] + +def set_status(status, window): + window.active_view().set_status(FL0W_STATUS, + "fl0w: %s" % status) + +class Target: + def __init__(self, id_, name): + self.id = id_ + self.name = name + + +class Fl0wClient(Client): + def setup(self, routes, fl0w, debug=False): + super().setup(routes, debug=debug) + self.fl0w = fl0w + + + def ready(self): + self.fl0w.connected = True + if self.fl0w.debug: + Logging.info("Connection ready!") + # Enlist on editor channel + self.send({"channel" : CHANNEL, "name" : get_hostname()}, "subscribe") + # Subscribe to controller channel + self.send({"subscribe" : [2]}, "peers") + + + def closed(self, code, reason): + self.fl0w.invoke_disconnect() + if self.fl0w.debug: + Logging.info("Connection closed: %s (%s)" % (reason, code)) + + + def peer_unavaliable(self, peer): + sublime.error_message("The specifed controller is not connected anymore.") + if self.fl0w.target.id == peer: + self.fl0w.target = None + + + class Info(Route): + def run(self, data, handler): + info = "" + for key in data: + info += "%s: %s\n" % (key.capitalize(), ", ".join(data[key])) + sublime.message_dialog(info) + handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu) + + + class Sensor(Pipe): + def run(self, data, peer, handler): + handler.fl0w.subscriptions_lock.acquire() + for sensor_phantom in handler.fl0w.subscriptions: + sensor_phantom.update_sensor_values(data) + handler.fl0w.subscriptions_lock.release() + + + class Peers(Route): + def start(self, handler): + self.selected_action_menu = None + + def run(self, data, handler): + handler.fl0w.controller_menu.clear() + if handler.fl0w.target != None: + if not handler.fl0w.target.id in data: + handler.fl0w.target = None + for id_ in data: + action_menu = Menu() + power_menu = Menu() + utilities_menu = Menu() + action_menu.id_ = id_ + action_menu.name = data[id_]["name"] + action_menu += Entry("Set Target", + "Set controller as target for program execution and sensor readouts", + action=partial(self.set_target, + handler, id_, data[id_]["name"])) + action_menu += Entry("Run program", + "Run a botball program on the controller", + action=partial(handler.pipe, None, "list_programs", id_)) + action_menu += Entry("Stop programs", + "Stop all currently running botball programs", + action=partial(handler.pipe, None, "stop_programs", id_)) + utilities_menu += Entry("Set Name", + "Sets the hostname of the selected controller", + action=partial(lambda handler, id_: Input("New Hostname:", + initial_text=data[id_]["name"], + on_done=lambda hostname: handler.pipe( + {"set" : hostname}, + "hostname", id_)).invoke(handler.fl0w.window), handler, id_)) + utilities_menu += Entry("Processes", + "Lists processes currently running on controller", + action=partial(handler.pipe, None, "processes", id_)) + utilities_menu += Entry("Identify", + "Plays an identification sound on the controller.", + action=partial(handler.pipe, None, "identify", id_)) + action_menu += Entry("Utilities", "Stuff you might need but probably won't", + sub_menu=utilities_menu) + power_menu += Entry("Shutdown", + "Shutdown the controller", + action=partial(handler.pipe, None, "shutdown", id_)) + power_menu += Entry("Reboot", + "Reboot the controller", + action=partial(handler.pipe, None, "reboot", id_)) + action_menu += Entry("Power", "Power related actions", sub_menu=power_menu) + action_menu.back = handler.fl0w.controller_menu + handler.fl0w.controller_menu += Entry(data[id_]["name"], id_, sub_menu=action_menu, + action=self.set_selected_action_menu, + kwargs={"selected_action_menu" : action_menu}) + + + def set_target(self, handler, peer, name): + handler.fl0w.target = Target(peer, name) + if handler.fl0w.debug: + set_status("Target: %s" % peer, handler.fl0w.window) + + + def set_selected_action_menu(self, selected_action_menu): + self.selected_action_menu = selected_action_menu + + + class Processes(Pipe): + def run(self, data, peer, handler): + view = handler.fl0w.window.new_file() + view.set_name("Processes") + view.settings().set("draw_indent_guides", False) + for line in data: + view.run_command("append", {"characters": line + "\n"}) + view.set_read_only(True) + + + class ListPrograms(Pipe): + def run(self, data, peer, handler): + program_menu = Menu(subtitles=False) + for program in data: + program_menu += Entry(program, + action=partial(self.run_program, + handler, handler.routes["peers"].selected_action_menu.id_, + program)) + program_menu.invoke(handler.fl0w.window, + back=handler.routes["peers"].selected_action_menu) + + def run_program(self, handler, id_, program): + handler.pipe(program, "run_program", id_) + + + class RunProgram(Pipe): + PROGRAM_NOT_FOUND = 1 + + def run(self, data, peer, handler): + if data == self.__class__.PROGRAM_NOT_FOUND: + sublime.error_message("Program not found.") + + + class StopPrograms(Pipe): + NO_PROGRAMS_RUNNING = 1 + + def run(self, data, peer, handler): + if data == self.__class__.NO_PROGRAMS_RUNNING: + sublime.error_message("No programs running.") + + + class StdStream(Pipe): + def start(self, handler): + self.output_panels = {} + + self.lock = threading.RLock() + self.buffer = {} + + self.handler = None + + self.fetcher = self.Fetcher(self.buffer, self.write_to_panel, + self.lock) + self.fetcher.start() + + + def create_output_panel(self, window, peer): + view = window.create_output_panel(peer) + view.settings().set("draw_white_space", False) + view.settings().set("draw_indent_guides", False) + view.settings().set("gutter", False) + view.settings().set("line_numbers", False) + view.set_read_only(True) + return view + + + def run(self, data, peer, handler): + self.handler = handler + if type(data) is str: + self.lock.acquire() + # try/except is faster than an explicit if as long as the + # condition is not met + try: + self.buffer[peer].append(data) + except KeyError: + self.buffer[peer] = [] + self.create_output_panel(handler.fl0w.window, peer) + self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer) + self.buffer[peer].append(data) + self.lock.release() + # Meta info comes in so infrequently that the conditional logic would + # slow down the regular output streaming + elif type(data) is dict: + meta_text = "" + if "exit_code" in data: + meta_text += "Program finished with exit code: %d\n" % data["exit_code"] + self.lock.acquire() + # try/except is faster than an explicit if as long as the + # condition is not met + # function call is also slower + try: + self.buffer[peer].append(meta_text) + except KeyError: + self.buffer[peer] = [] + self.create_output_panel(handler.fl0w.window, peer) + self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer) + self.buffer[peer].append(meta_text) + self.lock.release() + + + + def write_to_panel(self, text, peer): + self.output_panels[peer].set_read_only(False) + self.output_panels[peer].run_command("append", {"characters": text, "scroll_to_end" : True}) + self.output_panels[peer].set_read_only(True) + self.handler.fl0w.window.run_command("show_panel", {"panel": "output.%s" % peer}) + + + # Sublime gets quite overwhelmed when faced with typical + # "while (1) { printf(...)}" output. + # That's why instead of directly writing to the view all received text + # is bundled together after a fixed period of time. + class Fetcher(threading.Thread): + def __init__(self, buffer, write_to_panel, lock, push_rate=0.2): + threading.Thread.__init__(self) + self.buffer = buffer + self.write_to_panel = write_to_panel + self.lock = lock + self.push_rate = push_rate + self.daemon = True + + def run(self): + while True: + self.lock.acquire() + for peer in self.buffer: + if len(self.buffer[peer]) > 0: + self.write_to_panel( + "".join(self.buffer[peer]), + peer) + self.buffer[peer] = [] + self.lock.release() + sleep(self.push_rate) + + +class Fl0w: + def __init__(self, window, debug=False): + self.settings = sublime.load_settings("fl0w.sublime-settings") + self.window = window + self.folder = window.folders()[0] + if self.folder != "/": + self.folder = self.folder + "/" + + self.connected = False + + self.subscriptions = {} + self.subscriptions_lock = threading.Lock() + self._combined_subscriptions = {"analog" : [], "digital" : []} + + self._target = None + self._debug = debug + + + self.start_menu = Menu() + self.start_menu += Entry("Connect", "Connect to a fl0w server", + action=partial(Input("Address:Port (auto-connect nyi)", + initial_text=self.settings.get("address", "127.0.0.1:3077"), + on_done=self.invoke_connect).invoke, self.window)) + self.start_menu += Entry("About", "Information about fl0w", + action=self.invoke_about) + + self.debug_menu = Menu(subtitles=False) + self.debug_menu += Entry("On", + action=lambda: self.set_debug(True)) + self.debug_menu += Entry("Off", + action=lambda: self.set_debug(False)) + + + self.settings = Menu() + self.settings += Entry("Debug", "Toggle debug mode", + sub_menu=self.debug_menu) + + + self.meta = Menu() + self.meta += Entry("Info", "Server info", + action=lambda: self.ws.send(None, "info")) + self.meta_entry = Entry("Meta", "Debug information about fl0w", + sub_menu=self.meta) + if self.debug: + self.main_menu += self.meta_entry + + + self.main_menu = Menu() + self.controller_menu = Menu() + self.main_menu += Entry("Controllers", "All connected controllers", + sub_menu=self.controller_menu) + self.main_menu += Entry("Settings", "General purpose settings", + sub_menu=self.settings) + self.main_menu += Entry("Disconnect", "Disconnect from server", + action=self.invoke_disconnect) + + # Patch all sensor phantom that have been created before a fl0w instance + # was attached to the window + for sensor_phantom in sensor_phantoms: + if sensor_phantom.window.id() == self.window.id(): + sensor_phantom.fl0w = self + if self.debug: + Logging.info("Patched sensor phantom '%s'" % str(sensor_phatom)) + + + @property + def target(self): + return self._target + + + @target.setter + def target(self, target): + if self.target != None: + self.ws.pipe("unsubscribe", "sensor", self.target.id) + self._target = target + if target != None: + set_status("Target: %s (%s)" % (target.name, target.id), self.window) + if self.combined_subscriptions != {"analog" : [], "digital" : []}: + self.ws.pipe({"subscribe" : self.combined_subscriptions}, "sensor", target.id) + else: + set_status("The target has become unavaliable.", self.window) + + + @property + def debug(self): + return self._debug + + + def set_debug(self, debug): + self.debug = debug + + + @debug.setter + def debug(self, debug): + if debug: + self._debug = True + if not self.meta_entry in self.main_menu.entries.values(): + self.main_menu += self.meta_entry + else: + self._debug = False + self.main_menu -= self.meta_entry + set_status("Debug set to %s" % self._debug, self.window) + + + # Could be simplified because only one view can be active at any time. + # This would definetly lead to some major performace improvements on + # view switching and less unnecessary unsubscribes. + + # On the other hand it might be a good idea to leave it in and provide + # an option to disable aggressive unsubscribes + @property + def combined_subscriptions(self): + return self._combined_subscriptions + + + @combined_subscriptions.setter + def combined_subscriptions(self, combined_subscriptions_): + if self.combined_subscriptions != combined_subscriptions_: + self._combined_subscriptions = combined_subscriptions_ + if self.connected and self.target != None: + self.ws.pipe("unsubscribe", "sensor", self.target.id) + if combined_subscriptions_ != {"analog" : [], "digital" : []}: + self.ws.pipe({"subscribe" : combined_subscriptions_}, "sensor", + self.target.id) + + + def subscribe(self, sensor_phatom, subscriptions): + self.subscriptions_lock.acquire() + self.subscriptions[sensor_phatom] = subscriptions + self.subscriptions_lock.release() + self.make_subscriptions() + + + + def unsubscribe(self, sensor_phantom): + if sensor_phantom in self.subscriptions: + """ + print("Lock will be aquired.") + self.subscriptions_lock.acquire() + print("Lock was aquired.") + del self.subscriptions[sensor_phantom] + print("Lock will be released.") + self.subscriptions_lock.release() + print("Lock was released.") + """ + # Temporary solution, locking caused sublime to freeze + # Could cause problems if lots (> 100) views are open. + self.subscriptions[sensor_phantom] = {"analog" : [], "digital" : []} + self.make_subscriptions() + + + def make_subscriptions(self): + combined_subscriptions = {"analog" : [], "digital" : []} + for sensor_phantom in self.subscriptions: + for sensor_type in ("analog", "digital"): + combined_subscriptions[sensor_type] = list( + set(combined_subscriptions[sensor_type]) | + set(self.subscriptions[sensor_phantom][sensor_type]) + ) + self.combined_subscriptions = combined_subscriptions + + + def run_program(self, path): + if self.connected and self.target != None: + relpath = os.path.relpath(path, self.folder) + if os.path.isfile(self.folder + relpath): + if self.debug: + Logging.info("Running program '%s'" % relpath) + self.ws.pipe(relpath.rstrip(".c"), "run_program", self.target.id) + + + def stop_programs(self): + if self.connected and self.target != None: + self.ws.pipe(None, "stop_programs", self.target.id) + + + def invoke_start_menu(self): + self.start_menu.invoke(self.window) + + + def invoke_main_menu(self): + self.main_menu.invoke(self.window) + + + def invoke_about(self): + if sublime.ok_cancel_dialog("fl0w by @robot0nfire", "robot0nfire.com"): + webbrowser.open("http://robot0nfire.com") + + + + def connect(self, address): + try: + self.ws = Fl0wClient(address) + self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(), + "processes" : Fl0wClient.Processes(), + "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor(), + "std_stream" : Fl0wClient.StdStream(), "run_program" : Fl0wClient.RunProgram(), + "stop_programs" : Fl0wClient.StopPrograms()}, + self, debug=True) + self.ws.connect() + sublime.set_timeout_async(self.ws.run_forever, 0) + set_status("Connection opened '%s'" % self.folder, self.window) + self.connected = True + self.settings.set("address", address) + except OSError as e: + sublime.error_message("Error during connection creation:\n %s" % str(e)) + + + + def invoke_connect(self, address): + # Will be removed once autoconnect works + self.connect("ws://%s" % address) + + + def invoke_disconnect(self): + if self.connected: + for sensor_phantom in sensor_phantoms: + if sensor_phantom.window.id() == self.window.id(): + sensor_phantom.enabled = False + self.target = None + self.ws.close() + set_status("Connection closed '%s'" % self.folder, self.window) + self.connected = False + + +class Fl0wCommand(sublime_plugin.WindowCommand): + def run(self): + valid_window_setup = True + folder_count = len(self.window.folders()) + if folder_count > 1: + sublime.error_message("Only one open folder per window is allowed.") + valid_window_setup = False + elif folder_count == 0: + sublime.error_message("No folder open in window.") + valid_window_setup = False + if valid_window_setup: + if not hasattr(self.window, "fl0w"): + folder = self.window.folders()[0] + files = os.listdir(folder) + if not ".no-fl0w" in files: + if not ".fl0w" in files: + open(folder + "/.fl0w", 'a').close() + self.window.fl0w = Fl0w(self.window) + windows.append(self.window) + self.window.fl0w.start_menu.invoke(self.window) + else: + self.window.fl0w = Fl0w(self.window) + windows.append(self.window) + self.window.fl0w.start_menu.invoke(self.window) + else: + sublime.error_message("fl0w can't be opened in your current directory (.no-fl0w file exists)") + else: + if not self.window.fl0w.connected: + self.window.fl0w.invoke_start_menu() + else: + self.window.fl0w.invoke_main_menu() + else: + if hasattr(self.window, "fl0w"): + sublime.error_message("Window setup was invalidated (Don't close or open any additional folders in a fl0w window)") + self.window.fl0w.invoke_disconnect() + + +class RunCommand(sublime_plugin.WindowCommand): + def run(self): + if hasattr(self.window, "fl0w"): + if self.window.fl0w.connected: + if self.window.fl0w.target == None: + sublime.error_message("A target controller has to be set to " + "run programs.") + else: + file_name = self.window.active_view().file_name() + if file_name != None and file_name.endswith(".c"): + self.window.fl0w.run_program(file_name) + else: + sublime.error_message("fl0w is not connected.") + else: + sublime.error_message("fl0w is not running in your current window.") + +class StopCommand(sublime_plugin.WindowCommand): + def run(self): + if hasattr(self.window, "fl0w"): + if self.window.fl0w.connected: + if self.window.fl0w.target == None: + sublime.error_message("A target controller has to be set to " + "stop programs.") + else: + self.window.fl0w.stop_programs() + else: + sublime.error_message("fl0w is not connected.") + else: + sublime.error_message("fl0w is not running in your current window.") + + +class SensorCommand(sublime_plugin.WindowCommand): + def run(self): + if hasattr(self.window, "fl0w"): + if self.window.fl0w.connected: + if self.window.fl0w.target == None: + sublime.error_message("A target controller has to be set to " + "enable inline sensor readouts.") + else: + view_id = self.window.active_view().id() + for view in views: + if view.id() == view_id: + view.sensor_phantom.enabled = not view.sensor_phantom.enabled + view_file_name = view.file_name() + if view_file_name == None: + view_file_name = "untitled" + set_status("%s sensor phantoms for '%s'." % ( + "Enabled" if view.sensor_phantom.enabled else "Disabled", + view_file_name), + self.window) + else: + sublime.error_message("fl0w is not connected.") + else: + sublime.error_message("fl0w is not running in your current window.") + + + +class SensorPhantom(sublime_plugin.ViewEventListener): + def __init__(self, view): + self.view = view + self.view.sensor_phantom = self + if not view in views: + views.append(view) + self.window = view.window() + + # Is patched by the fl0w instance that is in control of the same window + self.fl0w = None + self._enabled = False + + self.previously_enabled = False + + self._matches = {"analog" : [], "digital" : []} + + self.timeout_scheduled = False + self.needs_update = False + + for window in windows: + if hasattr(window, "fl0w"): + self.fl0w = window.fl0w + if not self in sensor_phantoms: + sensor_phantoms.append(self) + + + @property + def enabled(self): + return self._enabled + + + @enabled.setter + def enabled(self, enabled_): + if enabled_: + if self.fl0w != None: + self.find_matches() + self.fl0w.subscribe(self, self.subscriptions) + self._enabled = True + else: + if self.fl0w != None: + self.fl0w.unsubscribe(self) + for sensor_type in ("analog", "digital"): + self.view.erase_phantoms(sensor_type) + self._enabled = False + + + @property + def matches(self): + return self._matches + + @matches.setter + def matches(self, matches_): + if not matches_ == self.matches: + self._matches = matches_ + self.fl0w.subscribe(self, self.subscriptions) + + + @property + def subscriptions(self): + subscriptions_ = {"analog" : [], "digital" : []} + for sensor_type in ("analog", "digital"): + subscriptions_[sensor_type] = [sensor[0] for sensor in self.matches[sensor_type]] + return subscriptions_ + + + def find_matches(self): + matches = {"analog" : [], "digital" : []} + # Don't do any calculations on 1MB or larger files + if self.view.size() < 2**20: + for method_name in ("analog", "digital"): + candidates = self.view.find_all("%s\(\d*\)" % method_name) + for candidate in candidates: + line = self.view.substr(candidate) + port_candidates = re.findall(PARENTHESES_REGEX, line) + if len(port_candidates) == 1: + if port_candidates[0].isnumeric(): + matches[method_name].append( + ( + int(port_candidates[0]), + sublime.Region(self.view.line(candidate.a).b) + )) + self.matches = matches + + # Called by fl0w instance + def update_sensor_values(self, readouts): + for sensor_type in ("analog", "digital"): + self.view.erase_phantoms(sensor_type) + for match in self.matches[sensor_type]: + try: + self.view.add_phantom(sensor_type, match[1], + STYLE_OPEN + str(readouts[sensor_type][str(match[0])]) + STYLE_CLOSE, + sublime.LAYOUT_INLINE) + except KeyError: + self.view.add_phantom(sensor_type, match[1], + ERROR_OPEN + "!" + ERROR_CLOSE, + sublime.LAYOUT_INLINE) + + + def handle_timeout(self): + self.timeout_scheduled = False + if self.needs_update: + self.needs_update = False + self.find_matches() + + + def on_modified(self): + if self.enabled: + if self.timeout_scheduled: + self.needs_update = True + else: + sublime.set_timeout(lambda: self.handle_timeout(), 500) + self.find_matches() + + + + def on_deactivated(self): + self.previously_enabled = self.enabled + if self.enabled: + self.enabled = False + + def on_activated(self): + if not self.enabled and self.previously_enabled: + self.enabled = True + + + def __del__(self): + self.enabled = False + if self in sensor_phantoms: + del sensor_phantoms[sensor_phantoms.index(self)] diff --git a/Sublime/fl0w/fl0w.sublime-commands b/Sublime/fl0w/fl0w.sublime-commands new file mode 100644 index 0000000..a2b89be --- /dev/null +++ b/Sublime/fl0w/fl0w.sublime-commands @@ -0,0 +1,6 @@ +[ + { + "caption": "fl0w: Menu", + "command": "fl0w" + } +] \ No newline at end of file diff --git a/Sublime/fl0w/fl0w.sublime-settings b/Sublime/fl0w/fl0w.sublime-settings new file mode 100644 index 0000000..51a7f3b --- /dev/null +++ b/Sublime/fl0w/fl0w.sublime-settings @@ -0,0 +1,4 @@ +{ + "server_address": "", // Last server address + "compression_level": 2 +} diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 7c85393..0b5e85a 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -13,21 +13,23 @@ from _thread import start_new_thread from ctypes import cdll import threading import json -import re - CHANNEL = 2 IS_WALLABY = Utils.is_wallaby() +USERS_LOCATION = "/home/root/Documents/KISS/users.json" + LIB_WALLABY = "/usr/lib/libwallaby.so" -CONFIG_PATH = "wallaby.cfg" -config = Config() -config.add(Option("server_address", "ws://127.0.0.1:3077")) -config.add(Option("output_unbuffer", "stdbuf")) -config.add(Option("identify_sound", "Wallaby/identify.wav", - validator=lambda sound: os.path.isfile(sound))) +if not IS_WALLABY: + Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.") + + +def get_users(): + if IS_WALLABY: + return list(json.loads(open(USERS_LOCATION, "r").read()).keys()) + return ["Default User"] class SensorReadout: @@ -151,11 +153,23 @@ class SensorReadout: class Identify(Pipe): def run(self, data, peer, handler): - try: - Utils.play_sound(config.identify_sound) - except Utils.PlaybackFailure: - Logging.error("Could not play identification sound") - Logging.success("I was identified!") + Utils.play_sound(config.identify_sound) + if handler.debug: + Logging.success("I was identified!") + + + +class ListPrograms(Pipe): + def run(self, data, peer, handler): + programs = [] + if os.path.isdir(PATH): + for program in os.listdir(PATH): + if "botball_user_program" in os.listdir(PATH + program): + programs.append(program) + else: + Logging.error("Harrogate folder structure does not exist. " + "You broke something, mate.") + handler.pipe(programs, handler.reverse_routes[self], peer) class StopPrograms(Pipe): @@ -238,8 +252,6 @@ class Sensor(Pipe): self.sensor_readout = SensorReadout(handler) - - class Shutdown(Pipe): def run(self, data, peer, handler): try: @@ -269,6 +281,21 @@ class WhoAmI(Route): handler.send(None, "whoami") +class Hostname(Pipe): + def run(self, data, peer, handler): + if type(data) is dict: + if "set" in data: + try: + Utils.set_hostname(str(data["set"])) + except Utils.HostnameNotChangedError: + if IS_WALLABY: + Logging.error("Hostname change unsuccessful. " + "Something is wrong with your Wallaby.") + else: + Logging.warning("Hostname change unsuccessful. " + "This seems to be a dev-system, so don't worry too " + "much about it.") + class Processes(Pipe): def run(self, data, peer, handler): @@ -277,66 +304,42 @@ class Processes(Pipe): "processes", peer) -class Output(Pipe): - def __init__(self): - self.content = [] - self.handler = None - self.subscribers = [] - - self.escape_regex = re.compile(r"(\[\d+m?)") - - self.stdout = Output.Std(Logging.stdout, self.push) - self.stderr = Output.Std(Logging.stderr, self.push) - - Logging.stdout = self.stdout - Logging.stderr = self.stderr - - def run(self, data, peer, handler): - if data == "subscribe": - self.subscribers.append(peer) - elif data == "unsubscribe": - self.unsubscribe(peer) - - - def start(self, handler): - self.handler = handler - - - def push(self, s): - s = self.escape_regex.sub("", s) - for peer in self.subscribers: - self.handler.pipe(s, "output", peer) - - - def unsubscribe(self, peer): - if peer in self.subscribers: - del self.subscribers[self.subscribers.index(peer)] - - class Std: - def __init__(self, old_std, write_callback): - self.old_std = old_std - self.write_callback = write_callback - - - def write(self, s): - self.old_std.write(s) - self.write_callback(s) - - - def flush(self): - self.old_std.flush() - - class Handler(Client): def setup(self, routes, debug=False): super().setup(routes, debug=debug) - def peer_unavaliable(self, peer): if self.debug: Logging.info("Unsubscribing '%s' from all sensor updates." % peer) self.routes["sensor"].sensor_readout.unsubscribe_all(peer) - self.routes["output"].unsubscribe(peer) + +if IS_WALLABY: + if not "fl0w" in get_users(): + json.loads(open(USERS_LOCATION, "r").read())["fl0w"] = {"mode" : "Advanced"} + try: + os.mkdir(FL0W_USER_PATH) + except FileExistsError: + pass + PATH = FL0W_USER_PATH + "bin/" +else: + if len(sys.argv) == 2: + if os.path.exists(sys.argv[1]): + PATH = os.path.abspath(sys.argv[1]) + else: + Logging.error("Location has to be provided in dev-env.") + exit(1) + +if PATH[-1] != "/": + PATH = PATH + "/" + +CONFIG_PATH = "wallaby.cfg" + +config = Config() +config.add(Option("server_address", "ws://127.0.0.1:3077")) +config.add(Option("debug", True, validator=lambda x: True if True or False else False)) +config.add(Option("output_unbuffer", "stdbuf")) +config.add(Option("identify_sound", "Wallaby/identify.wav", + validator=lambda sound: os.path.isfile(sound))) try: @@ -347,20 +350,16 @@ except (IOError, ExceptionInConfigError): try: - while 1: - ws = Handler(config.server_address) - # setup has to be called before the connection is established - ws.setup({"subscribe" : Subscribe(), "sensor" : Sensor(), - "identify" : Identify(), "whoami" : WhoAmI(), + ws = Handler(config.server_address) + # setup has to be called before the connection is established + ws.setup({"subscribe" : Subscribe(), "hostname" : Hostname(), + "processes" : Processes(), "sensor" : Sensor(), + "identify" : Identify(), "list_programs" : ListPrograms(), + "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), - "reboot" : Reboot(), "output" : Output()}, - debug=False) - try: - ws.connect() - break - except ConnectionRefusedError: - Logging.warning("Server not running... Retrying in 5s") - time.sleep(5) + "reboot" : Reboot()}, + debug=config.debug) + ws.connect() ws.run_forever() except KeyboardInterrupt: - ws.close() + ws.close() \ No newline at end of file diff --git a/Wallaby/identify.wav b/Wallaby/identify.wav index 8a444b1..4ab0ae3 100644 --- a/Wallaby/identify.wav +++ b/Wallaby/identify.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81832bc5b34359bc87173dc234cbb30ea23aa541814809ad16eb1ba9bd7335af -size 22858 +oid sha256:2c66eb1c4a17efbeaf27e2dae37d547886b447d72a75168fe0d439d699f25fcb +size 75918 diff --git a/fl0w.py b/fl0w.py index bac79b4..a446800 100644 --- a/fl0w.py +++ b/fl0w.py @@ -1,4 +1,3 @@ from sys import path -from os.path import abspath, dirname, realpath, join - -path.append(join(abspath(dirname(realpath(__file__))), 'Shared')) +from os.path import abspath +path.append(abspath("Shared")) \ No newline at end of file