├── .gitignore ├── .gitmodules ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api.Dockerfile ├── api.docker-compose.yml ├── code ├── api │ ├── README.md │ ├── database.py │ ├── enums.py │ ├── env.py │ ├── main.py │ ├── requirements.txt │ ├── timeline.json │ ├── utils.py │ └── uwsgi.ini ├── main_api.py ├── main_node.py ├── node │ ├── __init__.py │ ├── algo.py │ ├── dockerus.py │ ├── enums.py │ ├── env.py │ ├── functions.py │ ├── main.py │ ├── message_queue.py │ ├── messages.py │ ├── networks │ │ ├── __init__.py │ │ ├── receiver.py │ │ └── sender.py │ ├── tests.py │ └── todo.py ├── tests.py └── web │ ├── .bowerrc │ ├── Dockerfile │ ├── bower.json │ ├── d3 │ └── d3.v3.js │ ├── d3test.html │ ├── docker-compose.yml │ ├── entrypoint.sh │ ├── example │ ├── api │ │ └── v2 │ │ │ └── get_timeline │ │ │ └── index.html │ └── nodes.json │ ├── index.html │ ├── js.js │ ├── package.json │ ├── src │ ├── app.animations.css │ ├── app.animations.js │ ├── app.animations.less │ ├── app.config.js │ ├── app.css │ ├── app.js │ ├── app.less │ ├── app.module.js │ ├── config.js │ ├── core │ │ ├── core.module.js │ │ ├── d3 │ │ │ ├── d3.directive.js │ │ │ ├── d3.directive.module.js │ │ │ ├── d3.factory.js │ │ │ └── d3.factory.module.js │ │ ├── node │ │ │ ├── node.module.js │ │ │ └── node.service.js │ │ └── recompile │ │ │ ├── recompile.directive.js │ │ │ └── recompile.directive.module.js │ ├── desktop.css │ ├── desktop.less │ ├── failure-table-view │ │ ├── failure-table-view.component.js │ │ ├── failure-table-view.module.js │ │ └── failure-table-view.template.html │ ├── failure-table │ │ ├── failure-table.component.js │ │ ├── failure-table.module.js │ │ └── failure-table.template.html │ ├── hover-effect.js │ ├── index-async.html │ ├── index.html │ ├── mobile.css │ ├── mobile.less │ ├── node-handling.js │ ├── node-list-view │ │ ├── node-list-view.component.js │ │ ├── node-list-view.module.js │ │ └── node-list-view.template.html │ ├── node-list │ │ ├── node-list.component.js │ │ ├── node-list.module.js │ │ └── node-list.template.html │ ├── nodes.json │ ├── resources │ │ └── img │ │ │ └── nope.gif │ ├── test_timeline.json │ └── value-graph │ │ ├── value-graph.component.js │ │ ├── value-graph.d3.js │ │ ├── value-graph.module.js │ │ └── value-graph.template.html │ └── styles.css ├── docker-compose.yml ├── extras └── libs │ └── pycharm-debug-py3k.egg ├── node.docker-compose.yml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | out 8 | gen### Python template 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # IPython Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | ### JetBrains template 99 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 100 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 101 | 102 | # User-specific stuff: 103 | .idea/workspace.xml 104 | .idea/tasks.xml 105 | .idea/dictionaries 106 | .idea/vcs.xml 107 | .idea/jsLibraryMappings.xml 108 | 109 | # Sensitive or high-churn files: 110 | .idea/dataSources.ids 111 | .idea/dataSources.xml 112 | .idea/dataSources.local.xml 113 | .idea/sqlDataSources.xml 114 | .idea/dynamic.xml 115 | .idea/uiDesigner.xml 116 | 117 | # Gradle: 118 | .idea/gradle.xml 119 | .idea/libraries 120 | 121 | # Mongo Explorer plugin: 122 | .idea/mongoSettings.xml 123 | 124 | ## File-based project format: 125 | *.iws 126 | 127 | ## Plugin-specific files: 128 | 129 | # IntelliJ 130 | /out/ 131 | 132 | # mpeltonen/sbt-idea plugin 133 | .idea_modules/ 134 | 135 | # JIRA plugin 136 | atlassian-ide-plugin.xml 137 | 138 | # Crashlytics plugin (for Android Studio and IntelliJ) 139 | com_crashlytics_export_strings.xml 140 | crashlytics.properties 141 | crashlytics-build.properties 142 | fabric.properties 143 | ### VirtualEnv template 144 | # Virtualenv 145 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 146 | [Bb]in 147 | [Ii]nclude 148 | [Ll]ib 149 | [Ll]ib64 150 | [Ll]ocal 151 | [Ss]cripts 152 | pyvenv.cfg 153 | .venv 154 | pip-selfcheck.json 155 | 156 | ## USER STUFF! 157 | logs/* 158 | !.gitkeep 159 | node_modules/ 160 | bower_components/ 161 | tmp 162 | .DS_Store 163 | .idea -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extras/phppgadmin-docker"] 2 | path = extras/phppgadmin-docker 3 | url = https://github.com/luckydonald-forks/phppgadmin-docker.git 4 | [submodule "code/node_java"] 5 | path = code/node_java 6 | url = https://github.com/luckydonald/PBFT-JAVA.git 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.5" 5 | - "3.6" 6 | - "nightly" 7 | 8 | matrix: 9 | allow_failures: 10 | - python: nightly 11 | 12 | # command to install dependencies 13 | install: 14 | - "pip install -r code/api/requirements.txt" 15 | - "pip install coverage coveralls" 16 | 17 | # command to run tests 18 | script: 19 | - cd code && coverage run tests.py 20 | 21 | after_success: 22 | - coveralls 23 | 24 | notifications: 25 | # https://docs.travis-ci.com/user/notifications#Notifications 26 | webhooks: 27 | urls: 28 | - "https://bot.proxy.bronies.link/travis/webhook/tfgDCtRoiTR4LPP1ZHMvjcdBWzuyhMgiotsTSRgg6Dc" 29 | on_success: always # default: always 30 | on_failure: always # default: always 31 | on_start: always # default: never 32 | # [always|never|change] # change means to notify when the build status changes. 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | 3 | # dependencies: 4 | RUN mkdir /code 5 | WORKDIR /code/ 6 | 7 | # libs 8 | RUN mkdir /code/libs/ 9 | ADD ./extras/libs/ /code/libs 10 | ADD ./requirements.txt /code 11 | RUN pip install -r requirements.txt 12 | RUN rm requirements.txt 13 | # dependencies done 14 | 15 | # our code: 16 | WORKDIR /code/ 17 | ADD ./code /code/ 18 | 19 | # defaults for running it 20 | ENTRYPOINT ["python"] 21 | CMD ["main.py"] 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | update: 2 | git log -1 3 | git status 4 | git pull origin master 5 | git submodule foreach git pull origin master 6 | 7 | start: 8 | docker-compose up -d postgres 9 | docker-compose up --force-recreate -d postgres_browser 10 | docker-compose up -d --build api 11 | docker-compose build node node_java 12 | 13 | help: 14 | echo "docker-compose up --build -t 0 node_java" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pbft 2 | Implementation of the ~~Peters~~ Practical Byzantine Fault Tolerant Algorithm     3 | 4 | 5 | ## Web GUI 6 | 7 | This project supports a web interface to `b e a u t i f u l l y` represent what's going on. 8 | 9 | You'll get a overview over all the values the nodes measured. 10 | 11 | ![image](https://user-images.githubusercontent.com/2737108/33264568-63590e78-d36e-11e7-91e3-d0b2545546ae.png) 12 | 13 | You can also get insight which messages get send by which node to which node. 14 | 15 | ![image](https://user-images.githubusercontent.com/2737108/33264484-06f95a3e-d36e-11e7-9128-e3a2de4c37d5.png) 16 | 17 | 18 | ## Code status 19 | 20 | > Please note, the project for which this was made for has reached an end, 21 | > so the code will not be actively maintained any longer. 22 | > However, pull requests with fixes and improvements will be merged. 23 | > Have a look into the bugtracker, if someone else had a similar issue, and already made it work. 24 | 25 | #### Java PBFT Node 26 | [![Build Status](https://travis-ci.org/luckydonald/PBFT-JAVA.svg?branch=master)](https://travis-ci.org/luckydonald/PBFT-JAVA) [![Coverage Status](https://coveralls.io/repos/github/luckydonald/PBFT-JAVA/badge.svg?branch=master)](https://coveralls.io/github/luckydonald/PBFT-JAVA?branch=master) 27 | 28 | #### API Server 29 | [![Build Status](https://travis-ci.org/luckydonald/pbft.svg?branch=master)](https://travis-ci.org/luckydonald/pbft) [![Coverage Status](https://coveralls.io/repos/github/luckydonald/pbft/badge.svg?branch=master)](https://coveralls.io/github/luckydonald/pbft?branch=master) 30 | 31 | 32 | ## Get the Code 33 | ```bash 34 | git clone --recursive https://github.com/luckydonald/pbft.git 35 | ``` 36 | If you forget `--recursive`, the `phppgadmin` container won't be available. 37 | 38 | ## Starting everything 39 | You need Docker installed. 40 | 41 | 42 | ```shell 43 | $ docker-compose build 44 | ``` 45 | 46 | Because some services need longer to start it is best to start them in the following order: 47 | 48 | 1. Database and Database browser 49 | ```shell 50 | $ docker-compose up -d postgres postgres_browser 51 | ``` 52 | 53 | 2. The API 54 | ```shell 55 | $ docker-compose up -d api 56 | ``` 57 | 58 | 3. Start the web GUI 59 | ```shell 60 | $ docker-compose up -d web 61 | ``` 62 | 63 | 4. Scale the nodes to use e.g. `4` instances 64 | - a) Older compose syntax 65 | ```shell 66 | $ docker-compose scale node=4 67 | ``` 68 | - b) Newer compose syntax 69 | ```shell 70 | docker-compose up --scale node=4 71 | ``` 72 | 73 | 5. Start the nodes 74 | ```shell 75 | $ docker-compose up -d node 76 | ``` 77 | 78 | 6. Stop & reset everything 79 | ```shell 80 | $ docker-compose down 81 | ``` 82 | - [Remove unused containers](http://stackoverflow.com/a/32723127): 83 | ```shell 84 | $ docker rmi $(docker images --filter "dangling=true" -q --no-trunc) 85 | ``` 86 | 87 | ## Standart Ports and URLs 88 | Assuming your docker is publishing it's ports on `localhost`. 89 | 90 | | Server | URL | 91 | | -------- | --------------------------------- | 92 | | API | http://localhost:80/ | 93 | | Database | http://localhost:8080/phppgadmin/ | 94 | | Web GUI | http://localhost:8000/src/ | 95 | 96 | 97 | ## Links 98 | The whole project: https://github.com/luckydonald/pbft 99 | 100 | The Java node implementation: https://github.com/luckydonald/PBFT-JAVA 101 | 102 | DB Struktur (for debugging and powering the web gui): https://editor.ponyorm.com/user/luckydonald/pbft 103 | ![pbft database structure](https://user-images.githubusercontent.com/2737108/33264396-a8310146-d36d-11e7-8ec9-8485d5d625b5.png) 104 | -------------------------------------------------------------------------------- /api.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:flask-python3.5 2 | 3 | RUN mkdir -p /app/code 4 | WORKDIR /app/ 5 | COPY ./code/api/requirements.txt /app/ 6 | RUN pip install -r requirements.txt 7 | RUN rm requirements.txt 8 | COPY ./code /app 9 | COPY ./code/api/uwsgi.ini /app/uwsgi.ini 10 | -------------------------------------------------------------------------------- /api.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | api: 4 | build: 5 | context: . 6 | dockerfile: ./api.Dockerfile 7 | restart: "unless-stopped" 8 | environment: 9 | POSTGRES_HOST: "postgres" # service name 10 | POSTGRES_USER: "postgres" 11 | POSTGRES_PASS: "1234secure" 12 | POSTGRES_DB: "messages" 13 | 14 | postgres: 15 | image: postgres 16 | restart: "unless-stopped" 17 | environment: 18 | POSTGRES_USER: "postgres" 19 | POSTGRES_PASSWORD: "1234secure" 20 | POSTGRES_DB: "messages" 21 | 22 | postgres_browser: 23 | # image: jacksoncage/phppgadmin 24 | build: ./extras/phppgadmin-docker 25 | environment: 26 | POSTGRES_HOST: "postgres" # service name 27 | POSTGRES_USER: "postgres" 28 | POSTGRES_PASSWORD: "1234secure" 29 | POSTGRES_DEFAULTDB: "messages" 30 | # APACHE_SERVERNAME: "postgres_browser" 31 | APACHE_SERVERNAME: "localhost" -------------------------------------------------------------------------------- /code/api/README.md: -------------------------------------------------------------------------------- 1 | # The events 2 | 3 | 4 | | Type \ Class | Message | Init | Propose | Prevote | Vote | Ack | 5 | | --------------- | ------- | ---- | ------- | ------- | ---- | --- | 6 | | **Sequence** | X | X | X | X | X | X | 7 | | **Node** | X | X | X | X | X | X | 8 | | **Value** | - | X | - | X | X | - | 9 | | **Leader** | - | - | X | X | X | - | 10 | | **Value Store** | - | - | X | - | - | - | 11 | | **Sender** | - | - | - | - | - | X | 12 | | **Raw** | - | - | - | - | - | X | 13 | 14 | 15 | 16 | 17 | 18 | # `GET /get_value` 19 | 20 | Returns the latest value the nodes decided on, 21 | and the most recent measured value of each node. 22 | Only considers events in the last 10 seconds. 23 | 24 | ##### Returns 25 | An dictionary. 26 | Will have an entry for each node with its latest measured value. 27 | Also contains a `"summary"` field, containing the last value they have agreed on, if any. 28 | 29 | ##### Note 30 | If nothing happened in the last 10 seconds, that node / the summary will be missing. 31 | 32 | ##### Example 33 | ```curl 34 | $ curl http://$IP_OR_HOST/get_value/ 35 | ``` 36 | ```python 37 | { # if no data is present, the field just does not exist. 38 | "1": 0.5, 39 | "2": 0.6, 40 | "5": 0.5, 41 | "6": 0.5, 42 | "7": 0.5, 43 | "summary": 0.5 44 | } 45 | ``` 46 | 47 | 48 | # `GET /api/v2/get_value/` 49 | 50 | Similar to `/get_value/`, but 51 | 52 | ##### Returns 53 | An dictionary. 54 | Will have an entry for each node with its latest measured value. 55 | Also contains a `"summary"` field, containing the last value they have agreed on, if any. 56 | 57 | ##### Note 58 | If nothing happened in the last 10 seconds, that node / the summary will be missing. 59 | 60 | ##### Example 61 | ```curl 62 | $ curl http://$IP_OR_HOST/api/v2/get_value/ 63 | ``` 64 | ```python 65 | { 66 | "summary": 0.5, # or null 67 | "leader": 1, 68 | "nodes": [ 69 | {"node": "1", "value": 0.5}, 70 | {"node": "2", "value": 0.6}, 71 | {"node": "5", "value": 0.5}, 72 | {"node": "6", "value": 0.5}, 73 | {"node": "5", "value": 0.5} 74 | ] 75 | } 76 | ``` 77 | 78 | 79 | # `GET /get_data` 80 | 81 | Returns list of recent measurements, 82 | 83 | ##### Optional parameters 84 | - `node`: the node you want to filter for. You can specify this argument multible times. Omit to receive all of them. 85 | - `limit`: will only get the specified count of measurements. 86 | **Note**: This is the total count, not per node. So `limit=5` could mean 1 entry in _node 1_ and 4 measurement in _node 2_, depending of the time. 87 | 88 | 89 | ##### Returns 90 | An dictionary with nodes as keys and a subdictionary, with timestaps as keys for the measured values. 91 | 92 | 93 | ##### Example 94 | ```curl 95 | $ curl http://$IP_OR_HOST/get_data/ 96 | ``` 97 | ```python 98 | 99 | { 100 | "1": { 101 | "123134": 0.12, 102 | # timestamp : value 103 | "123135": 0.5 104 | }, 105 | "2": { 106 | "123134": 0.13, 107 | # timestamp : value 108 | "123135": 0.8 109 | } 110 | "3": { 111 | "123134": 0.13, 112 | # timestamp : value 113 | "123135": 0.5 114 | } 115 | } 116 | ``` 117 | 118 | ##### Example 2 119 | 120 | ```curl 121 | $ curl http://$IP_OR_HOST/get_data/?node=1&node=2 122 | ``` 123 | ```python 124 | 125 | { 126 | "1": { 127 | "123134": 0.12, 128 | # timestamp : value 129 | "123135": 0.5 130 | }, 131 | "2": { 132 | "123134": 0.13, 133 | # timestamp : value 134 | "123135": 0.8 135 | } 136 | } 137 | ``` 138 | 139 | # `PUT /dump` 140 | 141 | Sent an json encoded `Message` into the database. 142 | The json to be send is exactly the same as used internally between the nodes. 143 | -------------------------------------------------------------------------------- /code/api/database.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pony import orm 3 | import logging 4 | 5 | from node import messages 6 | from node.enums import UNSET, INIT, PROPOSE, PREVOTE, VOTE, ACKNOWLEDGE 7 | from .env import POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASS, POSTGRES_DB 8 | 9 | __author__ = "luckydonald" 10 | 11 | logger = logging.getLogger(__file__) 12 | db = orm.Database() 13 | 14 | VALUE_TYPE = float 15 | MSG_TYPE_TYPE = int 16 | NODE_TYPE = int 17 | SEQUENCE_TYPE = int 18 | 19 | # https://editor.ponyorm.com/user/luckydonald/pbft 20 | # Last permalink: 21 | # https://editor.ponyorm.com/user/luckydonald/pbft_2 22 | 23 | 24 | class DBMessage(db.Entity): 25 | type = orm.Discriminator(MSG_TYPE_TYPE) 26 | date = orm.Required(datetime, sql_default='CURRENT_TIMESTAMP') 27 | sequence_no = orm.Required(SEQUENCE_TYPE) 28 | node = orm.Optional(NODE_TYPE) 29 | value = orm.Optional(VALUE_TYPE) 30 | leader = orm.Optional(NODE_TYPE) 31 | sender = orm.Optional(NODE_TYPE) 32 | raw = orm.Optional(orm.Json) 33 | 34 | _discriminator_ = UNSET 35 | 36 | def from_db(self): 37 | clazz = MSG_TYPE_CLASS_MAP[self.type] 38 | assert issubclass(clazz, messages.Message) 39 | return clazz.from_dict(self.as_dict()) 40 | # end def 41 | 42 | @classmethod 43 | def to_db(cls, msg): 44 | return cls(**msg.to_dict()) 45 | # end def 46 | # end class 47 | 48 | 49 | class DBInitMessage(DBMessage): 50 | _discriminator_ = INIT 51 | 52 | def from_db(self): 53 | return messages.InitMessage(sequence_no=self.sequence_no, node=self.node, value=self.value) 54 | # end def 55 | 56 | @classmethod 57 | def to_db(cls, msg): 58 | assert isinstance(msg, messages.InitMessage) 59 | return super().to_db(msg) 60 | # end def 61 | # end class 62 | 63 | 64 | class DBProposeMessage(DBMessage): 65 | _discriminator_ = PROPOSE 66 | proposal = orm.Required(VALUE_TYPE) 67 | value_store = orm.Required(orm.Json) # json 68 | 69 | def from_db(self): 70 | return messages.ProposeMessage( 71 | sequence_no=self.sequence_no, node=self.node, leader=self.leader, proposal=self.proposal, 72 | value_store=self.value_store 73 | ) 74 | # end def 75 | 76 | @classmethod 77 | def to_db(cls, msg): 78 | assert isinstance(msg, messages.ProposeMessage) 79 | return super().to_db(msg) 80 | # end def 81 | # end class 82 | 83 | 84 | class DBPrevoteMessage(DBMessage): 85 | _discriminator_ = PREVOTE 86 | 87 | @classmethod 88 | def to_db(cls, msg): 89 | assert isinstance(msg, messages.PrevoteMessage) 90 | return super().to_db(msg) 91 | # end def 92 | 93 | def from_db(self): 94 | return messages.PrevoteMessage(sequence_no=self.sequence_no, node=self.node, leader=self.leader, value=self.value) 95 | # end def 96 | # end class 97 | 98 | 99 | class DBVoteMessage(DBMessage): 100 | _discriminator_ = VOTE 101 | 102 | @classmethod 103 | def to_db(cls, msg): 104 | assert isinstance(msg, messages.VoteMessage) 105 | return super().to_db(msg) 106 | # end def 107 | 108 | def from_db(self): 109 | return messages.VoteMessage(sequence_no=self.sequence_no, node=self.node, leader=self.leader, value=self.value) 110 | # end def 111 | # end class 112 | 113 | 114 | class DBAcknowledge(DBMessage): 115 | _discriminator_ = ACKNOWLEDGE 116 | 117 | @classmethod 118 | def to_db(cls, msg): 119 | assert isinstance(msg, messages.Acknowledge) 120 | return super().to_db(msg) 121 | # end def 122 | 123 | def from_db(self): 124 | return messages.Acknowledge(sequence_no=self.sequence_no, node=self.node, sender=self.sender, raw=self.raw) 125 | # end def 126 | # end class 127 | 128 | 129 | MSG_TYPE_CLASS_MAP = { 130 | INIT: DBInitMessage, 131 | PROPOSE: DBProposeMessage, 132 | PREVOTE: DBPrevoteMessage, 133 | VOTE: DBVoteMessage, 134 | # ... 135 | ACKNOWLEDGE: DBAcknowledge, 136 | } 137 | 138 | 139 | @orm.db_session 140 | def to_db(msg): 141 | if msg is None: 142 | return None 143 | if isinstance(msg, dict): 144 | # is still dict (json) 145 | msg = messages.Message.from_dict(msg) # make a Message subclass first. 146 | assert isinstance(msg, messages.Message) 147 | db_msg_clazz = MSG_TYPE_CLASS_MAP[msg.type] # Key error = not implemented yet. 148 | assert issubclass(db_msg_clazz, DBMessage) 149 | db_msg = db_msg_clazz.to_db(msg) 150 | assert db_msg.type == msg.type 151 | return db_msg 152 | # end def 153 | 154 | db.bind("postgres", host=POSTGRES_HOST, user=POSTGRES_USER, password=POSTGRES_PASS, database=POSTGRES_DB) 155 | db.generate_mapping(create_tables=True) -------------------------------------------------------------------------------- /code/api/enums.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | from node.enums import INIT, PROPOSE,PREVOTE, VOTE 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | 7 | JSON_TYPES = { 8 | INIT: "init", 9 | PROPOSE: "propose", 10 | PREVOTE: "prevote", 11 | VOTE: "vote", 12 | } -------------------------------------------------------------------------------- /code/api/env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from luckydonaldUtils.logger import logging 5 | 6 | __author__ = 'luckydonald' 7 | logger = logging.getLogger(__name__) 8 | 9 | POSTGRES_HOST = os.environ.get("POSTGRES_HOST", None) 10 | assert POSTGRES_HOST is not None 11 | 12 | POSTGRES_USER = os.environ.get("POSTGRES_USER", None) 13 | assert POSTGRES_USER is not None 14 | 15 | POSTGRES_PASS = os.environ.get("POSTGRES_PASS", None) 16 | assert POSTGRES_PASS is not None 17 | 18 | POSTGRES_DB = os.environ.get("POSTGRES_DB", None) 19 | assert POSTGRES_DB is not None 20 | 21 | 22 | -------------------------------------------------------------------------------- /code/api/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from DictObject import DictObject 5 | from flask import Flask, request 6 | from luckydonaldUtils.logger import logging 7 | from pony import orm 8 | 9 | from .enums import JSON_TYPES 10 | from .utils import jsonify 11 | from .database import to_db, db, DBVoteMessage, DBMessage, DBInitMessage, DBPrevoteMessage, DBProposeMessage, DBAcknowledge, \ 12 | MSG_TYPE_CLASS_MAP 13 | from node.enums import INIT # noqa # pylint: disable=unused-import 14 | from node.messages import Message # noqa # pylint: disable=unused-import 15 | 16 | __author__ = 'luckydonald' 17 | logger = logging.getLogger(__name__) 18 | 19 | VERSION = "0.0.1" 20 | __version__ = VERSION 21 | assert INIT == INIT # to prevent the unused import warning. Is used in SQL statement. 22 | 23 | from werkzeug.debug import DebuggedApplication 24 | app = Flask(__name__) 25 | debug = DebuggedApplication(app, console_path="/console/") 26 | 27 | API_V1 = "" 28 | API_V2 = "/api/v2" 29 | 30 | 31 | @app.route(API_V1+"/dump", methods=['POST', 'GET', 'PUT']) 32 | @app.route(API_V1+"/dump/", methods=['POST', 'GET', 'PUT']) 33 | @orm.db_session 34 | def dump_to_db(): 35 | try: 36 | logger.info("Incoming: {}".format(request.get_json(force=True))) 37 | msg = to_db(request.get_json(force=True)) 38 | if msg: 39 | db.commit() 40 | logger.info("Added {}: {id}".format(msg, id=msg.id)) 41 | return "ok: {}".format(msg) 42 | else: 43 | return "fail: None" 44 | # end if 45 | except Exception as e: 46 | logger.exception("lel") 47 | raise 48 | # end def 49 | 50 | 51 | @app.route(API_V1+"/get_value") 52 | @app.route(API_V1+"/get_value/") 53 | @orm.db_session 54 | def get_value(): 55 | """ 56 | Gets latest value they decided on, and the most recent measured value of each node. 57 | Only considers events in the last 10 seconds. 58 | 59 | > {"summary": 3.456, "1": 2.345, "2": 3.456, "3": 4.567, "4": 5.678}} 60 | 61 | :return: 62 | """ 63 | latest_vote = orm.select(m for m in DBVoteMessage if m.date > orm.raw_sql("NOW() - '10 seconds'::INTERVAL")).order_by(orm.desc(DBVoteMessage.date)).first() 64 | if not latest_vote: 65 | return jsonify({}, allow_all_origin=True) 66 | # end if 67 | assert isinstance(latest_vote, DBVoteMessage) 68 | latest_values = DBMessage.select_by_sql(""" 69 | SELECT DISTINCT ON (m.node) * FROM ( 70 | SELECT * FROM DBmessage 71 | WHERE type = $INIT 72 | AND date >= NOW() - '10 seconds'::INTERVAL 73 | ) as m ORDER BY m.node, m.date DESC 74 | """) 75 | data = {"summary": latest_vote.value} 76 | for msg in latest_values: 77 | assert isinstance(msg, DBInitMessage) 78 | data[str(msg.node)] = msg.value 79 | # end for 80 | return jsonify(data, allow_all_origin=True) 81 | # end def 82 | 83 | 84 | @app.route(API_V2+"/get_value") 85 | @app.route(API_V2+"/get_value/") 86 | @orm.db_session 87 | def get_value_v2(): 88 | """ 89 | Gets latest value they decided on, and the most recent measured value of each node. 90 | Only considers events in the last 10 seconds. 91 | 92 | { 93 | "summary": None, 94 | "leader": 1, # done later via observing latest LeaderChange events. 95 | "nodes": [] 96 | } 97 | 98 | :return: 99 | """ 100 | latest_vote = orm.select(m for m in DBVoteMessage if m.date > orm.raw_sql("NOW() - '10 seconds'::INTERVAL")).order_by(orm.desc(DBVoteMessage.date)).first() 101 | latest_values = DBMessage.select_by_sql(""" 102 | SELECT DISTINCT ON (m.node) * FROM ( 103 | SELECT * FROM DBmessage 104 | WHERE type = $INIT 105 | AND date >= NOW() - '10 seconds'::INTERVAL 106 | ) as m ORDER BY m.node, m.date DESC 107 | """) 108 | data = { 109 | "summary": None, 110 | "leader": 1, # done later via observing latest LeaderChange events. 111 | "nodes": [] 112 | } 113 | if latest_vote: 114 | assert isinstance(latest_vote, DBVoteMessage) 115 | data["summary"] = {"value": latest_vote.value} 116 | # end if 117 | 118 | for msg in latest_values: 119 | assert isinstance(msg, DBInitMessage) 120 | data["nodes"].append({"node": str(msg.node), "value": msg.value}) 121 | # end for 122 | return jsonify(data, allow_all_origin=True) 123 | # end def 124 | 125 | 126 | @app.route(API_V2+"/get_timeline") 127 | @app.route(API_V2+"/get_timeline/") 128 | @orm.db_session 129 | def get_timeline(): 130 | node_list = set() 131 | event_list = list() 132 | date_min = None 133 | date_max = None 134 | node_events = DBMessage.select_by_sql(""" 135 | SELECT * FROM DBmessage WHERE date >= NOW() - '60 seconds'::INTERVAL 136 | """) 137 | for node_event in node_events: 138 | event_dict = DictObject.objectify({ 139 | "id": {}, # for deduplication in the GUI 140 | "action": None, # "send" or "acknowledge" 141 | "type": None, 142 | "nodes": {}, 143 | "timestamps": {}, 144 | "data": {} 145 | }) 146 | node_list.add(node_event.node) # update node list 147 | if date_min is None or node_event.date < date_min: 148 | date_min = node_event.date 149 | # end if 150 | if date_max is None or node_event.date > date_max: 151 | date_max = node_event.date 152 | # end if 153 | if isinstance(node_event, DBAcknowledge): 154 | received_msg = Message.from_dict(node_event.raw) 155 | event_dict.id["receive"] = node_event.id 156 | event_dict.action = "acknowledge" 157 | event_dict.nodes["send"] = received_msg.node 158 | event_dict.nodes["receive"] = node_event.node 159 | event_dict.timestamps["receive"] = generate_date_data(node_event.date) 160 | event_dict.type = JSON_TYPES[received_msg.type] 161 | event_dict.data = generate_msg_data(received_msg) 162 | node_list.add(received_msg.node) # update node list 163 | # additional DB query, to get sender 164 | DBClazz = MSG_TYPE_CLASS_MAP[received_msg.type] 165 | try: 166 | db_received_msg = DBClazz.get( 167 | sequence_no=received_msg.sequence_no, 168 | node=received_msg.node 169 | ) 170 | event_dict.id["send"] = db_received_msg.id 171 | event_dict.timestamps["send"] = generate_date_data(db_received_msg.date) 172 | if date_min is None or db_received_msg.date < date_min: 173 | date_min = node_event.date 174 | # end if 175 | if date_max is None or db_received_msg.date > date_max: 176 | date_max = node_event.date 177 | # end if 178 | except orm.DatabaseError: 179 | event_dict.id["send"] = None 180 | event_dict.timestamps["send"] = generate_date_data(None) 181 | # end try 182 | else: 183 | event_dict.action = "send" 184 | event_dict.id["send"] = node_event.id 185 | event_dict.nodes["send"] = node_event.node 186 | event_dict.timestamps["send"] = generate_date_data(node_event.date) 187 | event_dict.type = JSON_TYPES[node_event.type] 188 | event_dict.data = generate_msg_data(node_event) 189 | # end if 190 | event_list.append(event_dict) 191 | # end for 192 | result = DictObject.objectify({ 193 | "nodes": node_list, 194 | "timestamps": {"min": generate_date_data(date_min), "max": generate_date_data(date_max)}, 195 | "events": event_list, 196 | }) 197 | return jsonify(result, allow_all_origin=True) 198 | # end def 199 | 200 | 201 | def generate_date_data(datetime_obj): 202 | if datetime_obj is None: 203 | return {"string": "unknown", "unix": None} 204 | # end if 205 | assert isinstance(datetime_obj, datetime) 206 | return {"string": datetime_obj, "unix": datetime_obj.timestamp()} 207 | # end def 208 | 209 | 210 | def generate_msg_data(msg): 211 | if isinstance(msg, DBMessage): 212 | msg = msg.from_db() 213 | assert isinstance(msg, Message) 214 | msg = msg.to_dict() 215 | return msg 216 | # end def 217 | 218 | 219 | @app.route(API_V1+"/get_data") 220 | @app.route(API_V1+"/get_data/") 221 | @orm.db_session 222 | def get_data(): 223 | node = request.args.getlist('node', None) 224 | limit = request.args.get('limit', 100) 225 | assert isinstance(limit, int) or str.isnumeric(limit) # TODO: specify error message, on error # like int("abc") 226 | limit = int(limit) 227 | if node: 228 | for i in node: 229 | assert str.isnumeric(i) # TODO: specify error message 230 | # end for 231 | node_values = orm.select(m for m in DBInitMessage if m.node in list(node)).order_by(orm.desc(DBInitMessage.date)).limit(limit) 232 | else: 233 | node_values = orm.select(m for m in DBInitMessage).order_by(orm.desc(DBInitMessage.date)).limit(limit) 234 | if not node_values: 235 | return jsonify({}, allow_all_origin=True) 236 | # end if 237 | data = {} 238 | for msg in node_values: 239 | assert isinstance(msg, DBInitMessage) 240 | assert isinstance(msg.date, datetime) 241 | if str(msg.node) not in data: 242 | data[str(msg.node)] = dict() 243 | # end if 244 | data[str(msg.node)][msg.date.timestamp()] = msg.value 245 | # end for 246 | return jsonify(data, allow_all_origin=True) 247 | # end def 248 | 249 | 250 | @app.route(API_V1+"/test") 251 | @app.route(API_V1+"/test/") 252 | @orm.db_session 253 | def test(): 254 | node = request.args.getlist('node', None) 255 | if request.environ.get('HTTP_ORIGIN', None) is not None: 256 | logger.warning("HTTP_ORIGIN: {!r}".format(request.environ['HTTP_ORIGIN'])) 257 | # res.headers["Access-Control-Allow-Origin"] = request.environ['HTTP_ORIGIN'] 258 | # end if 259 | return str(request.environ.get('HTTP_ORIGIN', None)) 260 | # end def 261 | 262 | 263 | @app.route("/console/") 264 | def console(): 265 | return debug.display_console(request) 266 | # end def 267 | 268 | 269 | @app.route("/") 270 | def root(): 271 | return "Ready to take your requests." 272 | # end def 273 | -------------------------------------------------------------------------------- /code/api/requirements.txt: -------------------------------------------------------------------------------- 1 | luckydonald-utils==0.51 2 | docker-py 3 | 4 | flask # api 5 | pony # db 6 | psycopg2cffi # db -------------------------------------------------------------------------------- /code/api/timeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": ["1", "2", "3", "4"], 3 | "timestamps": {"min": "23428001", "max": "23428013"}, 4 | "events": [ 5 | { 6 | "id": {"send": 1}, 7 | "action": "send", 8 | "type": "init", 9 | "nodes": {"send": "1"}, 10 | "timestamps": {"send": "23428001"}, 11 | "data": {"value": "0.5"} 12 | }, 13 | { 14 | "id": {"send": 1, "receive": 2}, 15 | "action": "acknowledge", 16 | "type": "init", 17 | "nodes": {"send": "1", "receive": "2"}, 18 | "timestamps": {"send": "23428001", "receive": "23428011"}, 19 | "data": {"value": "0.5"} 20 | 21 | }, 22 | { 23 | "id": {"send": null, "receive": 3}, 24 | "action": "acknowledge", 25 | "type": "init", 26 | "nodes": {"send": "1", "receive": "3"}, 27 | "timestamps": {"send": null, "receive": "23428013"}, 28 | "data": {"value": "0.5"} 29 | }, 30 | { 31 | "id": {"send": 1, "receive": 4}, 32 | "action": "acknowledge", 33 | "type": "init", 34 | "nodes": {"send": "1", "receive": "3"}, 35 | "timestamps": {"send": "23428001", "receive": "23428013"}, 36 | "data": {"value": "0.5"} 37 | } 38 | 39 | ] 40 | } 41 | 42 | -------------------------------------------------------------------------------- /code/api/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import request 3 | from luckydonaldUtils.logger import logging 4 | 5 | __author__ = 'luckydonald' 6 | logger = logging.getLogger(__name__) 7 | 8 | ORIGIN_LIST = ["http://localhost"] 9 | 10 | 11 | def jsonify(data, allow_all_origin=False): 12 | from flask import Response, jsonify as json_ify 13 | res = json_ify(data) 14 | assert isinstance(res, Response) 15 | origin = request.environ.get('HTTP_ORIGIN') 16 | if not origin: 17 | origin = request.environ.get('ORIGIN') 18 | # end if 19 | if allow_all_origin: 20 | res.headers["Access-Control-Allow-Origin"] = '*' 21 | elif origin and origin in ORIGIN_LIST: 22 | res.headers["Access-Control-Allow-Origin"] = origin 23 | # end if 24 | return res 25 | # end def 26 | -------------------------------------------------------------------------------- /code/api/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = main_api 3 | callable = app -------------------------------------------------------------------------------- /code/main_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging, LevelByNameFilter 3 | 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | 7 | from api import main 8 | 9 | app = main.app 10 | 11 | print("## LOADED ##") 12 | def setup_logging(): 13 | filter = LevelByNameFilter(logging.WARNING, debug="api.main, node.messages", info="node, api") 14 | logging.add_colored_handler(level=logging.DEBUG, date_formatter="%Y-%m-%d %H:%M:%S", filter=filter) 15 | logging.test_logger_levels() 16 | # end def 17 | 18 | setup_logging() 19 | 20 | if __name__ == "__main__": 21 | # no nginx, else the __name__ would be "api" (because api.py) 22 | app.run(host='0.0.0.0', debug=True, port=80) 23 | # end if 24 | 25 | -------------------------------------------------------------------------------- /code/main_node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | from node.main import setup_logging, main 5 | 6 | __author__ = 'luckydonald' 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | if __name__ == '__main__': # if this is the executed file 11 | setup_logging() 12 | main() 13 | # end if main() 14 | -------------------------------------------------------------------------------- /code/node/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckydonald/pbft/a7d39515bb2aa5c48bf7ea242ed11fc563955a4f/code/node/__init__.py -------------------------------------------------------------------------------- /code/node/algo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # built in modules 4 | import sys 5 | from datetime import timedelta 6 | from statistics import median 7 | 8 | # dependency modules 9 | from luckydonaldUtils.functions import cached, gone 10 | from luckydonaldUtils.logger import logging 11 | 12 | # own modules 13 | from .message_queue import MessageQueueReceiver 14 | from .networks.sender import send_message 15 | from .functions import flatten_list 16 | from .messages import InitMessage, LeaderChangeMessage, ProposeMessage, PrevoteMessage, VoteMessage 17 | from .env import DEBUGGER 18 | from . import todo 19 | 20 | __author__ = 'luckydonald' 21 | logger = logging.getLogger(__name__) 22 | 23 | if DEBUGGER: 24 | sys.path.append("libs/pycharm-debug-py3k.egg") 25 | try: 26 | import pydevd 27 | except ImportError: 28 | sys.path.remove("libs/pycharm-debug-py3k.egg") 29 | logger.warning("Debug disabled.") 30 | # end def 31 | try: 32 | pydevd.settrace('192.168.188.20', port=49872, stdoutToServer=True, stderrToServer=True, suspend=False) 33 | logger.success("Debugger connected.") 34 | except Exception: 35 | logger.warning("No debugger.") 36 | # end try 37 | else: 38 | logger.debug("Debugger disabled via $NODE_DEBUGGER.") 39 | # end if 40 | 41 | # init 42 | # sensor_value = 0.4 # vp 43 | # self.node_number = 0 # p 44 | # TOTAL_NODES = 4 # n 45 | # POSSIBLE_FAILURES = 1 # t 46 | # value_store = {} # INIT_Store 47 | # current_leader = 0 # o 48 | # sequence_no = None # cid 49 | # P = {} 50 | # LC = {} # leader change 51 | 52 | 53 | # MAIN FILE IS main_node.py IN THE /code DIRECTORY! 54 | 55 | 56 | class BFT_ARM(): 57 | sequence_no = None 58 | should_timeout = False 59 | 60 | def __init__(self, sequence_number=None, receiver=None): 61 | """ 62 | Starts a new sequence. 63 | You can provide a sequence number to use as `sequence_number` 64 | and a (started) MessageQueueReceiver `receiver`, too. 65 | 66 | :param sequence_number: the sequence number to use. Is stored as `self.sequence_no` 67 | :param receiver: Reuse a existing :class:`MessageQueueReceiver`, 68 | allowing to keep the messages, and minimizing the socket port problems 69 | 70 | """ 71 | self.value_store = {} # INIT_Store 72 | self.current_leader = 1 # ø 73 | if receiver: 74 | assert isinstance(receiver, MessageQueueReceiver) 75 | self.rec = receiver 76 | else: 77 | self.rec = MessageQueueReceiver() 78 | self.rec.start() 79 | # end if 80 | self.sequence_no = sequence_number 81 | # end def 82 | 83 | def MsgCollect(self): 84 | received_message = self.get_specific_message_type(InitMessage, LeaderChangeMessage) 85 | if isinstance(received_message, InitMessage): 86 | self.value_store[received_message.node] = received_message 87 | elif isinstance(received_message, LeaderChangeMessage): 88 | # TODO: LC <- LC u {received_message} 89 | logger.warning("not implemented.") 90 | pass 91 | if todo.timeout(): 92 | # BFT_ARM.task_leader_change() 93 | # todo.timeout.reset() 94 | pass 95 | # end if 96 | # end def 97 | 98 | def task_normal_case(self): 99 | value = todo.get_sensor_value() # vp 100 | logger.critical("Step 0 INIT>") 101 | send_message(InitMessage(self.sequence_no, self.node_number, value)) 102 | logger.info("I'm node [{self!r}], [{leader!r}] is leader, {filler}.".format( 103 | leader=self.current_leader, self=self.node_number, 104 | filler="it's a me" if self.node_number == self.current_leader else "not me" 105 | )) 106 | if self.node_number == self.current_leader: 107 | logger.critical("Step 1.0 (leader)") 108 | # CURRENT LEADER 109 | # self.new_sequence() 110 | while not (len(self.value_store) >= (self.nodes_total - self.nodes_faulty)): 111 | # wait until |INIT_Store| > n - t 112 | logger.success("INITs: {} > {}".format(len(self.value_store), (self.nodes_total - self.nodes_faulty))) 113 | init_msg = self.rec.init_queue.get_message(sequence_number=self.sequence_no) 114 | assert isinstance(init_msg, InitMessage) 115 | self.value_store[init_msg.node] = init_msg 116 | # end 117 | proposal = median([x.value for x in self.value_store.values()]) 118 | send_message(ProposeMessage( 119 | self.sequence_no, self.node_number, self.current_leader, proposal, list(self.value_store.values()) 120 | )) 121 | logger.critical("Step 1.1 PROPOSAL>") 122 | # end if 123 | logger.critical("Step 2.0 >PROPOSAL") 124 | prop_message = self.rec.propose_queue.get_message(sequence_number=self.sequence_no) 125 | assert isinstance(prop_message, ProposeMessage) 126 | if self.verify_proposal(prop_message): 127 | send_message(PrevoteMessage(self.sequence_no, self.node_number, self.current_leader, value)) 128 | logger.critical("Step 2.1 PROPOSAL>") 129 | # if exist v:|(n+t) 130 | 131 | # hier auch, weil timeout uns notfalls rettet. 132 | prevote_buffer = dict() # dict with P inside 133 | vote_buffer = dict() # just decide 134 | logger.critical("Step 3.0 >(PRE)VOTE") 135 | while not self.should_timeout: 136 | if self.rec.prevote_queue.has_message(): 137 | logger.critical("Step 3.A >PREVOTE") 138 | msg = self.rec.prevote_queue.get_message(sequence_number=self.sequence_no) 139 | value, is_enough = self.buffer_incomming(msg, prevote_buffer) 140 | if is_enough: 141 | send_message(VoteMessage(self.sequence_no, self.node_number, self.current_leader, value)) 142 | logger.critical("Step 3.A VOTE>") 143 | # end def 144 | elif self.rec.vote_queue.has_message(): 145 | msg = self.rec.vote_queue.get_message(sequence_number=self.sequence_no) 146 | logger.critical("Step 3.B >VOTE") 147 | value, is_enough = self.buffer_incomming(msg, vote_buffer) 148 | if is_enough: 149 | logger.critical("Step 4 (commit {value})".format(value=value)) 150 | return value 151 | # end if 152 | # end if 153 | # end while 154 | logger.warning("Hit end unexpectedly.") 155 | # end def run 156 | 157 | def stop(self): 158 | logger.info("Requested to stop.") 159 | self.should_timeout = True 160 | assert isinstance(self.rec, MessageQueueReceiver) 161 | self.rec.stop() 162 | self.rec = None 163 | # end def 164 | 165 | def new_sequence(self): 166 | if self.sequence_no is None: 167 | self.sequence_no = 0 168 | else: 169 | self.sequence_no = (self.sequence_no + 1) % 256 170 | # end if 171 | logger.info("Sequence: {i}".format(i=self.sequence_no)) 172 | return self.sequence_no 173 | # end def 174 | 175 | def buffer_incomming(self, msg, buffer): 176 | if not msg.value in buffer: 177 | buffer[msg.value] = list() 178 | # end if 179 | assert isinstance(buffer[msg.value], list) 180 | buffer[msg.value].append(msg) 181 | return msg.value, len(buffer[msg.value]) > (self.nodes_total + self.nodes_faulty) / 2 182 | # end def 183 | 184 | def verify_proposal(self, msg): 185 | """ 186 | Überprüfe ob proposal vom leader ist und 187 | Rechnen nach, das die von msg empfangenen InitMessages den von ihm berechneten Wert (proposal) ergeben. 188 | :param msg: 189 | :return: 190 | """ 191 | # TODO: optimieren, indem man leader abfrage nach oben schiebt? 192 | # if not msg.leader == self.current_leader: 193 | # return False 194 | values = list() 195 | known_nodes = list() 196 | 197 | if not isinstance(msg, ProposeMessage): 198 | raise AttributeError("msg is not ProposeMessage type, but {type}:\n{val}".format(type=type(msg), val=msg)) 199 | for init_msg in msg.value_store: 200 | assert isinstance(init_msg, InitMessage) # right message type 201 | assert init_msg.node not in known_nodes # no duplicates 202 | values.append(init_msg.value) # store the value 203 | known_nodes.append(init_msg.node) # remember this node 204 | # end for 205 | return msg.leader == self.current_leader and median(values) == msg.proposal 206 | # end def 207 | 208 | @gone # TODO: get_specific_message_type function not needed anymore 209 | def get_specific_message_type(self, *classes_or_types, sequence_number=None): # TODO Remove this def 210 | msg = None 211 | classes_or_types = flatten_list(classes_or_types) 212 | classes_or_types = tuple(classes_or_types) 213 | 214 | while True: # todo: something better 215 | msg = self.rec.pop_message() 216 | if isinstance(msg, classes_or_types): 217 | logger.success("Got Message: {}".format(msg)) 218 | if sequence_number is not None and msg.sequence_no != sequence_number: 219 | logger.warning("Discarded Message (wrong sequence number): {}".format(msg)) 220 | msg = None 221 | else: 222 | break 223 | # end if 224 | else: 225 | logger.warning("Discarded Message (wrong type): {}".format(msg)) 226 | msg = None 227 | # end if 228 | # end while 229 | return msg 230 | # end def 231 | 232 | @property 233 | @cached(max_age=timedelta(seconds=60)) 234 | def nodes_total(self): 235 | from .dockerus import ServiceInfos 236 | return len(ServiceInfos().other_numbers(exclude_self=False)) 237 | # end def 238 | 239 | @property 240 | @cached(max_age=timedelta(seconds=60)) 241 | def nodes_faulty(self): 242 | return (self.nodes_total - 1)/3 243 | # end def 244 | 245 | @property 246 | def node_number(self): 247 | from .dockerus import ServiceInfos 248 | return ServiceInfos().number 249 | # end def 250 | 251 | def get_receiver(self): 252 | return self.rec 253 | # end def 254 | # end class 255 | -------------------------------------------------------------------------------- /code/node/dockerus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from DictObject import DictObject 4 | from luckydonaldUtils.logger import logging 5 | from luckydonaldUtils.functions import cached 6 | from luckydonaldUtils.clazzes import Singleton 7 | from docker import Client 8 | from datetime import timedelta 9 | 10 | __author__ = 'luckydonald' 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class ServiceInfos(object, metaclass=Singleton): 16 | """ 17 | Infos about a `docker-compose scale` group. 18 | """ 19 | __author__ = 'luckydonald' 20 | LABEL_COMPOSE_CONTAINER_NUMBER = 'com.docker.compose.container-number' 21 | LABEL_COMPOSE_PROJECT = 'com.docker.compose.project' 22 | LABEL_COMPOSE_SERVICE = 'com.docker.compose.service' 23 | CACHING_TIME = timedelta(seconds=5) 24 | 25 | def __init__(self, caching_time=None): 26 | """ 27 | Infos about a `docker-compose scale` group. 28 | 29 | If caching_time is not specified, the $DOCKER_CACHING_TIME environment variable will be used. 30 | If that is empty, too, it will fallback to the default CACHING_TIME of 5 seconds. 31 | 32 | :param caching_time: must be a datetime.timedelta object 33 | """ 34 | if caching_time: 35 | assert isinstance(caching_time, timedelta) 36 | self.CACHING_TIME = caching_time 37 | else: 38 | import os 39 | timedelta(seconds=float(os.environ.get("DOCKER_CACHING_TIME", ServiceInfos.CACHING_TIME.total_seconds()))) 40 | # end if 41 | # end def 42 | 43 | @property 44 | @cached 45 | def cli(self): 46 | return Client(base_url='unix://var/run/docker.sock') 47 | # end def 48 | 49 | @property 50 | @cached(max_age=max(timedelta(hours=1), CACHING_TIME)) 51 | def hostname_env(self): 52 | import os 53 | return os.environ.get("HOSTNAME") 54 | # end def 55 | 56 | @property 57 | @cached(max_age=CACHING_TIME) 58 | def me(self): 59 | return [DictObject.objectify(c) for c in self.cli.containers() if c['Id'][:12] == self.hostname_env[:12]][0] 60 | # end def 61 | 62 | @property 63 | @cached(max_age=CACHING_TIME) 64 | def id(self): 65 | return self.me.Id 66 | # end def 67 | 68 | @property 69 | @cached(max_age=CACHING_TIME) 70 | def service(self): 71 | return self.me.Labels[self.LABEL_COMPOSE_SERVICE] 72 | # end def 73 | 74 | @property 75 | @cached(max_age=CACHING_TIME) 76 | def name(self): 77 | return self.service 78 | # end def 79 | 80 | @property 81 | @cached(max_age=CACHING_TIME) 82 | def project(self): 83 | return self.me.Labels[self.LABEL_COMPOSE_PROJECT] 84 | # end def 85 | 86 | @property 87 | @cached(max_age=CACHING_TIME) 88 | def number(self): 89 | return int(self.me.Labels[self.LABEL_COMPOSE_CONTAINER_NUMBER]) 90 | # end def 91 | 92 | @cached(max_age=CACHING_TIME) 93 | def containers(self, exclude_self=False): 94 | """ 95 | Gets metadata for all containers in this scale grouping. 96 | 97 | :return: 98 | """ 99 | filters = [ 100 | '{0}={1}'.format(self.LABEL_COMPOSE_PROJECT, self.project), 101 | '{0}={1}'.format(self.LABEL_COMPOSE_SERVICE, self.service), 102 | # '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False") 103 | ] 104 | return DictObject.objectify([ 105 | c for c in self.cli.containers(filters={'label': filters}) 106 | if not (exclude_self and c['Id'][:12] == self.hostname_env[:12]) 107 | ]) 108 | # end def 109 | 110 | @property 111 | @cached(max_age=CACHING_TIME) 112 | def hostname(self): 113 | c = self.me 114 | return "{project}_{service}_{i}".format( 115 | project=c.Labels[self.LABEL_COMPOSE_PROJECT], 116 | service=c.Labels[self.LABEL_COMPOSE_SERVICE], 117 | i=c.Labels[self.LABEL_COMPOSE_CONTAINER_NUMBER] 118 | ) 119 | # end def 120 | 121 | @cached(max_age=CACHING_TIME) 122 | def other_hostnames(self, exclude_self=False): 123 | return [ 124 | "{project}_{service}_{i}".format( 125 | project=c.Labels[self.LABEL_COMPOSE_PROJECT], 126 | service=c.Labels[self.LABEL_COMPOSE_SERVICE], 127 | i=c.Labels[self.LABEL_COMPOSE_CONTAINER_NUMBER] 128 | ) for c in self.containers(exclude_self=exclude_self) 129 | 130 | ] 131 | # end def 132 | 133 | @cached(max_age=CACHING_TIME) 134 | def other_numbers(self, exclude_self=False): 135 | """ 136 | :param exclude_self: 137 | :return: 138 | """ 139 | return [ 140 | c.Labels[self.LABEL_COMPOSE_CONTAINER_NUMBER] 141 | for c in self.containers(exclude_self=exclude_self) 142 | ] 143 | # end def 144 | # end class 145 | -------------------------------------------------------------------------------- /code/node/enums.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | 7 | __all__ = ["UNSET", "INIT", "PROPOSE", "PREVOTE", "VOTE", "LEADER_CHANGE", "ACKNOWLEDGE", "all"] 8 | 9 | UNSET = 0 10 | INIT = 1 11 | PROPOSE = 2 12 | PREVOTE = 3 13 | VOTE = 4 14 | LEADER_CHANGE = 5 15 | 16 | ACKNOWLEDGE = -1 17 | 18 | all = [UNSET, INIT, PROPOSE, PREVOTE, VOTE, LEADER_CHANGE, ACKNOWLEDGE] 19 | -------------------------------------------------------------------------------- /code/node/env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from datetime import timedelta 4 | 5 | from luckydonaldUtils.logger import logging 6 | 7 | __author__ = 'luckydonald' 8 | __all__ = ["NODE_PORT", "NODES_CACHING_TIME", "DEBUGGER"] 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | NODE_PORT = int(os.environ.get("NODE_PORT", None)) 14 | 15 | 16 | # DOCKER_CACHING_TIME in seconds 17 | docker_caching_time_seconds = os.environ.get("DOCKER_CACHING_TIME", None) 18 | if docker_caching_time_seconds is None: 19 | DOCKER_CACHING_TIME = None 20 | else: 21 | DOCKER_CACHING_TIME = timedelta(seconds=float(docker_caching_time_seconds)) 22 | # end if 23 | 24 | 25 | DEBUGGER = os.environ.get("NODE_DEBUGGER", "") 26 | if DEBUGGER.lower() in ["true", "yes", "1"]: 27 | DEBUGGER = True 28 | else: 29 | DEBUGGER = False 30 | # end if 31 | 32 | DATABASE_URL = "http://api/dump/" 33 | -------------------------------------------------------------------------------- /code/node/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def flatten_list(args): 9 | if isinstance(args, tuple): 10 | args = list(args) 11 | elif not isinstance(args, list): 12 | args = [args] 13 | # end if 14 | assert isinstance(args, list) 15 | new_args = [] 16 | for arg in args: 17 | if isinstance(arg, (list,tuple)): 18 | new_args.extend(arg) 19 | else: 20 | new_args.append(arg) 21 | # end if 22 | return new_args 23 | # end def -------------------------------------------------------------------------------- /code/node/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from time import sleep 3 | 4 | from luckydonaldUtils.logger import logging, LevelByNameFilter 5 | 6 | from .algo import BFT_ARM 7 | 8 | __author__ = 'luckydonald' 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | do_quit = False 13 | 14 | 15 | def main(): 16 | algo = BFT_ARM() 17 | sequence = algo.new_sequence() 18 | receiver = algo.get_receiver() 19 | setup_cleanup(algo) 20 | 21 | logger.info("Sleeping 2 seconds to give other nodes the time to get the receiver ready.") 22 | sleep(2) 23 | 24 | while not do_quit: 25 | logger.debug("Starting new Round.") 26 | algo = BFT_ARM(sequence_number=sequence, receiver=receiver) 27 | sequence = algo.new_sequence() 28 | algo.task_normal_case() 29 | # end while 30 | logger.info("Exiting.") 31 | # end def 32 | 33 | 34 | def setup_cleanup(algo): 35 | import signal 36 | import sys 37 | assert isinstance(algo, BFT_ARM) 38 | 39 | def signal_handler(signal, frame): 40 | print('You pressed Ctrl+C!') 41 | global do_quit 42 | do_quit = True 43 | assert isinstance(algo, BFT_ARM) 44 | algo.stop() 45 | sys.exit(0) 46 | # end def 47 | signal.signal(signal.SIGINT, signal_handler) 48 | # end def 49 | 50 | 51 | def setup_logging(): 52 | filter = LevelByNameFilter(logging.WARNING, debug="node.main, node.todo, node.messages", info="node") 53 | logging.add_colored_handler(level=logging.DEBUG, date_formatter="%Y-%m-%d %H:%M:%S", filter=filter) 54 | logging.test_logger_levels() 55 | # end def 56 | -------------------------------------------------------------------------------- /code/node/message_queue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import threading 3 | from collections import deque 4 | 5 | from DictObject import DictObject 6 | import json 7 | from luckydonaldUtils.logger import logging 8 | 9 | from .messages import Message, InitMessage, ProposeMessage, PrevoteMessage, VoteMessage 10 | from .networks.receiver import Receiver 11 | 12 | __author__ = 'luckydonald' 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class LockedQueue(object): 17 | def __init__(self, clazz): 18 | assert issubclass(clazz, Message) 19 | self._clazz = clazz 20 | self._queue = deque() 21 | self._new_messages = threading.Semaphore(0) 22 | self._queue_access = threading.Lock() 23 | 24 | def pop_message(self): 25 | """ 26 | Get a message. 27 | :return: 28 | """ 29 | logger.debug("Called pop_message on {type}.".format(type=self._clazz)) 30 | self._new_messages.acquire() # waits until at least 1 message is in the queue. 31 | with self._queue_access: 32 | message = self._queue.popleft() # pop oldest item 33 | logger.debug('Messages waiting in queue: %d', len(self._queue)) 34 | if not isinstance(message, self._clazz): 35 | raise TypeError("Popped message is not type {_clazz} but type {type}:\n{msg}".format( 36 | _clazz=self._clazz, type=type(message), msg=message 37 | )) 38 | # end if 39 | assert isinstance(message, Message) 40 | assert isinstance(message, self._clazz) 41 | return message 42 | # end with 43 | # end def 44 | 45 | def get_message(self, sequence_number=None): 46 | if sequence_number is None: # no check needed: 47 | return self.pop_message() 48 | # end if 49 | msg = None 50 | while msg is None: 51 | msg = self.pop_message() 52 | if msg.sequence_no != sequence_number: 53 | logger.warning("Discarded Message, wrong sequence number ({a} instead of {b}): {msg}".format( 54 | a=msg.sequence_no, b=sequence_number, msg=msg 55 | )) 56 | msg = None 57 | # end if 58 | # end while 59 | assert isinstance(msg, Message) 60 | assert isinstance(msg, self._clazz) 61 | return msg 62 | # end def 63 | 64 | def append_message(self, message): 65 | if not isinstance(message, self._clazz): 66 | raise TypeError("Given message is not type {_clazz} but type {type}:\n{msg}".format( 67 | _clazz=self._clazz, type=type(message), msg=message 68 | )) 69 | # end if 70 | with self._queue_access: 71 | self._queue.append(message) 72 | # end if 73 | self._new_messages.release() 74 | # end def 75 | 76 | def queue_length(self): 77 | with self._queue_access: 78 | return len(self._queue) 79 | # end with 80 | # end def 81 | 82 | __len__ = queue_length 83 | 84 | def has_message(self): 85 | with self._queue_access: 86 | return len(self._queue) > 0 87 | # end with 88 | # end def 89 | # end class 90 | 91 | 92 | class MessageQueueReceiver(Receiver): 93 | init_queue = LockedQueue(InitMessage) 94 | propose_queue = LockedQueue(ProposeMessage) 95 | prevote_queue = LockedQueue(PrevoteMessage) 96 | vote_queue = LockedQueue(VoteMessage) 97 | 98 | pop_message = None # the function 99 | 100 | def _add_message(self, text): 101 | """ 102 | Appends a message to the message queue. 103 | 104 | :type text: builtins.str 105 | :return: 106 | """ 107 | try: 108 | logger.debug("Received Message: \"{str}\"".format(str=text)) 109 | json_dict = json.loads(text) 110 | message = DictObject.objectify(json_dict) 111 | message = self.parse_message(message) 112 | except ValueError as e: 113 | logger.warn("Received message could not be parsed.\nMessage:>{}<".format(text), exc_info=True) 114 | return 115 | if isinstance(message, InitMessage): 116 | self.init_queue.append_message(message) 117 | elif isinstance(message, ProposeMessage): 118 | self.propose_queue.append_message(message) 119 | elif isinstance(message, PrevoteMessage): 120 | self.prevote_queue.append_message(message) 121 | elif isinstance(message, VoteMessage): 122 | self.vote_queue.append_message(message) 123 | else: 124 | logger.warning("Discarded unknown message type: {msg_type}, {msg}".format( 125 | msg_type=type(message), msg=message 126 | )) 127 | # end if 128 | # end def 129 | # end class 130 | -------------------------------------------------------------------------------- /code/node/messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | from .enums import UNSET, INIT, LEADER_CHANGE, PROPOSE, PREVOTE, VOTE, ACKNOWLEDGE 5 | 6 | __author__ = 'luckydonald' 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Message(object): 11 | def __init__(self, type, sequence_no, node): 12 | if type is None: 13 | type = UNSET 14 | # end if 15 | assert isinstance(type, int) 16 | self.type = type 17 | self.sequence_no = sequence_no 18 | self.node = node # i 19 | 20 | 21 | @classmethod 22 | def from_dict(cls, data): 23 | assert "type" in data 24 | type = data["type"] 25 | assert type in [UNSET, INIT, LEADER_CHANGE, PROPOSE, PREVOTE, VOTE, ACKNOWLEDGE] 26 | if type == INIT: 27 | return InitMessage.from_dict(data) 28 | # end def 29 | if type == LEADER_CHANGE: # pragma: no cover 30 | return LeaderChangeMessage.from_dict(data) 31 | # end def 32 | if type == PROPOSE: 33 | return ProposeMessage.from_dict(data) 34 | # end def 35 | if type == PREVOTE: 36 | return PrevoteMessage.from_dict(data) 37 | # end def 38 | if type == VOTE: 39 | return VoteMessage.from_dict(data) 40 | # end def 41 | if type == ACKNOWLEDGE: 42 | return Acknowledge.from_dict(data) 43 | # end def 44 | return cls(**{ 45 | "type": data["type"], 46 | "sequence_no": data["sequence_no"], 47 | "node": data["node"] 48 | }) 49 | # end def 50 | 51 | def to_dict(self): 52 | return { 53 | "type": self.type, 54 | "sequence_no": self.sequence_no, 55 | "node": self.node 56 | } 57 | # end def 58 | 59 | def __str__(self): 60 | data = self.to_dict() 61 | return "{class_name}({values})".format( 62 | class_name=self.__class__.__name__, 63 | values=", ".join(["{key}={value!r}".format(key=k, value=data[k]) for k in sorted(data)]) 64 | ) 65 | # end class 66 | 67 | 68 | class InitMessage(Message): 69 | def __init__(self, sequence_no, node, value): 70 | super(InitMessage, self).__init__(INIT, sequence_no, node) 71 | self.value = value # vi 72 | # end def 73 | 74 | @classmethod 75 | def from_dict(cls, data): 76 | kwargs = { 77 | "sequence_no": data["sequence_no"], 78 | "node": data["node"], 79 | "value": data["value"], 80 | } 81 | return cls(**kwargs) 82 | # end def 83 | 84 | def to_dict(self): 85 | data = super().to_dict() 86 | data["value"] = self.value 87 | return data 88 | # end def 89 | # end class 90 | 91 | 92 | class LeaderChangeMessage(Message): # pragma: no cover 93 | def __init__(self, sequence_no, node_num, leader, P): 94 | raise NotImplementedError("LeaderChangeMessage") 95 | super(LeaderChangeMessage, self).__init__(LEADER_CHANGE, sequence_no) 96 | # end def 97 | 98 | @classmethod 99 | def from_dict(cls, data): 100 | raise NotImplementedError("LeaderChangeMessage") 101 | kwargs = { 102 | "type": data["type"], 103 | "sequence_no": data["sequence_no"], 104 | } 105 | return cls(**kwargs) 106 | # end def 107 | 108 | def to_dict(self): 109 | raise NotImplementedError("LeaderChangeMessage") 110 | return { 111 | "type": self.type, 112 | "sequence_no": self.sequence_no, 113 | } 114 | # end def 115 | # end class 116 | 117 | 118 | class ProposeMessage(Message): 119 | def __init__(self, sequence_no, node, leader, proposal, value_store): 120 | super(ProposeMessage, self).__init__(PROPOSE, sequence_no, node) 121 | self.leader = leader 122 | self.proposal = proposal 123 | assert isinstance(value_store, list) 124 | self.value_store = value_store 125 | 126 | @classmethod 127 | def from_dict(cls, data): 128 | value_store = [] 129 | for v in data.get("value_store", []): 130 | msg = InitMessage.from_dict(v) 131 | # value_store[msg.node] = msg 132 | value_store.append(msg) 133 | # end for 134 | kwargs = { 135 | "sequence_no": data["sequence_no"], 136 | "node": data.get("node"), 137 | "leader": data.get("leader"), 138 | "proposal": data.get("proposal"), 139 | "value_store": value_store 140 | } 141 | return cls(**kwargs) 142 | # end def 143 | 144 | def to_dict(self): 145 | data = super().to_dict() 146 | data["leader"] = self.leader 147 | data["proposal"] = self.proposal 148 | data["value_store"] = [x.to_dict() if hasattr(x, "to_dict") else x for x in self.value_store] 149 | return data 150 | # end def 151 | # end class 152 | 153 | 154 | class PrevoteMessage(Message): 155 | def __init__(self, sequence_no, node, leader, value): 156 | super().__init__(PREVOTE, sequence_no, node) 157 | self.leader = leader 158 | self.value = value 159 | # end if 160 | 161 | @classmethod 162 | def from_dict(cls, data): 163 | kwargs = { 164 | "sequence_no": data["sequence_no"], 165 | "node": data["node"], 166 | "leader": data["leader"], 167 | "value": data["value"], 168 | } 169 | return cls(**kwargs) 170 | 171 | # end def 172 | 173 | def to_dict(self): 174 | data = super().to_dict() 175 | data["leader"] = self.leader 176 | data["value"] = self.value 177 | return data 178 | # end def 179 | # end class 180 | 181 | 182 | class VoteMessage(Message): 183 | def __init__(self, sequence_no, node, leader, value): 184 | super().__init__(VOTE, sequence_no, node) 185 | self.leader = leader 186 | self.value = value 187 | # end def 188 | 189 | @classmethod 190 | def from_dict(cls, data): 191 | kwargs = { 192 | "sequence_no": data["sequence_no"], 193 | "node": data["node"], 194 | "leader": data["leader"], 195 | "value": data["value"], 196 | } 197 | return cls(**kwargs) 198 | # end def 199 | 200 | def to_dict(self): 201 | data = super().to_dict() 202 | data["leader"] = self.leader 203 | data["value"] = self.value 204 | return data 205 | # end def 206 | # end class 207 | 208 | 209 | class NewLeaderMessage(Message): # pragma: no cover 210 | def __init__(self, sequence_no, node, leader, value): 211 | super().__init__(VOTE, sequence_no, node) 212 | self.leader = leader 213 | self.value = value 214 | # end def 215 | # end class 216 | 217 | 218 | class Acknowledge(Message): 219 | def __init__(self, sequence_no, node, sender, raw): 220 | super().__init__(ACKNOWLEDGE, sequence_no, node) 221 | self.sender = sender 222 | self.raw = raw 223 | # end def 224 | 225 | @classmethod 226 | def from_dict(cls, data): 227 | kwargs = { 228 | "sequence_no": data["sequence_no"], 229 | "node": data["node"], 230 | "sender": data["sender"], 231 | "raw": data["raw"], 232 | } 233 | return cls(**kwargs) 234 | # end def 235 | 236 | def to_dict(self): 237 | data = super().to_dict() 238 | data["sender"] = self.sender 239 | data["raw"] = self.raw 240 | return data 241 | # end def 242 | # end class -------------------------------------------------------------------------------- /code/node/networks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | -------------------------------------------------------------------------------- /code/node/networks/receiver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import threading 4 | from collections import deque 5 | 6 | from DictObject import DictObject 7 | from luckydonaldUtils.logger import logging 8 | from luckydonaldUtils.encoding import to_binary as b 9 | from luckydonaldUtils.encoding import to_native as n 10 | import socket 11 | 12 | from ..messages import Message 13 | from ..dockerus import ServiceInfos 14 | 15 | __author__ = 'luckydonald' 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | _EMPTY_RAW_BYTE = b("") 20 | _ANSWER_SYNTAX = b("ANSWER ") 21 | _LINE_BREAK = b("\n") 22 | 23 | 24 | class Receiver(object): 25 | _queue = deque() 26 | _new_messages = threading.Semaphore(0) 27 | _queue_access = threading.Lock() 28 | 29 | def __init__(self): 30 | self._do_quit = False 31 | self.s = None # socket 32 | self.client = None 33 | # end def 34 | 35 | def __receiver_logging_wrapper(self): 36 | try: 37 | self._receiver() 38 | except Exception: 39 | logger.exception("Receiver failed. Exited.") 40 | # end try 41 | # end def 42 | 43 | def _receiver(self): 44 | from ..env import NODE_PORT 45 | from errno import ECONNREFUSED 46 | 47 | logger.info("Starting receiver on {host}:{port}".format(host=ServiceInfos().hostname, port=NODE_PORT)) 48 | while not self._do_quit: # retry connection 49 | self.s = socket.socket(socket.AF_INET, # Internet 50 | socket.SOCK_STREAM) # TCP 51 | try: 52 | self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 53 | self.s.bind((ServiceInfos().hostname, NODE_PORT)) 54 | self.s.listen(5) 55 | logger.debug("Socket Set up.") 56 | while not self._do_quit and self.s: 57 | self.client, address = self.s.accept() 58 | 59 | buffer = _EMPTY_RAW_BYTE 60 | answer = _EMPTY_RAW_BYTE 61 | completed = -1 # -1 = answer size yet unknown, >0 = got remaining answer size 62 | while (not self._do_quit) and self.s and self.client: # read loop 63 | while 1: # retry if CTRL+C'd 64 | try: 65 | # self.s.setblocking(True) 66 | answer = self.client.recv(1) 67 | # recv() returns an empty string if the remote end is closed 68 | if len(answer) == 0: 69 | logger.debug("Remote end closed.") 70 | self.reset_client() 71 | # end if 72 | # logger.debug("received byte: {}".format(answer)) 73 | break 74 | except socket.error as err: 75 | if self._do_quit: 76 | self.reset_client() 77 | # end if 78 | from errno import EINTR 79 | if err.errno != EINTR: # interrupted system call 80 | raise 81 | else: 82 | logger.exception( 83 | "Uncatched exception in reading message from {client}.".format(client=address) 84 | ) 85 | self.reset_client() 86 | break # to the retry connection look again. 87 | # end if 88 | # end try 89 | # end while: ctrl+c protection 90 | if not self.s or not self.client: # check if socket is still open 91 | break 92 | if completed == 0: 93 | logger.debug("Hit end.") 94 | if answer != _LINE_BREAK: 95 | raise ValueError("Message does not end with a double linebreak.") 96 | if buffer == _EMPTY_RAW_BYTE: 97 | logger.debug("skipping second linebreak.") 98 | completed = -1 99 | continue 100 | logger.debug( 101 | "Received Message from {client}: {buffer}".format(client=address, buffer=buffer) 102 | ) 103 | text = n(buffer) 104 | if len(text) > 0 and text.strip() != "": 105 | self._add_message(text) 106 | else: 107 | logger.warn("Striped text was empty.") 108 | answer = _EMPTY_RAW_BYTE 109 | buffer = _EMPTY_RAW_BYTE 110 | # completed = 0 (unchanged) 111 | continue 112 | buffer += answer 113 | if completed < -1 and buffer[:len(_ANSWER_SYNTAX)] != _ANSWER_SYNTAX[:len(buffer)]: 114 | raise ArithmeticError("Server response does not fit. (Got >{}<)".format(buffer)) 115 | if completed <= -1 and buffer.startswith(_ANSWER_SYNTAX) and buffer.endswith(_LINE_BREAK): 116 | completed = int(n(buffer[len(_ANSWER_SYNTAX):-1])) # TODO regex. 117 | buffer = _EMPTY_RAW_BYTE 118 | completed -= 1 119 | # end while: read loop 120 | # end while: for connected clients 121 | except socket.error as error: 122 | # if error.errno in [ECONNREFUSED] and not self._do_quit: 123 | # continue 124 | # # end if 125 | logger.error("Socket failed with network error: {e}\nRetrying...".format(e=error)) 126 | except Exception as error: 127 | logger.error("Socket failed: {e}\nRetrying...".format(e=error)) 128 | # end try 129 | self.reset_socket() 130 | # end while not ._do_quit: retry connection 131 | self.reset_socket() 132 | # end def 133 | 134 | def reset_client(self): 135 | if self.client: 136 | self.client.close() 137 | self.client = None 138 | # end if 139 | # end def 140 | 141 | def reset_socket(self): 142 | self.reset_client() 143 | if self.s: 144 | self.s.close() 145 | self.s = None 146 | # end if 147 | # end def 148 | 149 | def _add_message(self, text): 150 | """ 151 | Appends a message to the message queue. 152 | 153 | :type text: builtins.str 154 | :return: 155 | """ 156 | try: 157 | logger.debug("Received Message: \"{str}\"".format(str=text)) 158 | json_dict = json.loads(text) 159 | message = DictObject.objectify(json_dict) 160 | message = self.parse_message(message) 161 | except ValueError as e: 162 | logger.warn("Received message could not be parsed.\nMessage:>{}<".format(text), exc_info=True) 163 | return 164 | with self._queue_access: 165 | self._queue.append(message) 166 | self._new_messages.release() 167 | # end with 168 | # end def 169 | 170 | def parse_message(self, dict): 171 | return Message.from_dict(dict) 172 | # end def 173 | 174 | def start(self): 175 | """ 176 | Starts the receiver. 177 | When started, messages will be queued. 178 | :return: 179 | """ 180 | self._receiver_thread = threading.Thread(name="Receiver", target=self.__receiver_logging_wrapper, args=()) 181 | self._receiver_thread.daemon = True # exit if script reaches end. 182 | self._receiver_thread.start() 183 | logger.success("Started Receiver Thread.") 184 | # end def 185 | 186 | def stop(self): 187 | """ 188 | Shuts down the receivers server. 189 | No more messages will be received. 190 | You should not try to start() it again afterwards. 191 | """ 192 | self._do_quit = True 193 | if self.client: 194 | self.s.settimeout(0) 195 | if self.client: 196 | self.client.close() 197 | if self.s: 198 | self.s.settimeout(0) 199 | if self.s: 200 | self.s.close() 201 | if hasattr(self, "_receiver_thread"): 202 | logger.debug("receiver thread existing: {}".format(self._receiver_thread.isAlive())) 203 | else: 204 | logger.debug("receiver thread existing: Not created.") 205 | # end if 206 | 207 | def pop_message(self): 208 | """ 209 | Get a message. 210 | :return: 211 | """ 212 | self._new_messages.acquire() # waits until at least 1 message is in the queue. 213 | with self._queue_access: 214 | message = self._queue.popleft() # pop oldest item 215 | logger.debug('Messages waiting in queue: %d', len(self._queue)) 216 | assert isinstance(message, Message) 217 | return message 218 | # end with 219 | # end def 220 | -------------------------------------------------------------------------------- /code/node/networks/sender.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import socket 3 | from time import sleep 4 | 5 | from luckydonaldUtils.logger import logging 6 | 7 | from ..env import NODE_PORT, DATABASE_URL 8 | from ..messages import Message 9 | from ..todo import logger 10 | 11 | __author__ = 'luckydonald' 12 | logger = logging.getLogger(__name__) 13 | 14 | MSG_FORMAT = "ANSWER {length}\n{msg}\n" 15 | 16 | 17 | def send_message(msg): 18 | import json 19 | import requests 20 | logger.debug(msg) 21 | assert isinstance(msg, Message) 22 | data = msg.to_dict() 23 | data_string = json.dumps(data) 24 | broadcast(data_string) 25 | loggert = logging.getLogger("request") 26 | def print_url(r, *args, **kwargs): 27 | loggert.info(r.url) 28 | # end def 29 | while (True): 30 | try: 31 | requests.put(DATABASE_URL, data=data_string, hooks=dict(response=print_url)) 32 | break 33 | except requests.RequestException as e: 34 | logger.warning("Failed to report message to db: {e}".format(e=e)) 35 | # end def 36 | return 37 | # end def 38 | 39 | 40 | def broadcast(message): 41 | from ..dockerus import ServiceInfos 42 | if not isinstance(message, str): 43 | raise TypeError("Parameter `message` is not type `str` but {type}: {msg}".format(type=type(message), msg=message)) 44 | hosts = ServiceInfos().other_hostnames() 45 | # msg = MSG_FORMAT.format(length=len(message), msg=message) 46 | message += "\n" 47 | msg = "ANSWER " + str(len(message)) + "\n" + message 48 | logger.debug("Prepared sending to *:{port}:\n{msg}".format(port=NODE_PORT, msg=msg)) 49 | msg = bytes(msg, "utf-8") 50 | for node_host in hosts: 51 | sent = -1 52 | while not sent == 1: 53 | try: 54 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # UDP SOCK_DGRAM 55 | sock.connect((node_host, NODE_PORT)) 56 | sock.sendall(msg) 57 | logger.log( 58 | msg="Sending to {host}:{port} succeeded.".format(host=node_host, port=NODE_PORT), 59 | level=(logging.SUCCESS if sent == 0 else logging.DEBUG) 60 | ) 61 | sent = 1 62 | # end with 63 | except OSError as e: 64 | logger.error("Sending to {host}:{port} failed: {e} Retrying...".format(e=e, host=node_host, port=NODE_PORT)) 65 | sleep(0.1) 66 | sent = 0 67 | # end try 68 | # end while 69 | # end for 70 | # end def -------------------------------------------------------------------------------- /code/node/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | import unittest 5 | from .messages import Message, InitMessage, ProposeMessage, PrevoteMessage, VoteMessage, Acknowledge 6 | from .enums import UNSET, INIT, PROPOSE, PREVOTE, VOTE, ACKNOWLEDGE 7 | 8 | __author__ = 'luckydonald' 9 | __all__ = ["TestJsonToObject"] 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class TestJsonToObject(unittest.TestCase): 15 | data_InitMessage = { 16 | "type": INIT, 17 | "sequence_no": 12, 18 | "node": 1, 19 | "value": 0.5, 20 | } 21 | 22 | def check_InitMessage(self, msg, data): 23 | self.assertIsInstance(msg, InitMessage) 24 | self.assertEqual(msg.type, data["type"]) 25 | self.assertEqual(msg.sequence_no, data["sequence_no"]) 26 | self.assertEqual(msg.node, data["node"]) 27 | self.assertEqual(msg.value, data["value"]) 28 | # end def 29 | 30 | def test_InitMessage_toObject(self): 31 | msg = InitMessage.from_dict(self.data_InitMessage) 32 | self.check_InitMessage(msg, self.data_InitMessage) 33 | # end def 34 | 35 | def test_Message_toObject_InitMessage(self): 36 | msg = Message.from_dict(self.data_InitMessage) 37 | self.check_InitMessage(msg, self.data_InitMessage) 38 | # end def 39 | 40 | def test_InitMessage_toDict(self): 41 | data = self.data_InitMessage 42 | msg = InitMessage(data["sequence_no"], data["node"], data["value"]) 43 | self.check_InitMessage(msg, self.data_InitMessage) 44 | self.assertDictEqual(msg.to_dict(), data) 45 | # end def 46 | 47 | def test_InitMessage_toString(self): 48 | msg = Message.from_dict(self.data_InitMessage) 49 | self.assertEqual("InitMessage(node=1, sequence_no=12, type=1, value=0.5)", str(msg)) 50 | # end def 51 | 52 | data_ProposeMessage = { 53 | "type": PROPOSE, 54 | "sequence_no": 12, 55 | "node": 1, 56 | "leader": 1, 57 | "proposal": 0.5, 58 | "value_store": [], 59 | } 60 | 61 | def check_ProposeMessage(self, msg): 62 | self.assertIsInstance(msg, ProposeMessage) 63 | self.assertEqual(msg.type, self.data_ProposeMessage["type"]) 64 | self.assertEqual(msg.sequence_no, self.data_ProposeMessage["sequence_no"]) 65 | self.assertEqual(msg.node, self.data_ProposeMessage["node"]) 66 | self.assertEqual(msg.leader, self.data_ProposeMessage["leader"]) 67 | self.assertEqual(msg.proposal, self.data_ProposeMessage["proposal"]) 68 | # end def 69 | 70 | def test_ProposeMessage_toObject(self): 71 | msg = ProposeMessage.from_dict(self.data_ProposeMessage) 72 | self.check_ProposeMessage(msg) 73 | self.assertListEqual(msg.value_store, []) 74 | # end def 75 | 76 | def test_Message_toObject_ProposeMessage(self): 77 | msg = Message.from_dict(self.data_ProposeMessage) 78 | self.check_ProposeMessage(msg) 79 | self.assertListEqual(msg.value_store, []) 80 | # end def 81 | 82 | def test_ProposeMessage_toDict(self): 83 | data = self.data_ProposeMessage 84 | msg = ProposeMessage(data["sequence_no"], data["node"], data["leader"], data["proposal"], []) 85 | self.check_ProposeMessage(msg) 86 | self.assertListEqual(msg.value_store, []) 87 | self.assertDictEqual(msg.to_dict(), data) 88 | # end def 89 | 90 | def test_ProposeMessage_toString(self): 91 | msg = Message.from_dict(self.data_ProposeMessage) 92 | self.assertEqual("ProposeMessage(leader=1, node=1, proposal=0.5, sequence_no=12, type=2, value_store=[])", str(msg)) 93 | # end def 94 | 95 | data_ProposeMessage_with_InitMessage = { 96 | "type": PROPOSE, 97 | "sequence_no": 12, 98 | "node": 1, 99 | "leader": 1, 100 | "proposal": 0.5, 101 | "value_store": [data_InitMessage], 102 | } 103 | 104 | def test_ProposeMessage_with_InitMessage(self): 105 | msg = Message.from_dict(self.data_ProposeMessage_with_InitMessage) 106 | self.check_ProposeMessage(msg) 107 | self.assertEqual(len(msg.value_store), 1) 108 | self.check_InitMessage(msg.value_store[0], self.data_ProposeMessage_with_InitMessage["value_store"][0], ) 109 | # end def 110 | 111 | data_PrevoteMessage = { 112 | "type": PREVOTE, 113 | "sequence_no": 12, 114 | "node": 1, 115 | "leader": 1, 116 | "value": 0.5, 117 | } 118 | 119 | def check_PrevoteMessage(self, msg): 120 | self.assertIsInstance(msg, PrevoteMessage) 121 | self.assertEqual(msg.type, self.data_PrevoteMessage["type"]) 122 | self.assertEqual(msg.sequence_no, self.data_PrevoteMessage["sequence_no"]) 123 | self.assertEqual(msg.node, self.data_PrevoteMessage["node"]) 124 | self.assertEqual(msg.leader, self.data_PrevoteMessage["leader"]) 125 | self.assertEqual(msg.value, self.data_PrevoteMessage["value"]) 126 | # end def 127 | 128 | def test_PrevoteMessage_toObject(self): 129 | msg = PrevoteMessage.from_dict(self.data_PrevoteMessage) 130 | self.check_PrevoteMessage(msg) 131 | # end def 132 | 133 | def test_Message_toObject_PrevoteMessage(self): 134 | msg = Message.from_dict(self.data_PrevoteMessage) 135 | self.check_PrevoteMessage(msg) 136 | # end def 137 | 138 | def test_PrevoteMessage_toDict(self): 139 | data = self.data_PrevoteMessage 140 | msg = PrevoteMessage(data["sequence_no"], data["node"], data["leader"], data["value"]) 141 | self.check_PrevoteMessage(msg) 142 | self.assertDictEqual(msg.to_dict(), data) 143 | # end def 144 | 145 | def test_PrevoteMessage_toString(self): 146 | msg = Message.from_dict(self.data_PrevoteMessage) 147 | self.assertEqual("PrevoteMessage(leader=1, node=1, sequence_no=12, type=3, value=0.5)", str(msg)) 148 | # end def 149 | 150 | data_VoteMessage = { 151 | "type": VOTE, 152 | "sequence_no": 12, 153 | "node": 1, 154 | "leader": 1, 155 | "value": 0.5, 156 | } 157 | 158 | def check_VoteMessage(self, msg): 159 | self.assertIsInstance(msg, VoteMessage) 160 | self.assertEqual(msg.type, self.data_VoteMessage["type"]) 161 | self.assertEqual(msg.sequence_no, self.data_VoteMessage["sequence_no"]) 162 | self.assertEqual(msg.node, self.data_VoteMessage["node"]) 163 | self.assertEqual(msg.leader, self.data_VoteMessage["leader"]) 164 | self.assertEqual(msg.value, self.data_VoteMessage["value"]) 165 | # end def 166 | 167 | def test_VoteMessage_toObject(self): 168 | msg = VoteMessage.from_dict(self.data_VoteMessage) 169 | self.check_VoteMessage(msg) 170 | # end def 171 | 172 | def test_Message_toObject_VoteMessage(self): 173 | msg = Message.from_dict(self.data_VoteMessage) 174 | self.check_VoteMessage(msg) 175 | # end def 176 | 177 | def test_VoteMessage_toDict(self): 178 | data = self.data_VoteMessage 179 | msg = VoteMessage(data["sequence_no"], data["node"], data["leader"], data["value"]) 180 | self.check_VoteMessage(msg) 181 | self.assertDictEqual(msg.to_dict(), data) 182 | # end def 183 | 184 | def test_VoteMessage_toString(self): 185 | msg = Message.from_dict(self.data_VoteMessage) 186 | self.assertEqual("VoteMessage(leader=1, node=1, sequence_no=12, type=4, value=0.5)", str(msg)) 187 | # end def 188 | 189 | data_Acknowledge = { 190 | "type": ACKNOWLEDGE, 191 | "sequence_no": 12, 192 | "node": 1, 193 | "sender": 1, 194 | "raw": {}, 195 | } 196 | 197 | def check_Acknowledge(self, msg): 198 | self.assertIsInstance(msg, Acknowledge) 199 | self.assertEqual(msg.type, self.data_Acknowledge["type"]) 200 | self.assertEqual(msg.sequence_no, self.data_Acknowledge["sequence_no"]) 201 | self.assertEqual(msg.node, self.data_Acknowledge["node"]) 202 | self.assertEqual(msg.sender, self.data_Acknowledge["sender"]) 203 | self.assertDictEqual(msg.raw, self.data_Acknowledge["raw"]) 204 | # end def 205 | 206 | def test_Acknowledge_toObject(self): 207 | msg = Acknowledge.from_dict(self.data_Acknowledge) 208 | self.check_Acknowledge(msg) 209 | # end def 210 | 211 | def test_Message_Acknowledge_toObject(self): 212 | msg = Message.from_dict(self.data_Acknowledge) 213 | self.check_Acknowledge(msg) 214 | # end def 215 | 216 | def test_Acknowledge_toDict(self): 217 | data = self.data_Acknowledge 218 | msg = Acknowledge(data["sequence_no"], data["node"], data["sender"], data["raw"]) 219 | self.check_Acknowledge(msg) 220 | self.assertDictEqual(msg.to_dict(), data) 221 | # end def 222 | 223 | def test_Acknowledge_toString(self): 224 | msg = Message.from_dict(self.data_Acknowledge) 225 | self.assertEqual("Acknowledge(node=1, raw={}, sender=1, sequence_no=12, type=-1)", str(msg)) 226 | # end def 227 | 228 | data_unknown_type1 = { 229 | "type": UNSET, 230 | "sequence_no": 12, 231 | "node": 1, 232 | "foo": "bar", 233 | "best_pony": "Littlepip" 234 | } 235 | 236 | def test_Init_unknown_type1_toObject(self): 237 | data = self.data_unknown_type1 238 | msg = Message.from_dict(data) 239 | self.assertIsInstance(msg, Message) 240 | self.assertEqual(msg.type, data["type"]) 241 | self.assertEqual(msg.sequence_no, data["sequence_no"]) 242 | # self.assertEqual(msg.node, data["node"]) # TODO put node to super, into Message 243 | # end def 244 | 245 | data_unknown_type2 = { 246 | "type": 4458, # <- this is different to data_unknown_type1 247 | "sequence_no": 12, 248 | "node": 1, 249 | "foo": "bar", 250 | "best_pony": "Littlepip" 251 | } 252 | 253 | def test_Init_unknown_type2_toObject(self): 254 | self.assertRaises(AssertionError, Message.from_dict, self.data_unknown_type2) 255 | # end def 256 | 257 | def test_Message_new(self): 258 | # data is inline 259 | msg = Message(None, 12, 1) 260 | self.assertIsInstance(msg, Message) 261 | self.assertEqual(UNSET, msg.type) 262 | self.assertEqual(12, msg.sequence_no) 263 | # end def 264 | 265 | def test_Message_new_toString(self): 266 | msg = Message(None, 12, 1) 267 | self.assertEqual("Message(node=1, sequence_no=12, type=0)", str(msg)) 268 | # end def 269 | 270 | # end class 271 | 272 | 273 | if __name__ == '__main__': # pragma: no cover 274 | unittest.main() -------------------------------------------------------------------------------- /code/node/todo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from luckydonaldUtils.logger import logging 3 | 4 | __author__ = 'luckydonald' 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def get_sensor_value(): 9 | return 0.5 10 | # import random 11 | # return round(random.uniform(0.0, 1.0), 1) 12 | # end def 13 | 14 | 15 | 16 | def timeout(): 17 | return False 18 | # end def -------------------------------------------------------------------------------- /code/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from node.tests import * 3 | 4 | 5 | if __name__ == '__main__': # pragma: no cover 6 | unittest.main() 7 | # end if -------------------------------------------------------------------------------- /code/web/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "/app/bower_components" 3 | } -------------------------------------------------------------------------------- /code/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | EXPOSE 8000 3 | 4 | # Code Dir 5 | RUN mkdir -p /app/code 6 | WORKDIR /app/ 7 | 8 | # Node lib dir 9 | RUN npm config set prefix /app/libs 10 | 11 | #RUN echo $PATH 12 | ENV PATH /app/libs/bin:$PATH 13 | RUN echo $PATH 14 | 15 | # code install 16 | RUN npm install -g bower 17 | RUN npm install -g http-server 18 | 19 | ADD package.json /app 20 | 21 | # Bower install 22 | ADD .bowerrc /app 23 | ADD bower.json /app 24 | RUN bower install --allow-root 25 | 26 | ADD ./src /app/src 27 | ADD ./example /app/example 28 | COPY ./entrypoint.sh /entrypoint.sh 29 | RUN chmod +x /entrypoint.sh 30 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /code/web/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pbft-gui", 3 | "description": "A starter project for AngularJS", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/angular/angular-seed", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "angular": "~1.5.0", 10 | "angular-route": "1.5.0", 11 | "angular-loader": "1.5.0", 12 | "angular-mocks": "1.5.0", 13 | "angular-resource": "1.5.0", 14 | "angular-animate": "1.5.x", 15 | "html5-boilerplate": "^5.3.0", 16 | "bootstrap": "3.3.x", 17 | "jquery": "2.2.x", 18 | "d3": "~3.4", 19 | "highcharts": "5.0.7", 20 | "tooltipster": "4.2.3", 21 | "svg.js": "2.5.0", 22 | "svg.screenbbox.js": "0.1.2" 23 | }, 24 | "resolutions": { 25 | "angular": "~1.5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/web/d3test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | D3 Test n stuff 6 | 7 | 31 | 32 | 33 |

