Wizard server#

The wizard server allow communications between wizard clients. For exemple, if a member of your team create an asset in wizard, a signal is sent to this server and broadcasted to all other users on the team.

Running the server on your windows machine#

Important

To work with your team on a server you are running on your local machine, you will need to be on the same network. ( lan )

Go to the installation directory of wizard and run the file ‘server.exe’. While this executable is running, the server should be accessible. Communicate your computer ip adress and the port ( 50333 ) to all wizard users of your team.

Alternative text

Warning

If you are trying to access the server trough wifi, please ensure all the users are connected to the wifi with the same parameters, prefer a ‘Private network’

Alternative text

Running the server on a linux machine#

Download Python 3.9 on your machine. Download the server python file here and rename it as server.py. Open this file and modify the ip_adress variable ( line 29 ) with the ip adress of the server machine. Now run this file with Python 3.9 and communicate the ip_adress and port to all the users of wizard.

Here is the server.py code:

# coding: utf-8
# Author: Leo BRUNEL
# Contact: contact@leobrunel.com

# This file is part of Wizard

# MIT License

# Copyright (c) 2021 Leo brunel

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

ip_address = 'localhost'
port = 50333
log_file = "wizard_server.log"

# Python modules
import socket
import sys
import threading
import time
import traceback
import json
import logging
from logging.handlers import RotatingFileHandler
import os
import struct

# create logger
logger = logging.getLogger('WIZARD-SERVER')
logger.setLevel(logging.DEBUG)
# create file handler and set level to debug
file_handler = RotatingFileHandler(log_file, mode='a', maxBytes=1000000, backupCount=1000, encoding=None, delay=False)
# create console handler and set level to debug
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.DEBUG)
# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to handlers
stream_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# add handlers to logger
logger.addHandler(stream_handler)
logger.addHandler(file_handler)
logger.info("Python : " + str(sys.version))

def get_server(DNS):
    server = None
    server_address = None
    try:
        server_address = socket.gethostbyname(DNS[0])
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind(DNS)
        server.listen(100)
    except ConnectionRefusedError:
        logger.debug(f"Socket connection refused : host={DNS[0]}, port={DNS[1]}")
        return None
    except socket.timeout:
        logger.debug(f"Socket timeout ({str(timeout)}s) : host={DNS[0]}, port={DNS[1]}")
        return None
    except:
        logger.debug(str(traceback.format_exc()))
        return None
    return server, server_address

def send_signal_with_conn(conn, msg_raw, only_debug = False):
    try:
        msg = json.dumps(msg_raw).encode('utf8')
        msg = struct.pack('>I', len(msg)) + msg
        conn.sendall(msg)
        return 1
    except ConnectionRefusedError:
        if only_debug:
            logger.debug(f"Socket connection refused : host={DNS[0]}, port={DNS[1]}")
        else:
            logger.error(f"Socket connection refused : host={DNS[0]}, port={DNS[1]}")
        return None
    except socket.timeout:
        if only_debug:
            logger.debug(f"Socket timeout ({str(timeout)}s) : host={DNS[0]}, port={DNS[1]}")
        else:
            logger.error(f"Socket timeout ({str(timeout)}s) : host={DNS[0]}, port={DNS[1]}")
        return None
    except:
        if only_debug:
            logger.debug(str(traceback.format_exc()))
        else:
            logger.error(str(traceback.format_exc()))
        return None

def recvall(sock):
    try:
        raw_msglen = recvall_with_given_len(sock, 4)
        if not raw_msglen:
            return None
        msglen = struct.unpack('>I', raw_msglen)[0]
        return recvall_with_given_len(sock, msglen)
    except ConnectionRefusedError:
        logger.debug(f"Socket connection refused : host={DNS[0]}, port={DNS[1]}")
        return None
    except socket.timeout:
        logger.debug(f"Socket timeout ({str(timeout)}s) : host={DNS[0]}, port={DNS[1]}")
        return None
    except:
        logger.debug(str(traceback.format_exc()))
        return None

