Merge pull request #86 from Py-KMS-Organization/next

Next Release 🎉
This commit is contained in:
simonmicro 2023-04-22 14:39:20 +02:00 committed by GitHub
commit 314cefb773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 653 additions and 2389 deletions

View file

@ -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
View 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
View 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
View file

@ -2,7 +2,6 @@
pykms_logserver.log*
pykms_logclient.log*
pykms_database.db*
etrigan.log*
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -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.

View file

@ -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.

View file

@ -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
- _py-kms_ is [![Unlicense](https://img.shields.io/badge/license-unlicense-lightgray.svg)](https://github.com/SystemRage/py-kms/blob/master/LICENSE)

View file

@ -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 | `{}` | |

View file

@ -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 }}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
dnspython==2.2.1
tzlocal==4.2

View file

@ -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

View file

@ -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

View file

@ -1,4 +0,0 @@
Flask==2.1.2
Pygments==2.12.0
dnspython==2.2.1
tzlocal==4.2

View file

@ -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,9 +73,9 @@ def start_kms():
except KeyboardInterrupt:
pass
if enableSQLITE:
if None != sqlite_process: sqlite_process.terminate()
pykms_process.terminate()
if pykms_webui_process:
pykms_webui_process.terminate()
pykms_process.terminate()
# Main

View file

@ -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`).

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -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

View file

@ -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)

View file

@ -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,11 +296,10 @@ 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()
# Parse options.
client_options()
# Check options.
client_check()
@ -393,4 +391,4 @@ def readKmsResponseV6(data):
return message
if __name__ == "__main__":
clt_main(with_gui = False)
clt_main()

View file

@ -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,

View file

@ -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)

View file

@ -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()

View file

@ -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'

View file

@ -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,13 +169,7 @@ 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
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,16 +267,14 @@ 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),
'connect' : (connect_zeroarg, connect_onearg, connection_parser.parse_args)
}
subdict = {
'connect' : (connect_zeroarg, connect_onearg, connection_parser.parse_args)
}
subpars = list(subdict.keys())
pykmssrv_zeroarg += subpars # add subparsers
@ -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,25 +362,25 @@ 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}")
put_text = "{reverse}{yellow}{bold}Module 'sqlite3' not installed, database support disabled.{end}")
srv_config['sqlite'] = False
# Check other specific server options.
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,35 +460,14 @@ 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.
try:
while serverthread.is_alive():
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()
# Run threaded server.
serverqueue.put('start')
# Wait to finish.
try:
while serverthread.is_alive():
serverthread.join(timeout = 0.5)
except (KeyboardInterrupt, SystemExit):
server_terminate(serverthread, exit_server = True, exit_thread = True)
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()
server_main_terminal()

View file

@ -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
View 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

File diff suppressed because one or more lines are too long

View 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>

View 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 %}

View file

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block title %}license{% endblock %}
{% block content %}
<div class="block">
<pre>{{ license }}</pre>
</div>
{% endblock %}

View 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
View file

@ -0,0 +1 @@
docker/docker-py3-kms/requirements.txt