Click me to randomize (add, delete, update) values.

34 |

Click me to add a value.

35 |

Click me to delete a value.

36 | 391 | 392 | -------------------------------------------------------------------------------- /code/web/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | build: 5 | context: . 6 | dockerfile: ./Dockerfile 7 | ports: 8 | - "8000:8000" 9 | entrypoint: http-server -p 8000 -c-1 /app/ 10 | # interactive: true 11 | #environment: 12 | # DOCKER_CACHING_TIME: 5 13 | # NODE_PORT: 4458 14 | # NODE_DEBUGGER: 0 15 | -------------------------------------------------------------------------------- /code/web/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e; 3 | 4 | # first use first program parameter, if not set use $PORT, if that is undefined use default 8000. 5 | PORT=${PORT:-8000} 6 | PORT=${1:-$PORT} 7 | API_URL=${API_URL:-http://localhost} 8 | JS_PATH=${JS_PATH:-/app/} 9 | 10 | SECRET_FILE='src/config.js' 11 | echo "'use strict';" > $SECRET_FILE; 12 | echo "" >> $SECRET_FILE; 13 | echo "var _API_URL = \"$API_URL\";" >> $SECRET_FILE; 14 | 15 | echo "Starting server on port $PORT (\$PORT), serving path \"$JS_PATH\" (\$JS_PATH)." 16 | echo "The browser will connect to the API at \"$API_URL\" (\$API_URL)." 17 | 18 | COMMAND="http-server -p $PORT -c-1 $JS_PATH" 19 | echo "\$ $COMMAND" 20 | 21 | $COMMAND -------------------------------------------------------------------------------- /code/web/example/nodes.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "value": 0.5, 4 | "primary": true 5 | }, { 6 | "id": 2, 7 | "value": 0.6, 8 | "primary": false 9 | }, { 10 | "id": 3, 11 | "value": 0.5, 12 | "primary": false 13 | }, { 14 | "id": 4, 15 | "value": 0.5, 16 | "primary": false 17 | }, { 18 | "id": "summary", 19 | "value": 0.5 20 | }] -------------------------------------------------------------------------------- /code/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | pbft gui 7 | 8 | 9 | 10 | 11 | 12 |
13 | 17 |
18 |
19 |
20 |

