mirror of
https://github.com/Py-KMS-Organization/py-kms.git
synced 2024-09-19 07:18:51 -04:00
commit
314cefb773
40 changed files with 653 additions and 2389 deletions
|
@ -1,16 +1,13 @@
|
|||
name: Build Image On Release
|
||||
name: Build release-tags
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
bake:
|
||||
bake-latest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
|
@ -24,11 +21,6 @@ jobs:
|
|||
platforms: all
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.10.0
|
||||
with:
|
||||
|
@ -42,7 +34,10 @@ jobs:
|
|||
file: ./docker/docker-py3-kms/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
tags: pykmsorg/py-kms:python3,ghcr.io/py-kms-organization/py-kms:python3
|
||||
tags: ghcr.io/py-kms-organization/py-kms:python3
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
|
@ -50,4 +45,7 @@ jobs:
|
|||
file: ./docker/docker-py3-kms-minimal/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
tags: pykmsorg/py-kms:latest,ghcr.io/py-kms-organization/py-kms:latest,pykmsorg/py-kms:minimal,ghcr.io/py-kms-organization/py-kms:minimal
|
||||
tags: ghcr.io/py-kms-organization/py-kms:latest,ghcr.io/py-kms-organization/py-kms:minimal
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
51
.github/workflows/bake_to_next.yml
vendored
Normal file
51
.github/workflows/bake_to_next.yml
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
name: Build next-tags
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- next
|
||||
|
||||
jobs:
|
||||
bake-next:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
platforms: all
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.10.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/docker-py3-kms/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
tags: ghcr.io/py-kms-organization/py-kms:python3-next
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/docker-py3-kms-minimal/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
tags: ghcr.io/py-kms-organization/py-kms:latest-next,ghcr.io/py-kms-organization/py-kms:minimal-next
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
38
.github/workflows/bake_to_test.yml
vendored
Normal file
38
.github/workflows/bake_to_test.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
name: Test-Build Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
bake-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
platforms: all
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/docker-py3-kms/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: false
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/docker-py3-kms-minimal/Dockerfile
|
||||
platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||
push: false
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,7 +2,6 @@
|
|||
pykms_logserver.log*
|
||||
pykms_logclient.log*
|
||||
pykms_database.db*
|
||||
etrigan.log*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -1,5 +1,49 @@
|
|||
# Changelog
|
||||
|
||||
### py-kms_2022-12-16
|
||||
- Added support for new web-gui into Docker
|
||||
- Implemented whole-new web-based GUI with Flask
|
||||
- Removed old GUI (Etrigan) from code and resources
|
||||
- Removed sqliteweb
|
||||
- Removed Etrigan (GUI)
|
||||
|
||||
### py-kms_2022-12-07
|
||||
- Added warning about Etrigan (GUI) being deprecated
|
||||
- More docs (do not run on same machine as client)
|
||||
- Added Docker support for multiple listen IPs
|
||||
- Added graceful Docker shutdowns
|
||||
|
||||
### py-kms_2021-12-23
|
||||
- More Windows 10/11 keys
|
||||
- Fixed some deprecation warnings
|
||||
- Fixed SO_REUSEPORT platform checks
|
||||
- Fixed loglevel "MININFO" with Docker
|
||||
- Added Docker healthcheck
|
||||
- Added UID/GID change support for Docker
|
||||
- Dependabot alerts
|
||||
|
||||
### py-kms_2021-10-22
|
||||
- Integrated Office 2021 GLVK keys & database
|
||||
- Docker entrypoint fixes
|
||||
- Updated docs to include SQLite stuff
|
||||
- Fix for undefined timezones
|
||||
- Removed LOGFILE extension checks
|
||||
- Added support for Windows 11
|
||||
|
||||
### py-kms_2021-10-07
|
||||
- Helm charts for Kubernetes deployment
|
||||
- Windows 2022 updates
|
||||
- Faster Github Action builds
|
||||
|
||||
### py-kms_2021-11-12
|
||||
- Addded GHCR support
|
||||
- Docs table reformatted
|
||||
- Updated GUI
|
||||
- Windows Sandbox fix
|
||||
- Added contribution guidelines
|
||||
- Docker multiarch
|
||||
- Reshot screenshots in docs
|
||||
|
||||
### py-kms_2020-10-01
|
||||
- Sql database path customizable.
|
||||
- Sql database file keeps different AppId.
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Matteo ℱan
|
||||
|
||||
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.
|
|
@ -2,7 +2,6 @@
|
|||
![repo-size](https://img.shields.io/github/repo-size/Py-KMS-Organization/py-kms)
|
||||
![open-issues](https://img.shields.io/github/issues/Py-KMS-Organization/py-kms)
|
||||
![last-commit](https://img.shields.io/github/last-commit/Py-KMS-Organization/py-kms/master)
|
||||
![docker-pulls](https://img.shields.io/docker/pulls/pykmsorg/py-kms)
|
||||
![read-the-docs](https://img.shields.io/readthedocs/py-kms)
|
||||
***
|
||||
|
||||
|
@ -42,10 +41,9 @@ This version of _py-kms_ is for itself a fork of the original implementation by
|
|||
The wiki has been completly reworked and is now available on [readthedocs.com](https://py-kms.readthedocs.io/en/latest/). It should you provide all necessary information how to setup and to use _py-kms_ , all without clumping this readme. The documentation also houses more details about activation with _py-kms_ and how to get GVLK keys.
|
||||
|
||||
## Quick start
|
||||
- To start the server, execute `python3 pykms_Server.py [IPADDRESS] [PORT]`, the default _IPADDRESS_ is `0.0.0.0` ( all interfaces ) and the default _PORT_ is `1688`. Note that both the address and port are optional. It's allowed to use IPv4 and IPv6 addresses. If you have a IPv6-capable dual-stack OS, a dual-stack socket is created when using a IPv6 address.
|
||||
- To start the server, execute `python3 pykms_Server.py [IPADDRESS] [PORT]`, the default _IPADDRESS_ is `::` ( all interfaces ) and the default _PORT_ is `1688`. Note that both the address and port are optional. It's allowed to use IPv4 and IPv6 addresses. If you have a IPv6-capable dual-stack OS, a dual-stack socket is created when using a IPv6 address. **In case your OS does not support IPv6, make sure to explicitly specify the legacy IPv4 of `0.0.0.0`!**
|
||||
- To start the server automatically using Docker, execute `docker run -d --name py-kms --restart always -p 1688:1688 ghcr.io/py-kms-organization/py-kms`.
|
||||
- To show the help pages type: `python3 pykms_Server.py -h` and `python3 pykms_Client.py -h`.
|
||||
|
||||
## License
|
||||
- _py-kms_ is [![Unlicense](https://img.shields.io/badge/license-unlicense-lightgray.svg)](https://github.com/SystemRage/py-kms/blob/master/LICENSE)
|
||||
- _py-kms GUI_ is [![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SystemRage/py-kms/blob/master/LICENSE.gui.md) © Matteo ℱan
|
||||
|
|
|
@ -29,7 +29,7 @@ For more information please refer to the Helm Install command documentation loca
|
|||
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"pykmsorg/py-kms"` | |
|
||||
| image.repository | string | `"ghcr.io/py-kms-organization/py-kms"` | |
|
||||
| image.tag | string | `"python3"` | |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.annotations | object | `{}` | |
|
||||
|
@ -44,10 +44,9 @@ For more information please refer to the Helm Install command documentation loca
|
|||
| podAnnotations | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| py-kms.environment.HWID | string | `"RANDOM"` | |
|
||||
| py-kms.environment.IP | string | `"0.0.0.0"` | |
|
||||
| py-kms.environment.IP | string | `"::"` | |
|
||||
| py-kms.environment.LOGLEVEL | string | `"INFO"` | |
|
||||
| py-kms.environment.LOGSIZE | int | `2` | |
|
||||
| py-kms.environment.SQLITE | bool | `true` | |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext | object | `{}` | |
|
||||
|
|
|
@ -45,14 +45,17 @@ spec:
|
|||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
httpGet:
|
||||
port: http
|
||||
path: /readyz
|
||||
failureThreshold: 30 # 30 seconds seem to be enough under heavy workloads
|
||||
periodSeconds: 1
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /livez
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: pykmsorg/py-kms
|
||||
repository: ghcr.io/py-kms-organization/py-kms
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: python3
|
||||
|
@ -20,8 +20,7 @@ py-kms:
|
|||
LOGSIZE: 2
|
||||
LOGFILE: /var/log/py-kms.log
|
||||
HWID: RANDOM
|
||||
SQLITE: true
|
||||
IP: 0.0.0.0
|
||||
IP: '::'
|
||||
|
||||
serviceAccount: {}
|
||||
# # Specifies whether a service account should be created
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# This is a minimized version from docker/docker-py3-kms/Dockerfile without SQLite support to further reduce image size
|
||||
FROM alpine:3.15
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV IP ::
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
|
@ -12,15 +12,12 @@ ENV HWID RANDOM
|
|||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE STDOUT
|
||||
ENV LOGSIZE ""
|
||||
ENV TYPE MINIMAL
|
||||
|
||||
COPY ./py-kms /home/py-kms
|
||||
COPY docker/requirements_minimal.txt /home/py-kms/requirements.txt
|
||||
COPY docker/docker-py3-kms-minimal/requirements.txt /home/py-kms/requirements.txt
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
python3 \
|
||||
py3-pip \
|
||||
python3-tkinter \
|
||||
ca-certificates \
|
||||
shadow \
|
||||
tzdata \
|
||||
|
@ -31,6 +28,7 @@ bash \
|
|||
# Fix undefined timezone, in case the user did not mount the /etc/localtime
|
||||
&& ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
||||
|
||||
COPY ./py-kms /home/py-kms
|
||||
COPY docker/entrypoint.py /usr/bin/entrypoint.py
|
||||
COPY docker/start.py /usr/bin/start.py
|
||||
|
||||
|
|
2
docker/docker-py3-kms-minimal/requirements.txt
Normal file
2
docker/docker-py3-kms-minimal/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
dnspython==2.2.1
|
||||
tzlocal==4.2
|
|
@ -1,28 +1,27 @@
|
|||
# Switch to the target image
|
||||
FROM alpine:3.15
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ARG BUILD_COMMIT=unknown
|
||||
ARG BUILD_BRANCH=unknown
|
||||
|
||||
ENV IP ::
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV SQLITE true
|
||||
ENV SQLITE_PORT 8080
|
||||
ENV HWID RANDOM
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE STDOUT
|
||||
ENV LOGSIZE ""
|
||||
ENV TZ America/Chicago
|
||||
|
||||
COPY py-kms /home/py-kms/
|
||||
COPY docker/requirements.txt /home/py-kms/
|
||||
COPY docker/docker-py3-kms/requirements.txt /home/py-kms/
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
python3 \
|
||||
py3-pip \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
|
@ -36,15 +35,20 @@ RUN apk add --no-cache --update \
|
|||
# Fix undefined timezone, in case the user did not mount the /etc/localtime
|
||||
&& ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
||||
|
||||
COPY py-kms /home/py-kms/
|
||||
COPY docker/entrypoint.py /usr/bin/entrypoint.py
|
||||
COPY docker/start.py /usr/bin/start.py
|
||||
|
||||
# Web-interface specifics
|
||||
COPY LICENSE /LICENSE
|
||||
RUN echo "$BUILD_COMMIT" > /VERSION && echo "$BUILD_BRANCH" >> /VERSION
|
||||
|
||||
RUN chmod 755 /usr/bin/entrypoint.py
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
EXPOSE 8080
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=3s --start-period=10s --retries=4 CMD echo | nc -z ${IP%% *} ${PORT} || exit 1
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Flask==2.1.2
|
||||
Pygments==2.12.0
|
||||
dnspython==2.2.1
|
||||
tzlocal==4.2
|
||||
sqlite-web==0.4.0
|
||||
|
||||
Flask==2.1.2
|
||||
gunicorn==20.1.0
|
|
@ -1,4 +0,0 @@
|
|||
Flask==2.1.2
|
||||
Pygments==2.12.0
|
||||
dnspython==2.2.1
|
||||
tzlocal==4.2
|
|
@ -20,35 +20,20 @@ argumentVariableMapping = {
|
|||
'-e': 'EPID'
|
||||
}
|
||||
|
||||
sqliteWebPath = '/home/sqlite_web/sqlite_web.py'
|
||||
enableSQLITE = os.environ.get('SQLITE', 'false').lower() == 'true' and os.environ.get('TYPE') != 'MINIMAL'
|
||||
dbPath = os.path.join(os.sep, 'home', 'py-kms', 'db', 'pykms_database.db')
|
||||
db_path = os.path.join(os.sep, 'home', 'py-kms', 'db', 'pykms_database.db')
|
||||
log_level_bootstrap = log_level = os.environ.get('LOGLEVEL', 'INFO')
|
||||
if log_level_bootstrap == "MININFO":
|
||||
log_level_bootstrap = "INFO"
|
||||
log_file = os.environ.get('LOGFILE', 'STDOUT')
|
||||
listen_ip = os.environ.get('IP', '0.0.0.0').split()
|
||||
listen_ip = os.environ.get('IP', '::').split()
|
||||
listen_port = os.environ.get('PORT', '1688')
|
||||
sqlite_port = os.environ.get('SQLITE_PORT', '8080')
|
||||
|
||||
|
||||
def start_kms_client():
|
||||
if not os.path.isfile(dbPath):
|
||||
# Start a dummy activation to ensure the database file is created
|
||||
client_cmd = [PYTHON3, '-u', 'pykms_Client.py', listen_ip[0], listen_port,
|
||||
'-m', 'Windows10', '-n', 'DummyClient', '-c', 'ae3a27d1-b73a-4734-9878-70c949815218',
|
||||
'-V', log_level, '-F', log_file]
|
||||
if os.environ.get('LOGSIZE', '') != "":
|
||||
client_cmd.append('-S')
|
||||
client_cmd.append(os.environ.get('LOGSIZE'))
|
||||
loggersrv.info("Starting a dummy activation to ensure the database file is created")
|
||||
loggersrv.debug("client_cmd: %s" % (" ".join(str(x) for x in client_cmd).strip()))
|
||||
|
||||
subprocess.run(client_cmd)
|
||||
|
||||
want_webui = os.environ.get('WEBUI', '0')
|
||||
|
||||
def start_kms():
|
||||
sqlite_process = None
|
||||
# Make sure the full path to the db exists
|
||||
if want_webui and not os.path.exists(os.path.dirname(db_path)):
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
# Build the command to execute
|
||||
command = [PYTHON3, '-u', 'pykms_Server.py', listen_ip[0], listen_port]
|
||||
for (arg, env) in argumentVariableMapping.items():
|
||||
|
@ -60,25 +45,25 @@ def start_kms():
|
|||
for i in range(1, len(listen_ip)):
|
||||
command.append("-n")
|
||||
command.append(listen_ip[i] + "," + listen_port)
|
||||
|
||||
if enableSQLITE:
|
||||
loggersrv.info("Storing database file to %s" % dbPath)
|
||||
if want_webui:
|
||||
command.append('-s')
|
||||
command.append(dbPath)
|
||||
os.makedirs(os.path.dirname(dbPath), exist_ok=True)
|
||||
command.append(db_path)
|
||||
|
||||
loggersrv.debug("server_cmd: %s" % (" ".join(str(x) for x in command).strip()))
|
||||
pykms_process = subprocess.Popen(command)
|
||||
pykms_webui_process = None
|
||||
|
||||
# In case SQLITE is defined: Start the web interface
|
||||
if enableSQLITE:
|
||||
time.sleep(5) # The server may take a while to start
|
||||
start_kms_client()
|
||||
sqlite_cmd = ['sqlite_web', '-H', listen_ip[0], '--read-only', '-x',
|
||||
dbPath, '-p', sqlite_port]
|
||||
|
||||
loggersrv.debug("sqlite_cmd: %s" % (" ".join(str(x) for x in sqlite_cmd).strip()))
|
||||
sqlite_process = subprocess.Popen(sqlite_cmd)
|
||||
try:
|
||||
if want_webui:
|
||||
time.sleep(2) # Wait for the server to start up
|
||||
pykms_webui_env = os.environ.copy()
|
||||
pykms_webui_env['PYKMS_SQLITE_DB_PATH'] = db_path
|
||||
pykms_webui_env['PORT'] = '8080'
|
||||
pykms_webui_env['PYKMS_LICENSE_PATH'] = '/LICENSE'
|
||||
pykms_webui_env['PYKMS_VERSION_PATH'] = '/VERSION'
|
||||
pykms_webui_process = subprocess.Popen(['gunicorn', '--log-level', os.environ.get('LOGLEVEL'), 'pykms_WebUI:app'], env=pykms_webui_env)
|
||||
except Exception as e:
|
||||
loggersrv.error("Failed to start webui: %s" % e)
|
||||
|
||||
try:
|
||||
pykms_process.wait()
|
||||
|
@ -88,8 +73,8 @@ def start_kms():
|
|||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
if enableSQLITE:
|
||||
if None != sqlite_process: sqlite_process.terminate()
|
||||
if pykms_webui_process:
|
||||
pykms_webui_process.terminate()
|
||||
pykms_process.terminate()
|
||||
|
||||
|
||||
|
|
|
@ -6,23 +6,23 @@ What follows are some guides how to start the `pykms_Server.py` script, which pr
|
|||
You can simply manage a daemon that runs as a background process. This can be achieved by using any of the notes below or by writing your own solution.
|
||||
|
||||
### Docker
|
||||
![docker-pulls](https://img.shields.io/docker/pulls/pykmsorg/py-kms)
|
||||
![docker-size](https://img.shields.io/docker/image-size/pykmsorg/py-kms)
|
||||
|
||||
If you wish to get _py-kms_ just up and running without installing any dependencies or writing own scripts: Just use Docker !
|
||||
Docker also solves problems regarding the explicit IPv4 and IPv6 usage (it just supports both). The following
|
||||
command will download, "install" and start _py-kms_ and also keep it alive after any service disruption.
|
||||
```bash
|
||||
docker run -d --name py-kms --restart always -p 1688:1688 -v /etc/localtime:/etc/localtime:ro ghcr.io/py-kms-organization/py-kms
|
||||
```
|
||||
If you just want to use the image and don't want to build them yourself, you can always use the official image at the [Docker Hub](https://hub.docker.com/r/pykmsorg/py-kms) (`pykmsorg/py-kms`) or [GitHub Container Registry](https://github.com/Py-KMS-Organization/py-kms/pkgs/container/py-kms) (`ghcr.io/py-kms-organization/py-kms`). To ensure that you are using always the
|
||||
latest version you should check something like [watchtower](https://github.com/containrrr/watchtower) out!
|
||||
If you just want to use the image and don't want to build them yourself, you can always use the official image at the [GitHub Container Registry](https://github.com/Py-KMS-Organization/py-kms/pkgs/container/py-kms) (`ghcr.io/py-kms-organization/py-kms`). To ensure that you are using always the latest version you should check something like [watchtower](https://github.com/containrrr/watchtower) out!
|
||||
|
||||
#### Tags
|
||||
There are currently three tags of the image available (select one just by appending `:<tag>` to the image from above):
|
||||
* `latest`, currently the same like `minimal`.
|
||||
* `minimal`, which is based on the python3 minimal configuration of py-kms. _This tag does NOT include `sqlite` support !_
|
||||
* `python3`, which is fully configurable and equipped with `sqlite` support and a web interface (make sure to expose port 8080) for management.
|
||||
* `python3`, which is fully configurable and equipped with `sqlite` support and a web-interface (make sure to expose port `8080`) for management.
|
||||
|
||||
Wait... Web-interface? Yes! `py-kms` now comes with a simple web-ui to let you browse the known clients or its supported products. In case you wonder, here is a screenshot of the web-ui (*note that this screenshot may not reflect the current state of the ui*):
|
||||
|
||||
![web-ui](img/webinterface.png)
|
||||
|
||||
#### Architectures
|
||||
There are currently the following architectures available (if you need an other, feel free to open an issue):
|
||||
|
@ -46,8 +46,7 @@ services:
|
|||
- 1688:1688
|
||||
- 8080:8080
|
||||
environment:
|
||||
- IP=0.0.0.0
|
||||
- SQLITE=true
|
||||
- IP='::'
|
||||
- HWID=RANDOM
|
||||
- LOGLEVEL=INFO
|
||||
restart: always
|
||||
|
@ -62,11 +61,10 @@ Below is a little bit more extended run command, detailing all the different sup
|
|||
docker run -it -d --name py3-kms \
|
||||
-p 8080:8080 \
|
||||
-p 1688:1688 \
|
||||
-e SQLITE=true \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
--restart unless-stopped ghcr.io/py-kms-organization/py-kms:[TAG]
|
||||
```
|
||||
You can omit the `-e SQLITE=...` and `-p 8080:8080` option if you plan to use the `minimal` or `latest` image, which does not include the respective module support.
|
||||
You can omit the `-p 8080:8080` option if you plan to use the `minimal` or `latest` image, which does not include the `sqlite` module support.
|
||||
|
||||
### Systemd
|
||||
If you are running a Linux distro using `systemd`, create the file: `sudo nano /etc/systemd/system/py3-kms.service`, then add the following (change it where needed) and save:
|
||||
|
@ -82,7 +80,7 @@ Restart=always
|
|||
RestartSec=1
|
||||
KillMode=process
|
||||
User=root
|
||||
ExecStart=/usr/bin/python3 </path/to/your/pykms/files/folder>/py-kms/pykms_Server.py 0.0.0.0 1688 -V DEBUG -F </path/to/your/log/files/folder>/pykms_logserver.log
|
||||
ExecStart=/usr/bin/python3 </path/to/your/pykms/files/folder>/py-kms/pykms_Server.py :: 1688 -V DEBUG -F </path/to/your/log/files/folder>/pykms_logserver.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@ -91,10 +89,6 @@ Check syntax with `sudo systemd-analyze verify py3-kms.service`, correct file pe
|
|||
start the daemon `sudo systemctl start py3-kms.service` and view its status `sudo systemctl status py3-kms.service`. Check if daemon is correctly running with `cat </path/to/your/log/files/folder>/pykms_logserver.log`. Finally a
|
||||
few generic commands useful for interact with your daemon [here](https://linoxide.com/linux-how-to/enable-disable-services-ubuntu-systemd-upstart/).
|
||||
|
||||
### Etrigan (deprecated)
|
||||
You can run py-kms daemonized (via [Etrigan](https://github.com/SystemRage/Etrigan)) using a command like `python3 pykms_Server.py etrigan start` and stop it with `python3 pykms_Server.py etrigan stop`. With Etrigan you have another
|
||||
way to launch py-kms GUI (specially suitable if you're using a virtualenv), so `python3 pykms_Server.py etrigan start -g` and stop the GUI with `python3 pykms_Server.py etrigan stop` (or interact with the `EXIT` button).
|
||||
|
||||
### Upstart (deprecated)
|
||||
If you are running a Linux distro using `upstart` (deprecated), create the file: `sudo nano /etc/init/py3-kms.conf`, then add the following (change it where needed) and save:
|
||||
```
|
||||
|
@ -105,7 +99,7 @@ env PYKMSPATH=</path/to/your/pykms/files/folder>/py-kms
|
|||
env LOGPATH=</path/to/your/log/files/folder>/pykms_logserver.log
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [016]
|
||||
exec $PYTHONPATH/python3 $PYKMSPATH/pykms_Server.py 0.0.0.0 1688 -V DEBUG -F $LOGPATH
|
||||
exec $PYTHONPATH/python3 $PYKMSPATH/pykms_Server.py :: 1688 -V DEBUG -F $LOGPATH
|
||||
respawn
|
||||
```
|
||||
Check syntax with `sudo init-checkconf -d /etc/init/py3-kms.conf`, then reload upstart to recognise this process `sudo initctl reload-configuration`. Now start the service `sudo start py3-kms`, and you can see the logfile
|
||||
|
@ -125,7 +119,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
|
|||
_svc_name_ = "py-kms"
|
||||
_svc_display_name_ = "py-kms"
|
||||
_proc = None
|
||||
_cmd = ["C:\Windows\Python27\python.exe", "C:\Windows\Python27\py-kms\pykms_Server.py"]
|
||||
_cmd = ["C:\Windows\Python27\python.exe", "C:\Windows\Python27\py-kms\pykms_Server.py"] # UPDATE THIS - because Python 2.7 is end of life and you will use other parameters anyway
|
||||
|
||||
def __init__(self,args):
|
||||
win32serviceutil.ServiceFramework.__init__(self,args)
|
||||
|
@ -168,10 +162,10 @@ They might be useful to you:
|
|||
- Python 3.x.
|
||||
- If the `tzlocal` module is installed, the "Request Time" in the verbose output will be converted into local time. Otherwise, it will be in UTC.
|
||||
- It can use the `sqlite3` module, storing activation data in a database so it can be recalled again.
|
||||
- Installation example on Ubuntu / Mint:
|
||||
- Installation example on Ubuntu / Mint (`requirements.txt` is from the sources):
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install python3-tk python3-pip`
|
||||
- `sudo pip3 install tzlocal pysqlite3` (on Ubuntu Server 22, you'll need `pysqlite3-binary` - see [this issue](https://github.com/Py-KMS-Organization/py-kms/issues/76))
|
||||
- `sudo apt-get install python3-pip`
|
||||
- `pip3 install -r requirements.txt` (on Ubuntu Server 22, you'll need `pysqlite3-binary` - see [this issue](https://github.com/Py-KMS-Organization/py-kms/issues/76))
|
||||
|
||||
### Startup
|
||||
A Linux user with `ip addr` command can get his KMS IP (Windows users can try `ipconfig /all`).
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
Follows a list of usable parameters:
|
||||
|
||||
ip <IPADDRESS>
|
||||
> Instructs py-kms to listen on _IPADDRESS_ (can be an hostname too). If this option is not specified, _IPADDRESS_ 0.0.0.0 is used.
|
||||
> Instructs py-kms to listen on _IPADDRESS_ (can be an hostname too). If this option is not specified, _IPADDRESS_ `::` is used.
|
||||
|
||||
port <PORT>
|
||||
> Define TCP _PORT_ the KMS service is listening on. Default is 1688.
|
||||
|
@ -53,7 +53,6 @@ e.g. because it could not reach the server. The default is 120 minutes (2 hours)
|
|||
|
||||
-s or --sqlite [<SQLFILE>]
|
||||
> Use this option to store request information from unique clients in an SQLite database. Deactivated by default.
|
||||
If enabled the default database file is _pykms_database.db_. You can also provide a specific location.
|
||||
|
||||
-t0 or --timeout-idle <TIMEOUTIDLE>
|
||||
> Maximum inactivity time (in seconds) after which the connection with the client is closed.
|
||||
|
@ -75,7 +74,7 @@ user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py -V INFO
|
|||
```
|
||||
creates _pykms_logserver.log_ with these initial messages:
|
||||
```
|
||||
Mon, 12 Jun 2017 22:09:00 INFO TCP server listening at 0.0.0.0 on port 1688.
|
||||
Mon, 12 Jun 2017 22:09:00 INFO TCP server listening at :: on port 1688.
|
||||
Mon, 12 Jun 2017 22:09:00 INFO HWID: 364F463A8863D35F
|
||||
```
|
||||
|
||||
|
@ -83,11 +82,11 @@ Mon, 12 Jun 2017 22:09:00 INFO HWID: 364F463A8863D35F
|
|||
> Creates a _LOGFILE.log_ logging file. The default is named _pykms_logserver.log_.
|
||||
example:
|
||||
```
|
||||
user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py 192.168.1.102 8080 -F ~/path/to/folder/py-kms/newlogfile.log -V INFO -w RANDOM
|
||||
user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py 192.168.1.102 1688 -F ~/path/to/folder/py-kms/newlogfile.log -V INFO -w RANDOM
|
||||
```
|
||||
creates _newlogfile.log_ with these initial messages:
|
||||
```
|
||||
Mon, 12 Jun 2017 22:09:00 INFO TCP server listening at 192.168.1.102 on port 8080.
|
||||
Mon, 12 Jun 2017 22:09:00 INFO TCP server listening at 192.168.1.102 on port 1688.
|
||||
Mon, 12 Jun 2017 22:09:00 INFO HWID: 58C4F4E53AE14224
|
||||
```
|
||||
|
||||
|
@ -125,14 +124,14 @@ examples (with fictitious addresses and ports):
|
|||
|
||||
| command | address (main) | backlog (main) | reuse port (main) | address (listen) | backlog (listen) | reuse port (listen) | dualstack (main / listen) |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `python3 pykms_Server.py connect -b 12` | ('0.0.0.0', 1688) | 12 | True | [] | [] | [] | False |
|
||||
| `python3 pykms_Server.py connect -b 12` | ('::', 1688) | 12 | True | [] | [] | [] | False |
|
||||
| `python3 pykms_Server.py :: connect -b 12 -u -d` | ('::', 1688) | 12 | False | [] | [] | [] | True |
|
||||
| `python3 pykms_Server.py connect -n 1.1.1.1,1699 -b 10` | ('0.0.0.0', 1688) | 5 | True | [('1.1.1.1', 1699)] | [10] | [True] | False |
|
||||
| `python3 pykms_Server.py connect -n 1.1.1.1,1699 -b 10` | ('::', 1688) | 5 | True | [('1.1.1.1', 1699)] | [10] | [True] | False |
|
||||
| `python3 pykms_Server.py :: 1655 connect -n 2001:db8:0:200::7,1699 -d -b 10 -n 2.2.2.2,1677 -u` | ('::', 1655) | 5 | True | [('2001:db8:0:200::7', 1699), ('2.2.2.2', 1677)] | [10, 5] | [True, False] | True |
|
||||
| `python3 pykms_Server.py connect -b 12 -u -n 1.1.1.1,1699 -b 10 -n 2.2.2.2,1677 -b 15` | ('0.0.0.0', 1688) | 12 | False | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [10, 15] | [False, False] | False |
|
||||
| `python3 pykms_Server.py connect -b 12 -n 1.1.1.1,1699 -u -n 2.2.2.2,1677` | ('0.0.0.0', 1688) | 12 | True | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [12, 12] | [False, True] | False |
|
||||
| `python3 pykms_Server.py connect -d -u -b 8 -n 1.1.1.1,1699 -n 2.2.2.2,1677 -b 12` | ('0.0.0.0', 1688) | 8 | False | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [8, 12] | [False, False] | True |
|
||||
| `python3 pykms_Server.py connect -b 11 -u -n ::,1699 -n 2.2.2.2,1677` | ('0.0.0.0', 1688) | 11 | False | [('::', 1699), ('2.2.2.2', 1677)] | [11, 11] | [False, False] | False |
|
||||
| `python3 pykms_Server.py connect -b 12 -u -n 1.1.1.1,1699 -b 10 -n 2.2.2.2,1677 -b 15` | ('::', 1688) | 12 | False | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [10, 15] | [False, False] | False |
|
||||
| `python3 pykms_Server.py connect -b 12 -n 1.1.1.1,1699 -u -n 2.2.2.2,1677` | ('::', 1688) | 12 | True | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [12, 12] | [False, True] | False |
|
||||
| `python3 pykms_Server.py connect -d -u -b 8 -n 1.1.1.1,1699 -n 2.2.2.2,1677 -b 12` | ('::', 1688) | 8 | False | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [8, 12] | [False, False] | True |
|
||||
| `python3 pykms_Server.py connect -b 11 -u -n ::,1699 -n 2.2.2.2,1677` | ('::', 1688) | 11 | False | [('::', 1699), ('2.2.2.2', 1677)] | [11, 11] | [False, False] | False |
|
||||
|
||||
### pykms_Client.py
|
||||
If _py-kms_ server doesn't works correctly, you can test it with the KMS client `pykms_Client.py`, running on the same machine where you started `pykms_Server.py`.
|
||||
|
@ -202,8 +201,8 @@ You can enable same _pykms_Server.py_ suboptions of `-F`.
|
|||
This are the currently used `ENV` statements from the Dockerfile(s). For further references what exactly the parameters mean, please see the start parameters for the [server](Usage.html#pykms-server-py).
|
||||
```
|
||||
# IP-address
|
||||
# The IP address to listen on. The default is "0.0.0.0" (all interfaces).
|
||||
ENV IP 0.0.0.0
|
||||
# The IP address to listen on. The default is "::" (all interfaces).
|
||||
ENV IP ::
|
||||
|
||||
# TCP-port
|
||||
# The network port to listen on. The default is "1688".
|
||||
|
@ -230,14 +229,6 @@ ENV ACTIVATION_INTERVAL 120
|
|||
# Use this flag to specify the renewal interval (in minutes). Default is 10080 minutes (7 days).
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
|
||||
# Use SQLITE
|
||||
# Use this flag to store request information from unique clients in an SQLite database.
|
||||
ENV SQLITE false
|
||||
|
||||
# TCP-port
|
||||
# The network port to listen with the web interface on. The default is "8080".
|
||||
ENV SQLITE_PORT 8080
|
||||
|
||||
# hwid
|
||||
# Use this flag to specify a HWID.
|
||||
# The HWID must be an 16-character string of hex characters.
|
||||
|
|
BIN
docs/img/webinterface.png
Normal file
BIN
docs/img/webinterface.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
|
@ -1,609 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import atexit
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
import argparse
|
||||
from collections.abc import Sequence
|
||||
|
||||
__version__ = "0.1"
|
||||
__license__ = "MIT License"
|
||||
__author__ = u"Matteo ℱan <SystemRage@protonmail.com>"
|
||||
__copyright__ = "© Copyright 2020"
|
||||
__url__ = "https://github.com/SystemRage/Etrigan"
|
||||
__description__ = "Etrigan: a python daemonizer that rocks."
|
||||
|
||||
|
||||
class Etrigan(object):
|
||||
"""
|
||||
Daemonizer based on double-fork method
|
||||
--------------------------------------
|
||||
Each option can be passed as a keyword argument or modified by assigning
|
||||
to an attribute on the instance:
|
||||
|
||||
jasonblood = Etrigan(pidfile,
|
||||
argument_example_1 = foo,
|
||||
argument_example_2 = bar)
|
||||
|
||||
that is equivalent to:
|
||||
|
||||
jasonblood = Etrigan(pidfile)
|
||||
jasonblood.argument_example_1 = foo
|
||||
jasonblood.argument_example_2 = bar
|
||||
|
||||
Object constructor expects always `pidfile` argument.
|
||||
`pidfile`
|
||||
Path to the pidfile.
|
||||
|
||||
The following other options are defined:
|
||||
`stdin`
|
||||
`stdout`
|
||||
`stderr`
|
||||
:Default: `os.devnull`
|
||||
File objects used as the new file for the standard I/O streams
|
||||
`sys.stdin`, `sys.stdout`, and `sys.stderr` respectively.
|
||||
|
||||
`funcs_to_daemonize`
|
||||
:Default: `[]`
|
||||
Define a list of your custom functions
|
||||
which will be executed after daemonization.
|
||||
If None, you have to subclass Etrigan `run` method.
|
||||
Note that these functions can return elements that will be
|
||||
added to Etrigan object (`etrigan_add` list) so the other subsequent
|
||||
ones can reuse them for further processing.
|
||||
You only have to provide indexes of `etrigan_add` list,
|
||||
(an int (example: 2) for single index or a string (example: '1:4') for slices)
|
||||
as first returning element.
|
||||
|
||||
`want_quit`
|
||||
:Default: `False`
|
||||
If `True`, runs Etrigan `quit_on_start` or `quit_on_stop`
|
||||
lists of your custom functions at the end of `start` or `stop` operations.
|
||||
These can return elements as `funcs_to_daemonize`.
|
||||
|
||||
`logfile`
|
||||
:Default: `None`
|
||||
Path to the output log file.
|
||||
|
||||
`loglevel`
|
||||
:Default: `None`
|
||||
Set the log level of logging messages.
|
||||
|
||||
`mute`
|
||||
:Default: `False`
|
||||
Disable all stdout and stderr messages (before double forking).
|
||||
|
||||
`pause_loop`
|
||||
:Default: `None`
|
||||
Seconds of pause between the calling, in an infinite loop,
|
||||
of every function in `funcs_to_daemonize` list.
|
||||
If `-1`, no pause between the calling, in an infinite loop,
|
||||
of every function in `funcs_to_daemonize` list.
|
||||
If `None`, only one run (no infinite loop) of functions in
|
||||
`funcs_to_daemonize` list, without pause.
|
||||
"""
|
||||
|
||||
def __init__(self, pidfile,
|
||||
stdin = os.devnull, stdout = os.devnull, stderr = os.devnull,
|
||||
funcs_to_daemonize = [], want_quit = False,
|
||||
logfile = None, loglevel = None,
|
||||
mute = False, pause_loop = None):
|
||||
|
||||
self.pidfile = pidfile
|
||||
self.funcs_to_daemonize = funcs_to_daemonize
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.logfile = logfile
|
||||
self.loglevel = loglevel
|
||||
self.mute = mute
|
||||
self.want_quit = want_quit
|
||||
self.pause_loop = pause_loop
|
||||
# internal only.
|
||||
self.homedir = '/'
|
||||
self.umask = 0o22
|
||||
self.etrigan_restart, self.etrigan_reload = (False for _ in range(2))
|
||||
self.etrigan_alive = True
|
||||
self.etrigan_add = []
|
||||
self.etrigan_index = None
|
||||
# seconds of pause between stop and start during the restart of the daemon.
|
||||
self.pause_restart = 5
|
||||
# when terminate a process, seconds to wait until kill the process with signal.
|
||||
# self.pause_kill = 3
|
||||
|
||||
# create logfile.
|
||||
self.setup_files()
|
||||
|
||||
def handle_terminate(self, signum, frame):
|
||||
if os.path.exists(self.pidfile):
|
||||
self.etrigan_alive = False
|
||||
# eventually run quit (on stop) function/s.
|
||||
if self.want_quit:
|
||||
if not isinstance(self.quit_on_stop, (list, tuple)):
|
||||
self.quit_on_stop = [self.quit_on_stop]
|
||||
self.execute(self.quit_on_stop)
|
||||
# then always run quit standard.
|
||||
self.quit_standard()
|
||||
else:
|
||||
self.view(self.logdaemon.error, self.emit_error, "Failed to stop the daemon process: can't find PIDFILE '%s'" %self.pidfile)
|
||||
sys.exit(0)
|
||||
|
||||
def handle_reload(self, signum, frame):
|
||||
self.etrigan_reload = True
|
||||
|
||||
def setup_files(self):
|
||||
self.pidfile = os.path.abspath(self.pidfile)
|
||||
|
||||
if self.logfile is not None:
|
||||
self.logdaemon = logging.getLogger('logdaemon')
|
||||
self.logdaemon.setLevel(self.loglevel)
|
||||
|
||||
filehandler = logging.FileHandler(self.logfile)
|
||||
filehandler.setLevel(self.loglevel)
|
||||
formatter = logging.Formatter(fmt = '[%(asctime)s] [%(levelname)8s] --- %(message)s',
|
||||
datefmt = '%Y-%m-%d %H:%M:%S')
|
||||
filehandler.setFormatter(formatter)
|
||||
self.logdaemon.addHandler(filehandler)
|
||||
else:
|
||||
nullhandler = logging.NullHandler()
|
||||
self.logdaemon.addHandler(nullhandler)
|
||||
|
||||
def emit_error(self, message, to_exit = True):
|
||||
""" Print an error message to STDERR. """
|
||||
if not self.mute:
|
||||
sys.stderr.write(message + '\n')
|
||||
sys.stderr.flush()
|
||||
if to_exit:
|
||||
sys.exit(1)
|
||||
|
||||
def emit_message(self, message, to_exit = False):
|
||||
""" Print a message to STDOUT. """
|
||||
if not self.mute:
|
||||
sys.stdout.write(message + '\n')
|
||||
sys.stdout.flush()
|
||||
if to_exit:
|
||||
sys.exit(0)
|
||||
|
||||
def view(self, logobj, emitobj, msg, **kwargs):
|
||||
options = {'to_exit' : False,
|
||||
'silent' : False
|
||||
}
|
||||
options.update(kwargs)
|
||||
|
||||
if logobj:
|
||||
logobj(msg)
|
||||
if emitobj:
|
||||
if not options['silent']:
|
||||
emitobj(msg, to_exit = options['to_exit'])
|
||||
|
||||
def daemonize(self):
|
||||
"""
|
||||
Double-forks the process to daemonize the script.
|
||||
see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177)
|
||||
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
"""
|
||||
self.view(self.logdaemon.debug, None, "Attempting to daemonize the process...")
|
||||
|
||||
# First fork.
|
||||
self.fork(msg = "First fork")
|
||||
# Decouple from parent environment.
|
||||
self.detach()
|
||||
# Second fork.
|
||||
self.fork(msg = "Second fork")
|
||||
# Write the PID file.
|
||||
self.create_pidfile()
|
||||
self.view(self.logdaemon.info, self.emit_message, "The daemon process has started.")
|
||||
# Redirect standard file descriptors.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
self.attach('stdin', mode = 'r')
|
||||
self.attach('stdout', mode = 'a+')
|
||||
|
||||
try:
|
||||
self.attach('stderr', mode = 'a+', buffering = 0)
|
||||
except ValueError:
|
||||
# Python 3 can't have unbuffered text I/O.
|
||||
self.attach('stderr', mode = 'a+', buffering = 1)
|
||||
|
||||
# Handle signals.
|
||||
signal.signal(signal.SIGINT, self.handle_terminate)
|
||||
signal.signal(signal.SIGTERM, self.handle_terminate)
|
||||
signal.signal(signal.SIGHUP, self.handle_reload)
|
||||
#signal.signal(signal.SIGKILL....)
|
||||
|
||||
def fork(self, msg):
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
self.view(self.logdaemon.debug, None, msg + " success with PID %d." %pid)
|
||||
# Exit from parent.
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
msg += " failed: %s." %str(e)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def detach(self):
|
||||
# cd to root for a guarenteed working dir.
|
||||
try:
|
||||
os.chdir(self.homedir)
|
||||
except Exception as e:
|
||||
msg = "Unable to change working directory: %s." %str(e)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
# clear the session id to clear the controlling tty.
|
||||
pid = os.setsid()
|
||||
if pid == -1:
|
||||
sys.exit(1)
|
||||
|
||||
# set the umask so we have access to all files created by the daemon.
|
||||
try:
|
||||
os.umask(self.umask)
|
||||
except Exception as e:
|
||||
msg = "Unable to change file creation mask: %s." %str(e)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def attach(self, name, mode, buffering = -1):
|
||||
with open(getattr(self, name), mode, buffering) as stream:
|
||||
os.dup2(stream.fileno(), getattr(sys, name).fileno())
|
||||
|
||||
def checkfile(self, path, typearg, typefile):
|
||||
filename = os.path.basename(path)
|
||||
pathname = os.path.dirname(path)
|
||||
if not os.path.isdir(pathname):
|
||||
msg = "argument %s: invalid directory: '%s'. Exiting..." %(typearg, pathname)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
elif not filename.lower().endswith(typefile):
|
||||
msg = "argument %s: not a %s file, invalid extension: '%s'. Exiting..." %(typearg, typefile, filename)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def create_pidfile(self):
|
||||
atexit.register(self.delete_pidfile)
|
||||
pid = os.getpid()
|
||||
try:
|
||||
with open(self.pidfile, 'w+') as pf:
|
||||
pf.write("%s\n" %pid)
|
||||
self.view(self.logdaemon.debug, None, "PID %d written to '%s'." %(pid, self.pidfile))
|
||||
except Exception as e:
|
||||
msg = "Unable to write PID to PIDFILE '%s': %s" %(self.pidfile, str(e))
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def delete_pidfile(self, pid):
|
||||
# Remove the PID file.
|
||||
try:
|
||||
os.remove(self.pidfile)
|
||||
self.view(self.logdaemon.debug, None, "Removing PIDFILE '%s' with PID %d." %(self.pidfile, pid))
|
||||
except Exception as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
self.view(self.logdaemon.error, self.emit_error, str(e))
|
||||
|
||||
def get_pidfile(self):
|
||||
# Get the PID from the PID file.
|
||||
if self.pidfile is None:
|
||||
return None
|
||||
if not os.path.isfile(self.pidfile):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(self.pidfile, 'r') as pf:
|
||||
pid = int(pf.read().strip())
|
||||
self.view(self.logdaemon.debug, None, "Found PID %d in PIDFILE '%s'" %(pid, self.pidfile))
|
||||
except Exception as e:
|
||||
self.view(self.logdaemon.warning, None, "Empty or broken PIDFILE")
|
||||
pid = None
|
||||
|
||||
def pid_exists(pid):
|
||||
# psutil _psposix.py.
|
||||
if pid == 0:
|
||||
return True
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH:
|
||||
return False
|
||||
elif e.errno == errno.EPERM:
|
||||
return True
|
||||
else:
|
||||
self.view(self.logdaemon.error, self.emit_error, str(e))
|
||||
else:
|
||||
return True
|
||||
|
||||
if pid is not None and pid_exists(pid):
|
||||
return pid
|
||||
else:
|
||||
# Remove the stale PID file.
|
||||
self.delete_pidfile(pid)
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
""" Start the daemon. """
|
||||
self.view(self.logdaemon.info, self.emit_message, "Starting the daemon process...", silent = self.etrigan_restart)
|
||||
|
||||
# Check for a PID file to see if the Daemon is already running.
|
||||
pid = self.get_pidfile()
|
||||
if pid is not None:
|
||||
msg = "A previous daemon process with PIDFILE '%s' already exists. Daemon already running ?" %self.pidfile
|
||||
self.view(self.logdaemon.warning, self.emit_error, msg, to_exit = False)
|
||||
return
|
||||
|
||||
# Daemonize the main process.
|
||||
self.daemonize()
|
||||
# Start a infinitive loop that periodically runs `funcs_to_daemonize`.
|
||||
self.loop()
|
||||
# eventualy run quit (on start) function/s.
|
||||
if self.want_quit:
|
||||
if not isinstance(self.quit_on_start, (list, tuple)):
|
||||
self.quit_on_start = [self.quit_on_start]
|
||||
self.execute(self.quit_on_start)
|
||||
|
||||
def stop(self):
|
||||
""" Stop the daemon. """
|
||||
self.view(None, self.emit_message, "Stopping the daemon process...", silent = self.etrigan_restart)
|
||||
|
||||
self.logdaemon.disabled = True
|
||||
pid = self.get_pidfile()
|
||||
self.logdaemon.disabled = False
|
||||
if not pid:
|
||||
# Just to be sure. A ValueError might occur
|
||||
# if the PIDFILE is empty but does actually exist.
|
||||
if os.path.exists(self.pidfile):
|
||||
self.delete_pidfile(pid)
|
||||
|
||||
msg = "Can't find the daemon process with PIDFILE '%s'. Daemon not running ?" %self.pidfile
|
||||
self.view(self.logdaemon.warning, self.emit_error, msg, to_exit = False)
|
||||
return
|
||||
|
||||
# Try to kill the daemon process.
|
||||
try:
|
||||
while True:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
if (e.errno != errno.ESRCH):
|
||||
self.view(self.logdaemon.error, self.emit_error, "Failed to stop the daemon process: %s" %str(e))
|
||||
else:
|
||||
self.view(None, self.emit_message, "The daemon process has ended correctly.", silent = self.etrigan_restart)
|
||||
|
||||
def restart(self):
|
||||
""" Restart the daemon. """
|
||||
self.view(self.logdaemon.info, self.emit_message, "Restarting the daemon process...")
|
||||
self.etrigan_restart = True
|
||||
self.stop()
|
||||
if self.pause_restart:
|
||||
time.sleep(self.pause_restart)
|
||||
self.etrigan_alive = True
|
||||
self.start()
|
||||
|
||||
def reload(self):
|
||||
pass
|
||||
|
||||
def status(self):
|
||||
""" Get status of the daemon. """
|
||||
self.view(self.logdaemon.info, self.emit_message, "Viewing the daemon process status...")
|
||||
|
||||
if self.pidfile is None:
|
||||
self.view(self.logdaemon.error, self.emit_error, "Cannot get the status of daemon without PIDFILE.")
|
||||
|
||||
pid = self.get_pidfile()
|
||||
if pid is None:
|
||||
self.view(self.logdaemon.info, self.emit_message, "The daemon process is not running.", to_exit = True)
|
||||
else:
|
||||
try:
|
||||
with open("/proc/%d/status" %pid, 'r') as pf:
|
||||
pass
|
||||
self.view(self.logdaemon.info, self.emit_message, "The daemon process is running.", to_exit = True)
|
||||
except Exception as e:
|
||||
msg = "There is not a process with the PIDFILE '%s': %s" %(self.pidfile, str(e))
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def flatten(self, alistoflists, ltypes = Sequence):
|
||||
# https://stackoverflow.com/questions/2158395/flatten-an-irregular-list-of-lists/2158532#2158532
|
||||
alistoflists = list(alistoflists)
|
||||
while alistoflists:
|
||||
while alistoflists and isinstance(alistoflists[0], ltypes):
|
||||
alistoflists[0:1] = alistoflists[0]
|
||||
if alistoflists: yield alistoflists.pop(0)
|
||||
|
||||
def exclude(self, func):
|
||||
from inspect import getargspec
|
||||
args = getargspec(func)
|
||||
if callable(func):
|
||||
try:
|
||||
args[0].pop(0)
|
||||
except IndexError:
|
||||
pass
|
||||
return args
|
||||
else:
|
||||
self.view(self.logdaemon.error, self.emit_error, "Not a function.")
|
||||
return
|
||||
|
||||
def execute(self, some_functions):
|
||||
returned = None
|
||||
if isinstance(some_functions, (list, tuple)):
|
||||
for func in some_functions:
|
||||
l_req = len(self.exclude(func)[0])
|
||||
|
||||
if l_req == 0:
|
||||
returned = func()
|
||||
else:
|
||||
l_add = len(self.etrigan_add)
|
||||
if l_req > l_add:
|
||||
self.view(self.logdaemon.error, self.emit_error,
|
||||
"Can't evaluate function: given %s, required %s." %(l_add, l_req))
|
||||
return
|
||||
else:
|
||||
arguments = self.etrigan_add[self.etrigan_index]
|
||||
l_args = (len(arguments) if isinstance(arguments, list) else 1)
|
||||
if (l_args > l_req) or (l_args < l_req):
|
||||
self.view(self.logdaemon.error, self.emit_error,
|
||||
"Can't evaluate function: given %s, required %s." %(l_args, l_req))
|
||||
return
|
||||
else:
|
||||
if isinstance(arguments, list):
|
||||
returned = func(*arguments)
|
||||
else:
|
||||
returned = func(arguments)
|
||||
|
||||
if returned:
|
||||
if isinstance(returned, (list, tuple)):
|
||||
if isinstance(returned[0], int):
|
||||
self.etrigan_index = returned[0]
|
||||
else:
|
||||
self.etrigan_index = slice(*map(int, returned[0].split(':')))
|
||||
if returned[1:] != []:
|
||||
self.etrigan_add.append(returned[1:])
|
||||
self.etrigan_add = list(self.flatten(self.etrigan_add))
|
||||
else:
|
||||
self.view(self.logdaemon.error, self.emit_error, "Function should return list or tuple.")
|
||||
returned = None
|
||||
else:
|
||||
if some_functions is None:
|
||||
self.run()
|
||||
|
||||
def loop(self):
|
||||
try:
|
||||
if self.pause_loop is None:
|
||||
# one-shot.
|
||||
self.execute(self.funcs_to_daemonize)
|
||||
else:
|
||||
if self.pause_loop >= 0:
|
||||
# infinite with pause.
|
||||
time.sleep(self.pause_loop)
|
||||
while self.etrigan_alive:
|
||||
self.execute(self.funcs_to_daemonize)
|
||||
time.sleep(self.pause_loop)
|
||||
elif self.pause_loop == -1:
|
||||
# infinite without pause.
|
||||
while self.etrigan_alive:
|
||||
self.execute(self.funcs_to_daemonize)
|
||||
except Exception as e:
|
||||
msg = "The daemon process start method failed: %s" %str(e)
|
||||
self.view(self.logdaemon.error, self.emit_error, msg)
|
||||
|
||||
def quit_standard(self):
|
||||
self.view(self.logdaemon.info, None, "Stopping the daemon process...")
|
||||
self.delete_pidfile(self.get_pidfile())
|
||||
self.view(self.logdaemon.info, None, "The daemon process has ended correctly.")
|
||||
|
||||
def quit_on_start(self):
|
||||
"""
|
||||
Override this method when you subclass Daemon.
|
||||
"""
|
||||
self.quit_standard()
|
||||
|
||||
def quit_on_stop(self):
|
||||
"""
|
||||
Override this method when you subclass Daemon.
|
||||
"""
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Override this method when you subclass Daemon.
|
||||
It will be called after the process has been
|
||||
daemonized by start() or restart().
|
||||
"""
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class JasonBlood(Etrigan):
|
||||
def run(self):
|
||||
jasonblood_func()
|
||||
|
||||
def jasonblood_func():
|
||||
with open(os.path.join('.', 'etrigan_test.txt'), 'a') as file:
|
||||
file.write("Yarva Demonicus Etrigan " + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) + '\n')
|
||||
|
||||
def Etrigan_parser(parser = None):
|
||||
if parser is None:
|
||||
# create a new parser.
|
||||
parser = argparse.ArgumentParser(description = __description__, epilog = __version__)
|
||||
if not parser.add_help:
|
||||
# create help argument.
|
||||
parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit")
|
||||
|
||||
# attach to an existent parser.
|
||||
parser.add_argument("operation", action = "store", choices = ["start", "stop", "restart", "status", "reload"],
|
||||
help = "Select an operation for daemon.", type = str)
|
||||
parser.add_argument("--etrigan-pid",
|
||||
action = "store", dest = "etriganpid", default = "/tmp/etrigan.pid",
|
||||
help = "Choose a pidfile path. Default is \"/tmp/etrigan.pid\".", type = str) #'/var/run/etrigan.pid'
|
||||
parser.add_argument("--etrigan-log",
|
||||
action = "store", dest = "etriganlog", default = os.path.join('.', "etrigan.log"),
|
||||
help = "Use this option to choose an output log file; for not logging don't select it. Default is \"etrigan.log\".", type = str)
|
||||
parser.add_argument("--etrigan-lev",
|
||||
action = "store", dest = "etriganlev", default = "DEBUG",
|
||||
choices = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
help = "Use this option to set a log level. Default is \"DEBUG\".", type = str)
|
||||
parser.add_argument("--etrigan-mute",
|
||||
action = "store_const", dest = 'etriganmute', const = True, default = False,
|
||||
help = "Disable all stdout and stderr messages.")
|
||||
return parser
|
||||
|
||||
class Etrigan_check(object):
|
||||
def emit_opt_err(self, msg):
|
||||
print(msg)
|
||||
sys.exit(1)
|
||||
|
||||
def checkfile(self, path, typearg, typefile):
|
||||
filename, extension = os.path.splitext(path)
|
||||
pathname = os.path.dirname(path)
|
||||
if not os.path.isdir(pathname):
|
||||
msg = "argument `%s`: invalid directory: '%s'. Exiting..." %(typearg, pathname)
|
||||
self.emit_opt_err(msg)
|
||||
elif not extension == typefile:
|
||||
msg = "argument `%s`: not a %s file, invalid extension: '%s'. Exiting..." %(typearg, typefile, extension)
|
||||
self.emit_opt_err(msg)
|
||||
|
||||
def checkfunction(self, funcs, booleans):
|
||||
if not isinstance(funcs, (list, tuple)):
|
||||
if funcs is not None:
|
||||
msg = "argument `funcs_to_daemonize`: provide list, tuple or None"
|
||||
self.emit_opt_err(msg)
|
||||
|
||||
for elem in booleans:
|
||||
if not type(elem) == bool:
|
||||
msg = "argument `want_quit`: not a boolean."
|
||||
self.emit_opt_err(msg)
|
||||
|
||||
def Etrigan_job(type_oper, daemon_obj):
|
||||
Etrigan_check().checkfunction(daemon_obj.funcs_to_daemonize,
|
||||
[daemon_obj.want_quit])
|
||||
if type_oper == "start":
|
||||
daemon_obj.start()
|
||||
elif type_oper == "stop":
|
||||
daemon_obj.stop()
|
||||
elif type_oper == "restart":
|
||||
daemon_obj.restart()
|
||||
elif type_oper == "status":
|
||||
daemon_obj.status()
|
||||
elif type_oper == "reload":
|
||||
daemon_obj.reload()
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
# Parse arguments.
|
||||
parser = Etrigan_parser()
|
||||
args = vars(parser.parse_args())
|
||||
# Check arguments.
|
||||
Etrigan_check().checkfile(args['etriganpid'], '--etrigan-pid', '.pid')
|
||||
Etrigan_check().checkfile(args['etriganlog'], '--etrigan-log', '.log')
|
||||
|
||||
# Setup daemon.
|
||||
jasonblood_1 = Etrigan(pidfile = args['etriganpid'], logfile = args['etriganlog'], loglevel = args['etriganlev'],
|
||||
mute = args['etriganmute'],
|
||||
funcs_to_daemonize = [jasonblood_func], pause_loop = 5)
|
||||
|
||||
## jasonblood_2 = JasonBlood(pidfile = args['etriganpid'], logfile = args['etriganlog'], loglevel = args['etriganlev'],
|
||||
## mute = args['etriganmute'],
|
||||
## funcs_to_daemonize = None, pause_loop = 5)
|
||||
# Do job.
|
||||
Etrigan_job(args['operation'], jasonblood_1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 524 KiB |
|
@ -4,13 +4,12 @@ import binascii
|
|||
import logging
|
||||
import time
|
||||
import uuid
|
||||
import socket
|
||||
|
||||
from pykms_Structure import Structure
|
||||
from pykms_DB2Dict import kmsDB2Dict
|
||||
from pykms_PidGenerator import epidGenerator
|
||||
from pykms_Filetimes import filetime_to_dt
|
||||
from pykms_Sql import sql_initialize, sql_update, sql_update_epid
|
||||
from pykms_Sql import sql_update, sql_update_epid
|
||||
from pykms_Format import justify, byterize, enco, deco, pretty_printer
|
||||
|
||||
#--------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
@ -214,7 +213,6 @@ could be detected as not genuine !{end}" %currentClientCount)
|
|||
'product' : infoDict["skuId"]})
|
||||
# Create database.
|
||||
if self.srv_config['sqlite']:
|
||||
sql_initialize(self.srv_config['sqlite'])
|
||||
sql_update(self.srv_config['sqlite'], infoDict)
|
||||
|
||||
return self.createKmsResponse(kmsRequest, currentClientCount, appName)
|
||||
|
|
|
@ -45,10 +45,9 @@ class client_thread(threading.Thread):
|
|||
def __init__(self, name):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = name
|
||||
self.with_gui = False
|
||||
|
||||
def run(self):
|
||||
clt_main(with_gui = self.with_gui)
|
||||
clt_main()
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
@ -56,7 +55,7 @@ loggerclt = logging.getLogger('logclt')
|
|||
|
||||
# 'help' string - 'default' value - 'dest' string.
|
||||
clt_options = {
|
||||
'ip' : {'help' : 'The IP address or hostname of the KMS server.', 'def' : "0.0.0.0", 'des' : "ip"},
|
||||
'ip' : {'help' : 'The IP address or hostname of the KMS server.', 'def' : "::", 'des' : "ip"},
|
||||
'port' : {'help' : 'The port the KMS service is listening on. The default is \"1688\".', 'def' : 1688, 'des' : "port"},
|
||||
'mode' : {'help' : 'Use this flag to manually specify a Microsoft product for testing the server. The default is \"Windows81\"',
|
||||
'def' : "Windows8.1", 'des' : "mode",
|
||||
|
@ -297,9 +296,8 @@ def client_create(clt_sock):
|
|||
pretty_printer(log_obj = loggerclt.warning, to_exit = True, where = "clt",
|
||||
put_text = "{reverse}{magenta}{bold}Something went wrong. Exiting...{end}")
|
||||
|
||||
def clt_main(with_gui = False):
|
||||
def clt_main():
|
||||
try:
|
||||
if not with_gui:
|
||||
# Parse options.
|
||||
client_options()
|
||||
|
||||
|
@ -393,4 +391,4 @@ def readKmsResponseV6(data):
|
|||
return message
|
||||
|
||||
if __name__ == "__main__":
|
||||
clt_main(with_gui = False)
|
||||
clt_main()
|
||||
|
|
|
@ -274,9 +274,7 @@ class ShellMessage(object):
|
|||
ShellMessage.indx += 1
|
||||
|
||||
def print_logging_setup(self, logger, async_flag, formatter = logging.Formatter('%(name)s %(message)s')):
|
||||
from pykms_GuiBase import gui_redirector
|
||||
stream = gui_redirector(StringIO())
|
||||
handler = logging.StreamHandler(stream)
|
||||
handler = logging.StreamHandler(StringIO())
|
||||
handler.name = 'LogStream'
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(formatter)
|
||||
|
@ -293,9 +291,6 @@ class ShellMessage(object):
|
|||
|
||||
def print_logging(self, toprint):
|
||||
if (self.nshell and ((0 in self.nshell) or (2 in self.nshell and not ShellMessage.viewclt))) or ShellMessage.indx == 0:
|
||||
from pykms_GuiBase import gui_redirector_setup, gui_redirector_clear
|
||||
gui_redirector_setup()
|
||||
gui_redirector_clear()
|
||||
self.print_logging_setup(ShellMessage.loggersrv_pty, ShellMessage.asyncmsgsrv)
|
||||
self.print_logging_setup(ShellMessage.loggerclt_pty, ShellMessage.asyncmsgclt)
|
||||
|
||||
|
@ -405,7 +400,6 @@ def pretty_printer(**kwargs):
|
|||
if None `put_text` must be defined for printing process.
|
||||
`to_exit ` --> if True system exit is called.
|
||||
`where` --> specifies if message is server-side or client-side
|
||||
(useful for GUI redirect).
|
||||
"""
|
||||
# Set defaults for not defined options.
|
||||
options = {'log_obj' : None,
|
||||
|
|
|
@ -1,948 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from time import sleep
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
from tkinter import filedialog
|
||||
import tkinter.font as tkFont
|
||||
|
||||
from pykms_Server import srv_options, srv_version, srv_config, server_terminate, serverqueue, serverthread
|
||||
from pykms_GuiMisc import ToolTip, TextDoubleScroll, TextRedirect, ListboxOfRadiobuttons
|
||||
from pykms_GuiMisc import custom_background, custom_pages
|
||||
from pykms_Client import clt_options, clt_version, clt_config, client_thread
|
||||
|
||||
gui_version = "py-kms_gui_v3.0"
|
||||
__license__ = "MIT License"
|
||||
__author__ = u"Matteo ℱan <SystemRage@protonmail.com>"
|
||||
__copyright__ = "© Copyright 2020"
|
||||
__url__ = "https://github.com/SystemRage/py-kms"
|
||||
gui_description = "A GUI for py-kms."
|
||||
|
||||
##---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
def get_ip_address():
|
||||
if os.name == 'posix':
|
||||
import subprocess
|
||||
ip = subprocess.getoutput("hostname -I")
|
||||
elif os.name == 'nt':
|
||||
import socket
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
else:
|
||||
ip = 'Unknown'
|
||||
return ip
|
||||
|
||||
def gui_redirector(stream, redirect_to = TextRedirect.Pretty, redirect_conditio = True, stderr_side = "srv"):
|
||||
global txsrv, txclt, txcol
|
||||
if redirect_conditio:
|
||||
if stream == 'stdout':
|
||||
sys.stdout = redirect_to(txsrv, txclt, txcol)
|
||||
elif stream == 'stderr':
|
||||
sys.stderr = redirect_to(txsrv, txclt, txcol, stderr_side)
|
||||
else:
|
||||
stream = redirect_to(txsrv, txclt, txcol)
|
||||
return stream
|
||||
|
||||
def gui_redirector_setup():
|
||||
TextRedirect.Pretty.tag_num = 0
|
||||
TextRedirect.Pretty.newlinecut = [-1, -2, -4, -5]
|
||||
|
||||
def gui_redirector_clear():
|
||||
global txsrv, oysrv
|
||||
try:
|
||||
if oysrv:
|
||||
txsrv.configure(state = 'normal')
|
||||
txsrv.delete('1.0', 'end')
|
||||
txsrv.configure(state = 'disabled')
|
||||
except:
|
||||
# self.onlysrv not defined (menu not used)
|
||||
pass
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class KmsGui(tk.Tk):
|
||||
def __init__(self, *args, **kwargs):
|
||||
tk.Tk.__init__(self, *args, **kwargs)
|
||||
self.wraplength = 200
|
||||
serverthread.with_gui = True
|
||||
self.validation_int = (self.register(self.validate_int), "%S")
|
||||
self.validation_float = (self.register(self.validate_float), "%P")
|
||||
|
||||
## Define fonts and colors.
|
||||
self.customfonts = {'btn' : tkFont.Font(family = 'Fixedsys', size = 11, weight = 'bold'),
|
||||
'oth' : tkFont.Font(family = 'Times', size = 9, weight = 'bold'),
|
||||
'opt' : tkFont.Font(family = 'Fixedsys', size = 9, weight = 'bold'),
|
||||
'lst' : tkFont.Font(family = 'Fixedsys', size = 8, weight = 'bold', slant = 'italic'),
|
||||
'msg' : tkFont.Font(family = 'Monospace', size = 6), # need a monospaced type (like courier, etc..).
|
||||
}
|
||||
|
||||
self.customcolors = { 'black' : '#000000',
|
||||
'white' : '#FFFFFF',
|
||||
'green' : '#00EE76',
|
||||
'yellow' : '#FFFF00',
|
||||
'magenta' : '#CD00CD',
|
||||
'orange' : '#FFA500',
|
||||
'red' : '#FF4500',
|
||||
'blue' : '#1E90FF',
|
||||
'cyan' : '#AFEEEE',
|
||||
'lavender': '#E6E6FA',
|
||||
'brown' : '#A52A2A',
|
||||
}
|
||||
|
||||
self.option_add('*TCombobox*Listbox.font', self.customfonts['lst'])
|
||||
|
||||
self.gui_create()
|
||||
|
||||
def invert(self, widgets = []):
|
||||
for widget in widgets:
|
||||
if widget['state'] == 'normal':
|
||||
widget.configure(state = 'disabled')
|
||||
elif widget['state'] == 'disabled':
|
||||
widget.configure(state = 'normal')
|
||||
|
||||
def gui_menu(self):
|
||||
self.onlysrv, self.onlyclt = (False for _ in range(2))
|
||||
menubar = tk.Menu(self)
|
||||
prefmenu = tk.Menu(menubar, tearoff = 0, font = ("Noto Sans Regular", 10), borderwidth = 3, relief = 'ridge')
|
||||
menubar.add_cascade(label = 'Preferences', menu = prefmenu)
|
||||
prefmenu.add_command(label = 'Enable server-side mode', command = lambda: self.pref_onlysrv(prefmenu))
|
||||
prefmenu.add_command(label = 'Enable client-side mode', command = lambda: self.pref_onlyclt(prefmenu))
|
||||
self.config(menu = menubar)
|
||||
|
||||
def pref_onlysrv(self, menu):
|
||||
global oysrv
|
||||
|
||||
if self.onlyclt or serverthread.is_running_server:
|
||||
return
|
||||
self.onlysrv = not self.onlysrv
|
||||
if self.onlysrv:
|
||||
menu.entryconfigure(0, label = 'Disable server-side mode')
|
||||
self.clt_on_show(force_remove = True)
|
||||
else:
|
||||
menu.entryconfigure(0, label = 'Enable server-side mode')
|
||||
self.invert(widgets = [self.shbtnclt])
|
||||
oysrv = self.onlysrv
|
||||
|
||||
def pref_onlyclt(self, menu):
|
||||
if self.onlysrv or serverthread.is_running_server:
|
||||
return
|
||||
self.onlyclt = not self.onlyclt
|
||||
if self.onlyclt:
|
||||
menu.entryconfigure(1, label = 'Disable client-side mode')
|
||||
if self.shbtnclt['text'] == 'SHOW\nCLIENT':
|
||||
self.clt_on_show(force_view = True)
|
||||
self.optsrvwin.grid_remove()
|
||||
self.msgsrvwin.grid_remove()
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr, stderr_side = "clt")
|
||||
else:
|
||||
menu.entryconfigure(1, label = 'Enable client-side mode')
|
||||
self.optsrvwin.grid()
|
||||
self.msgsrvwin.grid()
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr)
|
||||
|
||||
self.invert(widgets = [self.runbtnsrv, self.shbtnclt, self.runbtnclt])
|
||||
|
||||
def gui_create(self):
|
||||
## Create server gui
|
||||
self.gui_srv()
|
||||
## Create client gui + other operations.
|
||||
self.gui_complete()
|
||||
## Create menu.
|
||||
self.gui_menu()
|
||||
## Create globals for printing process (redirect stdout).
|
||||
global txsrv, txclt, txcol
|
||||
txsrv = self.textboxsrv.get()
|
||||
txclt = self.textboxclt.get()
|
||||
txcol = self.customcolors
|
||||
## Redirect stderr.
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr)
|
||||
|
||||
def gui_pages_show(self, pagename, side):
|
||||
# https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter
|
||||
# https://www.reddit.com/r/learnpython/comments/7xxtsy/trying_to_understand_tkinter_and_how_to_switch/
|
||||
pageside = self.pagewidgets[side]
|
||||
tk.Misc.lift(pageside["PageWin"][pagename], aboveThis = None)
|
||||
keylist = list(pageside["PageWin"].keys())
|
||||
|
||||
for elem in [pageside["BtnAni"], pageside["LblAni"]]:
|
||||
if pagename == "PageStart":
|
||||
elem["Left"].config(state = "disabled")
|
||||
if len(keylist) == 2:
|
||||
elem["Right"].config(state = "normal")
|
||||
elif pagename == "PageEnd":
|
||||
elem["Right"].config(state = "disabled")
|
||||
if len(keylist) == 2:
|
||||
elem["Left"].config(state = "normal")
|
||||
else:
|
||||
for where in ["Left", "Right"]:
|
||||
elem[where].config(state = "normal")
|
||||
|
||||
if pagename != "PageStart":
|
||||
page_l = keylist[keylist.index(pagename) - 1]
|
||||
pageside["BtnAni"]["Left"]['command'] = lambda pag=page_l, pos=side: self.gui_pages_show(pag, pos)
|
||||
if pagename != "PageEnd":
|
||||
page_r = keylist[keylist.index(pagename) + 1]
|
||||
pageside["BtnAni"]["Right"]['command'] = lambda pag=page_r, pos=side: self.gui_pages_show(pag, pos)
|
||||
|
||||
def gui_pages_buttons(self, parent, side):
|
||||
btnwin = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
btnwin.grid(row = 14, column = 2, padx = 2, pady = 2, sticky = 'nsew')
|
||||
btnwin.grid_columnconfigure(1, weight = 1)
|
||||
self.pagewidgets[side]["BtnWin"] = btnwin
|
||||
|
||||
for position in ["Left", "Right"]:
|
||||
if position == "Left":
|
||||
col = [0, 0, 1]
|
||||
stick = 'e'
|
||||
elif position == "Right":
|
||||
col = [2, 1, 0]
|
||||
stick = 'w'
|
||||
|
||||
aniwin = tk.Canvas(btnwin, background = self.customcolors['white'], borderwidth = 0, relief = 'ridge')
|
||||
aniwin.grid(row = 0, column = col[0], padx = 5, pady = 5, sticky = 'nsew')
|
||||
self.pagewidgets[side]["AniWin"][position] = aniwin
|
||||
|
||||
lblani = tk.Label(aniwin, width = 1, height = 1)
|
||||
lblani.grid(row = 0, column = col[1], padx = 2, pady = 2, sticky = stick)
|
||||
self.pagewidgets[side]["LblAni"][position] = lblani
|
||||
|
||||
btnani = tk.Button(aniwin)
|
||||
btnani.grid(row = 0, column = col[2], padx = 2, pady = 2, sticky = stick)
|
||||
self.pagewidgets[side]["BtnAni"][position] = btnani
|
||||
## Customize buttons.
|
||||
custom_pages(self, side)
|
||||
|
||||
def gui_pages_create(self, parent, side, create = {}):
|
||||
self.pagewidgets.update({side : {"PageWin" : create,
|
||||
"BtnWin" : None,
|
||||
"BtnAni" : {"Left" : None,
|
||||
"Right" : None},
|
||||
"AniWin" : {"Left" : None,
|
||||
"Right" : None},
|
||||
"LblAni" : {"Left" : None,
|
||||
"Right" : None},
|
||||
}
|
||||
})
|
||||
|
||||
for pagename in self.pagewidgets[side]["PageWin"].keys():
|
||||
page = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.pagewidgets[side]["PageWin"][pagename] = page
|
||||
page.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = "nsew")
|
||||
page.grid_columnconfigure(1, weight = 1)
|
||||
self.gui_pages_buttons(parent = parent, side = side)
|
||||
self.gui_pages_show("PageStart", side = side)
|
||||
|
||||
def gui_store(self, side, typewidgets):
|
||||
stored = []
|
||||
for pagename in self.pagewidgets[side]["PageWin"].keys():
|
||||
for widget in self.pagewidgets[side]["PageWin"][pagename].winfo_children():
|
||||
if widget.winfo_class() in typewidgets:
|
||||
stored.append(widget)
|
||||
return stored
|
||||
|
||||
def gui_srv(self):
|
||||
## Create main containers. ------------------------------------------------------------------------------------------------------------------
|
||||
self.masterwin = tk.Canvas(self, borderwidth = 3, relief = tk.RIDGE)
|
||||
self.btnsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.optsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.msgsrvwin = tk.Frame(self.masterwin, background = self.customcolors['black'], relief = 'ridge', width = 300, height = 200)
|
||||
|
||||
## Layout main containers.
|
||||
self.masterwin.grid(row = 0, column = 0, sticky = 'nsew')
|
||||
self.btnsrvwin.grid(row = 0, column = 1, padx = 2, pady = 2, sticky = 'nw')
|
||||
self.optsrvwin.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = 'nsew')
|
||||
self.optsrvwin.grid_rowconfigure(0, weight = 1)
|
||||
self.optsrvwin.grid_columnconfigure(1, weight = 1)
|
||||
|
||||
self.pagewidgets = {}
|
||||
|
||||
## Subpages of "optsrvwin".
|
||||
self.gui_pages_create(parent = self.optsrvwin, side = "Srv", create = {"PageStart": None,
|
||||
"PageEnd": None})
|
||||
|
||||
## Continue to grid.
|
||||
self.msgsrvwin.grid(row = 1, column = 2, padx = 1, pady = 1, sticky = 'nsew')
|
||||
self.msgsrvwin.grid_propagate(False)
|
||||
self.msgsrvwin.grid_columnconfigure(0, weight = 1)
|
||||
self.msgsrvwin.grid_rowconfigure(0, weight = 1)
|
||||
|
||||
## Create widgets (btnsrvwin) ---------------------------------------------------------------------------------------------------------------
|
||||
self.statesrv = tk.Label(self.btnsrvwin, text = 'Server\nState:\nStopped', font = self.customfonts['oth'],
|
||||
foreground = self.customcolors['red'])
|
||||
self.runbtnsrv = tk.Button(self.btnsrvwin, text = 'START\nSERVER', background = self.customcolors['green'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.srv_on_start)
|
||||
self.shbtnclt = tk.Button(self.btnsrvwin, text = 'SHOW\nCLIENT', background = self.customcolors['magenta'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.clt_on_show)
|
||||
self.defaubtnsrv = tk.Button(self.btnsrvwin, text = 'DEFAULTS', background = self.customcolors['brown'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.on_defaults)
|
||||
self.clearbtnsrv = tk.Button(self.btnsrvwin, text = 'CLEAR', background = self.customcolors['orange'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = lambda: self.on_clear([txsrv, txclt]))
|
||||
self.exitbtnsrv = tk.Button(self.btnsrvwin, text = 'EXIT', background = self.customcolors['black'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.on_exit)
|
||||
|
||||
## Layout widgets (btnsrvwin)
|
||||
self.statesrv.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.runbtnsrv.grid(row = 1, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.shbtnclt.grid(row = 2, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.defaubtnsrv.grid(row = 3, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.clearbtnsrv.grid(row = 4, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.exitbtnsrv.grid(row = 5, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
|
||||
## Create widgets (optsrvwin:Srv:PageWin:PageStart) -----------------------------------------------------------------------------------------
|
||||
# Version.
|
||||
ver = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"],
|
||||
text = 'You are running server version: ' + srv_version, font = self.customfonts['oth'],
|
||||
foreground = self.customcolors['red'])
|
||||
# Ip Address.
|
||||
srvipaddlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.customfonts['opt'])
|
||||
self.srvipadd = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'ip')
|
||||
self.srvipadd.insert('end', srv_options['ip']['def'])
|
||||
ToolTip(self.srvipadd, text = srv_options['ip']['help'], wraplength = self.wraplength)
|
||||
myipadd = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Your IP address is: {}'.format(get_ip_address()),
|
||||
font = self.customfonts['oth'], foreground = self.customcolors['red'])
|
||||
# Port.
|
||||
srvportlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Port: ', font = self.customfonts['opt'])
|
||||
self.srvport = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'port',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.srvport.insert('end', str(srv_options['port']['def']))
|
||||
ToolTip(self.srvport, text = srv_options['port']['help'], wraplength = self.wraplength)
|
||||
# EPID.
|
||||
epidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'EPID: ', font = self.customfonts['opt'])
|
||||
self.epid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'epid')
|
||||
self.epid.insert('end', str(srv_options['epid']['def']))
|
||||
ToolTip(self.epid, text = srv_options['epid']['help'], wraplength = self.wraplength)
|
||||
# LCID.
|
||||
lcidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'LCID: ', font = self.customfonts['opt'])
|
||||
self.lcid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lcid',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.lcid.insert('end', str(srv_options['lcid']['def']))
|
||||
ToolTip(self.lcid, text = srv_options['lcid']['help'], wraplength = self.wraplength)
|
||||
# HWID.
|
||||
hwidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'HWID: ', font = self.customfonts['opt'])
|
||||
self.hwid = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = (str(srv_options['hwid']['def']), 'RANDOM'),
|
||||
width = 17, height = 10, font = self.customfonts['lst'], name = 'hwid')
|
||||
self.hwid.set(str(srv_options['hwid']['def']))
|
||||
ToolTip(self.hwid, text = srv_options['hwid']['help'], wraplength = self.wraplength)
|
||||
# Client Count
|
||||
countlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Client Count: ', font = self.customfonts['opt'])
|
||||
self.count = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'count')
|
||||
self.count.insert('end', str(srv_options['count']['def']))
|
||||
ToolTip(self.count, text = srv_options['count']['help'], wraplength = self.wraplength)
|
||||
# Activation Interval.
|
||||
activlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Activation Interval: ', font = self.customfonts['opt'])
|
||||
self.activ = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'activation',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.activ.insert('end', str(srv_options['activation']['def']))
|
||||
ToolTip(self.activ, text = srv_options['activation']['help'], wraplength = self.wraplength)
|
||||
# Renewal Interval.
|
||||
renewlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Renewal Interval: ', font = self.customfonts['opt'])
|
||||
self.renew = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'renewal',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.renew.insert('end', str(srv_options['renewal']['def']))
|
||||
ToolTip(self.renew, text = srv_options['renewal']['help'], wraplength = self.wraplength)
|
||||
# Logfile.
|
||||
srvfilelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.customfonts['opt'])
|
||||
self.srvfile = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lfile')
|
||||
self.srvfile.insert('end', srv_options['lfile']['def'])
|
||||
self.srvfile.xview_moveto(1)
|
||||
ToolTip(self.srvfile, text = srv_options['lfile']['help'], wraplength = self.wraplength)
|
||||
srvfilebtnwin = tk.Button(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Browse', font = self.customfonts['opt'],
|
||||
command = lambda: self.on_browse(self.srvfile, srv_options))
|
||||
# Loglevel.
|
||||
srvlevellbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.customfonts['opt'])
|
||||
self.srvlevel = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = tuple(srv_options['llevel']['choi']),
|
||||
width = 10, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'llevel')
|
||||
self.srvlevel.set(srv_options['llevel']['def'])
|
||||
ToolTip(self.srvlevel, text = srv_options['llevel']['help'], wraplength = self.wraplength)
|
||||
# Logsize.
|
||||
srvsizelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.customfonts['opt'])
|
||||
self.srvsize = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lsize',
|
||||
validate = "key", validatecommand = self.validation_float)
|
||||
self.srvsize.insert('end', srv_options['lsize']['def'])
|
||||
ToolTip(self.srvsize, text = srv_options['lsize']['help'], wraplength = self.wraplength)
|
||||
# Asynchronous messages.
|
||||
self.chkvalsrvasy = tk.BooleanVar()
|
||||
self.chkvalsrvasy.set(srv_options['asyncmsg']['def'])
|
||||
chksrvasy = tk.Checkbutton(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Async\nMsg',
|
||||
font = self.customfonts['opt'], var = self.chkvalsrvasy, relief = 'groove', name = 'asyncmsg')
|
||||
ToolTip(chksrvasy, text = srv_options['asyncmsg']['help'], wraplength = self.wraplength)
|
||||
|
||||
# Listbox radiobuttons server.
|
||||
self.chksrvfile = ListboxOfRadiobuttons(self.pagewidgets["Srv"]["PageWin"]["PageStart"],
|
||||
['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'],
|
||||
self.customfonts['lst'],
|
||||
changed = [(self.srvfile, srv_options['lfile']['def']),
|
||||
(srvfilebtnwin, ''),
|
||||
(self.srvsize, srv_options['lsize']['def']),
|
||||
(self.srvlevel, srv_options['llevel']['def'])],
|
||||
width = 10, height = 1, borderwidth = 2, relief = 'ridge')
|
||||
|
||||
## Layout widgets (optsrvwin:Srv:PageWin:PageStart)
|
||||
ver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
myipadd.grid(row = 2, column = 1, columnspan = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvportlbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvport.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
epidlbl.grid(row = 4, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.epid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
lcidlbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.lcid.grid(row = 5, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
hwidlbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.hwid.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
countlbl.grid(row = 7, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.count.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
activlbl.grid(row = 8, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.activ.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
renewlbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.renew.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvfilelbl.grid(row = 10, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvfile.grid(row = 10, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvfilebtnwin.grid(row = 10, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
self.chksrvfile.grid(row = 11, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
chksrvasy.grid(row = 11, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvlevellbl.grid(row = 12, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvlevel.grid(row = 12, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvsizelbl.grid(row = 13, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvsize.grid(row = 13, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
|
||||
## Create widgets (optsrvwin:Srv:PageWin:PageEnd)-------------------------------------------------------------------------------------------
|
||||
# Timeout connection.
|
||||
srvtimeout0lbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Timeout connection: ', font = self.customfonts['opt'])
|
||||
self.srvtimeout0 = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time0')
|
||||
self.srvtimeout0.insert('end', str(srv_options['time0']['def']))
|
||||
ToolTip(self.srvtimeout0, text = srv_options['time0']['help'], wraplength = self.wraplength)
|
||||
# Timeout send/recv.
|
||||
srvtimeout1lbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Timeout send-recv: ', font = self.customfonts['opt'])
|
||||
self.srvtimeout1 = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time1')
|
||||
self.srvtimeout1.insert('end', str(srv_options['time1']['def']))
|
||||
ToolTip(self.srvtimeout1, text = srv_options['time1']['help'], wraplength = self.wraplength)
|
||||
# Sqlite database.
|
||||
self.chkvalsql = tk.BooleanVar()
|
||||
self.chkvalsql.set(srv_options['sql']['def'])
|
||||
self.chkfilesql = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'sql')
|
||||
self.chkfilesql.insert('end', srv_options['sql']['file'])
|
||||
self.chkfilesql.xview_moveto(1)
|
||||
self.chkfilesql.configure(state = 'disabled')
|
||||
|
||||
chksql = tk.Checkbutton(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Create Sqlite\nDatabase',
|
||||
font = self.customfonts['opt'], var = self.chkvalsql, relief = 'groove',
|
||||
command = lambda: self.sql_status())
|
||||
ToolTip(chksql, text = srv_options['sql']['help'], wraplength = self.wraplength)
|
||||
|
||||
## Layout widgets (optsrvwin:Srv:PageWin:PageEnd)
|
||||
# a label for vertical aligning with PageStart
|
||||
tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 0,
|
||||
height = 0, bg = self.customcolors['lavender']).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw')
|
||||
srvtimeout0lbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvtimeout0.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
srvtimeout1lbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvtimeout1.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
chksql.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.chkfilesql.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
|
||||
# Store server-side widgets.
|
||||
self.storewidgets_srv = self.gui_store(side = "Srv", typewidgets = ['Button', 'Entry', 'TCombobox', 'Checkbutton'])
|
||||
self.storewidgets_srv.append(self.chksrvfile)
|
||||
|
||||
## Create widgets and layout (msgsrvwin) ---------------------------------------------------------------------------------------------------
|
||||
self.textboxsrv = TextDoubleScroll(self.msgsrvwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled',
|
||||
relief = 'ridge', font = self.customfonts['msg'])
|
||||
self.textboxsrv.put()
|
||||
|
||||
def sql_status(self):
|
||||
if self.chkvalsql.get():
|
||||
self.chkfilesql.configure(state = 'normal')
|
||||
else:
|
||||
self.chkfilesql.insert('end', srv_options['sql']['file'])
|
||||
self.chkfilesql.xview_moveto(1)
|
||||
self.chkfilesql.configure(state = 'disabled')
|
||||
|
||||
def always_centered(self, geo, centered, refs):
|
||||
x = (self.winfo_screenwidth() // 2) - (self.winfo_width() // 2)
|
||||
y = (self.winfo_screenheight() // 2) - (self.winfo_height() // 2)
|
||||
w, h, dx, dy = geo.split('+')[0].split('x') + geo.split('+')[1:]
|
||||
|
||||
if w == refs[1]:
|
||||
if centered:
|
||||
self.geometry('+%d+%d' %(x, y))
|
||||
centered = False
|
||||
elif w == refs[0]:
|
||||
if not centered:
|
||||
self.geometry('+%d+%d' %(x, y))
|
||||
centered = True
|
||||
|
||||
if dx != str(x) or dy != str(y):
|
||||
self.geometry('+%d+%d' %(x, 0))
|
||||
|
||||
self.after(200, self.always_centered, self.geometry(), centered, refs)
|
||||
|
||||
def gui_complete(self):
|
||||
## Create client widgets (optcltwin, msgcltwin, btncltwin)
|
||||
self.update_idletasks() # update Gui to get btnsrvwin values --> btncltwin.
|
||||
minw, minh = self.winfo_width(), self.winfo_height()
|
||||
self.iconify()
|
||||
self.gui_clt()
|
||||
maxw, minh = self.winfo_width(), self.winfo_height()
|
||||
## Main window custom background.
|
||||
self.update_idletasks() # update Gui for custom background
|
||||
self.iconify()
|
||||
custom_background(self)
|
||||
## Main window other modifications.
|
||||
self.eval('tk::PlaceWindow %s center' %self.winfo_pathname(self.winfo_id()))
|
||||
self.wm_attributes("-topmost", True)
|
||||
self.protocol("WM_DELETE_WINDOW", lambda: 0)
|
||||
## Disable maximize button.
|
||||
self.resizable(False, False)
|
||||
## Centered window.
|
||||
self.always_centered(self.geometry(), False, [minw, maxw])
|
||||
|
||||
def get_position(self, widget):
|
||||
x, y = (widget.winfo_x(), widget.winfo_y())
|
||||
w, h = (widget.winfo_width(), widget.winfo_height())
|
||||
return x, y, w, h
|
||||
|
||||
def gui_clt(self):
|
||||
self.count_clear, self.keep_clear = (0, '0.0')
|
||||
self.optcltwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.msgcltwin = tk.Frame(self.masterwin, background = self.customcolors['black'], relief = 'ridge', width = 300, height = 200)
|
||||
self.btncltwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
|
||||
xb, yb, wb, hb = self.get_position(self.btnsrvwin)
|
||||
self.btncltwin_X = xb
|
||||
self.btncltwin_Y = yb + hb + 6
|
||||
self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'outside', anchor = 'center')
|
||||
|
||||
self.optcltwin.grid(row = 0, column = 4, padx = 2, pady = 2, sticky = 'nsew')
|
||||
self.optcltwin.grid_rowconfigure(0, weight = 1)
|
||||
self.optcltwin.grid_columnconfigure(1, weight = 1)
|
||||
|
||||
## Subpages of "optcltwin".
|
||||
self.gui_pages_create(parent = self.optcltwin, side = "Clt", create = {"PageStart": None,
|
||||
"PageEnd": None})
|
||||
|
||||
## Continue to grid.
|
||||
self.msgcltwin.grid(row = 1, column = 4, padx = 1, pady = 1, sticky = 'nsew')
|
||||
self.msgcltwin.grid_propagate(False)
|
||||
self.msgcltwin.grid_columnconfigure(0, weight = 1)
|
||||
self.msgcltwin.grid_rowconfigure(0, weight = 1)
|
||||
|
||||
## Create widgets (btncltwin) ----------------------------------------------------------------------------------------------------------------
|
||||
self.runbtnclt = tk.Button(self.btncltwin, text = 'START\nCLIENT', background = self.customcolors['blue'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
state = 'disabled', command = self.clt_on_start, width = 8, height = 2)
|
||||
|
||||
## Layout widgets (btncltwin)
|
||||
self.runbtnclt.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
|
||||
## Create widgets (optcltwin:Clt:PageWin:PageStart) ------------------------------------------------------------------------------------------
|
||||
# Version.
|
||||
cltver = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'You are running client version: ' + clt_version,
|
||||
font = self.customfonts['oth'], foreground = self.customcolors['red'])
|
||||
# Ip Address.
|
||||
cltipaddlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.customfonts['opt'])
|
||||
self.cltipadd = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'ip')
|
||||
self.cltipadd.insert('end', clt_options['ip']['def'])
|
||||
ToolTip(self.cltipadd, text = clt_options['ip']['help'], wraplength = self.wraplength)
|
||||
# Port.
|
||||
cltportlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Port: ', font = self.customfonts['opt'])
|
||||
self.cltport = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'port',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.cltport.insert('end', str(clt_options['port']['def']))
|
||||
ToolTip(self.cltport, text = clt_options['port']['help'], wraplength = self.wraplength)
|
||||
# Mode.
|
||||
cltmodelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Mode: ', font = self.customfonts['opt'])
|
||||
self.cltmode = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['mode']['choi']),
|
||||
width = 17, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'mode')
|
||||
self.cltmode.set(clt_options['mode']['def'])
|
||||
ToolTip(self.cltmode, text = clt_options['mode']['help'], wraplength = self.wraplength)
|
||||
# CMID.
|
||||
cltcmidlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'CMID: ', font = self.customfonts['opt'])
|
||||
self.cltcmid = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'cmid')
|
||||
self.cltcmid.insert('end', str(clt_options['cmid']['def']))
|
||||
ToolTip(self.cltcmid, text = clt_options['cmid']['help'], wraplength = self.wraplength)
|
||||
# Machine Name.
|
||||
cltnamelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Machine Name: ', font = self.customfonts['opt'])
|
||||
self.cltname = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'name')
|
||||
self.cltname.insert('end', str(clt_options['name']['def']))
|
||||
ToolTip(self.cltname, text = clt_options['name']['help'], wraplength = self.wraplength)
|
||||
# Logfile.
|
||||
cltfilelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.customfonts['opt'])
|
||||
self.cltfile = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lfile')
|
||||
self.cltfile.insert('end', clt_options['lfile']['def'])
|
||||
self.cltfile.xview_moveto(1)
|
||||
ToolTip(self.cltfile, text = clt_options['lfile']['help'], wraplength = self.wraplength)
|
||||
cltfilebtnwin = tk.Button(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Browse', font = self.customfonts['opt'],
|
||||
command = lambda: self.on_browse(self.cltfile, clt_options))
|
||||
# Loglevel.
|
||||
cltlevellbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.customfonts['opt'])
|
||||
self.cltlevel = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['llevel']['choi']),
|
||||
width = 10, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'llevel')
|
||||
self.cltlevel.set(clt_options['llevel']['def'])
|
||||
ToolTip(self.cltlevel, text = clt_options['llevel']['help'], wraplength = self.wraplength)
|
||||
# Logsize.
|
||||
cltsizelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.customfonts['opt'])
|
||||
self.cltsize = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lsize',
|
||||
validate = "key", validatecommand = self.validation_float)
|
||||
self.cltsize.insert('end', clt_options['lsize']['def'])
|
||||
ToolTip(self.cltsize, text = clt_options['lsize']['help'], wraplength = self.wraplength)
|
||||
# Asynchronous messages.
|
||||
self.chkvalcltasy = tk.BooleanVar()
|
||||
self.chkvalcltasy.set(clt_options['asyncmsg']['def'])
|
||||
chkcltasy = tk.Checkbutton(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Async\nMsg',
|
||||
font = self.customfonts['opt'], var = self.chkvalcltasy, relief = 'groove', name = 'asyncmsg')
|
||||
ToolTip(chkcltasy, text = clt_options['asyncmsg']['help'], wraplength = self.wraplength)
|
||||
|
||||
# Listbox radiobuttons client.
|
||||
self.chkcltfile = ListboxOfRadiobuttons(self.pagewidgets["Clt"]["PageWin"]["PageStart"],
|
||||
['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'],
|
||||
self.customfonts['lst'],
|
||||
changed = [(self.cltfile, clt_options['lfile']['def']),
|
||||
(cltfilebtnwin, ''),
|
||||
(self.cltsize, clt_options['lsize']['def']),
|
||||
(self.cltlevel, clt_options['llevel']['def'])],
|
||||
width = 10, height = 1, borderwidth = 2, relief = 'ridge')
|
||||
|
||||
## Layout widgets (optcltwin:Clt:PageWin:PageStart)
|
||||
cltver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltportlbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltport.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltmodelbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltmode.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltcmidlbl.grid(row = 4, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltcmid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltnamelbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltname.grid(row = 5, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltfilelbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltfile.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltfilebtnwin.grid(row = 6, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
self.chkcltfile.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
chkcltasy.grid(row = 7, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltlevellbl.grid(row = 8, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltlevel.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltsizelbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltsize.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
|
||||
# ugly fix when client-side mode is activated.
|
||||
templbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"],
|
||||
bg = self.customcolors['lavender']).grid(row = 10, column = 0,
|
||||
padx = 35, pady = 54, sticky = 'e')
|
||||
|
||||
## Create widgets (optcltwin:Clt:PageWin:PageEnd) -------------------------------------------------------------------------------------------
|
||||
# Timeout connection.
|
||||
clttimeout0lbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], text = 'Timeout connection: ', font = self.customfonts['opt'])
|
||||
self.clttimeout0 = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time0')
|
||||
self.clttimeout0.insert('end', str(clt_options['time0']['def']))
|
||||
ToolTip(self.clttimeout0, text = clt_options['time0']['help'], wraplength = self.wraplength)
|
||||
# Timeout send/recv.
|
||||
clttimeout1lbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], text = 'Timeout send-recv: ', font = self.customfonts['opt'])
|
||||
self.clttimeout1 = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time1')
|
||||
self.clttimeout1.insert('end', str(clt_options['time1']['def']))
|
||||
ToolTip(self.clttimeout1, text = clt_options['time1']['help'], wraplength = self.wraplength)
|
||||
|
||||
## Layout widgets (optcltwin:Clt:PageWin:PageEnd)
|
||||
# a label for vertical aligning with PageStart
|
||||
tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 0,
|
||||
height = 0, bg = self.customcolors['lavender']).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw')
|
||||
clttimeout0lbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.clttimeout0.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
clttimeout1lbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.clttimeout1.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
|
||||
## Store client-side widgets.
|
||||
self.storewidgets_clt = self.gui_store(side = "Clt", typewidgets = ['Button', 'Entry', 'TCombobox', 'Checkbutton'])
|
||||
self.storewidgets_clt.append(self.chkcltfile)
|
||||
|
||||
## Create widgets and layout (msgcltwin) -----------------------------------------------------------------------------------------------------
|
||||
self.textboxclt = TextDoubleScroll(self.msgcltwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled',
|
||||
relief = 'ridge', font = self.customfonts['msg'])
|
||||
self.textboxclt.put()
|
||||
|
||||
def prep_option(self, value):
|
||||
try:
|
||||
# is an INT
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
# is a FLOAT
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
# is a STRING.
|
||||
return value
|
||||
|
||||
def prep_logfile(self, filepath, status):
|
||||
# FILE (pretty on, log view off, logfile yes)
|
||||
# FILEOFF (pretty on, log view off, no logfile)
|
||||
# STDOUT (pretty off, log view on, no logfile)
|
||||
# STDOUTOFF (pretty off, log view off, logfile yes)
|
||||
# FILESTDOUT (pretty off, log view on, logfile yes)
|
||||
|
||||
if status == 'FILE':
|
||||
return filepath
|
||||
elif status in ['FILESTDOUT', 'STDOUTOFF']:
|
||||
return [status, filepath]
|
||||
elif status in ['STDOUT', 'FILEOFF']:
|
||||
return status
|
||||
|
||||
def validate_int(self, value):
|
||||
return value == "" or value.isdigit()
|
||||
|
||||
def validate_float(self, value):
|
||||
if value == "":
|
||||
return True
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def clt_on_show(self, force_remove = False, force_view = False):
|
||||
if self.optcltwin.winfo_ismapped() or force_remove:
|
||||
self.shbtnclt.configure(text = 'SHOW\nCLIENT', relief = 'raised')
|
||||
self.optcltwin.grid_remove()
|
||||
self.msgcltwin.grid_remove()
|
||||
self.btncltwin.place_forget()
|
||||
elif not self.optcltwin.winfo_ismapped() or force_view:
|
||||
self.shbtnclt.configure(text = 'HIDE\nCLIENT', relief = 'sunken')
|
||||
self.optcltwin.grid()
|
||||
self.msgcltwin.grid()
|
||||
self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'inside', anchor = 'nw')
|
||||
|
||||
def srv_on_start(self):
|
||||
if self.runbtnsrv['text'] == 'START\nSERVER':
|
||||
self.on_clear([txsrv, txclt])
|
||||
self.srv_actions_start()
|
||||
# wait for switch.
|
||||
while not serverthread.is_running_server:
|
||||
pass
|
||||
|
||||
self.srv_toggle_all(on_start = True)
|
||||
# run thread for interrupting server when an error happens.
|
||||
self.srv_eject_thread = threading.Thread(target = self.srv_eject, name = "Thread-SrvEjt")
|
||||
self.srv_eject_thread.setDaemon(True)
|
||||
self.srv_eject_thread.start()
|
||||
|
||||
elif self.runbtnsrv['text'] == 'STOP\nSERVER':
|
||||
serverthread.terminate_eject()
|
||||
|
||||
def srv_eject(self):
|
||||
while not serverthread.eject:
|
||||
sleep(0.1)
|
||||
self.srv_actions_stop()
|
||||
|
||||
def srv_actions_start(self):
|
||||
srv_config[srv_options['ip']['des']] = self.srvipadd.get()
|
||||
srv_config[srv_options['port']['des']] = self.prep_option(self.srvport.get())
|
||||
srv_config[srv_options['epid']['des']] = self.epid.get()
|
||||
srv_config[srv_options['lcid']['des']] = self.prep_option(self.lcid.get())
|
||||
srv_config[srv_options['hwid']['des']] = self.hwid.get()
|
||||
srv_config[srv_options['count']['des']] = self.prep_option(self.count.get())
|
||||
srv_config[srv_options['activation']['des']] = self.prep_option(self.activ.get())
|
||||
srv_config[srv_options['renewal']['des']] = self.prep_option(self.renew.get())
|
||||
srv_config[srv_options['lfile']['des']] = self.prep_logfile(self.srvfile.get(), self.chksrvfile.state())
|
||||
srv_config[srv_options['asyncmsg']['des']] = self.chkvalsrvasy.get()
|
||||
srv_config[srv_options['llevel']['des']] = self.srvlevel.get()
|
||||
srv_config[srv_options['lsize']['des']] = self.prep_option(self.srvsize.get())
|
||||
|
||||
srv_config[srv_options['time0']['des']] = self.prep_option(self.srvtimeout0.get())
|
||||
srv_config[srv_options['time1']['des']] = self.prep_option(self.srvtimeout1.get())
|
||||
srv_config[srv_options['sql']['des']] = (self.chkfilesql.get() if self.chkvalsql.get() else self.chkvalsql.get())
|
||||
|
||||
## Redirect stdout.
|
||||
gui_redirector('stdout', redirect_to = TextRedirect.Log,
|
||||
redirect_conditio = (srv_config[srv_options['lfile']['des']] in ['STDOUT', 'FILESTDOUT']))
|
||||
serverqueue.put('start')
|
||||
|
||||
def srv_actions_stop(self):
|
||||
if serverthread.is_running_server:
|
||||
if serverthread.server is not None:
|
||||
server_terminate(serverthread, exit_server = True)
|
||||
# wait for switch.
|
||||
while serverthread.is_running_server:
|
||||
pass
|
||||
else:
|
||||
serverthread.is_running_server = False
|
||||
self.srv_toggle_all(on_start = False)
|
||||
self.count_clear, self.keep_clear = (0, '0.0')
|
||||
|
||||
def srv_toggle_all(self, on_start = True):
|
||||
self.srv_toggle_state()
|
||||
if on_start:
|
||||
self.runbtnsrv.configure(text = 'STOP\nSERVER', background = self.customcolors['red'],
|
||||
foreground = self.customcolors['white'], relief = 'sunken')
|
||||
for widget in self.storewidgets_srv:
|
||||
widget.configure(state = 'disabled')
|
||||
self.runbtnclt.configure(state = 'normal')
|
||||
else:
|
||||
self.runbtnsrv.configure(text = 'START\nSERVER', background = self.customcolors['green'],
|
||||
foreground = self.customcolors['white'], relief = 'raised')
|
||||
for widget in self.storewidgets_srv:
|
||||
widget.configure(state = 'normal')
|
||||
if isinstance(widget, ListboxOfRadiobuttons):
|
||||
widget.change()
|
||||
self.runbtnclt.configure(state = 'disabled')
|
||||
|
||||
def srv_toggle_state(self):
|
||||
if serverthread.is_running_server:
|
||||
txt, color = ('Server\nState:\nServing', self.customcolors['green'])
|
||||
else:
|
||||
txt, color = ('Server\nState:\nStopped', self.customcolors['red'])
|
||||
|
||||
self.statesrv.configure(text = txt, foreground = color)
|
||||
|
||||
def clt_on_start(self):
|
||||
if self.onlyclt:
|
||||
self.on_clear([txclt])
|
||||
else:
|
||||
rng, add_newline = self.on_clear_setup()
|
||||
self.on_clear([txsrv, txclt], clear_range = [rng, None], newline_list = [add_newline, False])
|
||||
|
||||
self.runbtnclt.configure(relief = 'sunken')
|
||||
self.clt_actions_start()
|
||||
# run thread for disabling interrupt server and client, when client running.
|
||||
self.clt_eject_thread = threading.Thread(target = self.clt_eject, name = "Thread-CltEjt")
|
||||
self.clt_eject_thread.setDaemon(True)
|
||||
self.clt_eject_thread.start()
|
||||
|
||||
for widget in self.storewidgets_clt + [self.runbtnsrv, self.runbtnclt, self.defaubtnsrv]:
|
||||
widget.configure(state = 'disabled')
|
||||
self.runbtnclt.configure(relief = 'raised')
|
||||
|
||||
def clt_actions_start(self):
|
||||
clt_config[clt_options['ip']['des']] = self.cltipadd.get()
|
||||
clt_config[clt_options['port']['des']] = self.prep_option(self.cltport.get())
|
||||
clt_config[clt_options['mode']['des']] = self.cltmode.get()
|
||||
clt_config[clt_options['cmid']['des']] = self.cltcmid.get()
|
||||
clt_config[clt_options['name']['des']] = self.cltname.get()
|
||||
clt_config[clt_options['lfile']['des']] = self.prep_logfile(self.cltfile.get(), self.chkcltfile.state())
|
||||
clt_config[clt_options['asyncmsg']['des']] = self.chkvalcltasy.get()
|
||||
clt_config[clt_options['llevel']['des']] = self.cltlevel.get()
|
||||
clt_config[clt_options['lsize']['des']] = self.prep_option(self.cltsize.get())
|
||||
|
||||
clt_config[clt_options['time0']['des']] = self.prep_option(self.clttimeout0.get())
|
||||
clt_config[clt_options['time1']['des']] = self.prep_option(self.clttimeout1.get())
|
||||
|
||||
## Redirect stdout.
|
||||
gui_redirector('stdout', redirect_to = TextRedirect.Log,
|
||||
redirect_conditio = (clt_config[clt_options['lfile']['des']] in ['STDOUT', 'FILESTDOUT']))
|
||||
|
||||
# run client (in a thread).
|
||||
self.clientthread = client_thread(name = "Thread-Clt")
|
||||
self.clientthread.setDaemon(True)
|
||||
self.clientthread.with_gui = True
|
||||
self.clientthread.start()
|
||||
|
||||
def clt_eject(self):
|
||||
while self.clientthread.is_alive():
|
||||
sleep(0.1)
|
||||
|
||||
widgets = self.storewidgets_clt + [self.runbtnclt] + [self.defaubtnsrv]
|
||||
if not self.onlyclt:
|
||||
widgets += [self.runbtnsrv]
|
||||
|
||||
for widget in widgets:
|
||||
if isinstance(widget, ttk.Combobox):
|
||||
widget.configure(state = 'readonly')
|
||||
else:
|
||||
widget.configure(state = 'normal')
|
||||
if isinstance(widget, ListboxOfRadiobuttons):
|
||||
widget.change()
|
||||
|
||||
def on_browse(self, entrywidget, options):
|
||||
path = filedialog.askdirectory()
|
||||
if os.path.isdir(path):
|
||||
entrywidget.delete('0', 'end')
|
||||
entrywidget.insert('end', path + os.sep + os.path.basename(options['lfile']['def']))
|
||||
|
||||
def on_exit(self):
|
||||
if serverthread.is_running_server:
|
||||
if serverthread.server is not None:
|
||||
server_terminate(serverthread, exit_server = True)
|
||||
else:
|
||||
serverthread.is_running_server = False
|
||||
server_terminate(serverthread, exit_thread = True)
|
||||
self.destroy()
|
||||
|
||||
def on_clear_setup(self):
|
||||
if any(opt in ['STDOUT', 'FILESTDOUT'] for opt in srv_config[srv_options['lfile']['des']]):
|
||||
add_newline = True
|
||||
if self.count_clear == 0:
|
||||
self.keep_clear = txsrv.index('end-1c')
|
||||
else:
|
||||
add_newline = False
|
||||
if self.count_clear == 0:
|
||||
self.keep_clear = txsrv.index('end')
|
||||
|
||||
rng = [self.keep_clear, 'end']
|
||||
self.count_clear += 1
|
||||
|
||||
return rng, add_newline
|
||||
|
||||
def on_clear(self, widget_list, clear_range = None, newline_list = []):
|
||||
if newline_list == []:
|
||||
newline_list = len(widget_list) * [False]
|
||||
|
||||
for num, couple in enumerate(zip(widget_list, newline_list)):
|
||||
widget, add_n = couple
|
||||
try:
|
||||
ini, fin = clear_range[num]
|
||||
except TypeError:
|
||||
ini, fin = '1.0', 'end'
|
||||
|
||||
widget.configure(state = 'normal')
|
||||
widget.delete(ini, fin)
|
||||
if add_n:
|
||||
widget.insert('end', '\n')
|
||||
widget.configure(state = 'disabled')
|
||||
|
||||
def on_defaults(self):
|
||||
|
||||
def put_defaults(widgets, chkasy, listofradio, options):
|
||||
for widget in widgets:
|
||||
wclass, wname = widget.winfo_class(), widget.winfo_name()
|
||||
if wname == '!checkbutton':
|
||||
continue
|
||||
|
||||
opt = options[wname]['def']
|
||||
if wclass == 'Entry':
|
||||
widget.delete(0, 'end')
|
||||
if wname == 'sql':
|
||||
self.chkvalsql.set(opt)
|
||||
self.sql_status()
|
||||
else:
|
||||
widget.insert('end', (opt if isinstance(opt, str) else str(opt)))
|
||||
elif wclass == 'Checkbutton':
|
||||
if wname == 'asyncmsg':
|
||||
chkasy.set(opt)
|
||||
elif wclass == 'TCombobox':
|
||||
widget.set(str(opt))
|
||||
|
||||
# ListboxOfRadiobuttons default.
|
||||
listofradio.radiovar.set('FILE')
|
||||
listofradio.textbox.yview_moveto(0)
|
||||
listofradio.change()
|
||||
|
||||
if self.runbtnsrv['text'] == 'START\nSERVER':
|
||||
apply_default = zip(["Srv", "Clt"],
|
||||
[self.chkvalsrvasy, self.chkvalcltasy],
|
||||
[self.chksrvfile, self.chkcltfile],
|
||||
[srv_options, clt_options])
|
||||
elif self.runbtnsrv['text'] == 'STOP\nSERVER':
|
||||
apply_default = zip(*[("Clt",),
|
||||
(self.chkvalcltasy,),
|
||||
(self.chkcltfile,),
|
||||
(clt_options,)])
|
||||
|
||||
for side, chkasy, listofradio, options in apply_default:
|
||||
widgets = self.gui_store(side = side, typewidgets = ['Entry', 'TCombobox', 'Checkbutton'])
|
||||
put_defaults(widgets, chkasy, listofradio, options)
|
|
@ -1,517 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from time import sleep
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import tkinter.font as tkFont
|
||||
|
||||
from pykms_Format import MsgMap, unshell_message, unformat_message
|
||||
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter
|
||||
class ToolTip(object):
|
||||
""" Create a tooltip for a given widget """
|
||||
def __init__(self, widget, bg = '#FFFFEA', pad = (5, 3, 5, 3), text = 'widget info', waittime = 400, wraplength = 250):
|
||||
self.waittime = waittime # ms
|
||||
self.wraplength = wraplength # pixels
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.widget.bind("<Enter>", self.onEnter)
|
||||
self.widget.bind("<Leave>", self.onLeave)
|
||||
self.widget.bind("<ButtonPress>", self.onLeave)
|
||||
self.bg = bg
|
||||
self.pad = pad
|
||||
self.id = None
|
||||
self.tw = None
|
||||
|
||||
def onEnter(self, event = None):
|
||||
self.schedule()
|
||||
|
||||
def onLeave(self, event = None):
|
||||
self.unschedule()
|
||||
self.hide()
|
||||
|
||||
def schedule(self):
|
||||
self.unschedule()
|
||||
self.id = self.widget.after(self.waittime, self.show)
|
||||
|
||||
def unschedule(self):
|
||||
id_ = self.id
|
||||
self.id = None
|
||||
if id_:
|
||||
self.widget.after_cancel(id_)
|
||||
|
||||
def show(self):
|
||||
def tip_pos_calculator(widget, label, tip_delta = (10, 5), pad = (5, 3, 5, 3)):
|
||||
w = widget
|
||||
s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight()
|
||||
width, height = (pad[0] + label.winfo_reqwidth() + pad[2],
|
||||
pad[1] + label.winfo_reqheight() + pad[3])
|
||||
mouse_x, mouse_y = w.winfo_pointerxy()
|
||||
x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
|
||||
x2, y2 = x1 + width, y1 + height
|
||||
|
||||
x_delta = x2 - s_width
|
||||
if x_delta < 0:
|
||||
x_delta = 0
|
||||
y_delta = y2 - s_height
|
||||
if y_delta < 0:
|
||||
y_delta = 0
|
||||
|
||||
offscreen = (x_delta, y_delta) != (0, 0)
|
||||
|
||||
if offscreen:
|
||||
if x_delta:
|
||||
x1 = mouse_x - tip_delta[0] - width
|
||||
if y_delta:
|
||||
y1 = mouse_y - tip_delta[1] - height
|
||||
|
||||
offscreen_again = y1 < 0 # out on the top
|
||||
|
||||
if offscreen_again:
|
||||
# No further checks will be done.
|
||||
|
||||
# TIP:
|
||||
# A further mod might automagically augment the
|
||||
# wraplength when the tooltip is too high to be
|
||||
# kept inside the screen.
|
||||
y1 = 0
|
||||
|
||||
return x1, y1
|
||||
|
||||
bg = self.bg
|
||||
pad = self.pad
|
||||
widget = self.widget
|
||||
|
||||
# creates a toplevel window
|
||||
self.tw = tk.Toplevel(widget)
|
||||
|
||||
# leaves only the label and removes the app window
|
||||
self.tw.wm_overrideredirect(True)
|
||||
|
||||
win = tk.Frame(self.tw, background = bg, borderwidth = 0)
|
||||
label = ttk.Label(win, text = self.text, justify = tk.LEFT, background = bg, relief = tk.SOLID, borderwidth = 0,
|
||||
wraplength = self.wraplength)
|
||||
label.grid(padx = (pad[0], pad[2]), pady = (pad[1], pad[3]), sticky=tk.NSEW)
|
||||
win.grid()
|
||||
|
||||
x, y = tip_pos_calculator(widget, label)
|
||||
|
||||
self.tw.wm_geometry("+%d+%d" % (x, y))
|
||||
|
||||
def hide(self):
|
||||
tw = self.tw
|
||||
if tw:
|
||||
tw.destroy()
|
||||
self.tw = None
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class TextRedirect(object):
|
||||
class Pretty(object):
|
||||
grpmsg = unformat_message([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]])
|
||||
arrows = [ item[0] for item in grpmsg ]
|
||||
clt_msg_nonewline = [ item[1] for item in grpmsg ]
|
||||
arrows = list(set(arrows))
|
||||
lenarrow = len(arrows[0])
|
||||
srv_msg_nonewline = [ item[0] for item in unformat_message([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) ]
|
||||
msg_align = [ msg[0].replace('\t', '').replace('\n', '') for msg in unformat_message([MsgMap[-2], MsgMap[-4]]) ]
|
||||
|
||||
def __init__(self, srv_text_space, clt_text_space, customcolors):
|
||||
self.srv_text_space = srv_text_space
|
||||
self.clt_text_space = clt_text_space
|
||||
self.customcolors = customcolors
|
||||
|
||||
def textbox_write(self, tag, message, color, extras):
|
||||
widget = self.textbox_choose(message)
|
||||
self.w_maxpix, self.h_maxpix = widget.winfo_width(), widget.winfo_height()
|
||||
self.xfont = tkFont.Font(font = widget['font'])
|
||||
widget.configure(state = 'normal')
|
||||
widget.insert('end', self.textbox_format(message), tag)
|
||||
self.textbox_color(tag, widget, color, self.customcolors['black'], extras)
|
||||
widget.after(100, widget.see('end'))
|
||||
widget.configure(state = 'disabled')
|
||||
|
||||
def textbox_choose(self, message):
|
||||
if any(item.startswith('logsrv') for item in [message, self.str_to_print]):
|
||||
self.srv_text_space.focus_set()
|
||||
self.where = "srv"
|
||||
return self.srv_text_space
|
||||
elif any(item.startswith('logclt') for item in [message, self.str_to_print]):
|
||||
self.clt_text_space.focus_set()
|
||||
self.where = "clt"
|
||||
return self.clt_text_space
|
||||
|
||||
def textbox_color(self, tag, widget, forecolor = 'white', backcolor = 'black', extras = []):
|
||||
for extra in extras:
|
||||
if extra == 'bold':
|
||||
self.xfont.configure(weight = "bold")
|
||||
elif extra == 'italic':
|
||||
self.xfont.configure(slant = "italic")
|
||||
elif extra == 'underlined':
|
||||
self.xfont.text_font.configure(underline = True)
|
||||
elif extra == 'strike':
|
||||
self.xfont.configure(overstrike = True)
|
||||
elif extra == 'reverse':
|
||||
forecolor, backcolor = backcolor, forecolor
|
||||
|
||||
widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = self.xfont)
|
||||
widget.tag_add(tag, "insert linestart", "insert lineend")
|
||||
|
||||
def textbox_newline(self, message):
|
||||
if not message.endswith('\n'):
|
||||
return message + '\n'
|
||||
else:
|
||||
return message
|
||||
|
||||
def textbox_format(self, message):
|
||||
# vertical align.
|
||||
self.w_maxpix = self.w_maxpix - 5 # pixel reduction for distance from border.
|
||||
w_fontpix, h_fontpix = (self.xfont.measure('0'), self.xfont.metrics('linespace'))
|
||||
msg_unformat = message.replace('\t', '').replace('\n', '')
|
||||
lenfixed_chars = int((self.w_maxpix / w_fontpix) - len(msg_unformat))
|
||||
|
||||
if message in self.srv_msg_nonewline + self.clt_msg_nonewline:
|
||||
lung = lenfixed_chars - self.lenarrow
|
||||
if message in self.clt_msg_nonewline:
|
||||
message = self.textbox_newline(message)
|
||||
else:
|
||||
lung = lenfixed_chars
|
||||
if (self.where == "srv") or (self.where == "clt" and message not in self.arrows):
|
||||
message = self.textbox_newline(message)
|
||||
# horizontal align.
|
||||
if msg_unformat in self.msg_align:
|
||||
msg_strip = message.lstrip('\n')
|
||||
message = '\n' * (len(message) - len(msg_strip) + TextRedirect.Pretty.newlinecut[0]) + msg_strip
|
||||
TextRedirect.Pretty.newlinecut.pop(0)
|
||||
|
||||
count = Counter(message)
|
||||
countab = (count['\t'] if count['\t'] != 0 else 1)
|
||||
message = message.replace('\t' * countab, ' ' * lung)
|
||||
return message
|
||||
|
||||
def textbox_do(self):
|
||||
msgs, TextRedirect.Pretty.tag_num = unshell_message(self.str_to_print, TextRedirect.Pretty.tag_num)
|
||||
for tag in msgs:
|
||||
self.textbox_write(tag, msgs[tag]['text'], self.customcolors[msgs[tag]['color']], msgs[tag]['extra'])
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def write(self, string):
|
||||
if string != '\n':
|
||||
self.str_to_print = string
|
||||
self.textbox_do()
|
||||
|
||||
class Stderr(Pretty):
|
||||
def __init__(self, srv_text_space, clt_text_space, customcolors, side):
|
||||
self.srv_text_space = srv_text_space
|
||||
self.clt_text_space = clt_text_space
|
||||
self.customcolors = customcolors
|
||||
self.side = side
|
||||
self.tag_err = 'STDERR'
|
||||
self.xfont = tkFont.Font(font = self.srv_text_space['font'])
|
||||
|
||||
def textbox_choose(self, message):
|
||||
if self.side == "srv":
|
||||
return self.srv_text_space
|
||||
elif self.side == "clt":
|
||||
return self.clt_text_space
|
||||
|
||||
def write(self, string):
|
||||
widget = self.textbox_choose(string)
|
||||
self.textbox_color(self.tag_err, widget, self.customcolors['red'], self.customcolors['black'])
|
||||
self.srv_text_space.configure(state = 'normal')
|
||||
self.srv_text_space.insert('end', string, self.tag_err)
|
||||
self.srv_text_space.see('end')
|
||||
self.srv_text_space.configure(state = 'disabled')
|
||||
|
||||
class Log(Pretty):
|
||||
def textbox_format(self, message):
|
||||
if message.startswith('logsrv'):
|
||||
message = message.replace('logsrv ', '')
|
||||
if message.startswith('logclt'):
|
||||
message = message.replace('logclt ', '')
|
||||
return message + '\n'
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class TextDoubleScroll(tk.Frame):
|
||||
def __init__(self, master, **kwargs):
|
||||
""" Initialize.
|
||||
- horizontal scrollbar
|
||||
- vertical scrollbar
|
||||
- text widget
|
||||
"""
|
||||
tk.Frame.__init__(self, master)
|
||||
self.master = master
|
||||
|
||||
self.textbox = tk.Text(self.master, **kwargs)
|
||||
self.sizegrip = ttk.Sizegrip(self.master)
|
||||
self.hs = ttk.Scrollbar(self.master, orient = "horizontal", command = self.on_scrollbar_x)
|
||||
self.vs = ttk.Scrollbar(self.master, orient = "vertical", command = self.on_scrollbar_y)
|
||||
self.textbox.configure(yscrollcommand = self.on_textscroll, xscrollcommand = self.hs.set)
|
||||
|
||||
def on_scrollbar_x(self, *args):
|
||||
""" Horizontally scrolls text widget. """
|
||||
self.textbox.xview(*args)
|
||||
|
||||
def on_scrollbar_y(self, *args):
|
||||
""" Vertically scrolls text widget. """
|
||||
self.textbox.yview(*args)
|
||||
|
||||
def on_textscroll(self, *args):
|
||||
""" Moves the scrollbar and scrolls text widget when the mousewheel is moved on a text widget. """
|
||||
self.vs.set(*args)
|
||||
self.on_scrollbar_y('moveto', args[0])
|
||||
|
||||
def put(self, **kwargs):
|
||||
""" Grid the scrollbars and textbox correctly. """
|
||||
self.textbox.grid(row = 0, column = 0, padx = 3, pady = 3, sticky = "nsew")
|
||||
self.vs.grid(row = 0, column = 1, sticky = "ns")
|
||||
self.hs.grid(row = 1, column = 0, sticky = "we")
|
||||
self.sizegrip.grid(row = 1, column = 1, sticky = "news")
|
||||
|
||||
def get(self):
|
||||
""" Return the "frame" useful to place inner controls. """
|
||||
return self.textbox
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
def custom_background(window):
|
||||
# first level canvas.
|
||||
allwidgets = window.grid_slaves(0,0)[0].grid_slaves() + window.grid_slaves(0,0)[0].place_slaves()
|
||||
widgets_alphalow = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas']
|
||||
widgets_alphahigh = []
|
||||
# sub-level canvas.
|
||||
for side in ["Srv", "Clt"]:
|
||||
widgets_alphahigh.append(window.pagewidgets[side]["BtnWin"])
|
||||
for position in ["Left", "Right"]:
|
||||
widgets_alphahigh.append(window.pagewidgets[side]["AniWin"][position])
|
||||
for pagename in window.pagewidgets[side]["PageWin"].keys():
|
||||
widgets_alphalow.append(window.pagewidgets[side]["PageWin"][pagename])
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# Open Image.
|
||||
img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keys.gif")
|
||||
img = img.convert('RGBA')
|
||||
# Resize image.
|
||||
img.resize((window.winfo_width(), window.winfo_height()), Image.ANTIALIAS)
|
||||
# Put semi-transparent background chunks.
|
||||
window.backcrops_alphalow, window.backcrops_alphahigh = ([] for _ in range(2))
|
||||
|
||||
def cutter(master, image, widgets, crops, alpha):
|
||||
for widget in widgets:
|
||||
x, y, w, h = master.get_position(widget)
|
||||
cropped = image.crop((x, y, x + w, y + h))
|
||||
cropped.putalpha(alpha)
|
||||
crops.append(ImageTk.PhotoImage(cropped))
|
||||
# Not in same loop to prevent reference garbage.
|
||||
for crop, widget in zip(crops, widgets):
|
||||
widget.create_image(1, 1, image = crop, anchor = 'nw')
|
||||
|
||||
cutter(window, img, widgets_alphalow, window.backcrops_alphalow, 36)
|
||||
cutter(window, img, widgets_alphahigh, window.backcrops_alphahigh, 96)
|
||||
|
||||
# Put semi-transparent background overall.
|
||||
img.putalpha(128)
|
||||
window.backimg = ImageTk.PhotoImage(img)
|
||||
window.masterwin.create_image(1, 1, image = window.backimg, anchor = 'nw')
|
||||
|
||||
except ImportError:
|
||||
for widget in widgets_alphalow + widgets_alphahigh:
|
||||
widget.configure(background = window.customcolors['lavender'])
|
||||
|
||||
# Hide client.
|
||||
window.clt_on_show(force_remove = True)
|
||||
# Show Gui.
|
||||
window.deiconify()
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class Animation(object):
|
||||
def __init__(self, gifpath, master, widget, loop = False):
|
||||
from PIL import Image, ImageTk, ImageSequence
|
||||
|
||||
self.master = master
|
||||
self.widget = widget
|
||||
self.loop = loop
|
||||
self.cancelid = None
|
||||
self.flagstop = False
|
||||
self.index = 0
|
||||
self.frames = []
|
||||
|
||||
img = Image.open(gifpath)
|
||||
size = img.size
|
||||
for frame in ImageSequence.Iterator(img):
|
||||
static_img = ImageTk.PhotoImage(frame.convert('RGBA'))
|
||||
try:
|
||||
static_img.delay = int(frame.info['duration'])
|
||||
except KeyError:
|
||||
static_img.delay = 100
|
||||
self.frames.append(static_img)
|
||||
|
||||
self.widget.configure(width = size[0], height = size[1])
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
self.widget.configure(image = self.frames[0])
|
||||
self.widget.image = self.frames[0]
|
||||
|
||||
def deanimate(self):
|
||||
while not self.flagstop:
|
||||
pass
|
||||
self.flagstop = False
|
||||
self.index = 0
|
||||
self.widget.configure(relief = "raised")
|
||||
|
||||
def animate(self):
|
||||
frame = self.frames[self.index]
|
||||
self.widget.configure(image = frame, relief = "sunken")
|
||||
self.index += 1
|
||||
self.cancelid = self.master.after(frame.delay, self.animate)
|
||||
if self.index == len(self.frames):
|
||||
if self.loop:
|
||||
self.index = 0
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def start(self, event = None):
|
||||
if str(self.widget['state']) != 'disabled':
|
||||
if self.cancelid is None:
|
||||
if not self.loop:
|
||||
self.btnani_thread = threading.Thread(target = self.deanimate, name = "Thread-BtnAni")
|
||||
self.btnani_thread.setDaemon(True)
|
||||
self.btnani_thread.start()
|
||||
self.cancelid = self.master.after(self.frames[0].delay, self.animate)
|
||||
|
||||
def stop(self, event = None):
|
||||
if self.cancelid:
|
||||
self.master.after_cancel(self.cancelid)
|
||||
self.cancelid = None
|
||||
self.flagstop = True
|
||||
self.initialize()
|
||||
|
||||
|
||||
def custom_pages(window, side):
|
||||
buttons = window.pagewidgets[side]["BtnAni"]
|
||||
labels = window.pagewidgets[side]["LblAni"]
|
||||
|
||||
for position in buttons.keys():
|
||||
buttons[position].config(anchor = "center",
|
||||
font = window.customfonts['btn'],
|
||||
background = window.customcolors['white'],
|
||||
activebackground = window.customcolors['white'],
|
||||
borderwidth = 2)
|
||||
|
||||
try:
|
||||
anibtn = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keyhole_%s.gif" %position,
|
||||
window, buttons[position], loop = False)
|
||||
anilbl = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Arrow_%s.gif" %position,
|
||||
window, labels[position], loop = True)
|
||||
|
||||
def animationwait(master, button, btn_animation, lbl_animation):
|
||||
while btn_animation.cancelid:
|
||||
pass
|
||||
sleep(1)
|
||||
x, y = master.winfo_pointerxy()
|
||||
if master.winfo_containing(x, y) == button:
|
||||
lbl_animation.start()
|
||||
|
||||
def animationcombo(master, button, btn_animation, lbl_animation):
|
||||
wait_thread = threading.Thread(target = animationwait,
|
||||
args = (master, button, btn_animation, lbl_animation),
|
||||
name = "Thread-WaitAni")
|
||||
wait_thread.setDaemon(True)
|
||||
wait_thread.start()
|
||||
lbl_animation.stop()
|
||||
btn_animation.start()
|
||||
|
||||
buttons[position].bind("<ButtonPress>", lambda event, anim1 = anibtn, anim2 = anilbl,
|
||||
bt = buttons[position], win = window:
|
||||
animationcombo(win, bt, anim1, anim2))
|
||||
buttons[position].bind("<Enter>", anilbl.start)
|
||||
buttons[position].bind("<Leave>", anilbl.stop)
|
||||
|
||||
except ImportError:
|
||||
buttons[position].config(activebackground = window.customcolors['blue'],
|
||||
foreground = window.customcolors['blue'])
|
||||
labels[position].config(background = window.customcolors['lavender'])
|
||||
|
||||
if position == "Left":
|
||||
buttons[position].config(text = '<<')
|
||||
elif position == "Right":
|
||||
buttons[position].config(text = '>>')
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class ListboxOfRadiobuttons(tk.Frame):
|
||||
def __init__(self, master, radios, font, changed, **kwargs):
|
||||
tk.Frame.__init__(self, master)
|
||||
|
||||
self.master = master
|
||||
self.radios = radios
|
||||
self.font = font
|
||||
self.changed = changed
|
||||
|
||||
self.scrollv = tk.Scrollbar(self, orient = "vertical")
|
||||
self.textbox = tk.Text(self, yscrollcommand = self.scrollv.set, **kwargs)
|
||||
self.scrollv.config(command = self.textbox.yview)
|
||||
# layout.
|
||||
self.scrollv.pack(side = "right", fill = "y")
|
||||
self.textbox.pack(side = "left", fill = "both", expand = True)
|
||||
# create radiobuttons.
|
||||
self.radiovar = tk.StringVar()
|
||||
self.radiovar.set('FILE')
|
||||
self.create()
|
||||
|
||||
def create(self):
|
||||
self.rdbtns = []
|
||||
for n, nameradio in enumerate(self.radios):
|
||||
rdbtn = tk.Radiobutton(self, text = nameradio, value = nameradio, variable = self.radiovar,
|
||||
font = self.font, indicatoron = 0, width = 15,
|
||||
borderwidth = 3, selectcolor = 'yellow', command = self.change)
|
||||
self.textbox.window_create("end", window = rdbtn)
|
||||
# to force one checkbox per line
|
||||
if n != len(self.radios) - 1:
|
||||
self.textbox.insert("end", "\n")
|
||||
self.rdbtns.append(rdbtn)
|
||||
self.textbox.configure(state = "disabled")
|
||||
|
||||
def change(self):
|
||||
st = self.state()
|
||||
for widget, default in self.changed:
|
||||
wclass = widget.winfo_class()
|
||||
if st in ['STDOUT', 'FILEOFF']:
|
||||
if wclass == 'Entry':
|
||||
widget.delete(0, 'end')
|
||||
widget.configure(state = "disabled")
|
||||
elif wclass == 'TCombobox':
|
||||
if st == 'STDOUT':
|
||||
widget.set(default)
|
||||
widget.configure(state = "readonly")
|
||||
elif st == 'FILEOFF':
|
||||
widget.set('')
|
||||
widget.configure(state = "disabled")
|
||||
elif st in ['FILE', 'FILESTDOUT', 'STDOUTOFF']:
|
||||
if wclass == 'Entry':
|
||||
widget.configure(state = "normal")
|
||||
widget.delete(0, 'end')
|
||||
widget.insert('end', default)
|
||||
widget.xview_moveto(1)
|
||||
elif wclass == 'TCombobox':
|
||||
widget.configure(state = "readonly")
|
||||
widget.set(default)
|
||||
elif wclass == 'Button':
|
||||
widget.configure(state = "normal")
|
||||
|
||||
def configure(self, state):
|
||||
for rb in self.rdbtns:
|
||||
rb.configure(state = state)
|
||||
|
||||
def state(self):
|
||||
return self.radiovar.get()
|
|
@ -194,9 +194,6 @@ def logger_create(log_obj, config, mode = 'a'):
|
|||
frmt_name = '%(name)s '
|
||||
|
||||
from pykms_Server import serverthread
|
||||
if serverthread.with_gui:
|
||||
frmt_std = frmt_name + frmt_std
|
||||
frmt_min = frmt_name + frmt_min
|
||||
|
||||
def apply_formatter(levelnum, formats, handler, color = False):
|
||||
levelformdict = {}
|
||||
|
@ -521,7 +518,7 @@ def check_setup(config, options, logger, where):
|
|||
# Check logfile.
|
||||
config['logfile'] = check_logfile(config['logfile'], options['lfile']['def'], where = where)
|
||||
|
||||
# Check logsize (py-kms Gui).
|
||||
# Check logsize
|
||||
if config['logsize'] == "":
|
||||
if any(opt in ['STDOUT', 'FILEOFF'] for opt in config['logfile']):
|
||||
# set a recognized size never used.
|
||||
|
@ -530,7 +527,7 @@ def check_setup(config, options, logger, where):
|
|||
pretty_printer(put_text = "{reverse}{red}{bold}argument `-S/--logsize`: invalid with: '%s'. Exiting...{end}" %config['logsize'],
|
||||
where = where, to_exit = True)
|
||||
|
||||
# Check loglevel (py-kms Gui).
|
||||
# Check loglevel
|
||||
if config['loglevel'] == "":
|
||||
# set a recognized level never used.
|
||||
config['loglevel'] = 'ERROR'
|
||||
|
|
|
@ -9,22 +9,20 @@ import uuid
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import pickle
|
||||
import socketserver
|
||||
import queue as Queue
|
||||
import selectors
|
||||
from tempfile import gettempdir
|
||||
from time import monotonic as time
|
||||
|
||||
import pykms_RpcBind, pykms_RpcRequest
|
||||
from pykms_RpcBase import rpcBase
|
||||
from pykms_Dcerpc import MSRPCHeader
|
||||
from pykms_Misc import check_setup, check_lcid, check_dir, check_other
|
||||
from pykms_Misc import check_setup, check_lcid, check_other
|
||||
from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp
|
||||
from pykms_Misc import kms_parser_get, kms_parser_check_optionals, kms_parser_check_positionals, kms_parser_check_connect
|
||||
from pykms_Format import enco, deco, pretty_printer, justify
|
||||
from Etrigan import Etrigan, Etrigan_parser, Etrigan_check, Etrigan_job
|
||||
from pykms_Connect import MultipleListener
|
||||
from pykms_Sql import sql_initialize
|
||||
|
||||
srv_version = "py-kms_2020-10-01"
|
||||
__license__ = "The Unlicense"
|
||||
|
@ -135,7 +133,8 @@ class server_thread(threading.Thread):
|
|||
self.name = name
|
||||
self.queue = queue
|
||||
self.server = None
|
||||
self.is_running_server, self.with_gui, self.checked = [False for _ in range(3)]
|
||||
self.is_running_server = False
|
||||
self.checked = False
|
||||
self.is_running_thread = threading.Event()
|
||||
|
||||
def terminate_serve(self):
|
||||
|
@ -170,12 +169,6 @@ class server_thread(threading.Thread):
|
|||
self.server.pykms_serve()
|
||||
except (SystemExit, Exception) as e:
|
||||
self.eject = True
|
||||
if not self.with_gui:
|
||||
raise
|
||||
else:
|
||||
if isinstance(e, SystemExit):
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
##---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
@ -184,7 +177,7 @@ loggersrv = logging.getLogger('logsrv')
|
|||
|
||||
# 'help' string - 'default' value - 'dest' string.
|
||||
srv_options = {
|
||||
'ip' : {'help' : 'The IP address (IPv4 or IPv6) to listen on. The default is \"0.0.0.0\" (all interfaces).', 'def' : "0.0.0.0", 'des' : "ip"},
|
||||
'ip' : {'help' : 'The IP address (IPv4 or IPv6) to listen on. The default is \"::\" (all interfaces).', 'def' : "::", 'des' : "ip"},
|
||||
'port' : {'help' : 'The network port to listen on. The default is \"1688\".', 'def' : 1688, 'des' : "port"},
|
||||
'epid' : {'help' : 'Use this option to manually specify an ePID to use. If no ePID is specified, a random ePID will be auto generated.',
|
||||
'def' : None, 'des' : "epid"},
|
||||
|
@ -196,8 +189,7 @@ for server OSes and Office >=5', 'def' : None, 'des' : "clientcount"},
|
|||
'def' : 120, 'des': "activation"},
|
||||
'renewal' : {'help' : 'Use this option to specify the renewal interval (in minutes). Default is \"10080\" minutes (7 days).',
|
||||
'def' : 1440 * 7, 'des' : "renewal"},
|
||||
'sql' : {'help' : 'Use this option to store request information from unique clients in an SQLite database. Deactivated by default. \
|
||||
If enabled the default .db file is \"pykms_database.db\". You can also provide a specific location.', 'def' : False,
|
||||
'sql' : {'help' : 'Use this option to store request information from unique clients in an SQLite database. Deactivated by default.', 'def' : False,
|
||||
'file': os.path.join('.', 'pykms_database.db'), 'des' : "sqlite"},
|
||||
'hwid' : {'help' : 'Use this option to specify a HWID. The HWID must be an 16-character string of hex characters. \
|
||||
The default is \"364F463A8863D35F\" or type \"RANDOM\" to auto generate the HWID.',
|
||||
|
@ -220,7 +212,7 @@ Use \"STDOUTOFF\" to disable stdout messages. Use \"FILEOFF\" if you not want to
|
|||
'reuse' : {'help' : 'Do not allows binding / listening to the same address and port. Reusing port is activated by default.', 'def' : True,
|
||||
'des': "reuse"},
|
||||
'dual' : {'help' : 'Allows listening to an IPv6 address also accepting connections via IPv4. Deactivated by default.',
|
||||
'def' : False, 'des': "dual"}
|
||||
'def' : True, 'des': "dual"}
|
||||
}
|
||||
|
||||
def server_options():
|
||||
|
@ -256,15 +248,6 @@ def server_options():
|
|||
|
||||
server_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit")
|
||||
|
||||
## Daemon (Etrigan) parsing.
|
||||
daemon_parser = KmsParser(description = "daemon options inherited from Etrigan", add_help = False)
|
||||
daemon_subparser = daemon_parser.add_subparsers(dest = "mode")
|
||||
|
||||
etrigan_parser = daemon_subparser.add_parser("etrigan", add_help = False)
|
||||
etrigan_parser.add_argument("-g", "--gui", action = "store_const", dest = 'gui', const = True, default = False,
|
||||
help = "Enable py-kms GUI usage.")
|
||||
etrigan_parser = Etrigan_parser(parser = etrigan_parser)
|
||||
|
||||
## Connection parsing.
|
||||
connection_parser = KmsParser(description = "connect options", add_help = False)
|
||||
connection_subparser = connection_parser.add_subparsers(dest = "mode")
|
||||
|
@ -284,14 +267,12 @@ def server_options():
|
|||
|
||||
# Run help.
|
||||
if any(arg in ["-h", "--help"] for arg in userarg):
|
||||
KmsParserHelp().printer(parsers = [server_parser, (daemon_parser, etrigan_parser),
|
||||
(connection_parser, connect_parser)])
|
||||
KmsParserHelp().printer(parsers = [server_parser, (connection_parser, connect_parser)])
|
||||
|
||||
# Get stored arguments.
|
||||
pykmssrv_zeroarg, pykmssrv_onearg = kms_parser_get(server_parser)
|
||||
etrigan_zeroarg, etrigan_onearg = kms_parser_get(etrigan_parser)
|
||||
connect_zeroarg, connect_onearg = kms_parser_get(connect_parser)
|
||||
subdict = {'etrigan' : (etrigan_zeroarg, etrigan_onearg, daemon_parser.parse_args),
|
||||
subdict = {
|
||||
'connect' : (connect_zeroarg, connect_onearg, connection_parser.parse_args)
|
||||
}
|
||||
subpars = list(subdict.keys())
|
||||
|
@ -309,14 +290,7 @@ def server_options():
|
|||
if subindx:
|
||||
# Set `daemon options` and/or `connect options` for server dict config.
|
||||
# example cases:
|
||||
# 1 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] etrigan daemon_positional [--daemon_optionals] \
|
||||
# connect [--connect_optionals]
|
||||
#
|
||||
# 2 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] connect [--connect_optionals] etrigan \
|
||||
# daemon_positional [--daemon_optionals]
|
||||
#
|
||||
# 3 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] etrigan daemon_positional [--daemon_optionals]
|
||||
# 4 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] connect [--connect_optionals]
|
||||
# 1 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] connect [--connect_optionals]
|
||||
first = subindx[0][0]
|
||||
# initial.
|
||||
kms_parser_check_optionals(userarg[0 : first], pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = exclude_kms)
|
||||
|
@ -338,7 +312,7 @@ def server_options():
|
|||
else:
|
||||
# Update `pykms options` for server dict config.
|
||||
# example case:
|
||||
# 5 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals]
|
||||
# 2 python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals]
|
||||
kms_parser_check_optionals(userarg, pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = exclude_kms)
|
||||
kms_parser_check_positionals(srv_config, server_parser.parse_args)
|
||||
|
||||
|
@ -347,63 +321,6 @@ def server_options():
|
|||
except KmsParserException as e:
|
||||
pretty_printer(put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e), to_exit = True)
|
||||
|
||||
class Etrigan_Check(Etrigan_check):
|
||||
def emit_opt_err(self, msg):
|
||||
pretty_printer(put_text = "{reverse}{red}{bold}%s{end}" %msg, to_exit = True)
|
||||
|
||||
class Etrigan(Etrigan):
|
||||
def emit_message(self, message, to_exit = False):
|
||||
if not self.mute:
|
||||
pretty_printer(put_text = "{reverse}{green}{bold}%s{end}" %message)
|
||||
if to_exit:
|
||||
sys.exit(0)
|
||||
|
||||
def emit_error(self, message, to_exit = True):
|
||||
if not self.mute:
|
||||
pretty_printer(put_text = "{reverse}{red}{bold}%s{end}" %message, to_exit = True)
|
||||
|
||||
def server_daemon():
|
||||
if 'etrigan' in srv_config.values():
|
||||
path = os.path.join(gettempdir(), 'pykms_config.pickle')
|
||||
|
||||
if srv_config['operation'] in ['stop', 'restart', 'status'] and len(sys.argv[1:]) > 2:
|
||||
pretty_printer(put_text = "{reverse}{red}{bold}too much arguments with etrigan '%s'. Exiting...{end}" %srv_config['operation'],
|
||||
to_exit = True)
|
||||
|
||||
# Check file arguments.
|
||||
Etrigan_Check().checkfile(srv_config['etriganpid'], '--etrigan-pid', '.pid')
|
||||
Etrigan_Check().checkfile(srv_config['etriganlog'], '--etrigan-log', '.log')
|
||||
|
||||
if srv_config['gui']:
|
||||
pass
|
||||
else:
|
||||
if srv_config['operation'] == 'start':
|
||||
with open(path, 'wb') as file:
|
||||
pickle.dump(srv_config, file, protocol = pickle.HIGHEST_PROTOCOL)
|
||||
elif srv_config['operation'] in ['stop', 'status', 'restart']:
|
||||
with open(path, 'rb') as file:
|
||||
old_srv_config = pickle.load(file)
|
||||
old_srv_config = {x: old_srv_config[x] for x in old_srv_config if x not in ['operation']}
|
||||
srv_config.update(old_srv_config)
|
||||
|
||||
serverdaemon = Etrigan(srv_config['etriganpid'],
|
||||
logfile = srv_config['etriganlog'], loglevel = srv_config['etriganlev'],
|
||||
mute = srv_config['etriganmute'], pause_loop = None)
|
||||
|
||||
if srv_config['operation'] in ['start', 'restart']:
|
||||
serverdaemon.want_quit = True
|
||||
if srv_config['gui']:
|
||||
serverdaemon.funcs_to_daemonize = [server_with_gui]
|
||||
else:
|
||||
server_without_gui = ServerWithoutGui()
|
||||
serverdaemon.funcs_to_daemonize = [server_without_gui.start, server_without_gui.join]
|
||||
indx_for_clean = lambda: (0, )
|
||||
serverdaemon.quit_on_stop = [indx_for_clean, server_without_gui.clean]
|
||||
elif srv_config['operation'] == 'stop':
|
||||
os.remove(path)
|
||||
|
||||
Etrigan_job(srv_config['operation'], serverdaemon)
|
||||
|
||||
def server_check():
|
||||
# Setup and some checks.
|
||||
check_setup(srv_config, srv_options, loggersrv, where = "srv")
|
||||
|
@ -445,13 +362,16 @@ def server_check():
|
|||
|
||||
# Check sqlite.
|
||||
if srv_config['sqlite']:
|
||||
if isinstance(srv_config['sqlite'], str):
|
||||
check_dir(srv_config['sqlite'], 'srv', log_obj = loggersrv.error, argument = '-s/--sqlite')
|
||||
elif srv_config['sqlite'] is True:
|
||||
if srv_config['sqlite'] is True: # Resolve bool to the default path
|
||||
srv_config['sqlite'] = srv_options['sql']['file']
|
||||
if os.path.isdir(srv_config['sqlite']):
|
||||
pretty_printer(log_obj = loggersrv.warning,
|
||||
put_text = "{reverse}{yellow}{bold}You specified a folder instead of a database file! This behavior is not officially supported anymore, please change your start parameters soon.{end}")
|
||||
srv_config['sqlite'] = os.path.join(srv_config['sqlite'], 'pykms_database.db')
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
sql_initialize(srv_config['sqlite'])
|
||||
except ImportError:
|
||||
pretty_printer(log_obj = loggersrv.warning,
|
||||
put_text = "{reverse}{yellow}{bold}Module 'sqlite3' not installed, database support disabled.{end}")
|
||||
|
@ -461,9 +381,6 @@ def server_check():
|
|||
opts = [('clientcount', '-c/--client-count'),
|
||||
('timeoutidle', '-t0/--timeout-idle'),
|
||||
('timeoutsndrcv', '-t1/--timeout-sndrcv')]
|
||||
if serverthread.with_gui:
|
||||
opts += [('activation', '-a/--activation-interval'),
|
||||
('renewal', '-r/--renewal-interval')]
|
||||
check_other(srv_config, opts, loggersrv, where = 'srv')
|
||||
|
||||
# Check further addresses / ports.
|
||||
|
@ -543,8 +460,6 @@ def server_main_terminal():
|
|||
server_check()
|
||||
serverthread.checked = True
|
||||
|
||||
if 'etrigan' not in srv_config.values():
|
||||
# (without GUI) and (without daemon).
|
||||
# Run threaded server.
|
||||
serverqueue.put('start')
|
||||
# Wait to finish.
|
||||
|
@ -553,25 +468,6 @@ def server_main_terminal():
|
|||
serverthread.join(timeout = 0.5)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
server_terminate(serverthread, exit_server = True, exit_thread = True)
|
||||
else:
|
||||
# (with or without GUI) and (with daemon)
|
||||
# Setup daemon (eventually).
|
||||
pretty_printer(log_obj = loggersrv.warning, put_text = "{reverse}{yellow}{bold}Etrigan support is deprecated and will be removed in the future!{end}")
|
||||
server_daemon()
|
||||
|
||||
def server_with_gui():
|
||||
import pykms_GuiBase
|
||||
|
||||
pretty_printer(log_obj = loggersrv.warning, put_text = "{reverse}{yellow}{bold}Etrigan GUI support is deprecated and will be removed in the future!{end}")
|
||||
|
||||
root = pykms_GuiBase.KmsGui()
|
||||
root.title(pykms_GuiBase.gui_description + ' (' + pykms_GuiBase.gui_version + ')')
|
||||
root.mainloop()
|
||||
|
||||
def server_main_no_terminal():
|
||||
# Run tkinter GUI.
|
||||
# (with GUI) and (without daemon).
|
||||
server_with_gui()
|
||||
|
||||
class kmsServerHandler(socketserver.BaseRequestHandler):
|
||||
def setup(self):
|
||||
|
@ -636,10 +532,4 @@ serverthread.daemon = True
|
|||
serverthread.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.stdout.isatty():
|
||||
server_main_terminal()
|
||||
else:
|
||||
try:
|
||||
server_main_no_terminal()
|
||||
except:
|
||||
server_main_terminal()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
@ -23,17 +24,35 @@ def sql_initialize(dbName):
|
|||
try:
|
||||
con = sqlite3.connect(dbName)
|
||||
cur = con.cursor()
|
||||
cur.execute("CREATE TABLE clients(clientMachineId TEXT, machineName TEXT, applicationId TEXT, skuId TEXT, \
|
||||
licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER)")
|
||||
cur.execute("CREATE TABLE clients(clientMachineId TEXT , machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
pretty_printer(log_obj = loggersrv.error, to_exit = True,
|
||||
put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
|
||||
pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
|
||||
finally:
|
||||
if con:
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
def sql_get_all(dbName):
|
||||
if not os.path.isfile(dbName):
|
||||
return None
|
||||
with sqlite3.connect(dbName) as con:
|
||||
cur = con.cursor()
|
||||
cur.execute("SELECT * FROM clients")
|
||||
clients = []
|
||||
for row in cur.fetchall():
|
||||
clients.append({
|
||||
'clientMachineId': row[0],
|
||||
'machineName': row[1],
|
||||
'applicationId': row[2],
|
||||
'skuId': row[3],
|
||||
'licenseStatus': row[4],
|
||||
'lastRequestTime': datetime.datetime.fromtimestamp(row[5]).isoformat(),
|
||||
'kmsEpid': row[6],
|
||||
'requestCount': row[7]
|
||||
})
|
||||
return clients
|
||||
|
||||
def sql_update(dbName, infoDict):
|
||||
con = None
|
||||
try:
|
||||
|
|
141
py-kms/pykms_WebUI.py
Normal file
141
py-kms/pykms_WebUI.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
import os, uuid, datetime
|
||||
from flask import Flask, render_template
|
||||
from pykms_Sql import sql_get_all
|
||||
from pykms_DB2Dict import kmsDB2Dict
|
||||
|
||||
def _random_uuid():
|
||||
return str(uuid.uuid4()).replace('-', '_')
|
||||
|
||||
_serve_count = 0
|
||||
def _increase_serve_count():
|
||||
global _serve_count
|
||||
_serve_count += 1
|
||||
|
||||
def _get_serve_count():
|
||||
return _serve_count
|
||||
|
||||
_kms_items = None
|
||||
_kms_items_ignored = None
|
||||
def _get_kms_items_cache():
|
||||
global _kms_items, _kms_items_ignored
|
||||
if _kms_items is None:
|
||||
_kms_items = {}
|
||||
_kms_items_ignored = 0
|
||||
queue = [kmsDB2Dict()]
|
||||
while len(queue):
|
||||
item = queue.pop(0)
|
||||
if isinstance(item, list):
|
||||
for i in item:
|
||||
queue.append(i)
|
||||
elif isinstance(item, dict):
|
||||
if 'KmsItems' in item:
|
||||
queue.append(item['KmsItems'])
|
||||
elif 'SkuItems' in item:
|
||||
queue.append(item['SkuItems'])
|
||||
elif 'Gvlk' in item:
|
||||
if len(item['Gvlk']):
|
||||
_kms_items[item['DisplayName']] = item['Gvlk']
|
||||
else:
|
||||
_kms_items_ignored += 1
|
||||
#else:
|
||||
# print(item)
|
||||
else:
|
||||
raise NotImplementedError(f'Unknown type: {type(item)}')
|
||||
return _kms_items, _kms_items_ignored
|
||||
|
||||
app = Flask('pykms_webui')
|
||||
app.jinja_env.globals['start_time'] = datetime.datetime.now()
|
||||
app.jinja_env.globals['get_serve_count'] = _get_serve_count
|
||||
app.jinja_env.globals['random_uuid'] = _random_uuid
|
||||
app.jinja_env.globals['version_info'] = None
|
||||
|
||||
_version_info_path = os.environ.get('PYKMS_VERSION_PATH', '../VERSION')
|
||||
if os.path.exists(_version_info_path):
|
||||
with open(_version_info_path, 'r') as f:
|
||||
app.jinja_env.globals['version_info'] = {
|
||||
'hash': f.readline().strip(),
|
||||
'branch': f.readline().strip()
|
||||
}
|
||||
|
||||
_dbEnvVarName = 'PYKMS_SQLITE_DB_PATH'
|
||||
def _env_check():
|
||||
if _dbEnvVarName not in os.environ:
|
||||
raise Exception(f'Environment variable is not set: {_dbEnvVarName}')
|
||||
|
||||
@app.route('/')
|
||||
def root():
|
||||
_increase_serve_count()
|
||||
error = None
|
||||
# Get the db name / path
|
||||
dbPath = None
|
||||
if _dbEnvVarName in os.environ:
|
||||
dbPath = os.environ.get(_dbEnvVarName)
|
||||
else:
|
||||
error = f'Environment variable is not set: {_dbEnvVarName}'
|
||||
# Fetch all clients from the database.
|
||||
clients = None
|
||||
try:
|
||||
if dbPath:
|
||||
clients = sql_get_all(dbPath)
|
||||
except Exception as e:
|
||||
error = f'Error while loading database: {e}'
|
||||
countClients = len(clients) if clients else 0
|
||||
countClientsWindows = len([c for c in clients if c['applicationId'] == 'Windows']) if clients else 0
|
||||
countClientsOffice = countClients - countClientsWindows
|
||||
return render_template(
|
||||
'clients.html',
|
||||
path='/',
|
||||
error=error,
|
||||
clients=clients,
|
||||
count_clients=countClients,
|
||||
count_clients_windows=countClientsWindows,
|
||||
count_clients_office=countClientsOffice,
|
||||
count_projects=len(_get_kms_items_cache()[0])
|
||||
), 200 if error is None else 500
|
||||
|
||||
@app.route('/readyz')
|
||||
def readyz():
|
||||
try:
|
||||
_env_check()
|
||||
except Exception as e:
|
||||
return f'Whooops! {e}', 503
|
||||
if (datetime.datetime.now() - app.jinja_env.globals['start_time']).seconds > 10: # Wait 10 seconds before accepting requests
|
||||
return 'OK', 200
|
||||
else:
|
||||
return 'Not ready', 503
|
||||
|
||||
@app.route('/livez')
|
||||
def livez():
|
||||
try:
|
||||
_env_check()
|
||||
return 'OK', 200 # There are no checks for liveness, so we just return OK
|
||||
except Exception as e:
|
||||
return f'Whooops! {e}', 503
|
||||
|
||||
@app.route('/license')
|
||||
def license():
|
||||
_increase_serve_count()
|
||||
with open(os.environ.get('PYKMS_LICENSE_PATH', '../LICENSE'), 'r') as f:
|
||||
return render_template(
|
||||
'license.html',
|
||||
path='/license/',
|
||||
license=f.read()
|
||||
)
|
||||
|
||||
@app.route('/products')
|
||||
def products():
|
||||
_increase_serve_count()
|
||||
items, ignored = _get_kms_items_cache()
|
||||
countProducts = len(items)
|
||||
countProductsWindows = len([i for i in items if 'windows' in i.lower()])
|
||||
countProductsOffice = len([i for i in items if 'office' in i.lower()])
|
||||
return render_template(
|
||||
'products.html',
|
||||
path='/products/',
|
||||
products=items,
|
||||
filtered=ignored,
|
||||
count_products=countProducts,
|
||||
count_products_windows=countProductsWindows,
|
||||
count_products_office=countProductsOffice
|
||||
)
|
||||
|
1
py-kms/static/css/bulma.min.css
vendored
Normal file
1
py-kms/static/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
56
py-kms/templates/base.html
Normal file
56
py-kms/templates/base.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>py-kms {% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename= 'css/bulma.min.css') }}">
|
||||
<style>
|
||||
#content {
|
||||
margin: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
}
|
||||
{% if path != '/' %}
|
||||
div.backtohome {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
{% endif %}
|
||||
{% block style %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% if path != '/' %}
|
||||
<div class="block backtohome">
|
||||
<a class="button is-normal is-responsive" href="/">
|
||||
Back to home
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<strong>py-kms</strong> is online since <span class="convert_timestamp">{{ start_time }}</span>.
|
||||
This instance was accessed {{ get_serve_count() }} times. View this softwares license <a href="/license">here</a>.
|
||||
{% if version_info %}
|
||||
<br>This instance is running version "{{ version_info['hash'] }}" from branch "{{ version_info['branch'] }}" of py-kms.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
for(let element of document.getElementsByClassName('convert_timestamp')) {
|
||||
element.innerText = new Date(element.innerText).toLocaleString();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
103
py-kms/templates/clients.html
Normal file
103
py-kms/templates/clients.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}clients{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if error %}
|
||||
<article class="message is-danger">
|
||||
<div class="message-header">
|
||||
Whoops! Something went wrong...
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ error }}
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Clients</p>
|
||||
<p class="title">{{ count_clients }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Windows</p>
|
||||
<p class="title">{{ count_clients_windows }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Office</p>
|
||||
<p class="title">{{ count_clients_office }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Products</p>
|
||||
<p class="title"><a href="/products">{{ count_projects }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if clients %}
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Machine Name</th>
|
||||
<th>Application ID</th>
|
||||
<th><abbr title="Stock Keeping Unit">SKU</abbr> ID</th>
|
||||
<th>License Status</th>
|
||||
<th>Last Seen</th>
|
||||
<th>KMS <abbr title="Enhanced Privacy ID">EPID</abbr></th>
|
||||
<th>Seen Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for client in clients %}
|
||||
<tr>
|
||||
<th><pre class="clientMachineId">{{ client.clientMachineId }}</pre></th>
|
||||
<td class="machineName">
|
||||
{% if client.machineName | length > 16 %}
|
||||
<abbr title="{{ client.machineName }}">{{ client.machineName | truncate(16, True, '...') }}</abbr>
|
||||
{% else %}
|
||||
{{ client.machineName }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ client.applicationId }}</td>
|
||||
<td>{{ client.skuId }}</td>
|
||||
<td>{{ client.licenseStatus }}</td>
|
||||
<td class="convert_timestamp">{{ client.lastRequestTime }}</td>
|
||||
<td>
|
||||
{% if client.kmsEpid | length > 16 %}
|
||||
<abbr title="{{ client.kmsEpid }}">{{ client.kmsEpid | truncate(16, True, '...') }}</abbr>
|
||||
{% else %}
|
||||
{{ client.kmsEpid }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ client.requestCount }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<article class="message is-warning">
|
||||
<div class="message-header">
|
||||
<p>Whoops?</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
This page seems to be empty, because no clients are available. Try to use the server with a compartible client to add it to the database.
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
9
py-kms/templates/license.html
Normal file
9
py-kms/templates/license.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}license{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<pre>{{ license }}</pre>
|
||||
</div>
|
||||
{% endblock %}
|
53
py-kms/templates/products.html
Normal file
53
py-kms/templates/products.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}clients{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Products</p>
|
||||
<p class="title">{{ count_products + filtered }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Windows</p>
|
||||
<p class="title">{{ count_products_windows }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Office</p>
|
||||
<p class="title">{{ count_products_office }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading"><abbr title="Empty GLVK or not recognized">Other</abbr></p>
|
||||
<p class="title">{{ filtered }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th><abbr title="Group Volume License Key">GVLK</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name, gvlk in products | dictsort %}
|
||||
{% if gvlk %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td><pre>{{ gvlk }}</pre></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
1
requirements.txt
Symbolic link
1
requirements.txt
Symbolic link
|
@ -0,0 +1 @@
|
|||
docker/docker-py3-kms/requirements.txt
|
Loading…
Reference in a new issue