diff --git a/.github/workflows/create-windows-service.yml b/.github/workflows/create-windows-service.yml new file mode 100644 index 00000000..2c38f076 --- /dev/null +++ b/.github/workflows/create-windows-service.yml @@ -0,0 +1,54 @@ +name: Create Windows Service +on: + push: + tags: + - v* + +# For Code Signing please make sure the following secrets are defined in the repository: +# WINDOWS_CERTIFICATE_BASE64 - The Windows code signing certificate, base64 encoded +# WINDOWS_CERTIFICATE_PASSWORD - The password for Windows the certificate + +jobs: + windows: + name: Create the Windows Service + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Set backend environment + working-directory: backend + run: | + pip install --upgrade setuptools pip wheel + pip install t2wml-api + pip install -r requirements.txt + pip install pyinstaller pywin32 semver requests + pip uninstall -y typing typing-extensions + pip install waitress + - name: Create installer + working-directory: backend + env: # Replace unicode characters with ? + PYTHONIOENCODING: :replace + PYTHONLEGACYWINDOWSIOENCODING: true + CI: false # Otherwise React stops on warnings + CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + run: | + pyinstaller --clean --noupx t2wml-service.spec + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + shell: bash + - name: Upload to Release + id: upload-release-asset + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: backend/dist/t2wml-service.exe + asset_name: t2wml-service-${{ steps.get_version.outputs.VERSION }}.exe + tag: ${{ github.ref }} + overwrite: true + body: "Windows Setup" + diff --git a/README.md b/README.md index 3c940d17..3e60f85f 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,20 @@ You can also open the Chrome Developers Tools from the GUI's `Debug` menu. #### Backend Development To develop the backend, you need to launch the `Backend` from the debug menu. You will be able to set breakpoints and debug properly. If you want to run the GUI, start the `Build and Watch GUI` and `t2wml GUI` tasks, as well. +### Windows service +To run the backend as a service on windows: +Download the file windows-service.exe, and run with Administrator privilges: +Install: +`t2wml-service.exe install` +Start: +`t2wml-service.exe start` +Debug: +`t2wml-service.exe debug` +Stop: +`t2wml-service.exe stop` +Uninstall: +`t2wml-service.exe remove` + ## Usage with GUI diff --git a/backend/causx_application.py b/backend/causx_application.py index 70a26d21..b068a154 100644 --- a/backend/causx_application.py +++ b/backend/causx_application.py @@ -187,6 +187,15 @@ def get_mapping(preload=False): get_layers(response, calc_params) return response, 200 + +@app.route('/api/is-alive') +def is_alive1(): + return 'Causx Backend is here', 200 + +@app.route('/api/causx/is-alive') +def is_alive2(): + return 'Causx Backend is here', 200 + @app.route('/api/causx/token', methods=['GET']) def get_token(): return {"token": encode_auth_token()}, 200 diff --git a/backend/t2wml-service.py b/backend/t2wml-service.py new file mode 100644 index 00000000..340479fc --- /dev/null +++ b/backend/t2wml-service.py @@ -0,0 +1,116 @@ +""" +# Prerequisites: +cd backend +virtualenv env-service +(activate virtual env) +pip install -r requirements.txt +pip install --upgrade pyinstaller +pip install pywin32 +pip install waitress +pip install requests +pip install -e ../../t2wml-api +# Build: +pyinstaller --clean --noupx t2wml-service.spec +# With Administrator privilges +# Install: +dist\t2wml-service.exe install +# Start: +dist\t2wml-service.exe start +# Install with autostart: +dist\t2wml-service.exe --startup delayed install +# Debug: +dist\t2wml-service.exe debug +# Stop: +dist\t2wml-service.exe stop +# Uninstall: +dist\t2wml-service.exe remove +""" + +import time +import ctypes +import sys +import win32serviceutil # ServiceFramework and commandline helper +import win32service # Events +import servicemanager # Simple setup and logging +import win32event +from waitress import serve +import socket +import threading + +from causx_application import app +class WaitressService(threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + + def run(self): + print('thread start\n') + serve(app, host='localhost', port=13000, trusted_proxy='localhost') + print('thread done\n') + + def get_id(self): + # returns id of the respective thread + if hasattr(self, '_thread_id'): + return self._thread_id + for id, thread in threading._active.items(): + if thread is self: + return id + + def exit(self): + thread_id = self.get_id() + res = ctypes.pythonapi.PyThreadState_SetAsyncExc( + thread_id, ctypes.py_object(SystemExit)) + if res > 1: + ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) + print('Exception raise failure') + + +class MyServiceFramework(win32serviceutil.ServiceFramework): + + _svc_name_ = 't2wml_backend' + _svc_display_name_ = 'T2WML Backend Service' + _svc_description_ = 'T2WML Backend Service for Causx' + + def __init__(self, args): + win32serviceutil.ServiceFramework.__init__(self, args) + self.stopEvt = win32event.CreateEvent(None, 0, 0, None) + socket.setdefaulttimeout(60) + + def SvcStop(self): + """Stop the service""" + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_name_, '')) + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + win32event.SetEvent(self.stopEvt) + + def SvcDoRun(self): + """Start the service; does not return until stopped""" + print('main start') + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, + (self._svc_name_, '')) + self.service_impl = WaitressService() + self.ReportServiceStatus(win32service.SERVICE_RUNNING) + # Run the service + self.service_impl.start() + + print('waiting on win32event') + win32event.WaitForSingleObject(self.stopEvt, win32event.INFINITE) + self.service_impl.exit() # raise SystemExit in inner thread + print('waiting on thread') + self.service_impl.join() + print('main done') + + +def init(): + if len(sys.argv) == 1: + servicemanager.Initialize() + servicemanager.PrepareToHostSingle(MyServiceFramework) + servicemanager.StartServiceCtrlDispatcher() + else: + win32serviceutil.HandleCommandLine(MyServiceFramework) + + +if __name__ == '__main__': + init() diff --git a/backend/t2wml-service.spec b/backend/t2wml-service.spec new file mode 100644 index 00000000..5d114fd7 --- /dev/null +++ b/backend/t2wml-service.spec @@ -0,0 +1,73 @@ +# -*- mode: python ; coding: utf-8 -*- + +""" +# Prerequisites: +cd backend +virtualenv env-service +(activate virtual env) +pip install -r requirements.txt +pip install --upgrade pyinstaller +pip install pywin32 +pip install waitress +pip install requests +pip install t2wml-api +# Build: +pyinstaller --clean --noupx t2wml-service.spec +# With Administrator privilges +# Install: +dist\t2wml-service.exe install +# Start: +dist\t2wml-service.exe start +# Debug: +dist\t2wml-service.exe debug +# Stop: +dist\t2wml-service.exe stop +# Uninstall: +dist\t2wml-service.exe remove +""" + +import sys +from os import path, getcwd +site_packages = next(p for p in sys.path if 'site-packages' in p) + +def copy_package(name): + return [(path.join(site_packages, name), name)] + +block_cipher = None + + +a = Analysis(['t2wml-service.py'], + pathex=[getcwd()], + binaries=[], + datas=copy_package('distributed'), + hiddenimports=['win32timezone', 'pandas','pandas._libs.tslibs.base', 'rltk', 'logging.config', 'cmath'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) + +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='t2wml-service', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir='.', + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None + ) \ No newline at end of file diff --git a/backend/t2wml-waitress.py b/backend/t2wml-waitress.py new file mode 100644 index 00000000..d51136d5 --- /dev/null +++ b/backend/t2wml-waitress.py @@ -0,0 +1,10 @@ +from waitress import serve + +from causx_application import app + +#def run(): +# app.run(port=13000, debug=False, host=None, use_reloader=False) + +serve(app, port=13000, host='localhost') + +# serve(wsgiapp, host='localhost', port=13000) diff --git a/backend/t2wml-waitress.spec b/backend/t2wml-waitress.spec new file mode 100644 index 00000000..a713e40c --- /dev/null +++ b/backend/t2wml-waitress.spec @@ -0,0 +1,46 @@ +# -*- mode: python ; coding: utf-8 -*- +# Pyinstaller spec file. + +# pip install --upgrade pyinstaller +# pyinstaller --clean --noupx .\t2wml-waitress.spec + +from PyInstaller.utils.hooks import copy_metadata +import sys +from os import path, getcwd +site_packages = next(p for p in sys.path if 'site-packages' in p) + +def copy_package(name): + return [(path.join(site_packages, name), name)] + +block_cipher = None + + +a = Analysis(['t2wml-waitress.py'], + pathex=[getcwd()], + binaries=[], + datas=copy_package('distributed'), + hiddenimports=['pandas','pandas._libs.tslibs.base', 'rltk', 'logging.config', 'cmath'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) + +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='t2wml-waitress', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True ) \ No newline at end of file