Summary

21 |
Agreed Value:
22 | ??? 23 |
24 |
25 |

Node 1

26 |
Value:
27 | ??? 28 |
29 |
30 |

Node 2

31 |
Value:
32 | ??? 33 |
34 |
35 |

Node 3

36 |
Value:
37 | ??? 38 |
39 |
40 |

Node 4

41 |
Value:
42 | ??? 43 |
44 |
45 |
46 | 49 |
50 | 51 | -------------------------------------------------------------------------------- /code/web/js.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by luckydonald on 25/10/16. 3 | */ 4 | 5 | $( document ).ready(function(){ 6 | setInterval( function () { 7 | $.getJSON("http://localhost/get_value/", function (data) { //TODO host 8 | var container = $("#nodearea"); 9 | container.empty(); 10 | console.log(data); 11 | var node = $("
"); 12 | node.addClass("node").addClass("summary"); 13 | node.append($("

").text("Summary")); 14 | node.append("

Agreed Value:
"); 15 | if("summary" in data) { 16 | node.append($("").text(data["summary"]).addClass("value")); 17 | } else { 18 | node.append($("").text("No recent agreement").addClass("value")); 19 | } 20 | container.prepend(node); 21 | Object.keys(data).forEach(function (key, value) { 22 | if (key === "summary") { 23 | return; 24 | } 25 | var node = $("
"); 26 | node.addClass("node"); 27 | node.append($("

").text("Node " + key)); 28 | node.append("

Value:
"); 29 | node.append($("").text(this[key]).addClass("value")); 30 | container.append(node); 31 | }, data); 32 | }); 33 | }, 100); 34 | }); -------------------------------------------------------------------------------- /code/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pbft-gui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "Web GUI for PBFT", 6 | "repository": "https://github.com/luckydonald/pbft", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "bower": "^1.7.7", 10 | "http-server": "^0.9.0" 11 | }, 12 | "scripts": { 13 | "postinstall": "bower install", 14 | 15 | "prestart": "npm install", 16 | "start": "http-server -p 8000 -c-1 /code" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /code/web/src/app.animations.css: -------------------------------------------------------------------------------- 1 | .view-frame { 2 | position: relative; 3 | } 4 | .view-frame.ng-enter, 5 | .view-frame.ng-leave { 6 | position: absolute; 7 | left: 0; 8 | right: 0; 9 | top: inherit; 10 | } 11 | .view-frame.ng-enter { 12 | animation: 0.5s fade-in; 13 | z-index: 100; 14 | } 15 | .view-frame.ng-leave { 16 | animation: 0.5s fade-out; 17 | z-index: 99; 18 | } 19 | @keyframes fade-in { 20 | from { 21 | opacity: 0; 22 | } 23 | to { 24 | opacity: 1; 25 | } 26 | } 27 | @keyframes fade-out { 28 | from { 29 | opacity: 1; 30 | } 31 | to { 32 | opacity: 0; 33 | } 34 | } 35 | .node.ng-enter, 36 | .node.ng-leave, 37 | .node.ng-move { 38 | transition: 0.25s linear all; 39 | } 40 | .node.ng-enter, 41 | .node.ng-move { 42 | width: 0; 43 | opacity: 0; 44 | overflow: hidden; 45 | } 46 | .node.ng-enter.ng-enter-active, 47 | .node.ng-move.ng-move-active { 48 | width: 120px; 49 | opacity: 1; 50 | } 51 | .node.ng-leave { 52 | opacity: 1; 53 | overflow: hidden; 54 | } 55 | .node.ng-leave.ng-leave-active { 56 | width: 0; 57 | opacity: 0; 58 | padding: 0; 59 | } 60 | -------------------------------------------------------------------------------- /code/web/src/app.animations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 30.10.2016. 3 | */ 4 | -------------------------------------------------------------------------------- /code/web/src/app.animations.less: -------------------------------------------------------------------------------- 1 | .view-frame { 2 | position: relative; 3 | 4 | &.ng-enter, &.ng-leave { 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | top: inherit; 9 | } 10 | 11 | &.ng-enter { 12 | animation: 0.5s fade-in; 13 | z-index: 100; 14 | } 15 | 16 | &.ng-leave { 17 | animation: 0.5s fade-out; 18 | z-index: 99; 19 | } 20 | } 21 | 22 | 23 | @keyframes fade-in { 24 | from {opacity: 0;} 25 | to {opacity: 1;} 26 | } 27 | 28 | @keyframes fade-out { 29 | from {opacity: 1;} 30 | to {opacity: 0;} 31 | } 32 | 33 | .node { 34 | &.ng-enter, 35 | &.ng-leave, 36 | &.ng-move { 37 | transition: 0.25s linear all; 38 | } 39 | &.ng-enter, 40 | &.ng-move { 41 | width: 0; 42 | opacity: 0; 43 | overflow: hidden; 44 | } 45 | 46 | &.ng-enter.ng-enter-active, 47 | &.ng-move.ng-move-active { 48 | width: 120px; 49 | opacity: 1; 50 | } 51 | 52 | &.ng-leave { 53 | opacity: 1; 54 | overflow: hidden; 55 | 56 | &.ng-leave-active { 57 | width: 0; 58 | opacity: 0; 59 | padding: 0; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /code/web/src/app.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 27.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('pbftGui'). 8 | config(['$locationProvider', '$routeProvider', 9 | function($locationProvider, $routeProvider, $routeParams) { 10 | $locationProvider.hashPrefix('!'); 11 | 12 | $routeProvider. 13 | when('/nodes', { 14 | template: '' 15 | }). 16 | when('/failures', { 17 | template: '' 18 | }). 19 | when('/nodes/:nodeid', { 20 | template: function($routeParams) { return ""; } //
This is a detailed view of node " +$routeParams.nodeid+ "!
Return 21 | }). 22 | otherwise({redirectTo: '/nodes'}); 23 | }]); 24 | -------------------------------------------------------------------------------- /code/web/src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Declare app level module which depends on views, and components 4 | angular.module('myApp', [ 5 | 'ngRoute', 6 | 'myApp.view1', 7 | 'myApp.view2', 8 | 'myApp.version' 9 | ]). 10 | config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) { 11 | $locationProvider.hashPrefix('!'); 12 | 13 | $routeProvider.otherwise({redirectTo: '/view1'}); 14 | }]); 15 | -------------------------------------------------------------------------------- /code/web/src/app.less: -------------------------------------------------------------------------------- 1 | // MACROS 2 | .transition(...) { // http://stackoverflow.com/a/14988517/3423324 3 | @props: ~`"@{arguments}".replace(/[\[\]]/g, '')`; 4 | -webkit-transition: @props; 5 | -moz-transition: @props; 6 | -o-transition: @props; 7 | transition: @props; 8 | } 9 | 10 | 11 | // COLORS 12 | @node-highlight: cadetblue; 13 | @node-highlight-hover: #7cf1ca; 14 | 15 | @node-normal: cornflowerblue; 16 | @node-normal-hover: #1972ff; 17 | 18 | @node-leader: #c4e3f3; 19 | @node-leader-hover: @node-highlight-hover; 20 | 21 | @bg-color: #124; 22 | 23 | 24 | // Colors for Message Types 25 | @msg-type1: #7cf1cb; 26 | @msg-type2: #85b9f0; 27 | @msg-type3: #ffcd83; 28 | @msg-type4: #ffad83; 29 | 30 | @msg-init: @msg-type1; 31 | @msg-propose: @msg-type2; 32 | @msg-prevote: @msg-type3; 33 | @msg-vote: @msg-type4; 34 | 35 | 36 | // STYLES 37 | /* app css stylesheet */ 38 | body { 39 | margin: 0; 40 | font-family: "Lucida Sans Unicode","Lucida Grande",sans-serif; 41 | font-variant: small-caps; 42 | } 43 | 44 | 45 | //.menu { 46 | // list-style: none; 47 | // border-bottom: 0.1em solid black; 48 | // margin-bottom: 2em; 49 | // padding: 0 0 0.5em; 50 | //} 51 | // 52 | //.menu:before { 53 | // content: "["; 54 | //} 55 | // 56 | //.menu:after { 57 | // content: "]"; 58 | //} 59 | // 60 | //.menu > li { 61 | // display: inline; 62 | //} 63 | // 64 | //.menu > li + li:before { 65 | // content: "|"; 66 | // padding-right: 0.3em; 67 | //} 68 | 69 | th, td { 70 | padding: 5px; 71 | border: 1px dashed white; 72 | } 73 | 74 | input { 75 | color: black; 76 | } 77 | 78 | #container { 79 | width: 100%; 80 | height: 100vh; 81 | margin: 0 auto 0 auto; 82 | border: 0 solid transparent; 83 | background-color: @bg-color; 84 | } 85 | 86 | #menubar { 87 | width: 100%; 88 | padding-top: 2vh; 89 | background-color: lightgray; 90 | margin: 0 auto 0 auto; 91 | display: table; 92 | border-spacing: 10px 0; 93 | text-align: center; 94 | 95 | a { 96 | background-color: @bg-color; 97 | height: 5vh; 98 | vertical-align: middle; 99 | border-width: 0 5px 0 5px; 100 | border-radius: 7px 7px 0 0; 101 | margin: 0 3% 0 3%; 102 | padding: 1%; 103 | display: inline-block; 104 | color: white; 105 | text-decoration: none; 106 | width: 40%; 107 | 108 | &:hover { 109 | background-color: #126; 110 | color: @node-highlight-hover; 111 | } 112 | } 113 | } 114 | 115 | #main { 116 | width: 100%; 117 | background-color: @bg-color; 118 | color: white; 119 | } 120 | 121 | #footer { 122 | width: 100%; 123 | display: block; 124 | height: 5vh; 125 | position: fixed; 126 | bottom: 0; 127 | background-color: @bg-color; 128 | 129 | .footer-fillin { 130 | width: 10%; 131 | height: 100%; 132 | float: left; 133 | } 134 | 135 | #footer-content { 136 | width: 80%; 137 | height: 100%; 138 | float: left; 139 | text-align: center; 140 | color: white; 141 | border-top: 1px solid white; 142 | } 143 | } 144 | 145 | #nodearea { 146 | margin: 0 auto; 147 | width: 100%; 148 | box-sizing: border-box; 149 | padding: 3% 10px; //top+bottom left+right 150 | background-color: @bg-color; 151 | 152 | .no_data { 153 | text-align: center; 154 | width: 100%; 155 | padding-top: 2em; 156 | } 157 | 158 | &, .node-list { 159 | display: flex; 160 | flex-wrap: wrap; 161 | width: 100%; 162 | } 163 | 164 | .node { 165 | flex: 1 0 auto; 166 | display: inline-block; 167 | position: relative; 168 | opacity: 1; 169 | text-align: center; 170 | //width: 18%; 171 | margin-right: 1%; 172 | margin-left: 1%; 173 | margin-bottom: 2.1em; 174 | //margin: 3% 1% 1% 1%; 175 | box-sizing: border-box; 176 | color: black; 177 | .transition(width .3s ease-out, height .2s ease-out); 178 | 179 | a, a:visited { 180 | color: black; 181 | text-decoration: none; 182 | } 183 | 184 | .node-main { 185 | border-color: transparent; 186 | border-radius: 0.5em; 187 | border-width: 0 5px 0 5px; 188 | border-style: solid; 189 | background-color: @node-normal; 190 | box-sizing: border-box; 191 | padding: 0.5em 1%; 192 | } 193 | 194 | h3, h5 { 195 | margin-bottom: 0; 196 | margin-top: 0; 197 | .transition(font-size .3s ease-out, opacity .3s ease-out); 198 | } 199 | 200 | .click-hint-wrapper { 201 | height: 1.4em; 202 | margin-top: -0.5em; 203 | 204 | .click-hint { 205 | display: block; 206 | white-space: nowrap; 207 | padding: 0 0.3em; 208 | .transition(width .3s ease-out, height .2s ease-in-out); 209 | } 210 | } 211 | 212 | // One clicked -> Detail view. 213 | // The clicked one, in foreground 214 | &.upscaled { 215 | width: 100%; 216 | text-align: left; 217 | margin: 0; 218 | height: 70vh; 219 | 220 | h3.id-label { 221 | font-size: 2em; 222 | } 223 | 224 | div.node-main { 225 | height: 100%; 226 | } 227 | } 228 | 229 | // One clicked -> Detail view. 230 | // The not-clicked ones, hidden 231 | &.reduced { 232 | display: none; 233 | } 234 | 235 | // None clicked -> Overview. 236 | // Display all nodes. 237 | &.non-upscaled { 238 | &:hover { 239 | .node-main { 240 | border-bottom-left-radius: 0; 241 | border-bottom-right-radius: 0; 242 | background-color: @node-normal-hover; 243 | } 244 | .click-hint { 245 | color: black; 246 | height: 1.4em; 247 | background-color: @node-normal-hover; 248 | .transition(height .2s ease-in-out, width .2s ease-in-out, color .2s ease-in-out); 249 | } 250 | } 251 | .overview { 252 | display: block; 253 | } 254 | .details { 255 | display: none; 256 | } 257 | } 258 | 259 | &.upscaled { 260 | width: 100%-2%; 261 | .overview { 262 | display: none; 263 | .click-hint { 264 | height: 0; 265 | // opacity: 0; 266 | } 267 | } 268 | .details { 269 | display: block; 270 | 271 | .value-graph { 272 | width: 100%; 273 | height: 100%; 274 | text-align: center; 275 | } 276 | } 277 | } 278 | 279 | &.leader { 280 | .node-main { 281 | border-color: @node-leader; 282 | } 283 | 284 | &:hover { 285 | .node-main, .click-hint { 286 | border-color: @node-leader-hover; 287 | } 288 | 289 | .leader-label { 290 | color: @node-leader-hover; 291 | } 292 | 293 | } 294 | 295 | .leader-label { 296 | font-style: italic; 297 | font-size: 0.8em; 298 | color: @node-leader; 299 | } 300 | } 301 | 302 | &.summary { 303 | color: white; 304 | 305 | .node-main { 306 | background-color: @node-highlight; 307 | } 308 | 309 | &.non-upscaled:hover { 310 | .node-main, .click-hint { 311 | background-color: @node-highlight-hover; 312 | color: white; 313 | } 314 | } 315 | } 316 | 317 | .click-hint { 318 | color: transparent; 319 | overflow: hidden; 320 | height: 0; 321 | position: relative; 322 | border-color: transparent; 323 | border-width: 0 5px 0 5px; 324 | border-style: solid; 325 | border-bottom-left-radius: 0.5em; 326 | border-bottom-right-radius: 0.5em; 327 | box-sizing: border-box; 328 | padding: 0 1%; 329 | .transition(height .2s ease-in-out, width .2s ease-in-out, color .2s ease-in-out); 330 | } 331 | 332 | .click-hint-wrapper { 333 | border-bottom-left-radius: 0.5em; 334 | border-bottom-right-radius: 0.5em; 335 | } 336 | 337 | .leader-edge { 338 | width: 0; 339 | height: 0; 340 | } 341 | } 342 | } 343 | 344 | #failure-table { 345 | width: 100%; 346 | margin: 5vh 0 0 0; 347 | font-variant: small-caps; 348 | 349 | h3 { 350 | margin: 0 auto 5px auto; 351 | text-align: center; 352 | width: 50%; 353 | /*height: 5vh;*/ 354 | min-height: 30px; 355 | border-bottom: 1px solid white; 356 | } 357 | 358 | div#refresher { 359 | width: 30%; 360 | margin: 0 auto; 361 | 362 | button { 363 | color: black; 364 | } 365 | } 366 | 367 | #symbology { 368 | float:left; 369 | width: 8%; 370 | padding: 2%; 371 | height: 70vh; 372 | margin: 0; 373 | vertical-align: top; 374 | 375 | div.sym-section { 376 | width: 100%; 377 | float: left; 378 | margin-bottom: 5px; 379 | 380 | div.circle { 381 | margin-right: 5px; 382 | float: left; 383 | width: 24px; 384 | height: 24px; 385 | -moz-border-radius: 12px; 386 | -webkit-border-radius: 12px; 387 | border-radius: 12px; // half of width and height -> circle 388 | 389 | &.type-init { 390 | background: @msg-init; 391 | } 392 | 393 | &.type-propose { 394 | background: @msg-propose; 395 | } 396 | 397 | &.type-prevote { 398 | background: @msg-prevote; 399 | } 400 | 401 | &.type-vote { 402 | background: @msg-vote; 403 | } 404 | 405 | div.ringHole { // a circle contained in another circle to give the illusion of a ring 406 | margin: 6px; 407 | width: 12px; 408 | height: 12px; 409 | -moz-border-radius: 6px; 410 | -webkit-border-radius: 6px; 411 | border-radius: 6px; 412 | background: @bg-color; 413 | } 414 | } 415 | 416 | div.arrow { 417 | float: left; 418 | width: 24px; // full space the arrow occupies 419 | height: 24px; // " 420 | 421 | div.head { // head of an arrow 422 | float: left; 423 | margin: 6px 0; // top and bottom margin = (arrowAreaHeight - borderLeftHeight)/2 424 | width: 0; 425 | height: 0; 426 | border-top: 6px solid transparent; 427 | border-bottom: 6px solid transparent; 428 | 429 | &.type-init { 430 | border-left: 12px solid @msg-init; // arrow shall be half as big as the circles 431 | } 432 | 433 | &.type-propose { 434 | border-left: 12px solid @msg-propose; 435 | } 436 | 437 | &.type-prevote { 438 | border-left: 12px solid @msg-prevote; 439 | } 440 | 441 | &.type-vote { 442 | border-left: 12px solid @msg-vote; 443 | } 444 | } 445 | div.shaft { // shaft / "stick" of an arrow 446 | float: left; 447 | margin: 10px 0; // top and bottom margin = (arrowAreaHeight - shaftHeight)/2 448 | width: 12px; // half of the total arrow area 449 | height: 4px; // doesn't follow any particular rule, tried out what looks best 450 | 451 | &.type-init { 452 | background: @msg-init; 453 | } 454 | 455 | &.type-propose { 456 | background: @msg-propose; 457 | } 458 | 459 | &.type-prevote { 460 | background: @msg-prevote; 461 | } 462 | 463 | &.type-vote { 464 | background: @msg-vote; 465 | } 466 | } 467 | } 468 | } 469 | 470 | div.top-label { 471 | width: 100%; 472 | font-variant: small-caps; 473 | font-size: 1.5em; // I think it looks good this way.. 474 | } 475 | 476 | div.sym-label { 477 | width: 100%; 478 | font-variant: small-caps; 479 | } 480 | } 481 | 482 | #timeline { 483 | float: left; 484 | width: 80%; 485 | height: 70vh; 486 | margin: 0 auto; 487 | vertical-align: top; 488 | overflow-y: scroll; 489 | 490 | .svg-content { 491 | display: inline-block; 492 | position: absolute; 493 | top: 0; 494 | left: 0; 495 | 496 | } 497 | 498 | svg { 499 | .arrow, circle { 500 | &.type-init { 501 | stroke: @msg-init; 502 | } 503 | &.type-prevote { 504 | stroke: @msg-prevote; 505 | } 506 | &.type-propose { 507 | stroke: @msg-propose; 508 | } 509 | &.type-vote { 510 | stroke: @msg-vote; 511 | } 512 | } 513 | .arrow { 514 | stroke-width: 2px; 515 | fill: none; 516 | } 517 | circle { 518 | &.action-acknowledge { 519 | &.type-init { 520 | fill: @msg-init; 521 | } 522 | &.type-prevote { 523 | fill: @msg-prevote; 524 | } 525 | &.type-propose { 526 | fill: @msg-propose; 527 | } 528 | &.type-vote { 529 | fill: @msg-vote; 530 | } 531 | } 532 | &.action-send { 533 | fill-opacity: 0.0; 534 | stroke-width: 3; 535 | } 536 | } 537 | } 538 | } 539 | } 540 | 541 | 542 | 543 | 544 | 545 | // TOOLTIPS 546 | 547 | .tooltipster-content { 548 | p.type, div.type { 549 | text-align: center; 550 | font-size: 0.5em; 551 | margin-bottom: -0.5em; 552 | margin-top: 0; 553 | span { 554 | font-weight: bold; 555 | text-transform: capitalize; 556 | } 557 | } 558 | p.value_store label span .node.different { 559 | text-decoration: line-through; 560 | } 561 | } 562 | // THEME 563 | .makro_tooltipster_color(@color) { 564 | &.tooltipster-top, 565 | &.tooltipster-bottom { 566 | .tooltipster-box { 567 | border-left: 0 none #2a2a2a; 568 | border-right: 0 none #2a2a2a; 569 | border-bottom: 3px solid @color; 570 | border-top: 3px solid @color; 571 | } 572 | .tooltipster-arrow-border { 573 | border-bottom-color: @color; 574 | border-top-color: @color; 575 | } 576 | } 577 | &.tooltipster-left, 578 | &.tooltipster-right { 579 | .tooltipster-box { 580 | border-top: 0 none #2a2a2a; 581 | border-bottom: 0 none #2a2a2a; 582 | border-left: 3px solid @color; 583 | border-right: 3px solid @color; 584 | } 585 | .tooltipster-arrow-border { 586 | border-left-color: @color; 587 | border-right-color: @color; 588 | } 589 | } 590 | .tooltipster-content { 591 | color: @color; 592 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 593 | font-variant: normal; 594 | } 595 | } 596 | .tooltipster-sidetip.tooltipster-punk { 597 | &.tooltipster-punk-acknowledge-init, 598 | &.tooltipster-punk-send-init { 599 | .makro_tooltipster_color(@msg-init); 600 | } 601 | 602 | &.tooltipster-punk-acknowledge-propose, 603 | &.tooltipster-punk-send-propose { 604 | .makro_tooltipster_color(@msg-propose); 605 | } 606 | 607 | &.tooltipster-punk-acknowledge-prevote, 608 | &.tooltipster-punk-send-prevote { 609 | .makro_tooltipster_color(@msg-prevote); 610 | } 611 | 612 | &.tooltipster-punk-acknowledge-vote, 613 | &.tooltipster-punk-send-vote { 614 | .makro_tooltipster_color(@msg-vote); 615 | } 616 | } 617 | -------------------------------------------------------------------------------- /code/web/src/app.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Declare app level module which depends on views, and components 4 | angular.module('pbftGui', [ 5 | 'ngAnimate', 6 | 'ngRoute', 7 | 'ngResource', 8 | 'core', 9 | 'nodeList', 10 | 'valueGraph', 11 | 'nodeListView', 12 | 'failureTable', 13 | 'failureTableView' 14 | ]); 15 | -------------------------------------------------------------------------------- /code/web/src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // WARNING 4 | // If you use docker, to start this 5 | // this file will be overwritten on startup. 6 | // This happens in entrypoint.sh, 7 | // _API_URL is set to $API_URL. 8 | // WARNING 9 | 10 | var _API_URL = "http://192.168.99.100"; -------------------------------------------------------------------------------- /code/web/src/core/core.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 30.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core', [ 7 | 'core.node', 8 | 'core.d3Factory' 9 | //'core.d3Directive', 10 | //'core.recompile' 11 | ]); 12 | -------------------------------------------------------------------------------- /code/web/src/core/d3/d3.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 15.11.2016. 3 | */ 4 | (function() { 5 | 'use strict'; 6 | 7 | angular.module('core.d3Directive') 8 | .directive('d3Directive', ['d3Factory', function (d3) { 9 | return { 10 | restrict: 'EA', 11 | scope: {}, 12 | link: function (scope, iElement, iAttrs) { 13 | var svg = d3.select(iElement[0]) 14 | .append("svg") 15 | .attr("width", "100%") 16 | .attr("height", "100%"); 17 | 18 | // dummy data 19 | scope.data = [ 20 | /*{name: "Greg", score: 98}, 21 | {name: "Ari", score: 96}, 22 | {name: "Loser", score: 48}, 23 | {name: "Me", score: 66}, 24 | {name: "Rita", score: 20}, 25 | {name: "Nameless", score: 75}*/ 26 | 0.5, 0.6, 0.5, 3.0, 0.4, 0.5, 0.5, 0.6, 0.5, 1.0, 0.5, 0.5, 0.4, 0.7 27 | ]; 28 | 29 | // on window resize, re-render d3 canvas 30 | window.onresize = function () { 31 | return scope.$apply(); 32 | }; 33 | scope.$watch(function () { 34 | return angular.element(window)[0].innerWidth; 35 | }, function () { 36 | return scope.render(scope.data); 37 | }); 38 | 39 | // define render function 40 | scope.render = function (data) { 41 | // remove all previous items before render 42 | svg.selectAll("*").remove(); 43 | 44 | // setup variables 45 | var str = "Output |"; 46 | /*var classes = d3.select(iElement[0]).attr("class"); 47 | for (var i = 0; i < classes.length; i++) { 48 | str = str+ "| " +classes[i]+ " "; 49 | } 50 | console.log(str);*/ 51 | /* 52 | var elements = d3.select(iElement[0]); 53 | for (var i = 0; i < elements.length; i++) { 54 | str = str + "| ::(" +i+ ") " 55 | for (var j = 0; j < elements[i].length; j++) { 56 | str = str + "| " +elements[i][j]+ " "; 57 | } 58 | } 59 | console.log(str);*/ 60 | console.log("Bounding rect (div): " +d3.select(iElement[0]).node().getBoundingClientRect().width); 61 | console.log("svg.width: " +svg.select("svg").offsetWidth); 62 | var divs = d3.selectAll("div"); 63 | for (var i = 0; i < divs.length; i++) { 64 | console.log(divs.attr("class")); 65 | /*for (var j = 0; j < divs[i].length; j++) { 66 | console.log("(" +i+ ") Class: " +divs[i][j].attr("class")+ " || Width: " +divs[i][j].node().getBoundingClientRect().width); 67 | }*/ 68 | } 69 | 70 | var width, height, max; 71 | width = d3.select(iElement[0])[0][0];/*d3.select(iElement[0])[0][0].offsetWidth - 20;*/ // 20 is for margins 72 | console.log("iElement[0]: " +d3.select(iElement[0])+ " | offsetWidth: " +d3.select(iElement[0]).offsetWidth); 73 | console.log("(iElement[0])[0][0]: " +d3.select(iElement[0])[0][0]+ " | (iElement[0])[0][0].offsetWidth: " +d3.select(iElement[0])[0][0].offsetWidth); 74 | height = scope.data.length * 35; // 35 = bar height (30) + margin (5) 75 | max = 98; // just some value 76 | 77 | var parent = svg.node().parentNode; 78 | console.log("PARENT :: element : " +parent+ " ; offsetWidth : " +parent.offsetWidth+ " ; clientWidth : " +parent.clientWidth+ " ; scrollWidth : " +parent.scrollWidth); 79 | 80 | var xScale = d3.scale.linear() 81 | .domain([0,d3.max(scope.data,function(d){return d;})]) 82 | .range([0,/*WIDTH, die ich nicht berechnet kriege! WAH!*/]); 83 | var yScale = d3.scale.linear() 84 | .domain([0,d3.max(scope.data,function(d){return d;})]) 85 | .range([0,height]); 86 | 87 | svg.attr("height", height); 88 | svg.selectAll("circle") 89 | .data(data) 90 | .enter() 91 | /*.append("rect") 92 | .attr("height", 30) 93 | .attr("width", 0) 94 | .attr("x", 10) 95 | .attr("y", function (d, i) { 96 | return i * 35; 97 | })*/ 98 | .append("circle") 99 | .attr("cx",function(d,i){return i*100+100;}) 100 | .attr("cy",function(d,i){return ;}) 101 | .attr("r",10) 102 | .attr("fill",function(d){return "rgb(" +(d+100)+ "," +(d+50)+ "," +d+ ")";}); 103 | /*.transition() 104 | .duration(1000) 105 | .attr("width", function (d) { 106 | return d.score / (max / width); 107 | })*/ 108 | svg.append("text") 109 | .text("D3 funktioniert! Theoretisch!") 110 | .attr("x","0px") 111 | .attr("y","100px") 112 | .attr("font-family","sans-serif") 113 | .attr("font-size","20px") 114 | .attr("color", "white"); 115 | 116 | console.log("NACH APPEND :: (iElement[0])[0][0]: " +d3.select(iElement[0])[0][0]+ " | (iElement[0])[0][0].offsetWidth: " +d3.select(iElement[0])[0][0].offsetWidth); 117 | } 118 | } 119 | // directive code 120 | }; 121 | }]); 122 | }()); -------------------------------------------------------------------------------- /code/web/src/core/d3/d3.directive.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 15.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core.d3Directive', ['core.d3Factory']); -------------------------------------------------------------------------------- /code/web/src/core/d3/d3.factory.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 15.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core.d3Factory', []); -------------------------------------------------------------------------------- /code/web/src/core/node/node.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 28.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core.node', ['ngResource']); 7 | -------------------------------------------------------------------------------- /code/web/src/core/node/node.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 28.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('core.node'). 8 | factory('Node', ['$resource', 9 | function($resource) { 10 | return $resource('nodes.json'/*'http://HOSTNAME/get_value/'*/, {}, { 11 | query: { 12 | method: 'GET', 13 | //params: {nodeId: 'nodes'}, 14 | isArray: true 15 | } 16 | }); 17 | } 18 | ]); 19 | -------------------------------------------------------------------------------- /code/web/src/core/recompile/recompile.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 23.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core.recompile') 7 | .directive('recompile', function($compile) { 8 | return { 9 | restrict: 'AE', 10 | link: function(scope, ele, attr) { 11 | /* this should watch if the state of an HTML object changes, f.e. when its inner text gets changed or 12 | * the objects attributes get changed, and, if so, call a function to compile the new HTML for Angular 13 | * execution. 14 | * In our specific case: when a value graph object gets inserted into a node it should be set up for 15 | * compilation by Angular.*/ 16 | scope.$watch(/*<1st param: attribute, what should be watched>, <2nd param: function, what happens if watched stuff changes state>*/); 17 | } 18 | } 19 | }); -------------------------------------------------------------------------------- /code/web/src/core/recompile/recompile.directive.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 23.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('core.recompile',[]); -------------------------------------------------------------------------------- /code/web/src/desktop.css: -------------------------------------------------------------------------------- 1 | .node { 2 | width: auto; 3 | } 4 | @media only screen and (min-width: 481px) and (max-width: 600px) { 5 | .node { 6 | width: 48%; 7 | } 8 | } 9 | @media only screen and (min-width: 601px) and (max-width: 900px) { 10 | .node { 11 | width: 31.33333333%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/web/src/desktop.less: -------------------------------------------------------------------------------- 1 | .node { 2 | width: auto; 3 | @media only screen and (min-width: 481px) and (max-width: 600px) { 4 | width: (100%/2) - 2%; 5 | } 6 | @media only screen and (min-width: 601px) and (max-width: 900px) { 7 | width: (100%/3) - 2%; 8 | } 9 | } -------------------------------------------------------------------------------- /code/web/src/failure-table-view/failure-table-view.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 30.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('failureTableView'). 8 | component('failureTableView', { 9 | templateUrl: 'failure-table-view/failure-table-view.template.html', 10 | controller: function FailureTableViewController() { 11 | 12 | } 13 | }); -------------------------------------------------------------------------------- /code/web/src/failure-table-view/failure-table-view.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('failureTableView', ['failureTable']); -------------------------------------------------------------------------------- /code/web/src/failure-table-view/failure-table-view.template.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /code/web/src/failure-table/failure-table.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 27.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('failureTable', []); -------------------------------------------------------------------------------- /code/web/src/failure-table/failure-table.template.html: -------------------------------------------------------------------------------- 1 |
2 |