def recvall_with_given_len(sock, n):
    try:
        data = bytearray()
        while len(data) < n:
            packet = sock.recv(n - len(data))
            if not packet:
                return None
            data.extend(packet)
        return data
    except ConnectionRefusedError:
        logger.debug(f"Socket connection refused : host={DNS[0]}, port={DNS[1]}")
        return None
    except socket.timeout:
        logger.debug(f"Socket timeout ({str(timeout)}s) : host={DNS[0]}, port={DNS[1]}")
        return None
    except:
        logger.debug(str(traceback.format_exc()))
        return None
    return data

class server(threading.Thread):
    def __init__(self):
        super(server, self).__init__()
        logger.info("Starting server on : '" + str(ip_address) + "'")
        logger.info("Default port : '" + str(port) + "'")
        self.server, self.server_adress = get_server((ip_address, port))
        logger.info("Server started")
        self.client_ids = dict()

    def run(self):
        while True:
            try:
                conn, addr = self.server.accept()
                entry_message = recvall(conn)
                self.analyse_signal(entry_message, conn, addr)
            except:
                logger.error(str(traceback.format_exc()))
                continue

    def analyse_signal(self, msg_raw, conn, addr):
        data = json.loads(msg_raw)
        if data['type'] == 'test_conn':
            logger.info('test_conn')
        elif data['type'] == 'refresh_team':
            client_dic = dict()
            client_dic['id'] = None
            self.broadcast(data, client_dic)
        elif data['type'] == 'new_client':
            self.add_client(data['user_name'], conn, addr, data['project'])

    def add_client(self, user_name, conn, addr, project):
        client_dic = dict()
        client_dic['user_name'] = user_name
        client_dic['conn'] = conn
        client_dic['addr'] = addr
        client_dic['project'] = project
        client_id = str(time.time())
        client_dic['id'] = client_id
        self.client_ids[client_id]=client_dic

        threading.Thread(target=self.clientThread, args=(client_id, user_name, conn, addr, project)).start()
        logger.info("New client : {}, {}, {}, {}".format(client_id, user_name, addr, project))
        signal_dic = dict()
        signal_dic['type'] = 'new_user'
        signal_dic['user_name'] = user_name
        signal_dic['project'] = project
        self.broadcast(signal_dic, self.client_ids[client_id])
        self.send_users_to_new_client(client_dic)

    def send_users_to_new_client(self, client_dic):
        for client in self.client_ids.keys():
            if client != client_dic['id']:
                signal_dic = dict()
                signal_dic['type'] = 'new_user'
                signal_dic['user_name'] = self.client_ids[client]['user_name']
                signal_dic['project'] = self.client_ids[client]['project']
                send_signal_with_conn(client_dic['conn'], signal_dic)

    def clientThread(self, client_id, user_name, conn, addr, project):
        client_dic = dict()
        client_dic['id'] = client_id
        client_dic['user_name'] = user_name
        client_dic['conn'] = conn
        client_dic['addr'] = addr
        client_dic['project'] = project
        running = True
        while running:
            try:
                raw_data = recvall(client_dic['conn'])
                if raw_data is not None:
                    data = json.loads(raw_data)
                    print(data)
                    self.broadcast(data, client_dic)
                else:
                    if conn is not None:
                        self.remove_client(client_dic)
                        running = False
            except:
                logger.error(str(traceback.format_exc()))
                continue

    def broadcast(self, data, client_dic):
        logger.debug("Broadcasting : " + str(data))
        for client in self.client_ids.keys():
            if client != client_dic['id']:
                if not send_signal_with_conn(self.client_ids[client]['conn'], data):
                    self.remove_client(self.client_ids[client])

    def remove_client(self, client_dic):
        if client_dic['id'] in self.client_ids.keys():
            logger.info("Removing client : {}, {}, {}, {}".format(client_dic['id'], client_dic['user_name'], client_dic['addr'], client_dic['project']))
            del self.client_ids[client_dic['id']]
            client_dic['conn'].close()
            signal_dic = dict()
            signal_dic['type'] = 'remove_user'
            signal_dic['user_name'] = client_dic['user_name']
            signal_dic['project'] = client_dic['project']
            self.broadcast(signal_dic, client_dic)

if __name__ == "__main__":
    try:
        server = server()
        server.daemon = True
        server.start()
        print('Press Ctrl+C to quit...')
        while 1:time.sleep(1)
    except KeyboardInterrupt:
        print('Stopping server...')
        raise SystemExit
        sys.exit()