Timeline for Message Flow of Nodes

3 |
4 | 5 | 6 |
7 | 11 |
12 |
Symbols:
13 |
14 |
Init
15 |
16 |
17 |
18 |
19 |
20 |
Pre-Prepare
21 |
22 |
23 |
24 |
25 |
26 |
Prepare
27 |
28 |
29 |
30 |
31 |
32 |
Commit
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 | 124 |
-------------------------------------------------------------------------------- /code/web/src/hover-effect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 01.11.2016. 3 | */ 4 | 5 | /*$(document).ready(function() { 6 | $('.node').hover(function() { 7 | $('.click-hint').show(); 8 | }, function() { 9 | $('.click-hint').hide(); 10 | }); 11 | });*/ 12 | 13 | function toggleDisplay(elements) { 14 | var element; 15 | 16 | elements = elements.length ? elements : [elements]; 17 | for (var i = 0; i < elements.length; i++) { 18 | element = elements[i]; 19 | 20 | if (isHidden(element)) { 21 | element.style.display = 'block'; 22 | //element.style.visibility = 'visible'; 23 | } else { 24 | element.style.display = 'none'; 25 | //element.style.visibility = 'hidden'; 26 | } 27 | } 28 | 29 | function isHidden(element) { 30 | return window.getComputedStyle(element, null).getPropertyValue('visibility') === 'hidden'; 31 | } 32 | } 33 | 34 | function getClickHintElement(element) { 35 | var childs = element.childNodes; 36 | for (var x in childs) { 37 | if (x.classList.item(0) === 'click-hint') { 38 | alert('Eyup.'); 39 | } 40 | } 41 | //return element.getElementsByClassName('click-hint')[0]; 42 | } 43 | -------------------------------------------------------------------------------- /code/web/src/index-async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 44 | My AngularJS App 45 | 46 | 47 | 48 | 52 | 53 |
54 | 55 |
Angular seed app: v
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /code/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Practical Byzantine Fault Tolerance GUI 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 76 |
77 | 80 | 81 |
82 | 85 |
86 | 97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /code/web/src/mobile.css: -------------------------------------------------------------------------------- 1 | .node { 2 | width: 100%; 3 | } 4 | .node .click-hint { 5 | content: 'tap for details'; 6 | } 7 | -------------------------------------------------------------------------------- /code/web/src/mobile.less: -------------------------------------------------------------------------------- 1 | .node { 2 | width: 100%; 3 | .click-hint { 4 | content: 'tap for details'; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /code/web/src/node-handling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 03.11.2016. 3 | */ 4 | 5 | function handleNodeScaling(element) { 6 | var classes = element.classList; 7 | if (!classes.contains('clicked')) { 8 | classes.add('clicked'); 9 | } 10 | toggleBigSmall(element, getUnclickedNodes()); 11 | classes.remove('clicked'); 12 | } 13 | 14 | function toggleBigSmall(element, otherElements) { 15 | var classes = element.classList; 16 | if (classes.contains('reduced')) { 17 | return; 18 | } 19 | if (classes.contains('upscaled')) { 20 | var vl = getChildByClassName(element, 'value-graph'); 21 | vl.removeChild(vl.getElementsByTagName("value-graph")[0]); 22 | classes.remove('upscaled'); 23 | classes.add('non-upscaled'); 24 | for (var i = 0; i < otherElements.length; i++) { 25 | otherClasses = otherElements[i].classList; 26 | if (!otherClasses.contains('reduced')) { 27 | return; 28 | } else { 29 | otherClasses.remove('reduced'); 30 | } 31 | } 32 | } else { 33 | classes.add('upscaled'); 34 | classes.remove('non-upscaled'); 35 | var vl = getChildByClassName(element, 'value-graph'); 36 | vl.appendChild(document.createElement("value-graph")); 37 | for (var i = 0; i < otherElements.length; i++) { 38 | otherClasses = otherElements[i].classList; 39 | if (otherClasses.contains('reduced')) { 40 | return; 41 | } else { 42 | otherClasses.add('reduced'); 43 | } 44 | } 45 | } 46 | } 47 | 48 | function getUnclickedNodes() { 49 | var nodes = document.getElementsByClassName('node'); 50 | var unclickedNodes = []; 51 | 52 | for (var i = 0; i < nodes.length; i++) { 53 | classes = nodes[i].classList; 54 | if (classes.contains('clicked')) { 55 | continue; 56 | } else { 57 | unclickedNodes.push(nodes[i]); 58 | } 59 | } 60 | 61 | return unclickedNodes; 62 | } 63 | 64 | function toggleVisibility(element) { 65 | if (element.parentNode.classList.contains('reduced')) { 66 | console.log("reduced, don't toggle."); 67 | return; 68 | } else if (!element.parentNode.classList.contains('non-upscaled')) { 69 | element.style.visibility = 'hidden'; 70 | } else if (!element.style.length > 0) { 71 | element.style.visibility = 'visible'; 72 | } else { 73 | element.style.visibility = (element.style.visibility === 'hidden' ? 'visible' : 'hidden'); 74 | } 75 | } 76 | 77 | function toggleDisplay(element, defaultval) { 78 | if (!element.style.length > 0) { 79 | element.style.display = ''; //initialising 80 | console.log('setting to default.'); 81 | element.style.display = defaultval; 82 | } else { 83 | console.log('toggling.'); 84 | element.style.display = (element.style.display === 'none' ? 'block' : 'none'); 85 | } 86 | } 87 | 88 | function toggleDisplayForChildren(parent) { 89 | children = parent.childNodes; 90 | for (var i = 0; i < children.length; i++) { 91 | if (children[i].nodeType != 1 || children[i].classList.contains('value-graph')) { 92 | continue; 93 | } 94 | if (!children[i].style.length > 0) { 95 | children[i].style.display = 'block'; 96 | } 97 | children[i].style.display = (children[i].style.display === 'none' ? 'block' : 'none'); 98 | } 99 | } 100 | 101 | function getChildByClassName(parent,className) { 102 | var children = parent.childNodes; 103 | var result = null; 104 | for (var i = 0; i < children.length && result == null; i++) { 105 | //console.log("element: " +children[i]); 106 | if (children[i].nodeType != 1) { 107 | continue; 108 | } 109 | var chClasses = children[i].classList; 110 | //console.log(" > classes: " +chClasses+ " :: requested class name: " +className); 111 | if (chClasses.contains(className)) { 112 | //console.log("found it!"); 113 | return children[i]; 114 | } else { 115 | //console.log("going deeper.."); 116 | result = getChildByClassName(children[i],className); 117 | } 118 | } 119 | return result; 120 | } 121 | -------------------------------------------------------------------------------- /code/web/src/node-list-view/node-list-view.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 30.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('nodeListView'). 8 | component('nodeListView', { 9 | templateUrl: 'node-list-view/node-list-view.template.html', 10 | controller: function NodeListViewController() { 11 | 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /code/web/src/node-list-view/node-list-view.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('nodeListView', ['nodeList']); -------------------------------------------------------------------------------- /code/web/src/node-list-view/node-list-view.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /code/web/src/node-list/node-list.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 27.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('nodeList'). 8 | component('nodeList', { 9 | templateUrl: 'node-list/node-list.template.html', 10 | /*controller: ['Node', 11 | function NodeListController(Node) { 12 | this.nodes = Node.query(); 13 | } 14 | ]*/ 15 | controller: ['$http','$scope','$interval', function NodeListController($http,$scope,$interval) { 16 | var self = this; 17 | self.summary = null; 18 | self.nodes = []; 19 | self.leader = -1; 20 | var touched = false; 21 | 22 | /* 23 | $scope.intervalFunction = function(){ 24 | $timeout(function() { 25 | $scope.pollValues(); 26 | $scope.intervalFunction(); 27 | }, 5000) 28 | }; 29 | 30 | // Kick off the interval 31 | $scope.intervalFunction(); 32 | */ 33 | var pollValues = function() { 34 | $http.get(_API_URL+"/api/v2/get_value/").success(function (json) { 35 | var data = { 36 | "summary": 0.5, // or null 37 | "leader": 1, 38 | "nodes": [ 39 | {"node": "1", "value": 0.5}, 40 | {"node": "2", "value": 0.6}, 41 | {"node": "5", "value": 0.5}, 42 | {"node": "6", "value": 0.5}, 43 | {"node": "5", "value": 0.5} 44 | ] 45 | }; 46 | console.log("JSON:", json); 47 | data = json; 48 | touched = !touched; // this is a flag to check if it was updated yet (?) 49 | 50 | self.summary = data.summary; 51 | self.leader = data.leader; 52 | for (var i = 0; i < data.nodes.length; i++) { 53 | var node = data.nodes[i]; 54 | var searchedIndex; 55 | searchedIndex = searchIndex(self.nodes,node.node); // check if already exists. 56 | if (searchedIndex == -1) { // does not exists. 57 | self.nodes.push({ 58 | id:node.node, 59 | value:node.value, 60 | name:node.id, 61 | touched:touched 62 | }); 63 | console.log("Added", node.node); 64 | } else { 65 | self.nodes[searchedIndex].value = node.value; 66 | self.nodes[searchedIndex].touched = touched; 67 | console.log("Updated", node.node); 68 | } 69 | } 70 | 71 | for (var i = 0; i < self.nodes.length; i++) { 72 | if (self.nodes[i].touched != touched) { 73 | self.nodes.splice(i,1); // delete 1 element at index i. 74 | console.log("Removed", self.nodes[i].id); 75 | 76 | } 77 | } 78 | 79 | console.log(self.nodes.length); 80 | sortNodes(); 81 | }); 82 | }; 83 | 84 | var promise = $interval(pollValues, 5000); 85 | $scope.$on('$destroy',function(){ 86 | if(promise) 87 | $interval.cancel(promise); 88 | }); 89 | 90 | /*$.getJSON(url+"/get_data/?limit=10").done(function (json) { 91 | // do stuff 92 | self.nodes = []; 93 | for (var node in json) { 94 | if (json.hasOwnProperty(node)) { 95 | for (var timestamp in json[node]) { 96 | if (json[node].hasOwnProperty(timestamp)) { 97 | var value=json[node][timestamp]; 98 | self.nodes.push({id:node,value:value}); 99 | break; 100 | } 101 | } 102 | } 103 | } 104 | console.log(self.nodes.length); 105 | //self.nodes = json[]; 106 | for (var i = 0; i < self.nodes.length; i++) { 107 | console.log("i:"+i+",id:"+self.nodes[i].id+",value:"+self.nodes[i].value); 108 | sortNode(i); 109 | } 110 | }).fail(console.error);*/ 111 | 112 | function sortNodes() { 113 | /** 114 | * Sorts the nodes (in self.nodes) by their ID, ascending. 115 | */ 116 | for (var i = 0; i < self.nodes.length-1; i++) { 117 | for (var j = 0; j < self.nodes.length-1; j++) { 118 | if (self.nodes[j].id > self.nodes[j+1].id) { 119 | var temp = self.nodes[j]; 120 | self.nodes[j] = self.nodes[j+1]; 121 | self.nodes[j+1] = temp; 122 | } 123 | } 124 | } 125 | } 126 | 127 | function searchIndex(arr, id) { 128 | /** 129 | * searches array arr for an element with given (node) id. 130 | * 131 | * @returns Element index of array, or -1 if not found. 132 | **/ 133 | for (var i = 0; i < arr.length; i++) { 134 | if (arr[i].id === id) { 135 | return i; 136 | } 137 | } 138 | return -1; 139 | } 140 | }] 141 | }); 142 | -------------------------------------------------------------------------------- /code/web/src/node-list/node-list.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 27.10.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('nodeList',[ 8 | 'core.node', 9 | 'valueGraph' 10 | ]); 11 | -------------------------------------------------------------------------------- /code/web/src/node-list/node-list.template.html: -------------------------------------------------------------------------------- 1 |
No recent events
2 | 3 | 16 | 17 | 18 | 44 | 45 | 66 | -------------------------------------------------------------------------------- /code/web/src/nodes.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "value": 0.5, 4 | "leader": true 5 | }, { 6 | "id": 2, 7 | "value": 0.6, 8 | "leader": false 9 | }, { 10 | "id": 3, 11 | "value": 0.5, 12 | "leader": false 13 | }, { 14 | "id": 4, 15 | "value": 0.5, 16 | "leader": false 17 | }, { 18 | "id": 5, 19 | "value": 0.5, 20 | "leader": false 21 | }, { 22 | "id": 6, 23 | "value": 0.5, 24 | "leader": false 25 | }, { 26 | "id": 7, 27 | "value": 0.8, 28 | "leader": false 29 | }, { 30 | "id": 8, 31 | "value": 0.5, 32 | "leader": false 33 | }, { 34 | "id": "summary", 35 | "value": 0.5 36 | }] -------------------------------------------------------------------------------- /code/web/src/resources/img/nope.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckydonald/pbft/a7d39515bb2aa5c48bf7ea242ed11fc563955a4f/code/web/src/resources/img/nope.gif -------------------------------------------------------------------------------- /code/web/src/test_timeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "action": "send", 5 | "data": { 6 | "node": 4, 7 | "sequence_no": 148847745, 8 | "type": 1, 9 | "value": 7.63133525848389 10 | }, 11 | "id": { 12 | "send": 126277 13 | }, 14 | "nodes": { 15 | "send": 4 16 | }, 17 | "timestamps": { 18 | "send": { 19 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 20 | "unix": 1488477450.563605 21 | } 22 | }, 23 | "type": "init" 24 | }, 25 | { 26 | "action": "acknowledge", 27 | "data": { 28 | "node": 4, 29 | "sequence_no": 148847745, 30 | "type": 1, 31 | "value": 7.631335258483887 32 | }, 33 | "id": { 34 | "receive": 126278, 35 | "send": 126277 36 | }, 37 | "nodes": { 38 | "receive": 4, 39 | "send": 4 40 | }, 41 | "timestamps": { 42 | "receive": { 43 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 44 | "unix": 1488477450.576458 45 | }, 46 | "send": { 47 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 48 | "unix": 1488477450.563605 49 | } 50 | }, 51 | "type": "init" 52 | }, 53 | { 54 | "action": "send", 55 | "data": { 56 | "node": 3, 57 | "sequence_no": 148847745, 58 | "type": 1, 59 | "value": 1.97469127178192 60 | }, 61 | "id": { 62 | "send": 126279 63 | }, 64 | "nodes": { 65 | "send": 3 66 | }, 67 | "timestamps": { 68 | "send": { 69 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 70 | "unix": 1488477450.680981 71 | } 72 | }, 73 | "type": "init" 74 | }, 75 | { 76 | "action": "acknowledge", 77 | "data": { 78 | "node": 4, 79 | "sequence_no": 148847745, 80 | "type": 1, 81 | "value": 7.631335258483887 82 | }, 83 | "id": { 84 | "receive": 126280, 85 | "send": 126277 86 | }, 87 | "nodes": { 88 | "receive": 2, 89 | "send": 4 90 | }, 91 | "timestamps": { 92 | "receive": { 93 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 94 | "unix": 1488477450.698291 95 | }, 96 | "send": { 97 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 98 | "unix": 1488477450.563605 99 | } 100 | }, 101 | "type": "init" 102 | }, 103 | { 104 | "action": "acknowledge", 105 | "data": { 106 | "node": 4, 107 | "sequence_no": 148847745, 108 | "type": 1, 109 | "value": 7.631335258483887 110 | }, 111 | "id": { 112 | "receive": 126281, 113 | "send": 126277 114 | }, 115 | "nodes": { 116 | "receive": 1, 117 | "send": 4 118 | }, 119 | "timestamps": { 120 | "receive": { 121 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 122 | "unix": 1488477450.714077 123 | }, 124 | "send": { 125 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 126 | "unix": 1488477450.563605 127 | } 128 | }, 129 | "type": "init" 130 | }, 131 | { 132 | "action": "acknowledge", 133 | "data": { 134 | "node": 4, 135 | "sequence_no": 148847745, 136 | "type": 1, 137 | "value": 7.631335258483887 138 | }, 139 | "id": { 140 | "receive": 126282, 141 | "send": 126277 142 | }, 143 | "nodes": { 144 | "receive": 3, 145 | "send": 4 146 | }, 147 | "timestamps": { 148 | "receive": { 149 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 150 | "unix": 1488477450.724045 151 | }, 152 | "send": { 153 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 154 | "unix": 1488477450.563605 155 | } 156 | }, 157 | "type": "init" 158 | }, 159 | { 160 | "action": "acknowledge", 161 | "data": { 162 | "node": 3, 163 | "sequence_no": 148847745, 164 | "type": 1, 165 | "value": 1.9746912717819214 166 | }, 167 | "id": { 168 | "receive": 126283, 169 | "send": 126279 170 | }, 171 | "nodes": { 172 | "receive": 4, 173 | "send": 3 174 | }, 175 | "timestamps": { 176 | "receive": { 177 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 178 | "unix": 1488477450.735215 179 | }, 180 | "send": { 181 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 182 | "unix": 1488477450.680981 183 | } 184 | }, 185 | "type": "init" 186 | }, 187 | { 188 | "action": "send", 189 | "data": { 190 | "node": 2, 191 | "sequence_no": 148847745, 192 | "type": 1, 193 | "value": 0.883936822414398 194 | }, 195 | "id": { 196 | "send": 126284 197 | }, 198 | "nodes": { 199 | "send": 2 200 | }, 201 | "timestamps": { 202 | "send": { 203 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 204 | "unix": 1488477450.751479 205 | } 206 | }, 207 | "type": "init" 208 | }, 209 | { 210 | "action": "acknowledge", 211 | "data": { 212 | "node": 3, 213 | "sequence_no": 148847745, 214 | "type": 1, 215 | "value": 1.9746912717819214 216 | }, 217 | "id": { 218 | "receive": 126285, 219 | "send": 126279 220 | }, 221 | "nodes": { 222 | "receive": 2, 223 | "send": 3 224 | }, 225 | "timestamps": { 226 | "receive": { 227 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 228 | "unix": 1488477450.761778 229 | }, 230 | "send": { 231 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 232 | "unix": 1488477450.680981 233 | } 234 | }, 235 | "type": "init" 236 | }, 237 | { 238 | "action": "acknowledge", 239 | "data": { 240 | "node": 3, 241 | "sequence_no": 148847745, 242 | "type": 1, 243 | "value": 1.9746912717819214 244 | }, 245 | "id": { 246 | "receive": 126286, 247 | "send": 126279 248 | }, 249 | "nodes": { 250 | "receive": 1, 251 | "send": 3 252 | }, 253 | "timestamps": { 254 | "receive": { 255 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 256 | "unix": 1488477450.780048 257 | }, 258 | "send": { 259 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 260 | "unix": 1488477450.680981 261 | } 262 | }, 263 | "type": "init" 264 | }, 265 | { 266 | "action": "acknowledge", 267 | "data": { 268 | "node": 3, 269 | "sequence_no": 148847745, 270 | "type": 1, 271 | "value": 1.9746912717819214 272 | }, 273 | "id": { 274 | "receive": 126287, 275 | "send": 126279 276 | }, 277 | "nodes": { 278 | "receive": 3, 279 | "send": 3 280 | }, 281 | "timestamps": { 282 | "receive": { 283 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 284 | "unix": 1488477450.796575 285 | }, 286 | "send": { 287 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 288 | "unix": 1488477450.680981 289 | } 290 | }, 291 | "type": "init" 292 | }, 293 | { 294 | "action": "acknowledge", 295 | "data": { 296 | "node": 2, 297 | "sequence_no": 148847745, 298 | "type": 1, 299 | "value": 0.8839368224143982 300 | }, 301 | "id": { 302 | "receive": 126288, 303 | "send": 126284 304 | }, 305 | "nodes": { 306 | "receive": 4, 307 | "send": 2 308 | }, 309 | "timestamps": { 310 | "receive": { 311 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 312 | "unix": 1488477450.809868 313 | }, 314 | "send": { 315 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 316 | "unix": 1488477450.751479 317 | } 318 | }, 319 | "type": "init" 320 | }, 321 | { 322 | "action": "acknowledge", 323 | "data": { 324 | "node": 2, 325 | "sequence_no": 148847745, 326 | "type": 1, 327 | "value": 0.8839368224143982 328 | }, 329 | "id": { 330 | "receive": 126289, 331 | "send": 126284 332 | }, 333 | "nodes": { 334 | "receive": 2, 335 | "send": 2 336 | }, 337 | "timestamps": { 338 | "receive": { 339 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 340 | "unix": 1488477450.827377 341 | }, 342 | "send": { 343 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 344 | "unix": 1488477450.751479 345 | } 346 | }, 347 | "type": "init" 348 | }, 349 | { 350 | "action": "acknowledge", 351 | "data": { 352 | "node": 2, 353 | "sequence_no": 148847745, 354 | "type": 1, 355 | "value": 0.8839368224143982 356 | }, 357 | "id": { 358 | "receive": 126290, 359 | "send": 126284 360 | }, 361 | "nodes": { 362 | "receive": 1, 363 | "send": 2 364 | }, 365 | "timestamps": { 366 | "receive": { 367 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 368 | "unix": 1488477450.845287 369 | }, 370 | "send": { 371 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 372 | "unix": 1488477450.751479 373 | } 374 | }, 375 | "type": "init" 376 | }, 377 | { 378 | "action": "acknowledge", 379 | "data": { 380 | "node": 2, 381 | "sequence_no": 148847745, 382 | "type": 1, 383 | "value": 0.8839368224143982 384 | }, 385 | "id": { 386 | "receive": 126291, 387 | "send": 126284 388 | }, 389 | "nodes": { 390 | "receive": 3, 391 | "send": 2 392 | }, 393 | "timestamps": { 394 | "receive": { 395 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 396 | "unix": 1488477450.861022 397 | }, 398 | "send": { 399 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 400 | "unix": 1488477450.751479 401 | } 402 | }, 403 | "type": "init" 404 | }, 405 | { 406 | "action": "send", 407 | "data": { 408 | "node": 1, 409 | "sequence_no": 148847745, 410 | "type": 1, 411 | "value": 0.957314074039459 412 | }, 413 | "id": { 414 | "send": 126292 415 | }, 416 | "nodes": { 417 | "send": 1 418 | }, 419 | "timestamps": { 420 | "send": { 421 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 422 | "unix": 1488477451.097073 423 | } 424 | }, 425 | "type": "init" 426 | }, 427 | { 428 | "action": "acknowledge", 429 | "data": { 430 | "node": 1, 431 | "sequence_no": 148847745, 432 | "type": 1, 433 | "value": 0.9573140740394592 434 | }, 435 | "id": { 436 | "receive": 126293, 437 | "send": 126292 438 | }, 439 | "nodes": { 440 | "receive": 1, 441 | "send": 1 442 | }, 443 | "timestamps": { 444 | "receive": { 445 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 446 | "unix": 1488477451.116111 447 | }, 448 | "send": { 449 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 450 | "unix": 1488477451.097073 451 | } 452 | }, 453 | "type": "init" 454 | }, 455 | { 456 | "action": "acknowledge", 457 | "data": { 458 | "node": 1, 459 | "sequence_no": 148847745, 460 | "type": 1, 461 | "value": 0.9573140740394592 462 | }, 463 | "id": { 464 | "receive": 126294, 465 | "send": 126292 466 | }, 467 | "nodes": { 468 | "receive": 2, 469 | "send": 1 470 | }, 471 | "timestamps": { 472 | "receive": { 473 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 474 | "unix": 1488477451.122682 475 | }, 476 | "send": { 477 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 478 | "unix": 1488477451.097073 479 | } 480 | }, 481 | "type": "init" 482 | }, 483 | { 484 | "action": "acknowledge", 485 | "data": { 486 | "node": 1, 487 | "sequence_no": 148847745, 488 | "type": 1, 489 | "value": 0.9573140740394592 490 | }, 491 | "id": { 492 | "receive": 126295, 493 | "send": 126292 494 | }, 495 | "nodes": { 496 | "receive": 4, 497 | "send": 1 498 | }, 499 | "timestamps": { 500 | "receive": { 501 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 502 | "unix": 1488477451.128667 503 | }, 504 | "send": { 505 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 506 | "unix": 1488477451.097073 507 | } 508 | }, 509 | "type": "init" 510 | }, 511 | { 512 | "action": "acknowledge", 513 | "data": { 514 | "node": 1, 515 | "sequence_no": 148847745, 516 | "type": 1, 517 | "value": 0.9573140740394592 518 | }, 519 | "id": { 520 | "receive": 126296, 521 | "send": 126292 522 | }, 523 | "nodes": { 524 | "receive": 3, 525 | "send": 1 526 | }, 527 | "timestamps": { 528 | "receive": { 529 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 530 | "unix": 1488477451.134851 531 | }, 532 | "send": { 533 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 534 | "unix": 1488477451.097073 535 | } 536 | }, 537 | "type": "init" 538 | }, 539 | { 540 | "action": "acknowledge", 541 | "data": { 542 | "leader": 1, 543 | "node": 1, 544 | "proposal": 3.57743239402771, 545 | "sequence_no": 148847745, 546 | "type": 2, 547 | "value_store": [ 548 | { 549 | "node": 1, 550 | "sequence_no": 148847744, 551 | "type": 1, 552 | "value": 1.2020764350891113 553 | }, 554 | { 555 | "node": 2, 556 | "sequence_no": 148847744, 557 | "type": 1, 558 | "value": 5.357077121734619 559 | }, 560 | { 561 | "node": 3, 562 | "sequence_no": 148847744, 563 | "type": 1, 564 | "value": 3.57743239402771 565 | }, 566 | { 567 | "node": 4, 568 | "sequence_no": 148847744, 569 | "type": 1, 570 | "value": 5.470797538757324 571 | } 572 | ] 573 | }, 574 | "id": { 575 | "receive": 126297, 576 | "send": 126299 577 | }, 578 | "nodes": { 579 | "receive": 4, 580 | "send": 1 581 | }, 582 | "timestamps": { 583 | "receive": { 584 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 585 | "unix": 1488477451.177011 586 | }, 587 | "send": { 588 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 589 | "unix": 1488477451.190611 590 | } 591 | }, 592 | "type": "propose" 593 | }, 594 | { 595 | "action": "acknowledge", 596 | "data": { 597 | "leader": 1, 598 | "node": 1, 599 | "proposal": 3.57743239402771, 600 | "sequence_no": 148847745, 601 | "type": 2, 602 | "value_store": [ 603 | { 604 | "node": 1, 605 | "sequence_no": 148847744, 606 | "type": 1, 607 | "value": 1.2020764350891113 608 | }, 609 | { 610 | "node": 2, 611 | "sequence_no": 148847744, 612 | "type": 1, 613 | "value": 5.357077121734619 614 | }, 615 | { 616 | "node": 3, 617 | "sequence_no": 148847744, 618 | "type": 1, 619 | "value": 3.57743239402771 620 | }, 621 | { 622 | "node": 4, 623 | "sequence_no": 148847744, 624 | "type": 1, 625 | "value": 5.470797538757324 626 | } 627 | ] 628 | }, 629 | "id": { 630 | "receive": 126298, 631 | "send": 126299 632 | }, 633 | "nodes": { 634 | "receive": 3, 635 | "send": 1 636 | }, 637 | "timestamps": { 638 | "receive": { 639 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 640 | "unix": 1488477451.184246 641 | }, 642 | "send": { 643 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 644 | "unix": 1488477451.190611 645 | } 646 | }, 647 | "type": "propose" 648 | }, 649 | { 650 | "action": "send", 651 | "data": { 652 | "leader": 1, 653 | "node": 1, 654 | "proposal": 3.57743239402771, 655 | "sequence_no": 148847745, 656 | "type": 2, 657 | "value_store": [ 658 | { 659 | "node": 1, 660 | "sequence_no": 148847744, 661 | "type": 1, 662 | "value": 1.2020764350891113 663 | }, 664 | { 665 | "node": 2, 666 | "sequence_no": 148847744, 667 | "type": 1, 668 | "value": 5.357077121734619 669 | }, 670 | { 671 | "node": 3, 672 | "sequence_no": 148847744, 673 | "type": 1, 674 | "value": 3.57743239402771 675 | }, 676 | { 677 | "node": 4, 678 | "sequence_no": 148847744, 679 | "type": 1, 680 | "value": 5.470797538757324 681 | } 682 | ] 683 | }, 684 | "id": { 685 | "send": 126299 686 | }, 687 | "nodes": { 688 | "send": 1 689 | }, 690 | "timestamps": { 691 | "send": { 692 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 693 | "unix": 1488477451.190611 694 | } 695 | }, 696 | "type": "propose" 697 | } 698 | ], 699 | "nodes": [ 700 | 1, 701 | 2, 702 | 3, 703 | 4 704 | ], 705 | "timestamps": { 706 | "max": { 707 | "string": "Thu, 02 Mar 2017 17:57:31 GMT", 708 | "unix": 1488477451.190611 709 | }, 710 | "min": { 711 | "string": "Thu, 02 Mar 2017 17:57:30 GMT", 712 | "unix": 1488477450.563605 713 | } 714 | } 715 | } -------------------------------------------------------------------------------- /code/web/src/value-graph/value-graph.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 07.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('valueGraph'). 8 | component('valueGraph', { 9 | templateUrl: 'value-graph/value-graph.template.html', 10 | bindings: { 11 | nodeid: '=' 12 | }, 13 | controller: ['$http','$scope','$interval', function ValueGraphController($http,$scope,$interval) { 14 | var self = this; 15 | self.nodeData = {}; 16 | var myChart = null; 17 | self.series = null; 18 | 19 | var pollGraphs = function() { 20 | var url; 21 | if (self.nodeid === 'summary' || self.nodeid == undefined) { 22 | url = _API_URL + "/get_data/?limit=40"; 23 | } else { 24 | url = _API_URL + "/get_data/?limit=10&node="+self.nodeid; 25 | } 26 | 27 | $http.get(url).then(function (json) { 28 | for (var node in json.data) { 29 | if (json.data.hasOwnProperty(node)) { 30 | self.nodeData[node] = []; 31 | for (var timestamp in json.data[node]) { 32 | if (json.data[node].hasOwnProperty(timestamp)) { 33 | var value=json.data[node][timestamp]; 34 | self.nodeData[node][self.nodeData[node].length] = {timestamp:timestamp,value:value}; 35 | } 36 | } 37 | } 38 | } 39 | if (myChart == null) { 40 | constructVG(self.nodeData); 41 | } 42 | drawGraphs(self.nodeData); 43 | }, function(json) { 44 | out("Yeah, that did not work, at all."); 45 | }, function(json) { 46 | out("What does this even do?"); 47 | }) 48 | }; 49 | 50 | var promise = $interval(pollGraphs, 5000); 51 | $scope.$on('$destroy',function(){ 52 | if(promise) 53 | $interval.cancel(promise); 54 | }); 55 | 56 | out("data outer :: " +self.nodeData); 57 | 58 | function dataToValues(nodeData){ 59 | var list = []; 60 | for (var node in nodeData) { 61 | if (!nodeData.hasOwnProperty(node)) { 62 | continue; 63 | } 64 | list[list.length] = { 65 | name: 'Node ' + node, 66 | data: [nodeData[node]] 67 | } 68 | } 69 | return list; 70 | } 71 | 72 | function constructVG(data) { 73 | console.log("CONSTRUCT STUFF: ",data); 74 | setHighchartsTheme(); 75 | var chart_data = dataToValues(data); 76 | myChart = Highcharts.chart('value-graph-container', { 77 | chart: { 78 | type: 'spline', 79 | events: { 80 | load: function() { 81 | self.series = this.series; 82 | } 83 | } 84 | }, 85 | title: { 86 | text: 'Values over Time' 87 | }, 88 | xAxis: { 89 | type: 'datetime', 90 | title: { 91 | text: 'time' 92 | } 93 | }, 94 | yAxis: { 95 | title: { 96 | text: 'values' 97 | } 98 | }, 99 | series: chart_data 100 | }); 101 | d3.select("text.highcharts-credits").remove(); 102 | 103 | /*var svg = d3.select("div.value-graph") 104 | .append("svg") 105 | .attr("width", "100%") 106 | .attr("height", "100%") 107 | .attr("preserveAspectRatio","xMidYMin"); 108 | 109 | var vg = d3.select("div.value-graph")[0][0]; 110 | var max = data.length; 111 | var width = vg.offsetWidth; 112 | var height = vg.offsetHeight; 113 | out("### MEASUREMENTS :: width " +width+ " height " +height+ " max " +max); 114 | 115 | window.onresize = (function(){ 116 | width = vg.offsetWidth; 117 | height = vg.offsetHeight; 118 | drawGraphs(null); 119 | console.log("resize!"); 120 | });*/ 121 | 122 | /*var xScale = d3.scale.linear() 123 | .domain([0,d3.max()]) 124 | .range(); 125 | var yScale = d3.scale.linear() 126 | .domain() 127 | .range();*/ 128 | } 129 | 130 | function drawGraphs(nodes) { 131 | for (var node in nodes) { 132 | var d = []; 133 | for (var i = 0; i < nodes[node].length; i++) { 134 | var date = new Date(parseInt(nodes[node][i].timestamp,10)*1000); 135 | d[i] = [date, nodes[node][i].value]; 136 | } 137 | for (var i = 0; i < self.series.length; i++) { 138 | if (self.series[i].name === 'Node ' + node) { 139 | self.series[i].update({data: d}, true); 140 | } 141 | } 142 | } 143 | 144 | /*svg.selectAll("*").remove(); 145 | svg.append("rect").attr("width",width).attr("height",height).attr("fill","blue"); 146 | svg.selectAll("circle") 147 | .data(data) 148 | .enter() 149 | .append("circle") 150 | .attr("cx", function(d,i){return i*(width/max)+((width/2)/max);}) 151 | .attr("cy", function(){return height/2;}) 152 | .attr("r", function(d){return d.value*30;}) 153 | .attr("fill", "white"); 154 | */ 155 | /*var lineFunction = d3.svg.line() 156 | .x(function(d){return d.x;}) 157 | .y(function(d){return d.y;}) 158 | .interpolate("linear"); 159 | 160 | svg.selectAll("*").remove(); 161 | svg.append("rect").attr("width",width).attr("height",height).attr("fill","blue"); 162 | svg.append("path") 163 | .attr("d", lineFunction(data)) 164 | .attr("stroke", "black") 165 | .attr("stroke-width", 2) 166 | .attr("fill", "none"); 167 | */ 168 | } 169 | 170 | function setHighchartsTheme() { 171 | Highcharts.theme = { 172 | colors: ['#ffffff', '#fff3e2', '#ffcd83', '#ffad32', 'ffa51f', '#ff0066', '#eeaaee', 173 | '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], 174 | chart: { 175 | backgroundColor: 'transparent', 176 | style: { 177 | fontFamily: '\'Unica One\', sans-serif' 178 | }, 179 | plotBorderColor: '#606063' 180 | }, 181 | title: { 182 | style: { 183 | color: '#124', 184 | fontSize: '20px' 185 | } 186 | }, 187 | subtitle: { 188 | style: { 189 | color: '#323d51' 190 | } 191 | }, 192 | xAxis: { 193 | gridLineColor: '#323d51', 194 | labels: { 195 | style: { 196 | color: '#124' 197 | } 198 | }, 199 | lineColor: '#323d51', 200 | minorGridLineColor: '#124', 201 | tickColor: '#323d51', 202 | title: { 203 | style: { 204 | color: '#124' 205 | 206 | } 207 | } 208 | }, 209 | yAxis: { 210 | gridLineColor: '#323d51', 211 | labels: { 212 | style: { 213 | color: '#124' 214 | } 215 | }, 216 | lineColor: '#323d51', 217 | minorGridLineColor: '#124', 218 | tickColor: '#323d51', 219 | tickWidth: 1, 220 | title: { 221 | style: { 222 | color: '#124' 223 | } 224 | } 225 | }, 226 | tooltip: { 227 | backgroundColor: '#124', 228 | style: { 229 | color: '#F0F0F0' 230 | } 231 | } 232 | }; 233 | 234 | // Apply the theme 235 | Highcharts.setOptions(Highcharts.theme); 236 | } 237 | 238 | // I'm a lazy fuck. 239 | function out(str) { 240 | console.log(str); 241 | } 242 | }] 243 | }); 244 | -------------------------------------------------------------------------------- /code/web/src/value-graph/value-graph.d3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 15.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular. 7 | module('valueGraph'). 8 | component('valueGraphD3', { 9 | template: '', 10 | bindings: { 11 | data: '=' 12 | }, 13 | controller: ['d3Factory', '$http', function ValueGraphD3Controller(d3, $http) { 14 | 15 | }] 16 | }); -------------------------------------------------------------------------------- /code/web/src/value-graph/value-graph.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PlayingBacon on 07.11.2016. 3 | */ 4 | 'use strict'; 5 | 6 | angular.module('valueGraph',[ 7 | 'core.d3Factory', 8 | 'nodeList' 9 | ]); 10 | -------------------------------------------------------------------------------- /code/web/src/value-graph/value-graph.template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/web/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: darkgrey; 3 | margin: 0; 4 | font-family: "Lucida Sans Unicode","Lucida Grande",sans-serif; 5 | } 6 | 7 | h3, h5 { 8 | font-variant: small-caps; 9 | margin-bottom: 0px; 10 | } 11 | 12 | #container { 13 | width: 100%; 14 | min-width: 550px; 15 | height: 98vh; 16 | margin: 0 auto 0 auto; 17 | background-color: black; 18 | border-style: solid; 19 | border-color: transparent; 20 | border-width: 2vh 0vh 0vh 0vh; 21 | } 22 | 23 | #menubar { 24 | width: 40%; 25 | height: 5%; 26 | margin: 0 auto 0 auto; 27 | color: aliceblue; 28 | display: table; 29 | border-spacing: 10px 0px; 30 | } 31 | 32 | #main { 33 | width: 100%; 34 | height: 90%; 35 | margin: 0% auto 0% auto; 36 | background-color: #124; 37 | overflow: hidden; 38 | } 39 | 40 | #nodearea { 41 | 42 | } 43 | 44 | #footer { 45 | width: 100%; 46 | height: 5%; 47 | margin: 0% auto 0% auto; 48 | text-align: center; 49 | background-color: gold; 50 | } 51 | 52 | .menuitem { 53 | text-align: center; 54 | display: table-cell; 55 | vertical-align: middle; 56 | width: 44%; 57 | font-size: 0.7em; 58 | height: 100%; 59 | margin: 0 3% 0 3%; 60 | border-width: 0 5px 0 5px; 61 | background-color: #124; 62 | border-radius: 7px 7px 0 0; 63 | } 64 | 65 | .node { 66 | float: left; 67 | text-align: center; 68 | width: 15%; 69 | height: 18%; 70 | margin: 3% 1% 1% 1%; 71 | padding: 1%; 72 | background-color: cornflowerblue; 73 | border-color: transparent; 74 | border-radius: 0.5em; 75 | border-width: 0 5px 0 5px; 76 | border-style: solid; 77 | } 78 | 79 | .node:hover { 80 | background-color: #1972ff; 81 | } 82 | 83 | .node.primary { 84 | border-color: cadetblue; 85 | } 86 | 87 | .node.primary:hover { 88 | border-color: #7cf1ca; 89 | } 90 | 91 | .primary-label { 92 | font-style: italic; 93 | color: midnightblue; 94 | } 95 | 96 | .primary-edge { 97 | width: 0; 98 | height: 0; 99 | 100 | } 101 | 102 | .node.summary { 103 | background-color: cadetblue; 104 | color: white; 105 | } 106 | 107 | .node.summary:hover { 108 | background-color: #7cf1ca; 109 | color: black; 110 | } 111 | 112 | .value { 113 | 114 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | # # Python node 4 | # node: 5 | # extends: 6 | # file: node.docker-compose.yml 7 | # service: node 8 | # volumes: 9 | # - /var/run/docker.sock:/var/run/docker.sock 10 | # environment: 11 | # NODE_DEBUGGER: "False" 12 | # 13 | # # entrypoint: ["/bin/bash"] 14 | # 15 | # # docker run -ti --entrypoint /bin/bash 9b6a81855c06 16 | # # d-c run --entrypoint /bin/bash node-3 17 | # # NODE_ID=3 NODE_TOTAL=4 POSSIBLE_FAILURES=1 NODE_HOST=teamproject16_node-{i}_1 NODE_PORT=4458 python main_node.py 18 | 19 | node: 20 | extends: 21 | file: code/node_java/docker-compose.yml 22 | service: node_java 23 | volumes: 24 | - /var/run/docker.sock:/var/run/docker.sock 25 | environment: 26 | NODE_DEBUGGER: "False" 27 | NODE_DEBUG: "False" 28 | # API_HOST: "http://192.168.0.42" 29 | API_HOST: ${API_HOST} 30 | 31 | api: 32 | extends: 33 | file: api.docker-compose.yml 34 | service: api 35 | ports: 36 | - "80:80" 37 | restart: "no" 38 | command: ["python", "main_api.py"] 39 | 40 | #entrypoint: [] 41 | #command: ["ls", "-la"] 42 | 43 | web: 44 | extends: 45 | file: code/web/docker-compose.yml 46 | service: web 47 | environment: 48 | PORT: 8000 49 | # API_URL: "http://192.168.0.42" 50 | API_URL: ${API_HOST} 51 | ports: 52 | - "8000:8000" 53 | entrypoint: ["/entrypoint.sh"] 54 | 55 | web_static: 56 | extends: 57 | file: code/web/docker-compose.yml 58 | service: web 59 | environment: 60 | PORT: 8001 61 | API_URL: "http://localhost:8001/example/" 62 | # `${VM_HOST:-localhost}` will evaluate to `localhost` if $VM_HOST is unset or empty in the environment. 63 | ports: 64 | - "8001:8001" 65 | entrypoint: ["/entrypoint.sh"] 66 | 67 | 68 | 69 | postgres: 70 | extends: 71 | file: api.docker-compose.yml 72 | service: postgres 73 | #volumes: 74 | # - /data/postgres:/data/postgres 75 | 76 | 77 | postgres_browser: 78 | extends: 79 | file: api.docker-compose.yml 80 | service: postgres_browser 81 | ports: 82 | - "8080:80" -------------------------------------------------------------------------------- /extras/libs/pycharm-debug-py3k.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckydonald/pbft/a7d39515bb2aa5c48bf7ea242ed11fc563955a4f/extras/libs/pycharm-debug-py3k.egg -------------------------------------------------------------------------------- /node.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | node: 4 | build: 5 | context: . 6 | dockerfile: ./Dockerfile 7 | command: ["main_node.py"] 8 | environment: 9 | DOCKER_CACHING_TIME: 5 10 | NODE_PORT: 4458 11 | NODE_DEBUGGER: 0 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | luckydonald-utils==0.51 2 | docker-py 3 | 4 | # api 5 | flask # api 6 | pony # db --------------------------------------------------------------------------------