mirror of https://github.com/SystemRage/py-kms.git
Merge 03992a851d
into a3b0c85b5b
This commit is contained in:
commit
364831a0e7
|
@ -0,0 +1,8 @@
|
|||
log/
|
||||
.idea
|
||||
.github
|
||||
*.db
|
||||
*.yml
|
||||
*.md
|
||||
*.sh
|
||||
Makefile
|
|
@ -0,0 +1,51 @@
|
|||
name: Build release-tags
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
bake-latest:
|
||||
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
|
||||
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,ghcr.io/py-kms-organization/py-kms:minimal
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ github.sha }}
|
||||
BUILD_BRANCH=${{ github.ref_name }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -2,7 +2,6 @@
|
|||
pykms_logserver.log*
|
||||
pykms_logclient.log*
|
||||
pykms_database.db*
|
||||
etrigan.log*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@ -128,3 +127,8 @@ dmypy.json
|
|||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Helm
|
||||
charts/*/*.tgz
|
||||
/.idea/
|
||||
docker-compose-*.yml
|
||||
*.sh
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
version: 2
|
||||
|
||||
build:
|
||||
os: "ubuntu-22.04"
|
||||
tools:
|
||||
python: "3.10"
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
formats: all
|
116
CHANGELOG.md
116
CHANGELOG.md
|
@ -1,26 +1,70 @@
|
|||
# Changelog
|
||||
|
||||
### py-kms_2020-10-01
|
||||
## 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.
|
||||
- Support for multi-address connection.
|
||||
- Added timeout send / receive.
|
||||
|
||||
### py-kms_2020-07-01
|
||||
## py-kms_2020-07-01
|
||||
- py-kms Gui: now matches all cli options, added modes onlyserver / onlyclient,
|
||||
added some animations.
|
||||
- Added suboptions FILEOFF and STDOUTOFF of -F.
|
||||
- Created option for asynchronous messages.
|
||||
- Cleaned options parsing process.
|
||||
|
||||
### py-kms_2020-02-02
|
||||
## py-kms_2020-02-02
|
||||
- Optimized pretty-print messages process.
|
||||
- Added -F FILESTDOUT option.
|
||||
- Added deamonization options (via [Etrigan](https://github.com/SystemRage/Etrigan) project).
|
||||
- py-kms GUI resurrected (and improved).
|
||||
- Cleaned, cleaned, cleaned.
|
||||
|
||||
### py-kms_2019-05-15
|
||||
## py-kms_2019-05-15
|
||||
- Merging for Python2 / Python3 compatibility all-in-one.
|
||||
- Added new options:
|
||||
- timeout, [logsize](https://github.com/SystemRage/py-kms/pull/21).
|
||||
|
@ -32,7 +76,7 @@
|
|||
- Fixed activation threshold.
|
||||
- Renamed files, cosmetics and many other little big adjustments.
|
||||
|
||||
### py-kms_2018-11-15
|
||||
## py-kms_2018-11-15
|
||||
- Implemented some good modifications inspired by [this](https://github.com/ThunderEX/py-kms) other fork.
|
||||
- Clean up code ( deleted no longer useful files randomHWID.py, randomEPID.py, timezones.py;
|
||||
erased useless functions and import modules )
|
||||
|
@ -41,128 +85,128 @@
|
|||
- Improved random EPID generation.
|
||||
- Corrected [this](https://github.com/SystemRage/py-kms/issues/8) in kmsBase.py
|
||||
|
||||
### py-kms_2018-03-01
|
||||
## py-kms_2018-03-01
|
||||
- *py-kms NOW is for Python3 too ( py3-kms ), the previous one ( written with Python2 ) is renamed py2-kms*
|
||||
- *Repaired logging messages*
|
||||
- *Added pretty processing messages*
|
||||
|
||||
### py-kms_2017-06-01
|
||||
## py-kms_2017-06-01
|
||||
- *Added option verbose logging in a file*
|
||||
- *Updated "kmsBase.py" with new SKUIDs*
|
||||
- *Added a brief guide "py-kms-Guide.pdf" ( replaced "client-activation.txt" )*
|
||||
- *Added a well formatted and more complete list of volume keys "py-kms-ClientKeys.pdf" ( replaced "client-keys.txt" )*
|
||||
|
||||
### py-kms_2016-12-30
|
||||
## py-kms_2016-12-30
|
||||
- *Updated kmsBase.py (Matches LicenseManager 4.6.0 by Hotbird64 HGM)*
|
||||
|
||||
### py-kms_2016-08-13
|
||||
## py-kms_2016-08-13
|
||||
- *Fixed major bug on Response function*
|
||||
- *Fixed Random PID Generator (thanks: mkuba50)*
|
||||
|
||||
### py-kms_2016-08-12
|
||||
## py-kms_2016-08-12
|
||||
- *Added missing UUID, credits: Hotbird64*
|
||||
- *Added Windows Server 2016 in random PID generator*
|
||||
|
||||
### py-kms_2016-08-11
|
||||
## py-kms_2016-08-11
|
||||
- *Added Windows Server 2016 UUID*
|
||||
- *Fixed GroupID and PIDRange*
|
||||
- *Added Office 2016 CountKMSID*
|
||||
|
||||
### py-kms_2015-07-29
|
||||
## py-kms_2015-07-29
|
||||
- *Added Windows 10 UUID*
|
||||
|
||||
### py-kms_2014-10-13 build 3:
|
||||
## py-kms_2014-10-13 build 3:
|
||||
- *Added Client Activation Examples: "client-activation.txt"*
|
||||
- *Added Volume Keys: "client-keys.txt"*
|
||||
|
||||
### py-kms_2014-10-13 build 2:
|
||||
## py-kms_2014-10-13 build 2:
|
||||
- *Added missing skuIds in file "kmsbase.py". Thanks (user_hidden)*
|
||||
|
||||
### py-kms_2014-10-13 build 1:
|
||||
## py-kms_2014-10-13 build 1:
|
||||
- *The server now outputs the hwid in use.*
|
||||
- *The server hwid can be random by using parameter: "-w random". Example: "python server.py -w random"*
|
||||
- *Included file "randomHWID.py" to generate random hwid on demand.*
|
||||
- *Included file "randomPID.py" to generate random epid and hwid on demand.*
|
||||
|
||||
### py-kms_2014-03-21T232943Z:
|
||||
## py-kms_2014-03-21T232943Z:
|
||||
- *The server HWID can now be specified on the command line.*
|
||||
- *The client will print the HWID when using the v6 protocol.*
|
||||
|
||||
### py-kms_2014-01-03T032458Z:
|
||||
## py-kms_2014-01-03T032458Z:
|
||||
- *Made the sqlite3 module optional.*
|
||||
- *Changed the "log" flag to an "sqlite" flag and made a real log flag in preparation for when real request logging is implemented.*
|
||||
|
||||
### py-kms_2014-01-03T025524Z:
|
||||
## py-kms_2014-01-03T025524Z:
|
||||
- *Added RPC response decoding to the KMS client emulator.*
|
||||
|
||||
### py-kms_2013-12-30T064443Z:
|
||||
## py-kms_2013-12-30T064443Z:
|
||||
- *The v4 hash now uses the proper pre-expanded key.*
|
||||
|
||||
### py-kms_2013-12-28T073506Z:
|
||||
## py-kms_2013-12-28T073506Z:
|
||||
- *Modified the v4 code to use the custom aes module in order to make it more streamlined and efficient.*
|
||||
|
||||
### py-kms_2013-12-20T051257Z:
|
||||
## py-kms_2013-12-20T051257Z:
|
||||
- *Removed the need for the pre-computed table (tablecomplex.py) for v4 CMAC calculation, cutting the zip file size in half.*
|
||||
|
||||
### py-kms_2013-12-16T214638Z:
|
||||
## py-kms_2013-12-16T214638Z:
|
||||
- *Switched to getting the to-be-logged request time from the KMS server instead of the client.*
|
||||
|
||||
### py-kms_2013-12-16T030001Z:
|
||||
## py-kms_2013-12-16T030001Z:
|
||||
- *You can now specify the CMID and the Machine Name to use with the client emulator.*
|
||||
|
||||
### py-kms_2013-12-16T021215Z:
|
||||
## py-kms_2013-12-16T021215Z:
|
||||
- *Added a request-logging feature to the server. It stores requests in an SQLite database and uses the ePIDs stored there on a per-CMID basis.*
|
||||
- *The client emulator now works for v4, v5, and v6 requests.*
|
||||
- *The client emulator now also verifies the KMS v4 responses it receives.*
|
||||
|
||||
### py-kms_2013-12-14T230215Z
|
||||
## py-kms_2013-12-14T230215Z
|
||||
- *Added a client (work in progress) that right now can only generate and send RPC bind requests.*
|
||||
- *Added a bunch of new classes to handle RPC client stuff, but I might just end up moving their functions back into the old classes at some point.*
|
||||
- *Lots of other code shuffling.*
|
||||
- *Made the verbose and debug option help easier to read.*
|
||||
- *Added some server error messages.*
|
||||
|
||||
### py-kms_2013-12-08T051332Z:
|
||||
## py-kms_2013-12-08T051332Z:
|
||||
- *Made some really huge internal changes to streamline packet parsing.*
|
||||
|
||||
### py-kms_2013-12-06T034100Z:
|
||||
## py-kms_2013-12-06T034100Z:
|
||||
- *Added tons of new SKU IDs*
|
||||
|
||||
### py-kms_2013-12-05T044849Z:
|
||||
## py-kms_2013-12-05T044849Z:
|
||||
- *Added Office SKU IDs*
|
||||
- *Small internal changes*
|
||||
|
||||
### py-kms_2013-12-04T010942Z:
|
||||
## py-kms_2013-12-04T010942Z:
|
||||
- *Made the rpcResponseArray in rpcRequest output closer to spec*
|
||||
|
||||
### py-kms_2013-12-01T063938Z:
|
||||
## py-kms_2013-12-01T063938Z:
|
||||
- *SKUID conversion: Converts the SKUID UUID into a human-readable product version for SKUIDs in its SKUID dictionary.*
|
||||
- *Fancy new timezone conversion stuff.*
|
||||
- *Enabled setting custom LCIDs.*
|
||||
- *Data parsing is now handled by structure.py.*
|
||||
- *Some other minor stuff you probably won't notice.*
|
||||
|
||||
### py-kms_2013-11-27T061658Z:
|
||||
## py-kms_2013-11-27T061658Z:
|
||||
- *Got rid of custom functions module (finally)*
|
||||
|
||||
### py-kms_2013-11-27T054744Z:
|
||||
## py-kms_2013-11-27T054744Z:
|
||||
- *Simplified custom functions module*
|
||||
- *Got rid of "v4" subfolder*
|
||||
- *Cleaned up a bunch of code*
|
||||
|
||||
### py-kms_2013-11-23T044244Z:
|
||||
## py-kms_2013-11-23T044244Z:
|
||||
- *Added timestamps to verbose output*
|
||||
- *Made the verbose output look better*
|
||||
|
||||
### py-kms_2013-11-21T014002Z:
|
||||
## py-kms_2013-11-21T014002Z:
|
||||
- *Moved some stuff into verbose output*
|
||||
- *Enabled server ePIDs of arbitrary length*
|
||||
|
||||
### py-kms_2013-11-20T180347Z:
|
||||
## py-kms_2013-11-20T180347Z:
|
||||
- *Permanently fixed machineName decoding*
|
||||
- *Adheres closer to the DCE/RPC protocol spec*
|
||||
- *Added client info to program output*
|
||||
- *Small formatting changes*
|
||||
|
||||
### py-kms_2013-11-13:
|
||||
## py-kms_2013-11-13:
|
||||
- *First working release added to the Mega folder.*
|
||||
|
|
|
@ -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.
|
35
README.md
35
README.md
|
@ -1,14 +1,15 @@
|
|||
# Readme
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
***
|
||||
|
||||
_Keep in mind that this project is not intended for production use. Feel free to use it to test your own systems or maybe even learn something from the protocol structure. :)_
|
||||
|
||||
## History
|
||||
_py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife.info/members/183074-markedsword), which is a port of either the C#, C++, or .NET implementations of KMS Emulator. The original version was written by [CODYQX4](http://forums.mydigitallife.info/members/89933-CODYQX4) and is derived from the reverse-engineered code of Microsoft's official KMS.
|
||||
This version of _py-kms_ is for itself a fork of the original implementation by [SystemRage](https://github.com/SystemRage/py-kms), which was abandoned early 2021.
|
||||
|
||||
## Features
|
||||
- Responds to `v4`, `v5`, and `v6` KMS requests.
|
||||
|
@ -18,31 +19,33 @@ _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife.
|
|||
- Windows 8
|
||||
- Windows 8.1
|
||||
- Windows 10 ( 1511 / 1607 / 1703 / 1709 / 1803 / 1809 )
|
||||
- Windows 10 ( 1903 / 1909 / 20H1 )
|
||||
- Windows 10 ( 1903 / 1909 / 20H1, 20H2, 21H1, 21H2 )
|
||||
- Windows 11 ( 21H2 )
|
||||
- Windows Server 2008
|
||||
- Windows Server 2008 R2
|
||||
- Windows Server 2012
|
||||
- Windows Server 2012 R2
|
||||
- Windows Server 2016
|
||||
- Windows Server 2019
|
||||
- Windows Server 2022
|
||||
- Windows Server 2025
|
||||
- Microsoft Office 2010 ( Volume License )
|
||||
- Microsoft Office 2013 ( Volume License )
|
||||
- Microsoft Office 2016 ( Volume License )
|
||||
- Microsoft Office 2019 ( Volume License )
|
||||
- It's written in Python (tested with Python 3.6.9).
|
||||
- Supports execution by `Docker`, `systemd`, `Upstart` and many more...
|
||||
- Includes a GUI for simple managing.
|
||||
- Uses `sqlite` for persistent data storage.
|
||||
- Microsoft Office 2021 ( Volume License )
|
||||
- Microsoft Office 2024 ( Volume License )
|
||||
- It's written in Python (tested with Python 3.10.1).
|
||||
- Supports execution by `Docker`, `systemd` and many more...
|
||||
- Uses `sqlite` for persistent data storage (with a simple web-based explorer).
|
||||
|
||||
## Documentation
|
||||
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.
|
||||
The wiki has been completly reworked and is now available on [readthedocs.io](https://py-kms.readthedocs.io/en/latest/). It should provide you all the necessary information about 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 automatically using Docker, execute `docker run -d --name py-kms --restart always -p 1688:1688 pykmsorg/py-kms`.
|
||||
- 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`.
|
||||
- For launching _py-kms_ GUI make the file `pykms_Server.py` executable with `chmod +x /path/to/folder/py-kms/pykms_Server.py`, then simply run `pykms_Server.py` by double-clicking.
|
||||
|
||||
## License
|
||||
- _py-kms_ is [](https://github.com/SystemRage/py-kms/blob/master/LICENSE)
|
||||
- _py-kms GUI_ is [](https://github.com/SystemRage/py-kms/blob/master/LICENSE.gui.md) © Matteo ℱan
|
||||
- _py-kms_ is [](./LICENSE)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
|
@ -0,0 +1,24 @@
|
|||
apiVersion: v2
|
||||
name: py-kms
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "python3"
|
|
@ -0,0 +1,60 @@
|
|||
# py-kms
|
||||
|
||||
  
|
||||
|
||||
A Helm chart for Kubernetes
|
||||
|
||||
## Deployment
|
||||
|
||||
Below is a basic overview of the steps required to deploy the Helm chart to an existing Kubernetes cluster which is accessible via Kubectl
|
||||
|
||||
### Create py-kms Namespace (recommended)
|
||||
|
||||
`kubectl create ns py-kms`
|
||||
|
||||
### Deploy chart with default values.yaml
|
||||
|
||||
`helm install -n py-kms -f myvalues.yaml charts/py-kms`
|
||||
|
||||
For more information please refer to the Helm Install command documentation located at: https://helm.sh/docs/helm/helm_install/
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| autoscaling.enabled | bool | `false` | |
|
||||
| autoscaling.maxReplicas | int | `100` | |
|
||||
| autoscaling.minReplicas | int | `1` | |
|
||||
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"ghcr.io/py-kms-organization/py-kms"` | |
|
||||
| image.tag | string | `"python3"` | |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.annotations | object | `{}` | |
|
||||
| ingress.className | string | `""` | |
|
||||
| ingress.enabled | bool | `false` | |
|
||||
| ingress.hosts[0].host | string | `"chart-example.local"` | |
|
||||
| ingress.hosts[0].paths[0].path | string | `"/"` | |
|
||||
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
|
||||
| ingress.tls | list | `[]` | |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| py-kms.environment.HWID | string | `"RANDOM"` | |
|
||||
| py-kms.environment.IP | string | `"::"` | |
|
||||
| py-kms.environment.LOGLEVEL | string | `"INFO"` | |
|
||||
| py-kms.environment.LOGSIZE | int | `2` | |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext | object | `{}` | |
|
||||
| service.httpPort | int | `80` | |
|
||||
| service.kmsPort | int | `1688` | |
|
||||
| service.type | string | `"ClusterIP"` | |
|
||||
| serviceAccount | object | `{}` | |
|
||||
| tolerations | list | `[]` | |
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.5.0](https://github.com/norwoodj/helm-docs/releases/v1.5.0)
|
|
@ -0,0 +1,22 @@
|
|||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "py-kms.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "py-kms.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "py-kms.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "py-kms.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
|
@ -0,0 +1,62 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "py-kms.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "py-kms.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "py-kms.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "py-kms.labels" -}}
|
||||
helm.sh/chart: {{ include "py-kms.chart" . }}
|
||||
{{ include "py-kms.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "py-kms.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "py-kms.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "py-kms.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "py-kms.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -0,0 +1,72 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "py-kms.fullname" . }}
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "py-kms.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "py-kms.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "py-kms.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
{{- range $key, $val := (index .Values "py-kms" "environment") }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: kms
|
||||
containerPort: 1688
|
||||
protocol: TCP
|
||||
- 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: /livez
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
|
@ -0,0 +1,28 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "py-kms.fullname" . }}
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "py-kms.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -0,0 +1,61 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "py-kms.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.httpPort -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "py-kms.fullname" . }}
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.kmsPort }}
|
||||
targetPort: 1688
|
||||
protocol: TCP
|
||||
name: kms
|
||||
- port: {{ .Values.service.httpPort }}
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "py-kms.selectorLabels" . | nindent 4 }}
|
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "py-kms.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "py-kms.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "py-kms.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "py-kms.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
|
@ -0,0 +1,91 @@
|
|||
# Default values for py-kms.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: ghcr.io/py-kms-organization/py-kms
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: python3
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
py-kms:
|
||||
environment:
|
||||
LOGLEVEL: INFO
|
||||
LOGSIZE: 2
|
||||
LOGFILE: /var/log/py-kms.log
|
||||
HWID: RANDOM
|
||||
IP: '::'
|
||||
|
||||
serviceAccount: {}
|
||||
# # Specifies whether a service account should be created
|
||||
# create: true
|
||||
# # Annotations to add to the service account
|
||||
# annotations: {}
|
||||
# # The name of the service account to use.
|
||||
# # If not set and create is true, a name is generated using the fullname template
|
||||
# name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
httpPort: 80
|
||||
kmsPort: 1688
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
|
@ -0,0 +1,2 @@
|
|||
Both docker files must access the source code of this repository. Therefore the build context must be the root of the project directory.
|
||||
Take a look into the build script for the normal py-kms version, as it demonstrates exactly that case and how to use these docker files.
|
|
@ -0,0 +1,44 @@
|
|||
# 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 ::
|
||||
ENV DUALSTACK 1
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID RANDOM
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE STDOUT
|
||||
ENV LOGSIZE ""
|
||||
ENV WEBUI 0
|
||||
|
||||
COPY docker/docker-py3-kms-minimal/requirements.txt /home/py-kms/requirements.txt
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
python3 \
|
||||
py3-pip \
|
||||
ca-certificates \
|
||||
shadow \
|
||||
tzdata \
|
||||
&& pip3 install --no-cache-dir -r /home/py-kms/requirements.txt \
|
||||
&& adduser -S py-kms -G users -s /bin/bash \
|
||||
&& chown py-kms:users /home/py-kms \
|
||||
# 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/healthcheck.py /usr/bin/healthcheck.py
|
||||
COPY docker/start.py /usr/bin/start.py
|
||||
RUN chmod 555 /usr/bin/entrypoint.py /usr/bin/healthcheck.py /usr/bin/start.py
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=10s --retries=3 CMD /usr/bin/python3 /usr/bin/healthcheck.py
|
||||
|
||||
ENTRYPOINT ["/usr/bin/python3", "-u", "/usr/bin/entrypoint.py"]
|
|
@ -1,36 +0,0 @@
|
|||
# This is a minimized version from docker/docker-py3-kms/Dockerfile without SQLite support to further reduce image size
|
||||
|
||||
FROM alpine:3.12
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID "RANDOM"
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
pip3 install peewee tzlocal && \
|
||||
git clone https://github.com/SystemRage/py-kms/ /tmp/py-kms && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT /usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
|
@ -1,44 +0,0 @@
|
|||
# This is a minimized version from docker/docker-py3-kms/Dockerfile without SQLite support to further reduce image size
|
||||
|
||||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-arm.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm32v6/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-arm-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID "RANDOM"
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
pip3 install peewee tzlocal && \
|
||||
git clone https://github.com/SystemRage/py-kms/ /tmp/py-kms && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT /usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
|
@ -1,44 +0,0 @@
|
|||
# This is a minimized version from docker/docker-py3-kms/Dockerfile without SQLite support to further reduce image size
|
||||
|
||||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-arm.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm32v7/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-arm-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID "RANDOM"
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
pip3 install peewee tzlocal && \
|
||||
git clone https://github.com/SystemRage/py-kms/ /tmp/py-kms && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT /usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
|
@ -1,44 +0,0 @@
|
|||
# This is a minimized version from docker/docker-py3-kms/Dockerfile without SQLite support to further reduce image size
|
||||
|
||||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-aarch64.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm64v8/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-aarch64-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID "RANDOM"
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
pip3 install peewee tzlocal && \
|
||||
git clone https://github.com/SystemRage/py-kms/ /tmp/py-kms && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT /usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
|
@ -1,10 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Use manifest-tool to create the manifest, given the experimental
|
||||
# "docker manifest" command isn't available yet on Docker Hub.
|
||||
|
||||
curl -Lo manifest-tool "https://github.com/estesp/manifest-tool/releases/download/v1.0.2/manifest-tool-linux-amd64"
|
||||
chmod +x manifest-tool
|
||||
|
||||
./manifest-tool push from-spec multi-arch-manifest-latest.yaml
|
||||
./manifest-tool push from-spec multi-arch-manifest-minimal.yaml
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Register qemu-*-static for all supported processors except the
|
||||
# current one, but also remove all registered binfmt_misc before
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
|
@ -1,21 +0,0 @@
|
|||
image: pykmsorg/py-kms:latest
|
||||
manifests:
|
||||
- image: pykmsorg/py-kms:minimal-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: pykmsorg/py-kms:minimal-arm32v6
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v6
|
||||
- image: pykmsorg/py-kms:minimal-arm32v7
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
- image: pykmsorg/py-kms:minimal-arm64v8
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
|
@ -1,21 +0,0 @@
|
|||
image: pykmsorg/py-kms:minimal
|
||||
manifests:
|
||||
- image: pykmsorg/py-kms:minimal-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: pykmsorg/py-kms:minimal-arm32v6
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v6
|
||||
- image: pykmsorg/py-kms:minimal-arm32v7
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
- image: pykmsorg/py-kms:minimal-arm64v8
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
|
@ -0,0 +1,2 @@
|
|||
dnspython==2.6.1
|
||||
tzlocal==4.2
|
|
@ -0,0 +1,55 @@
|
|||
# Switch to the target image
|
||||
FROM alpine:3.15
|
||||
|
||||
ARG BUILD_COMMIT=unknown
|
||||
ARG BUILD_BRANCH=unknown
|
||||
|
||||
ENV IP ::
|
||||
ENV DUALSTACK 1
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV HWID RANDOM
|
||||
ENV LOGLEVEL INFO
|
||||
ENV LOGFILE STDOUT
|
||||
ENV LOGSIZE ""
|
||||
ENV TZ America/Chicago
|
||||
ENV WEBUI 1
|
||||
|
||||
COPY docker/docker-py3-kms/requirements.txt /home/py-kms/
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
python3 \
|
||||
py3-pip \
|
||||
sqlite-libs \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
shadow \
|
||||
&& pip3 install --no-cache-dir -r /home/py-kms/requirements.txt \
|
||||
&& mkdir /db/ \
|
||||
&& adduser -S py-kms -G users -s /bin/bash \
|
||||
&& chown py-kms:users /home/py-kms \
|
||||
# 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/healthcheck.py /usr/bin/healthcheck.py
|
||||
COPY docker/start.py /usr/bin/start.py
|
||||
RUN chmod 555 /usr/bin/entrypoint.py /usr/bin/healthcheck.py /usr/bin/start.py
|
||||
|
||||
# Web-interface specifics
|
||||
COPY LICENSE /LICENSE
|
||||
RUN echo "$BUILD_COMMIT" > /VERSION && echo "$BUILD_BRANCH" >> /VERSION
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=10s --retries=3 CMD /usr/bin/python3 /usr/bin/healthcheck.py
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/python3", "-u", "/usr/bin/entrypoint.py" ]
|
|
@ -1,41 +0,0 @@
|
|||
FROM alpine:3.12
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV SQLITE false
|
||||
ENV HWID "364F463A8863D35F"
|
||||
ENV LOGLEVEL ERROR
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
COPY start.sh /usr/bin/start.sh
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
git clone https://github.com/SystemRage/py-kms.git /tmp/py-kms && \
|
||||
git clone https://github.com/coleifer/sqlite-web.git /tmp/sqlite_web && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
mv /tmp/sqlite_web/sqlite_web /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
rm -rf /tmp/sqlite_web && \
|
||||
pip3 install peewee tzlocal pysqlite3 && \
|
||||
chmod a+x /usr/bin/start.sh && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/start.sh"]
|
|
@ -1,49 +0,0 @@
|
|||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-arm.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm32v6/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-arm-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV SQLITE false
|
||||
ENV HWID "364F463A8863D35F"
|
||||
ENV LOGLEVEL ERROR
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
COPY start.sh /usr/bin/start.sh
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
git clone https://github.com/SystemRage/py-kms.git /tmp/py-kms && \
|
||||
git clone https://github.com/coleifer/sqlite-web.git /tmp/sqlite_web && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
mv /tmp/sqlite_web/sqlite_web /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
rm -rf /tmp/sqlite_web && \
|
||||
pip3 install peewee tzlocal pysqlite3 && \
|
||||
chmod a+x /usr/bin/start.sh && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/start.sh"]
|
|
@ -1,49 +0,0 @@
|
|||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-arm.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm32v7/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-arm-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV SQLITE false
|
||||
ENV HWID "364F463A8863D35F"
|
||||
ENV LOGLEVEL ERROR
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
COPY start.sh /usr/bin/start.sh
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
git clone https://github.com/SystemRage/py-kms.git /tmp/py-kms && \
|
||||
git clone https://github.com/coleifer/sqlite-web.git /tmp/sqlite_web && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
mv /tmp/sqlite_web/sqlite_web /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
rm -rf /tmp/sqlite_web && \
|
||||
pip3 install peewee tzlocal pysqlite3 && \
|
||||
chmod a+x /usr/bin/start.sh && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/start.sh"]
|
|
@ -1,49 +0,0 @@
|
|||
# Prepare the multiarch env
|
||||
FROM alpine AS builder
|
||||
RUN apk add curl && curl -L "https://github.com/balena-io/qemu/releases/download/v4.0.0%2Bbalena2/qemu-4.0.0.balena2-aarch64.tar.gz" | tar zxvf - -C . --strip-components 1
|
||||
|
||||
# Switch to the target image
|
||||
FROM arm64v8/alpine:3.12
|
||||
|
||||
# Import qemu from the preparation
|
||||
COPY --from=builder qemu-aarch64-static /usr/bin
|
||||
|
||||
ENV IP 0.0.0.0
|
||||
ENV PORT 1688
|
||||
ENV EPID ""
|
||||
ENV LCID 1033
|
||||
ENV CLIENT_COUNT 26
|
||||
ENV ACTIVATION_INTERVAL 120
|
||||
ENV RENEWAL_INTERVAL 10080
|
||||
ENV SQLITE false
|
||||
ENV HWID "364F463A8863D35F"
|
||||
ENV LOGLEVEL ERROR
|
||||
ENV LOGFILE /var/log/pykms_logserver.log
|
||||
ENV LOGSIZE ""
|
||||
|
||||
COPY start.sh /usr/bin/start.sh
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
bash \
|
||||
git \
|
||||
py3-argparse \
|
||||
py3-flask \
|
||||
py3-pygments \
|
||||
python3-tkinter \
|
||||
sqlite-libs \
|
||||
py3-pip && \
|
||||
git clone https://github.com/SystemRage/py-kms.git /tmp/py-kms && \
|
||||
git clone https://github.com/coleifer/sqlite-web.git /tmp/sqlite_web && \
|
||||
mv /tmp/py-kms/py-kms /home/ && \
|
||||
mv /tmp/sqlite_web/sqlite_web /home/ && \
|
||||
rm -rf /tmp/py-kms && \
|
||||
rm -rf /tmp/sqlite_web && \
|
||||
pip3 install peewee tzlocal pysqlite3 && \
|
||||
chmod a+x /usr/bin/start.sh && \
|
||||
apk del git
|
||||
|
||||
WORKDIR /home/py-kms
|
||||
|
||||
EXPOSE ${PORT}/tcp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/start.sh"]
|
|
@ -1 +0,0 @@
|
|||
docker build -t pykms/pykms:py3-kms . --file Dockerfile.amd64
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Use manifest-tool to create the manifest, given the experimental
|
||||
# "docker manifest" command isn't available yet on Docker Hub.
|
||||
|
||||
curl -Lo manifest-tool "https://github.com/estesp/manifest-tool/releases/download/v1.0.2/manifest-tool-linux-amd64"
|
||||
chmod +x manifest-tool
|
||||
|
||||
./manifest-tool push from-spec multi-arch-manifest-python3.yaml
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Register qemu-*-static for all supported processors except the
|
||||
# current one, but also remove all registered binfmt_misc before
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
|
@ -1,21 +0,0 @@
|
|||
image: pykmsorg/py-kms:python3
|
||||
manifests:
|
||||
- image: pykmsorg/py-kms:python3-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: pykmsorg/py-kms:python3-arm32v6
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v6
|
||||
- image: pykmsorg/py-kms:python3-arm32v7
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
- image: pykmsorg/py-kms:python3-arm64v8
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
|
@ -0,0 +1,5 @@
|
|||
dnspython==2.6.1
|
||||
tzlocal==4.2
|
||||
|
||||
Flask==2.3.2
|
||||
gunicorn==22.0.0
|
|
@ -1,16 +0,0 @@
|
|||
docker stop py3-kms
|
||||
docker rm py3-kms
|
||||
docker run -d --name py3-kms \
|
||||
-t \
|
||||
-p 8080:8080 \
|
||||
-p 1688:1688 \
|
||||
-e IP=0.0.0.0 \
|
||||
-e PORT=1688 \
|
||||
-e SQLITE=true \
|
||||
-e HWID=RANDOM \
|
||||
-e LOGLEVEL=INFO \
|
||||
-e LOGFILE=/var/log/pykms_logserver.log \
|
||||
-e LOGSIZE=2 \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-v /var/log:/var/log:rw \
|
||||
--restart unless-stopped pykmsorg/py-kms:python3
|
|
@ -1,50 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd /home/py-kms
|
||||
if [ "$SQLITE" == false ];
|
||||
then
|
||||
if [ "$EPID" == "" ];
|
||||
then
|
||||
if [ "$LOGSIZE" == "" ];
|
||||
then
|
||||
/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
||||
else
|
||||
/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE}
|
||||
fi
|
||||
else
|
||||
if [ "$LOGSIZE" == "" ];
|
||||
then
|
||||
/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE}
|
||||
else
|
||||
/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE}
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ "$EPID" == "" ];
|
||||
then
|
||||
if [ "$LOGSIZE" == "" ];
|
||||
then
|
||||
/bin/bash -c "/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s ${PWD}/pykms_database.db -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} &"
|
||||
sleep 5
|
||||
/usr/bin/python3 pykms_Client.py ${IP} ${PORT} -m Windows10 &
|
||||
/usr/bin/python3 /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/pykms_database.db --read-only
|
||||
else
|
||||
/bin/bash -c "/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s ${PWD}/pykms_database.db -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE} &"
|
||||
sleep 5
|
||||
/usr/bin/python3 pykms_Client.py ${IP} ${PORT} -m Windows10 &
|
||||
/usr/bin/python3 /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/pykms_database.db --read-only
|
||||
fi
|
||||
else
|
||||
if [ "$LOGSIZE" == "" ];
|
||||
then
|
||||
/bin/bash -c "/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s ${PWD}/pykms_database.db -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} &"
|
||||
sleep 5
|
||||
/usr/bin/python3 pykms_Client.py ${IP} ${PORT} -m Windows10 &
|
||||
/usr/bin/python3 /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/pykms_database.db --read-only
|
||||
else
|
||||
/bin/sh -c "/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s ${PWD}/pykms_database.db -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE} &"
|
||||
sleep 5
|
||||
/usr/bin/python3 pykms_Client.py ${IP} ${PORT} -m Windows10 &
|
||||
/usr/bin/python3 /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/pykms_database.db --read-only
|
||||
fi
|
||||
fi
|
||||
fi
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/python3 -u
|
||||
|
||||
# Need root privileges to change timezone, and user uid/gid, file/folder ownernship
|
||||
|
||||
import grp
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import time
|
||||
|
||||
PYTHON3 = '/usr/bin/python3'
|
||||
dbPath = os.path.join(os.sep, 'home', 'py-kms', 'db') # Do not include the database file name, as we must correct the folder permissions (the db file is recursively reachable)
|
||||
|
||||
def change_uid_grp(logger):
|
||||
if os.geteuid() != 0:
|
||||
logger.info(f'not root user, cannot change uid/gid.')
|
||||
return None
|
||||
user_db_entries = pwd.getpwnam("py-kms")
|
||||
user_grp_db_entries = grp.getgrnam("users")
|
||||
uid = int(user_db_entries.pw_uid)
|
||||
gid = int(user_grp_db_entries.gr_gid)
|
||||
new_gid = int(os.getenv('GID', str(gid)))
|
||||
new_uid = int(os.getenv('UID', str(uid)))
|
||||
os.chown("/home/py-kms", new_uid, new_gid)
|
||||
os.chown("/usr/bin/start.py", new_uid, new_gid)
|
||||
if os.path.isdir(dbPath):
|
||||
# Corret permissions recursively, as to access the database file, also its parent folder must be accessible
|
||||
logger.debug(f'Correcting owner permissions on {dbPath}.')
|
||||
os.chown(dbPath, new_uid, new_gid)
|
||||
for root, dirs, files in os.walk(dbPath):
|
||||
for dName in dirs:
|
||||
dPath = os.path.join(root, dName)
|
||||
logger.debug(f'Correcting owner permissions on {dPath}.')
|
||||
os.chown(dPath, new_uid, new_gid)
|
||||
for fName in files:
|
||||
fPath = os.path.join(root, fName)
|
||||
logger.debug(f'Correcting owner permissions on {fPath}.')
|
||||
os.chown(fPath, new_uid, new_gid)
|
||||
logger.debug(subprocess.check_output(['ls', '-la', dbPath]).decode())
|
||||
if 'LOGFILE' in os.environ and os.path.exists(os.environ['LOGFILE']):
|
||||
# Oh, the user also wants a custom log file -> make sure start.py can access it by setting the correct permissions (777)
|
||||
os.chmod(os.environ['LOGFILE'], 0o777)
|
||||
logger.error(str(subprocess.check_output(['ls', '-la', os.environ['LOGFILE']])))
|
||||
logger.info("Setting gid to '%s'." % str(new_gid))
|
||||
os.setgid(new_gid)
|
||||
|
||||
logger.info("Setting uid to '%s'." % str(new_uid))
|
||||
os.setuid(new_uid)
|
||||
|
||||
def change_tz(logger):
|
||||
tz = os.getenv('TZ', 'etc/UTC')
|
||||
# TZ is not symlinked and defined TZ exists
|
||||
if tz not in os.readlink('/etc/localtime') and os.path.isfile('/usr/share/zoneinfo/' + tz) and hasattr(time, 'tzset'):
|
||||
logger.info("Setting timzeone to %s" % tz )
|
||||
# time.tzet() should be called on Unix, but doesn't exist on Windows.
|
||||
time.tzset()
|
||||
|
||||
if __name__ == "__main__":
|
||||
log_level_bootstrap = log_level = os.getenv('LOGLEVEL', 'INFO')
|
||||
if log_level_bootstrap == "MININFO":
|
||||
log_level_bootstrap = "INFO"
|
||||
loggersrv = logging.getLogger('entrypoint.py')
|
||||
loggersrv.setLevel(log_level_bootstrap)
|
||||
streamhandler = logging.StreamHandler(sys.stdout)
|
||||
streamhandler.setLevel(log_level_bootstrap)
|
||||
formatter = logging.Formatter(fmt = '\x1b[94m%(asctime)s %(levelname)-8s %(message)s', datefmt = '%a, %d %b %Y %H:%M:%S',)
|
||||
streamhandler.setFormatter(formatter)
|
||||
loggersrv.addHandler(streamhandler)
|
||||
loggersrv.info("Log level: %s" % log_level)
|
||||
loggersrv.debug("user id: %s" % os.getuid())
|
||||
|
||||
change_tz(loggersrv)
|
||||
childProcess = subprocess.Popen(PYTHON3 + " -u /usr/bin/start.py", preexec_fn=change_uid_grp(loggersrv), shell=True)
|
||||
def shutdown(signum, frame):
|
||||
loggersrv.info("Received signal %s, shutting down..." % signum)
|
||||
childProcess.terminate() # This will also cause communicate() from below to continue
|
||||
signal.signal(signal.SIGTERM, shutdown) # This signal will be sent by Docker to request shutdown
|
||||
childProcess.communicate()
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/python3 -u
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
def do_check(logger):
|
||||
import socket
|
||||
listen_ip = os.environ.get('IP', '::').split()
|
||||
listen_ip.insert(0, '127.0.0.1') # always try to connect to localhost first
|
||||
listen_port = os.environ.get('PORT', '1688')
|
||||
for ip in listen_ip:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET6 if ':' in ip else socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(1) # 1 second timeout
|
||||
address = ip if ':' in ip else (ip, int(listen_port))
|
||||
logger.debug(f"Trying to connect to {address}...")
|
||||
s.connect(address)
|
||||
s.close()
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False # no connection could be established
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log_level_bootstrap = log_level = os.getenv('LOGLEVEL', 'INFO')
|
||||
if log_level_bootstrap == "MININFO":
|
||||
log_level_bootstrap = "INFO"
|
||||
loggersrv = logging.getLogger('healthcheck.py')
|
||||
loggersrv.setLevel(log_level_bootstrap)
|
||||
streamhandler = logging.StreamHandler(sys.stdout)
|
||||
streamhandler.setLevel(log_level_bootstrap)
|
||||
formatter = logging.Formatter(fmt='\x1b[94m%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S')
|
||||
streamhandler.setFormatter(formatter)
|
||||
loggersrv.addHandler(streamhandler)
|
||||
|
||||
sys.exit(0 if do_check(loggersrv) else 1)
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/python3 -u
|
||||
|
||||
# This replaces the old start.sh and ensures all arguments are bound correctly from the environment variables...
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
PYTHON3 = '/usr/bin/python3'
|
||||
argumentVariableMapping = {
|
||||
'-l': 'LCID',
|
||||
'-c': 'CLIENT_COUNT',
|
||||
'-a': 'ACTIVATION_INTERVAL',
|
||||
'-r': 'RENEWAL_INTERVAL',
|
||||
'-w': 'HWID',
|
||||
'-V': 'LOGLEVEL',
|
||||
'-F': 'LOGFILE',
|
||||
'-S': 'LOGSIZE',
|
||||
'-e': 'EPID'
|
||||
}
|
||||
|
||||
db_path = os.path.join(os.sep, 'home', 'py-kms', 'db', 'pykms_database.db')
|
||||
log_file = os.environ.get('LOGFILE', 'STDOUT')
|
||||
listen_ip = os.environ.get('IP', '::').split()
|
||||
listen_port = os.environ.get('PORT', '1688')
|
||||
want_webui = os.environ.get('WEBUI', '0') == '1' # if the variable is not provided, we assume the user does not want the webui
|
||||
|
||||
def start_kms(logger):
|
||||
# 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():
|
||||
if env in os.environ and os.environ.get(env) != '':
|
||||
command.append(arg)
|
||||
command.append(os.environ.get(env))
|
||||
if want_webui: # add this command directly before the "connect" subparser - otherwise you'll get silent crashes!
|
||||
command.append('-s')
|
||||
command.append(db_path)
|
||||
if len(listen_ip) > 1:
|
||||
command.append("connect")
|
||||
for i in range(1, len(listen_ip)):
|
||||
command.append("-n")
|
||||
command.append(listen_ip[i] + "," + listen_port)
|
||||
if dual := os.environ.get('DUALSTACK'):
|
||||
command.append("-d")
|
||||
command.append(dual)
|
||||
|
||||
logger.debug("server_cmd: %s" % (" ".join(str(x) for x in command).strip()))
|
||||
pykms_process = subprocess.Popen(command)
|
||||
pykms_webui_process = None
|
||||
|
||||
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:
|
||||
logger.error("Failed to start webui (ignoring and continuing anyways): %s" % e)
|
||||
|
||||
try:
|
||||
pykms_process.wait()
|
||||
except Exception:
|
||||
# In case of any error - just shut down
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
if pykms_webui_process:
|
||||
pykms_webui_process.terminate()
|
||||
pykms_process.terminate()
|
||||
|
||||
|
||||
# Main
|
||||
if __name__ == "__main__":
|
||||
log_level_bootstrap = log_level = os.environ.get('LOGLEVEL', 'INFO')
|
||||
if log_level_bootstrap == "MININFO":
|
||||
log_level_bootstrap = "INFO"
|
||||
loggersrv = logging.getLogger('start.py')
|
||||
loggersrv.setLevel(log_level_bootstrap)
|
||||
streamhandler = logging.StreamHandler(sys.stdout)
|
||||
streamhandler.setLevel(log_level_bootstrap)
|
||||
formatter = logging.Formatter(fmt='\x1b[94m%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S')
|
||||
streamhandler.setFormatter(formatter)
|
||||
loggersrv.addHandler(streamhandler)
|
||||
loggersrv.debug("user id: %s" % os.getuid())
|
||||
|
||||
start_kms(loggersrv)
|
|
@ -2,29 +2,26 @@
|
|||
What follows are some guides how to start the `pykms_Server.py` script, which provides the emulated server.
|
||||
|
||||
## Running as a service
|
||||
***
|
||||
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
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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 pykmsorg/py-kms
|
||||
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`). 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*):
|
||||
|
||||

|
||||
|
||||
#### Architectures
|
||||
There are currently the following architectures available (if you need an other, feel free to open an issue):
|
||||
|
@ -37,45 +34,36 @@ _Please note that any architecture other than the classic `amd64` is slightly bi
|
|||
|
||||
#### Docker Compose
|
||||
You can use `docker-compose` instead of building and running the Dockerfile, so you do not need to respecify your settings again and again. The following Docker Compose file will deploy the `latest` image with the log into your local directory.
|
||||
Make sure to take a look into the `entrypoint.py` script to see all supported variable mappings!
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
kms:
|
||||
image: pykmsorg/py-kms:latest
|
||||
image: ghcr.io/py-kms-organization/py-kms:python3
|
||||
ports:
|
||||
- 1688:1688
|
||||
- 1688:1688 # kms
|
||||
- 8080:8080 # web-interface
|
||||
environment:
|
||||
- IP=0.0.0.0
|
||||
- SQLITE=true
|
||||
- HWID=RANDOM
|
||||
- LOGLEVEL=INFO
|
||||
- LOGSIZE=2
|
||||
- LOGFILE=/var/log/pykms_logserver.log
|
||||
IP: "::"
|
||||
HWID: RANDOM
|
||||
LOGLEVEL: INFO
|
||||
restart: always
|
||||
volumes:
|
||||
- ./db:/home/py-kms/db
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./:/var/log:rw
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
Below is a fully expanded run command, detailing all the different supported environment variables to set. For further reference see the [start parameters](Usage.html#docker-environment) for the docker environment.
|
||||
Below is a little bit more extended run command, detailing all the different supported environment variables to set. For further reference see the [start parameters](#docker-environment) for the docker environment.
|
||||
```bash
|
||||
docker run -it -d --name py3-kms \
|
||||
-p 8080:8080 \
|
||||
-p 1688:1688 \
|
||||
-e IP=0.0.0.0 \
|
||||
-e PORT=1688 \
|
||||
-e SQLITE=true \
|
||||
-e HWID=RANDOM \
|
||||
-e LOGLEVEL=INFO \
|
||||
-e LOGSIZE=2 \
|
||||
-e LOGFILE=/var/log/pykms_logserver.log \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-v /var/log:/var/log:rw \
|
||||
--restart unless-stopped pykmsorg/py-kms:[TAG]
|
||||
--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:
|
||||
|
@ -91,7 +79,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
|
||||
|
@ -100,10 +88,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
|
||||
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:
|
||||
```
|
||||
|
@ -114,7 +98,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
|
||||
|
@ -134,7 +118,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)
|
||||
|
@ -171,17 +155,15 @@ They might be useful to you:
|
|||
- [FreeBSD](https://github.com/SystemRage/py-kms/issues/89)
|
||||
|
||||
## Manual Execution
|
||||
***
|
||||
|
||||
### Dependencies
|
||||
- Python 3.x.
|
||||
- Tkinter module (for the GUI).
|
||||
- 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`
|
||||
- `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`).
|
||||
|
@ -211,6 +193,9 @@ user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py 192.168.1.102 1688
|
|||
To stop `pykms_Server.py`, in the same bash window where code running, simply press `CTRL+C`.
|
||||
Alternatively, in a new bash window, use `kill <pid>` command (you can type `ps aux` first and have the process <pid>) or `killall <name_of_server>`.
|
||||
|
||||
### Web-Interface
|
||||
As you may have noticed, the Docker container contains a web-interface, replacing the old GUI. If you want to launch it manually, checkout this [issue discussion](https://github.com/Py-KMS-Organization/py-kms/issues/100#issuecomment-1710827824) to learn more.
|
||||
|
||||
### Quick Guide
|
||||
The following are just some brief notes about parameters handling. For a more detailed description see [here](Usage.md).
|
||||
|
||||
|
|
106
docs/Keys.md
106
docs/Keys.md
|
@ -3,12 +3,19 @@ These are keys, which can be used to activate a product with _py-kms_ (note this
|
|||
sometimes even reject it by itself (often due too many uses - in that case try to use an other one).
|
||||
|
||||
## Windows
|
||||
***
|
||||
|
||||
### Windows Server 2022
|
||||
|
||||
| Product | GVLK |
|
||||
| -------------------------------------------- | ------------------------------- |
|
||||
| Windows Server 2022 Datacenter | `WX4NM-KYWYW-QJJR4-XV3QB-6VM33` |
|
||||
| Windows Server 2022 Standard | `VDYBN-27WPP-V4HQT-9VMD4-VMK7H` |
|
||||
| Windows Server 2022 Datacenter Azure Edition | `NTBV8-9K7Q8-V27C6-M2BTV-KHMXV` |
|
||||
|
||||
### Windows Server 2019
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ----------------------------------------------------------- | ------------------------------- |
|
||||
| Windows Server 2019 Datacenter | `WMDGN-G9PQG-XVVXX-R3X43-63DFG` |
|
||||
| Windows Server 2019 Standard | `N69G4-B89J2-4G8F4-WWYCC-J464C` |
|
||||
| Windows Server 2019 Essentials | `WVDHN-86M7X-466P6-VHXV7-YY726` |
|
||||
|
@ -20,7 +27,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Server 2016
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Windows Server 2016 Standard Semi-Annual Channel (v.1803) | `PTXN8-JFHJM-4WC78-MPCBR-9W4KR` |
|
||||
| Windows Server 2016 Datacenter Semi-Annual Channel (v.1803) | `2HXDN-KRXHB-GPYC7-YCKFJ-7FVDG` |
|
||||
| Windows Server 2016 Datacenter Semi-Annual Channel (v.1709) | `6Y6KB-N82V8-D8CQV-23MJW-BWTG6` |
|
||||
|
@ -32,42 +39,42 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
| Windows Server 2016 Cloud Storage | `QN4C6-GBJD2-FB422-GHWJK-GJG2R` |
|
||||
| Windows Server 2016 Azure Core | `VP34G-4NPPG-79JTQ-864T4-R3MQX`<br>`WNCYY-GFBH2-M4WTT-XQ2FP-PG2K9` |
|
||||
|
||||
### Windows 10
|
||||
### Windows 10 & Windows 11
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| Windows 10 Professional Workstation | `NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J` |
|
||||
| Windows 10 Professional Workstation N | `9FNHH-K3HBT-3W4TD-6383H-6XYWF` |
|
||||
| Windows 10 Enterprise G | `YYVX9-NTFWV-6MDM3-9PT4T-4M68B` |
|
||||
| Windows 10 Enterprise G N | `44RPN-FTY23-9VTTB-MP9BX-T84FV` |
|
||||
| Windows 10 Enterprise LTSC 2019 | `M7XTQ-FN8P6-TTKYV-9D4CC-J462D` |
|
||||
| Windows 10 Enterprise LTSC 2019 N | `92NFX-8DJQP-P6BBQ-THF9C-7CG2H` |
|
||||
| Windows 10 Remote Server | `7NBT4-WGBQX-MP4H7-QXFF8-YP3KX` |
|
||||
| Windows 10 Enterprise for Remote Sessions | `CPWHC-NT2C7-VYW78-DHDB2-PG3GK` |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Windows 10/11 Professional Workstation | `NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J` |
|
||||
| Windows 10/11 Professional Workstation N | `9FNHH-K3HBT-3W4TD-6383H-6XYWF` |
|
||||
| Windows 10/11 Enterprise G | `YYVX9-NTFWV-6MDM3-9PT4T-4M68B` |
|
||||
| Windows 10/11 Enterprise G N | `44RPN-FTY23-9VTTB-MP9BX-T84FV` |
|
||||
| Windows 10 Enterprise LTSC 2019/2021 | `M7XTQ-FN8P6-TTKYV-9D4CC-J462D` |
|
||||
| Windows 10 Enterprise LTSC 2019/2021 N | `92NFX-8DJQP-P6BBQ-THF9C-7CG2H` |
|
||||
| Windows 10/11 Remote Server | `7NBT4-WGBQX-MP4H7-QXFF8-YP3KX` |
|
||||
| Windows 10 Enterprise for Remote Sessions <br> Windows 10 Enterprise for virtual desktops <br> Windows 11 Enterprise multi-session | `CPWHC-NT2C7-VYW78-DHDB2-PG3GK` |
|
||||
| Windows 10 S (Lean) | `NBTWJ-3DR69-3C4V8-C26MC-GQ9M6` |
|
||||
| Windows 10 Professional | `W269N-WFGWX-YVC9B-4J6C9-T83GX` |
|
||||
| Windows 10 Professional N | `MH37W-N47XK-V7XM9-C7227-GCQG9`<br>`HMNWJ-V69R6-B2CDC-8P7VT-2373K` |
|
||||
| Windows 10 Professional Education | `6TP4R-GNPTD-KYYHQ-7B7DP-J447Y` |
|
||||
| Windows 10 Professional Education N | `YVWGF-BXNMC-HTQYQ-CPQ99-66QFC` |
|
||||
| Windows 10 Education | `NW6C2-QMPVW-D7KKK-3GKT6-VCFB2`<br>`F48BJ-8NX82-MRVY9-PF8BW-HMHY2` |
|
||||
| Windows 10 Education N | `2WH4N-8QGBV-H22JP-CT43Q-MDWWJ`<br>`PPWGW-8NW9C-J77Q9-8WHB9-QV64W` |
|
||||
| Windows 10 Enterprise | `NPPR9-FWDCX-D2C8J-H872K-2YT43`<br>`96YNV-9X4RP-2YYKB-RMQH4-6Q72D`<br>`TN6CM-KCVXP-VVP8X-YVCF7-R9BDH`<br>`3PMKQ-YNVGT-HFJGG-2F4FQ-9D6T7` |
|
||||
| Windows 10 Enterprise N | `DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4`<br>`WGGHN-J84D6-QYCPR-T7PJ7-X766F` |
|
||||
| Windows 10 Enterprise S | `H76BG-QBNM7-73XY9-V6W2T-684BJ` |
|
||||
| Windows 10 Enterprise S N | `X4R4B-NV6WD-PKTVK-F98BH-4C2J8` |
|
||||
| Windows 10/11 Professional | `W269N-WFGWX-YVC9B-4J6C9-T83GX` |
|
||||
| Windows 10/11 Professional N | `MH37W-N47XK-V7XM9-C7227-GCQG9`<br>`HMNWJ-V69R6-B2CDC-8P7VT-2373K` |
|
||||
| Windows 10/11 Professional Education | `6TP4R-GNPTD-KYYHQ-7B7DP-J447Y` |
|
||||
| Windows 10/11 Professional Education N | `YVWGF-BXNMC-HTQYQ-CPQ99-66QFC` |
|
||||
| Windows 10/11 Education | `NW6C2-QMPVW-D7KKK-3GKT6-VCFB2`<br>`F48BJ-8NX82-MRVY9-PF8BW-HMHY2` |
|
||||
| Windows 10/11 Education N | `2WH4N-8QGBV-H22JP-CT43Q-MDWWJ`<br>`PPWGW-8NW9C-J77Q9-8WHB9-QV64W` |
|
||||
| Windows 10/11 Enterprise | `NPPR9-FWDCX-D2C8J-H872K-2YT43`<br>`96YNV-9X4RP-2YYKB-RMQH4-6Q72D`<br>`TN6CM-KCVXP-VVP8X-YVCF7-R9BDH`<br>`3PMKQ-YNVGT-HFJGG-2F4FQ-9D6T7` |
|
||||
| Windows 10/11 Enterprise N | `DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4`<br>`WGGHN-J84D6-QYCPR-T7PJ7-X766F` |
|
||||
| Windows 10/11 Enterprise S | `H76BG-QBNM7-73XY9-V6W2T-684BJ` |
|
||||
| Windows 10/11 Enterprise S N | `X4R4B-NV6WD-PKTVK-F98BH-4C2J8` |
|
||||
| Windows 10 Enterprise 2015 LTSB | `WNMTR-4C88C-JK8YV-HQ7T2-76DF9` |
|
||||
| Windows 10 Enterprise 2015 LTSB N | `2F77B-TNFGY-69QQF-B8YKP-D69TJ`<br>`RW7WN-FMT44-KRGBK-G44WK-QV7YK` |
|
||||
| Windows 10 Enterprise 2016 LTSB | `DCPHK-NFMTC-H88MJ-PFHPY-QJ4BJ` |
|
||||
| Windows 10 Enterprise 2016 LTSB N | `QFFDN-GRT3P-VKWWX-X7T3R-8B639` |
|
||||
| Windows 10 Home<br>Windows 10 Core | `TX9XD-98N7V-6WMQ6-BX7FG-H8Q99`<br>`33QT6-RCNYF-DXB4F-DGP7B-7MHX9` |
|
||||
| Windows 10 Home N<br>Windows 10 Core N | `3KHY7-WNT83-DGQKR-F7HPR-844BM`<br>`CP4KF-NG6TC-9K6QF-P6GTT-H8RBM` |
|
||||
| Windows 10 Home Single Language<br>Windows 10 Core Single Language | `7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH`<br>`9HGRW-NH2CQ-XQHJD-YCRWB-6VJV7`<br>`4NX46-6DHCG-MR3PH-9FMCX-3RQ3G` |
|
||||
| Windows 10 Home Country Specific<br>Windows 10 Core Country Specific | `PVMJN-6DFY6-9CCP6-7BKTT-D3WVR`<br>`JN9HR-MH7K4-DBPDD-TFTXF-Q9MMF` |
|
||||
| Windows 10/11 Home<br>Windows 10/11 Core | `TX9XD-98N7V-6WMQ6-BX7FG-H8Q99`<br>`33QT6-RCNYF-DXB4F-DGP7B-7MHX9` |
|
||||
| Windows 10/11 Home N<br>Windows 10/11 Core N | `3KHY7-WNT83-DGQKR-F7HPR-844BM`<br>`CP4KF-NG6TC-9K6QF-P6GTT-H8RBM` |
|
||||
| Windows 10/11 Home Single Language<br>Windows 10 Core Single Language | `7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH`<br>`9HGRW-NH2CQ-XQHJD-YCRWB-6VJV7`<br>`4NX46-6DHCG-MR3PH-9FMCX-3RQ3G` |
|
||||
| Windows 10/11 Home Country Specific<br>Windows 10 Core Country Specific | `PVMJN-6DFY6-9CCP6-7BKTT-D3WVR`<br>`JN9HR-MH7K4-DBPDD-TFTXF-Q9MMF` |
|
||||
|
||||
### Windows Server 2012 R2
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------------ | ------------------------------- |
|
||||
| Windows Server 2012 R2 Standard | `D2N9P-3P6X9-2R39C-7RTCD-MDVJX` |
|
||||
| Windows Server 2012 R2 Datacenter | `W3GGN-FT8W3-Y4M27-J84CP-Q3VJ9` |
|
||||
| Windows Server 2012 R2 Essentials | `KNC87-3J2TX-XB4WP-VCPJV-M4FWM` |
|
||||
|
@ -76,7 +83,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows 8.1
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Windows 8.1 Professional | `GCRJD-8NW9H-F2CDX-CCM8D-9D6T9` |
|
||||
| Windows 8.1 Professional N | `HMCNV-VVBFX-7HMBH-CTY9B-B4FXY` |
|
||||
| Windows 8.1 Professional WMC | `789NJ-TQK6T-6XTH8-J39CJ-J8D3P` |
|
||||
|
@ -100,7 +107,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Server 2012
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ----------------------------------------------------------------------- | ------------------------------- |
|
||||
| Windows Server 2012<br>Windows 8 Core | `BN3D2-R7TKB-3YPBD-8DRP2-27GG4` |
|
||||
| Windows Server 2012 N<br>Windows 8 Core N | `8N2M2-HWPGY-7PGT9-HGDD8-GVGGY` |
|
||||
| Windows Server 2012 Single Language<br>Windows 8 Core Single Language | `2WN2H-YGCQR-KFX6K-CD6TF-84YXQ` |
|
||||
|
@ -113,7 +120,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows 8
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Windows 8 Professional | `NG4HW-VH26C-733KW-K6F98-J8CK4` |
|
||||
| Windows 8 Professional N | `XCVCF-2NXM9-723PB-MHCB7-2RYQQ` |
|
||||
| Windows 8 Professional WMC | `GNBB8-YVD74-QJHX6-27H4K-8QHDG`<br>`NQ3PX-BBY8Y-RRHMM-TBHFW-PJ866` |
|
||||
|
@ -130,7 +137,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Server 2008 R2
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------------------------ | ------------------------------- |
|
||||
| Windows MultiPoint Server 2010 | `736RG-XDKJK-V34PF-BHK87-J6X3K` |
|
||||
| Windows Server 2008 R2 Web | `6TPJF-RBVHG-WBW2R-86QPH-6RTM4` |
|
||||
| Windows Server 2008 R2 HPC edition | `TT8MH-CG224-D3D7Q-498W2-9QCTX` |
|
||||
|
@ -142,7 +149,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows 7
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| --------------------------- | ------------------------------------------------------------------ |
|
||||
| Windows 7 Professional | `FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4`<br>`MYKDJ-XV4CV-M2D3P-KDVY4-MPTW8` |
|
||||
| Windows 7 Professional N | `MRPKT-YTG23-K7D7T-X2JMM-QY7MG` |
|
||||
| Windows 7 Professional E | `W82YF-2Q76Y-63HXB-FGJG9-GF7QX` |
|
||||
|
@ -156,7 +163,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Server 2008
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| -------------------------------------------------- | ------------------------------- |
|
||||
| Windows Server 2008 Web | `WYR28-R7TFJ-3X2YQ-YCY4H-M249D` |
|
||||
| Windows Server 2008 Standard | `TM24T-X9RMF-VWXK6-X8JC9-BFGM2` |
|
||||
| Windows Server 2008 Standard without Hyper-V | `W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ` |
|
||||
|
@ -170,7 +177,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Vista
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| -------------------------- | ------------------------------- |
|
||||
| Windows Vista Business | `YFKBB-PQJJV-G996G-VWGXY-2V3X8` |
|
||||
| Windows Vista Business N | `HMBQG-8H2RH-C77VX-27R82-VMQBT` |
|
||||
| Windows Vista Enterprise | `VKK3X-68KWM-X2YGT-QR4M6-4BWMV` |
|
||||
|
@ -179,7 +186,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Windows Previews
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Windows Server 2019 Datacenter [Preview] | `6XBNX-4JQGW-QX6QG-74P76-72V67` |
|
||||
| Windows Server 2019 Standard [Preview] | `MFY9F-XBN2F-TYFMP-CCV49-RMYVH` |
|
||||
| Windows 10 Home / Core [Pre-Release] | `?????-?????-?????-?????-?????` |
|
||||
|
@ -273,12 +280,29 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
| Windows Next Professional S N [Pre-Release] | `?????-?????-?????-?????-?????` |
|
||||
|
||||
## Office
|
||||
***
|
||||
|
||||
### Office 2021
|
||||
|
||||
| Product | GVLK |
|
||||
| ----------------------------------- | ------------------------------- |
|
||||
| Office Access LTSC 2021 | `WM8YG-YNGDD-4JHDC-PG3F4-FC4T4` |
|
||||
| Office Excel LTSC 2021 | `NWG3X-87C9K-TC7YY-BC2G7-G6RVC` |
|
||||
| Office Outlook LTSC 2021 | `C9FM6-3N72F-HFJXB-TM3V9-T86R9` |
|
||||
| Office Powerpoint LTSC 2021 | `TY7XF-NFRBR-KJ44C-G83KF-GX27K` |
|
||||
| Office LTSC Professional Plus 2021 | `FXYTK-NJJ8C-GB6DW-3DYQT-6F7TH` |
|
||||
| Office Project Pro 2021 | `FTNWT-C6WBT-8HMGF-K9PRX-QV9H8` |
|
||||
| Office Project Standard 2021 | `J2JDC-NJCYY-9RGQ4-YXWMH-T3D4T` |
|
||||
| Office Publisher LTSC 2021 | `2MW9D-N4BXM-9VBPG-Q7W6M-KFBGQ` |
|
||||
| Office Skype for Business LTSC 2021 | `HWCXN-K3WBT-WJBKY-R8BD9-XK29P` |
|
||||
| Office LTSC Standard 2021 | `KDX7X-BNVR8-TXXGX-4Q7Y8-78VT3` |
|
||||
| Office Visio LTSC Pro 2021 | `KNH8D-FGHT4-T8RK3-CTDYJ-K2HT4` |
|
||||
| Office Visio LTSC Standard 2021 | `MJVNY-BYWPY-CWV6J-2RKRT-4M8QG` |
|
||||
| Office Word LTSC 2021 | `TN8H9-M34D3-Y64V9-TR72V-X79KV` |
|
||||
|
||||
### Office 2019
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------- | ------------------------------- |
|
||||
| Professional Plus 2019 [C2R] | `VQ9DP-NVHPH-T9HJC-J9PDT-KTQRG` |
|
||||
| Professional Plus 2019 | `NMMKJ-6RK4F-KMJVX-8D9MJ-6MWKP` |
|
||||
| Standard 2019 | `6NWWJ-YQWMR-QKGCB-6TMB3-9D9HK` |
|
||||
|
@ -299,7 +323,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Office 2016
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------- | ------------------------------- |
|
||||
| Professional Plus 2016 | `XQNVK-8JYDB-WJ9W3-YJ8YR-WFG99` |
|
||||
| Standard 2016 | `JNRGM-WHDWX-FJJG3-K47QV-DRTFM` |
|
||||
| Project Professional 2016 | `YG9NW-3K39V-2T3HJ-93F3Q-G83KT` |
|
||||
|
@ -324,7 +348,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Office 2013
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ----------------------------------------------------- | ------------------------------- |
|
||||
| Professional Plus 2013 [Preview] | `PGD67-JN23K-JGVWV-KTHP4-GXR9G` |
|
||||
| Professional Plus 2013 | `YC7DK-G2NP3-2QQC3-J6H88-GVGXT` |
|
||||
| Standard 2013 | `KBKQT-2NMXY-JJWGP-M62JB-92CD4` |
|
||||
|
@ -365,7 +389,7 @@ sometimes even reject it by itself (often due too many uses - in that case try t
|
|||
### Office 2010
|
||||
|
||||
| Product | GVLK |
|
||||
| --- | --- |
|
||||
| ------------------------------------------- | ------------------------------- |
|
||||
| Professional Plus 2010 | `VYBBJ-TRJPB-QFQRF-QFT4D-H3GVB` |
|
||||
| Standard 2010 | `V7QKV-4XVVR-XYV4D-F7DFM-8R6BM` |
|
||||
| Project Professional 2010 | `YGX6F-PGV49-PGW3J-9BTGG-VHKC6` |
|
||||
|
|
|
@ -6,6 +6,7 @@ If you not follow this, do not expect that we can or want to help you!
|
|||
|
||||
* Are you activating a legit Windows copy checked with `sha256`, `md5` or is it maybe a warez torrent version ?
|
||||
* Did you tried a clean installation (format all) ? You skipped entering any key during installation, turning off internet connection, first activating and then updating Windows (and eventually later upgrading) ?
|
||||
* Are you activating Windows or Office on a different machine (physical or virtual) where py-kms runs?
|
||||
* Have you installed all latest packages ? Especially before upgrading ? Are you upgrading using the "Update Assistant"/"Media Creation" tool to switch from Windows 7 / 8 / 8.1 to 10 (for me has always worked) ?
|
||||
* If isn't a clean install, so far as you have kept activated your Windows copy ? Have you used some other activator (maybe not trusted) that injects or changes .dll files and therefore may have corrupted something ?
|
||||
* Have you forgot to reactivate at least once before 180 (45 or 30, depending on your version) days ?
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Usage
|
||||
|
||||
## Start Parameters
|
||||
***
|
||||
|
||||
(pykms-server-py)=
|
||||
### pykms_Server.py
|
||||
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.
|
||||
|
@ -17,7 +17,7 @@ Follows a list of usable parameters:
|
|||
Use _EPID_ as Windows _EPID_. If no _EPID_ is specified, a random one will be generated.
|
||||
|
||||
-l or --lcid <LCID>
|
||||
> Do not randomize the locale ID part of the _EPID_ and use _LCID_ instead.
|
||||
> Specify the _LCID_ part of the _EPID_. If an _EPID_ is manually specified, this setting is ignored. Default is 1033 (English - US).
|
||||
The Language Code Identifier (_LCID_) describes localizable information in Windows.
|
||||
This structure is used to identify specific languages for the purpose of customizing
|
||||
software for particular languages and cultures. For example, it can specify the way dates,
|
||||
|
@ -26,10 +26,9 @@ The _LCID_ must be specified as a decimal number (example: 1049 for "Russian - R
|
|||
By default py-kms generates a valid locale ID but this may lead to a value which is unlikely to occur in your country.
|
||||
You may want to select the locale ID of your country instead.
|
||||
See [here](https://msdn.microsoft.com/en-us/library/cc233982.aspx) for a list of valid _LCIDs_.
|
||||
If an _EPID_ is manually specified, this setting is ignored. Default is a fixed _LCID_ of 1033 (English - US).
|
||||
|
||||
-w or --hwid <HWID>
|
||||
> Use specified _HWID_ for all products.
|
||||
> Use specified _HWID_ for all products. Use `-w RANDOM` to generate a random HWID. Default is random.
|
||||
Hardware Identification is a security measure used by Microsoft upon the activation of
|
||||
the Windows operating system. As part of the Product Activation system, a unique
|
||||
HWID number is generated when the operating system is first installed. The _HWID_ identifies the hardware components that the system
|
||||
|
@ -40,7 +39,6 @@ If the two _HWID_ numbers differ too much then the operating system will shut do
|
|||
The theory behind _HWID_ is to ensure that the operating system is not being used on any device other than the one
|
||||
for which it was purchased and registered.
|
||||
HWID must be an 16-character string of hex characters that are interpreted as a series of 8 bytes (big endian).
|
||||
Default is _364F463A8863D35F_. To auto generate the _HWID_, type `-w RANDOM`.
|
||||
|
||||
-c or --client-count <CLIENTCOUNT>
|
||||
> Use this flag to specify the current _CLIENTCOUNT_. Default is None. Remember that a number >=25 is
|
||||
|
@ -55,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.
|
||||
|
@ -77,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
|
||||
```
|
||||
|
||||
|
@ -85,18 +82,18 @@ 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
|
||||
```
|
||||
|
||||
You can also enable other suboptions of `-F` doing what is reported in the following table:
|
||||
|
||||
| command | pretty msg | logging msg | logfile |
|
||||
| --- | --- | --- | --- |
|
||||
| ------------------------- | ---------- | ----------- | ------- |
|
||||
| `-F <logfile>` | ON | OFF | ON |
|
||||
| `-F STDOUT` | OFF | ON | OFF |
|
||||
| `-F FILESTDOUT <logfile>` | OFF | ON | ON |
|
||||
|
@ -106,7 +103,7 @@ You can also enable other suboptions of `-F` doing what is reported in the follo
|
|||
-S or --logsize <MAXSIZE>
|
||||
> Use this flag to set a maximum size (in MB) to the output log file. Deactivated by default.
|
||||
|
||||
##### subparser `connect`
|
||||
#### subparser `connect`
|
||||
|
||||
-n or --listen <'IP,PORT'>
|
||||
> Use this option to add multiple listening ip address - port couples. Note the format with the comma between the ip address and the port number. You can use this option more than once.
|
||||
|
@ -119,22 +116,22 @@ If placed just after `connect` refers to the main address and all additive coupl
|
|||
> Use this option not to allow binding / listening to the same ip address - port couple specified with `-n`.
|
||||
If placed just after `connect` refers to the main address and all additive couples without `-u` option. Reusing port is activated by default (except when running inside the Windows Sandbox and the current user is `WDAGUtilityAccount`).
|
||||
|
||||
-d or --dual
|
||||
> Use this option to allow listening to an IPv6 address also accepting connections via IPv4.
|
||||
If used it refers to all addresses (main and additional). Deactivated by default.
|
||||
-d or --dual <bool>
|
||||
> Allows listening to an IPv6 address while also accepting connections via IPv4. If used, it refers to all addresses (main and additional). Activated by default. Pass in "false" or "true" to disable or enable.
|
||||
|
||||
examples (with fictitious addresses and ports):
|
||||
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 -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 :: 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` | ('::', 1688) | 12 | True | [] | [] | [] | True |
|
||||
| `python3 pykms_Server.py :: connect -b 12 -u -d yes` | ('::', 1688) | 12 | False | [] | [] | [] | True |
|
||||
| `python3 pykms_Server.py :: connect -b 12 -u -d false` | ('::', 1688) | 12 | False | [] | [] | [] | False |
|
||||
| `python3 pykms_Server.py connect -n 1.1.1.1,1699 -b 10` | ('::', 1688) | 5 | True | [('1.1.1.1', 1699)] | [10] | [True] | True |
|
||||
| `python3 pykms_Server.py :: 1655 connect -n 2001:db8:0:200::7,1699 -d true -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` | ('::', 1688) | 12 | False | [('1.1.1.1', 1699), ('2.2.2.2', 1677)] | [10, 15] | [False, False] | True |
|
||||
| `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] | True |
|
||||
| `python3 pykms_Server.py connect -d 0 -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] | False |
|
||||
| `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] | True |
|
||||
|
||||
### 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`.
|
||||
|
@ -145,6 +142,12 @@ user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py -V DEBUG
|
|||
user@host ~/path/to/folder/py-kms $ python3 pykms_Client.py -V DEBUG
|
||||
```
|
||||
|
||||
If you wish to get KMS server from DNS server: (ie perform a DNS resolution on _vlmcs._tcp.domain.tld, if ever there are several answers, only the first one is selected.). Althought that mode is supposed to be specific to devices connect to an Active Directory domain, setting a fully qualified name and a workgroup may help to use that automatic KMS discovery feature.
|
||||
```
|
||||
user@host ~/path/to/folder/py-kms $ python3 pykms_Client.py -V DEBUG -F STDOUT -D contoso.com
|
||||
user@host ~/path/to/folder/py-kms $ python3 pykms_Client.py -V DEBUG -F STDOUT -D contoso.com
|
||||
```
|
||||
|
||||
Or if you want better specify:
|
||||
```
|
||||
user@host ~/path/to/folder/py-kms $ python3 pykms_Server.py <YOUR_IPADDRESS> 1688 -V DEBUG
|
||||
|
@ -194,12 +197,13 @@ You can enable same _pykms_Server.py_ suboptions of `-F`.
|
|||
-S or --logsize <MAXSIZE>
|
||||
> Use this flag to set a maximum size (in MB) to the output log file. Deactivated by default.
|
||||
|
||||
(docker-environment)=
|
||||
## Docker Environment
|
||||
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).
|
||||
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](#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".
|
||||
|
@ -226,15 +230,11 @@ 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
|
||||
|
||||
# hwid
|
||||
# Use this flag 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.
|
||||
ENV HWID 364F463A8863D35F
|
||||
# The default is "RANDOM" to auto-generate the HWID or type a specific value.
|
||||
ENV HWID RANDOM
|
||||
|
||||
# log level ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG")
|
||||
# Use this flag to set a Loglevel. The default is "ERROR".
|
||||
|
@ -253,7 +253,6 @@ ENV LOGSIZE ""
|
|||
The product asks for a key during installation, so it needs you to enter the GVLK. Then the user can set connection parameters, while KMS server must already be running on server machine. Finally with specific commands, activation occurs automatically and can be extended later every time for another 180 (or 30 or 45) days.
|
||||
|
||||
### Windows
|
||||
***
|
||||
The `//nologo` option of `cscript` was used only to hide the startup logo.
|
||||
|
||||

|
||||
|
@ -271,7 +270,6 @@ The `//nologo` option of `cscript` was used only to hide the startup logo.
|
|||
6. View license informations (optional).
|
||||
|
||||
### Office
|
||||
***
|
||||
Note that you’ll have to install a volume license (VL) version of Office. Office versions downloaded from MSDN and / or Technet are non-VL.
|
||||
|
||||

|
||||
|
|
18
docs/conf.py
18
docs/conf.py
|
@ -28,16 +28,10 @@ import os
|
|||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['recommonmark', 'sphinx_markdown_tables']
|
||||
extensions = ['myst_parser', 'sphinx_markdown_tables', 'sphinx_rtd_theme']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = {
|
||||
'.rst': 'restructuredtext',
|
||||
'.md': 'markdown',
|
||||
}
|
||||
# templates_path = ['_templates']
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
@ -54,9 +48,9 @@ copyright = u'2020, SystemRage'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0'
|
||||
# version = '1.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0'
|
||||
# release = '1.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -101,7 +95,7 @@ pygments_style = 'sphinx'
|
|||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -130,7 +124,7 @@ html_theme = 'default'
|
|||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
|
@ -1,44 +1,5 @@
|
|||
alabaster==0.7.12
|
||||
appdirs==1.4.4
|
||||
Babel==2.8.0
|
||||
CacheControl==0.12.6
|
||||
certifi==2020.4.5.1
|
||||
chardet==3.0.4
|
||||
colorama==0.4.3
|
||||
commonmark==0.9.1
|
||||
contextlib2==0.6.0
|
||||
distlib==0.3.0
|
||||
distro==1.5.0
|
||||
docutils==0.16
|
||||
html5lib==1.0.1
|
||||
idna==2.9
|
||||
imagesize==1.2.0
|
||||
Jinja2==2.11.2
|
||||
lockfile==0.12.2
|
||||
Markdown==3.2.2
|
||||
MarkupSafe==1.1.1
|
||||
msgpack==1.0.0
|
||||
ordered-set==4.0.1
|
||||
packaging==20.4
|
||||
pep517==0.8.2
|
||||
progress==1.5
|
||||
Pygments==2.6.1
|
||||
pyparsing==2.4.7
|
||||
pytoml==0.1.21
|
||||
pytz==2020.1
|
||||
recommonmark==0.6.0
|
||||
requests==2.23.0
|
||||
retrying==1.3.3
|
||||
six==1.15.0
|
||||
snowballstemmer==2.0.0
|
||||
Sphinx==3.1.2
|
||||
sphinx-markdown-tables==0.0.15
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
sphinxcontrib-htmlhelp==1.0.3
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
sphinxcontrib-serializinghtml==1.1.4
|
||||
toml==0.10.1
|
||||
urllib3==1.25.9
|
||||
webencodings==0.5.1
|
||||
Sphinx~=7.2.6
|
||||
sphinx-rtd-theme~=2.0.0
|
||||
readthedocs-sphinx-search~=0.3.2
|
||||
sphinx-markdown-tables~=0.0.17
|
||||
myst-parser~=2.0.0
|
||||
|
|
|
@ -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 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()
|
|
@ -103,6 +103,14 @@
|
|||
<Activate KmsItem="7ba0bf23-d0f5-4072-91d9-d55af5a481b6" />
|
||||
</CsvlkItem>
|
||||
|
||||
<CsvlkItem DisplayName="Windows Server 2022" VlmcsdIndex="0" GroupId="206" MinKeyId="551000000" MaxKeyId="570999999" IniFileName="Windows" EPid="06401-00206-566-174993-03-1033-9600.0000-2802018" Id="ef6cfc9f-8c5d-44ac-9aad-de6a2ea0ae03" InvalidWinBuild="[0,1,2]">
|
||||
<Activate KmsItem="ef6cfc9f-8c5d-44ac-9aad-de6a2ea0ae03" />
|
||||
</CsvlkItem>
|
||||
|
||||
<CsvlkItem DisplayName="Windows Server 2025" VlmcsdIndex="0" GroupId="206" MinKeyId="551000000" MaxKeyId="570999999" IniFileName="Windows" EPid="06401-00206-566-174993-03-1033-9600.0000-2802018" Id="c052f164-cdf6-409a-a0cb-853ba0f0f55a" InvalidWinBuild="[0,1,2]">
|
||||
<Activate KmsItem="c052f164-cdf6-409a-a0cb-853ba0f0f55a" />
|
||||
</CsvlkItem>
|
||||
|
||||
<CsvlkItem DisplayName="Windows Server 2019" VlmcsdIndex="0" GroupId="206" MinKeyId="551000000" MaxKeyId="570999999" IniFileName="Windows" EPid="06401-00206-566-174993-03-1033-9600.0000-2802018" Id="2e7a9ad1-a849-4b56-babe-17d5a29fe4b4" InvalidWinBuild="[0,1,2]">
|
||||
<Activate KmsItem="58e2134f-8e11-4d17-9cb2-91069c151148" />
|
||||
<Activate KmsItem="7fde5219-fbfa-484a-82c9-34d1ad53e856" />
|
||||
|
@ -530,6 +538,10 @@
|
|||
<Activate KmsItem="02000000-0000-0000-0000-000000000000" />
|
||||
</CsvlkItem>
|
||||
|
||||
<CsvlkItem DisplayName="Office 2021" VlmcsdIndex="6" GroupId="206" MinKeyId="571000000" MaxKeyId="590999999" IniFileName="Office2021" EPid="05426-00206-586-025264-03-1033-9200.0000-2602021" Id="47f3b983-7c53-4d45-abc6-bcd91e2dd90a" InvalidWinBuild="[0,1]">
|
||||
<Activate KmsItem="86d50b16-4808-41af-b83b-b338274318b2" />
|
||||
</CsvlkItem>
|
||||
|
||||
<CsvlkItem DisplayName="Office 2019" VlmcsdIndex="5" GroupId="206" MinKeyId="666000000" MaxKeyId="685999999" IniFileName="Office2019" EPid="06401-00206-678-008369-03-1033-9600.0000-2802018" Id="70512334-47b4-44db-a233-be5ea33b914c" InvalidWinBuild="[0,1]">
|
||||
<Activate KmsItem="617d9eb1-ef36-4f82-86e0-a65ae07b96c6" />
|
||||
</CsvlkItem>
|
||||
|
@ -560,6 +572,16 @@
|
|||
|
||||
<AppItem DisplayName="Windows" VlmcsdIndex="0" Id="55c92734-d682-4d71-983e-d6ec3f16059f" MinActiveClients="50">
|
||||
|
||||
<KmsItem DisplayName="Windows Server 2025" Id="c052f164-cdf6-409a-a0cb-853ba0f0f55a" DefaultKmsProtocol="6.0" NCountPolicy="5">
|
||||
<SkuItem DisplayName="Windows Server 2025 Datacenter" Id="c052f164-cdf6-409a-a0cb-853ba0f0f55a" Gvlk="D764K-2NDRG-47T6Q-P8T8W-YP6DF" />
|
||||
<SkuItem DisplayName="Windows Server 2025 Standard" Id="7dc26449-db21-4e09-ba37-28f2958506a6" Gvlk="TVRH6-WHNXV-R9WG3-9XRFY-MY832" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows Server 2022" Id="ef6cfc9f-8c5d-44ac-9aad-de6a2ea0ae03" DefaultKmsProtocol="6.0" NCountPolicy="5">
|
||||
<SkuItem DisplayName="Windows Server 2022 Datacenter" Id="ef6cfc9f-8c5d-44ac-9aad-de6a2ea0ae03" Gvlk="WX4NM-KYWYW-QJJR4-XV3QB-6VM33" />
|
||||
<SkuItem DisplayName="Windows Server 2022 Standard" Id="de32eafd-aaee-4662-9444-c1befb41bde2" Gvlk="VDYBN-27WPP-V4HQT-9VMD4-VMK7H" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows Server 2019" Id="8449b1fb-f0ea-497a-99ab-66ca96e9a0f5" DefaultKmsProtocol="6.0" NCountPolicy="5">
|
||||
<SkuItem DisplayName="Windows Server 2019 ARM64" Id="8de8eb62-bbe0-40ac-ac17-f75595071ea3" Gvlk="GRFBW-QNDC4-6QBHG-CCK3B-2PR88" />
|
||||
<SkuItem DisplayName="Windows Server 2019 Azure Core" Id="a99cc1f0-7719-4306-9645-294102fbff95" Gvlk="FDNH6-VW9RW-BXPJ7-4XTYG-239TB" />
|
||||
|
@ -573,16 +595,16 @@
|
|||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 2019 (Volume)" Id="11b15659-e603-4cf1-9c1f-f0ec01b81888" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10 Enterprise LTSC 2019" Id="32d2fab3-e4a8-42c2-923b-4bf4fd13e6ee" Gvlk="M7XTQ-FN8P6-TTKYV-9D4CC-J462D" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise LTSC 2019 N" Id="7103a333-b8c8-49cc-93ce-d37c09687f92" Gvlk="92NFX-8DJQP-P6BBQ-THF9C-7CG2H" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise LTSC 2019/2021" Id="32d2fab3-e4a8-42c2-923b-4bf4fd13e6ee" Gvlk="M7XTQ-FN8P6-TTKYV-9D4CC-J462D" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise LTSC 2019/2021 N" Id="7103a333-b8c8-49cc-93ce-d37c09687f92" Gvlk="92NFX-8DJQP-P6BBQ-THF9C-7CG2H" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 Unknown (Volume)" Id="d27cd636-1962-44e9-8b4f-27b6c23efb85" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 China Government" Id="7ba0bf23-d0f5-4072-91d9-d55af5a481b6" CanMapToDefaultCsvlk="false" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10 Enterprise G" Id="e0b2d383-d112-413f-8a80-97f373a5820c" Gvlk="YYVX9-NTFWV-6MDM3-9PT4T-4M68B" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise G N" Id="e38454fb-41a4-4f59-a5dc-25080e354730" Gvlk="44RPN-FTY23-9VTTB-MP9BX-T84FV" />
|
||||
<KmsItem DisplayName="Windows 10/11 China Government" Id="7ba0bf23-d0f5-4072-91d9-d55af5a481b6" CanMapToDefaultCsvlk="false" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise G" Id="e0b2d383-d112-413f-8a80-97f373a5820c" Gvlk="YYVX9-NTFWV-6MDM3-9PT4T-4M68B" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise G N" Id="e38454fb-41a4-4f59-a5dc-25080e354730" Gvlk="44RPN-FTY23-9VTTB-MP9BX-T84FV" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 2016 (Volume)" Id="969fe3c0-a3ec-491a-9f25-423605deb365" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
|
@ -590,44 +612,44 @@
|
|||
<SkuItem DisplayName="Windows 10 Enterprise 2016 LTSB N" Id="9f776d83-7156-45b2-8a5c-359b9c9f22a3" Gvlk="QFFDN-GRT3P-VKWWX-X7T3R-8B639" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 (Retail)" Id="e1c51358-fe3e-4203-a4a2-3b6b20c9734e" IsRetail="true" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10 Home / Core" Id="58e97c99-f377-4ef1-81d5-4ad5522b5fd8" Gvlk="TX9XD-98N7V-6WMQ6-BX7FG-H8Q99" />
|
||||
<KmsItem DisplayName="Windows 10/11 (Retail)" Id="e1c51358-fe3e-4203-a4a2-3b6b20c9734e" IsRetail="true" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10/11 Home / Core" Id="58e97c99-f377-4ef1-81d5-4ad5522b5fd8" Gvlk="TX9XD-98N7V-6WMQ6-BX7FG-H8Q99" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core [Pre-Release]" Id="903663f7-d2ab-49c9-8942-14aa9e0a9c72" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core Country Specific" Id="a9107544-f4a0-4053-a96a-1479abdef912" Gvlk="PVMJN-6DFY6-9CCP6-7BKTT-D3WVR" />
|
||||
<SkuItem DisplayName="Windows 10/11 Home / Core Country Specific" Id="a9107544-f4a0-4053-a96a-1479abdef912" Gvlk="PVMJN-6DFY6-9CCP6-7BKTT-D3WVR" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core Country Specific [Pre-Release]" Id="5fe40dd6-cf1f-4cf2-8729-92121ac2e997" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core N" Id="7b9e1751-a8da-4f75-9560-5fadfe3d8e38" Gvlk="3KHY7-WNT83-DGQKR-F7HPR-844BM" />
|
||||
<SkuItem DisplayName="Windows 10/11 Home / Core N" Id="7b9e1751-a8da-4f75-9560-5fadfe3d8e38" Gvlk="3KHY7-WNT83-DGQKR-F7HPR-844BM" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core N [Pre-Release]" Id="4dfd543d-caa6-4f69-a95f-5ddfe2b89567" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core Single Language" Id="cd918a57-a41b-4c82-8dce-1a538e221a83" Gvlk="7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH" />
|
||||
<SkuItem DisplayName="Windows 10/11 Home / Core Single Language" Id="cd918a57-a41b-4c82-8dce-1a538e221a83" Gvlk="7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core Single Language [Pre-Release]" Id="2cc171ef-db48-4adc-af09-7c574b37f139" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Home / Core [Technical Preview]" Id="6496e59d-89dc-49eb-a353-09ceb9404845" Gvlk="" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Windows 10 2015 (Volume)" Id="58e2134f-8e11-4d17-9cb2-91069c151148" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10 Education" Id="e0c42288-980c-4788-a014-c080d2e1926e" Gvlk="NW6C2-QMPVW-D7KKK-3GKT6-VCFB2" />
|
||||
<KmsItem DisplayName="Windows 10/11 2015 (Volume)" Id="58e2134f-8e11-4d17-9cb2-91069c151148" DefaultKmsProtocol="6.0" NCountPolicy="25">
|
||||
<SkuItem DisplayName="Windows 10/11 Education" Id="e0c42288-980c-4788-a014-c080d2e1926e" Gvlk="NW6C2-QMPVW-D7KKK-3GKT6-VCFB2" />
|
||||
<SkuItem DisplayName="Windows 10 Education [Pre-Release]" Id="af43f7f0-3b1e-4266-a123-1fdb53f4323b" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Education N" Id="3c102355-d027-42c6-ad23-2e7ef8a02585" Gvlk="2WH4N-8QGBV-H22JP-CT43Q-MDWWJ" />
|
||||
<SkuItem DisplayName="Windows 10/11 Education N" Id="3c102355-d027-42c6-ad23-2e7ef8a02585" Gvlk="2WH4N-8QGBV-H22JP-CT43Q-MDWWJ" />
|
||||
<SkuItem DisplayName="Windows 10 Education N [Pre-Release]" Id="075aca1f-05d7-42e5-a3ce-e349e7be7078" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise" Id="73111121-5638-40f6-bc11-f1d7b0d64300" Gvlk="NPPR9-FWDCX-D2C8J-H872K-2YT43" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise" Id="73111121-5638-40f6-bc11-f1d7b0d64300" Gvlk="NPPR9-FWDCX-D2C8J-H872K-2YT43" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise [Preview]" Id="43f2ab05-7c87-4d56-b27c-44d0f9a3dabd" Gvlk="QNMXX-GCD3W-TCCT4-872RV-G6P4J" IsGeneratedGvlk="true" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise S" Id="00000000-0000-0000-0000-000000000000" Gvlk="H76BG-QBNM7-73XY9-V6W2T-684BJ" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise S" Id="00000000-0000-0000-0000-000000000000" Gvlk="H76BG-QBNM7-73XY9-V6W2T-684BJ" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise 2015 LTSB" Id="7b51a46c-0c04-4e8f-9af4-8496cca90d5e" Gvlk="WNMTR-4C88C-JK8YV-HQ7T2-76DF9" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise 2015 LTSB [Pre-Release]" Id="2cf5af84-abab-4ff0-83f8-f040fb2576eb" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise 2015 LTSB N" Id="87b838b7-41b6-4590-8318-5797951d8529" Gvlk="2F77B-TNFGY-69QQF-B8YKP-D69TJ" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise 2015 LTSB N [Pre-Release]" Id="11a37f09-fb7f-4002-bd84-f3ae71d11e90" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise N" Id="e272e3e2-732f-4c65-a8f0-484747d0d947" Gvlk="DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise S N" Id="00000000-0000-0000-0000-000000000000" Gvlk="X4R4B-NV6WD-PKTVK-F98BH-4C2J8" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise N" Id="e272e3e2-732f-4c65-a8f0-484747d0d947" Gvlk="DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise S N" Id="00000000-0000-0000-0000-000000000000" Gvlk="X4R4B-NV6WD-PKTVK-F98BH-4C2J8" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise N [Pre-Release]" Id="6ae51eeb-c268-4a21-9aae-df74c38b586d" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Professional Workstation" Id="82bbc092-bc50-4e16-8e18-b74fc486aec3" Gvlk="NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J" />
|
||||
<SkuItem DisplayName="Windows 10 Professional Workstation N" Id="4b1571d3-bafb-4b40-8087-a961be2caf65" Gvlk="9FNHH-K3HBT-3W4TD-6383H-6XYWF" />
|
||||
<SkuItem DisplayName="Windows 10 Professional" Id="2de67392-b7a7-462a-b1ca-108dd189f588" Gvlk="W269N-WFGWX-YVC9B-4J6C9-T83GX" />
|
||||
<SkuItem DisplayName="Windows 10 Professional Education" Id="3f1afc82-f8ac-4f6c-8005-1d233e606eee" Gvlk="6TP4R-GNPTD-KYYHQ-7B7DP-J447Y" />
|
||||
<SkuItem DisplayName="Windows 10 Professional Education N" Id="5300b18c-2e33-4dc2-8291-47ffcec746dd" Gvlk="YVWGF-BXNMC-HTQYQ-CPQ99-66QFC" />
|
||||
<SkuItem DisplayName="Windows 10 Professional N" Id="a80b5abf-76ad-428b-b05d-a47d2dffeebf" Gvlk="MH37W-N47XK-V7XM9-C7227-GCQG9" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional Workstation" Id="82bbc092-bc50-4e16-8e18-b74fc486aec3" Gvlk="NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional Workstation N" Id="4b1571d3-bafb-4b40-8087-a961be2caf65" Gvlk="9FNHH-K3HBT-3W4TD-6383H-6XYWF" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional" Id="2de67392-b7a7-462a-b1ca-108dd189f588" Gvlk="W269N-WFGWX-YVC9B-4J6C9-T83GX" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional Education" Id="3f1afc82-f8ac-4f6c-8005-1d233e606eee" Gvlk="6TP4R-GNPTD-KYYHQ-7B7DP-J447Y" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional Education N" Id="5300b18c-2e33-4dc2-8291-47ffcec746dd" Gvlk="YVWGF-BXNMC-HTQYQ-CPQ99-66QFC" />
|
||||
<SkuItem DisplayName="Windows 10/11 Professional N" Id="a80b5abf-76ad-428b-b05d-a47d2dffeebf" Gvlk="MH37W-N47XK-V7XM9-C7227-GCQG9" />
|
||||
<SkuItem DisplayName="Windows 10 Professional N [Pre-Release]" Id="34260150-69ac-49a3-8a0d-4a403ab55763" Gvlk="" />
|
||||
<SkuItem DisplayName="Windows 10 Professional [Preview]" Id="ff808201-fec6-4fd4-ae16-abbddade5706" Gvlk="XQHPH-N4D9W-M8P96-DPDFP-TMVPY" IsGeneratedGvlk="true" />
|
||||
<SkuItem DisplayName="Windows 10 Professional WMC [Pre-Release]" Id="cc17e18a-fa93-43d6-9179-72950a1e931a" Gvlk="NKPM6-TCVPT-3HRFX-Q4H9B-QJ34Y" />
|
||||
<SkuItem DisplayName="Windows 10 Enterprise for Virtual Desktops" Id="ec868e65-fadf-4759-b23e-93fe37f2cc29" Gvlk="CPWHC-NT2C7-VYW78-DHDB2-PG3GK" />
|
||||
<SkuItem DisplayName="Windows 10 Remote Server" Id="e4db50ea-bda1-4566-b047-0ca50abc6f07" Gvlk="7NBT4-WGBQX-MP4H7-QXFF8-YP3KX" />
|
||||
<SkuItem DisplayName="Windows 10/11 Enterprise for Virtual Desktops" Id="ec868e65-fadf-4759-b23e-93fe37f2cc29" Gvlk="CPWHC-NT2C7-VYW78-DHDB2-PG3GK" />
|
||||
<SkuItem DisplayName="Windows 10/11 Remote Server" Id="e4db50ea-bda1-4566-b047-0ca50abc6f07" Gvlk="7NBT4-WGBQX-MP4H7-QXFF8-YP3KX" />
|
||||
<SkuItem DisplayName="Windows 10 S (Lean)" Id="0df4f814-3f57-4b8b-9a9d-fddadcd69fac" Gvlk="NBTWJ-3DR69-3C4V8-C26MC-GQ9M6" />
|
||||
<SkuItem DisplayName="Windows 10 IoT Core [Pre-Release]" Id="b554b49f-4d57-4f08-955e-87886f514d49" Gvlk="7NX88-X6YM3-9Q3YT-CCGBF-KBVQF" />
|
||||
<SkuItem DisplayName="Windows 10 Core Connected [Pre-Release]" Id="827a0032-dced-4609-ab6e-16b9d8a40280" Gvlk="DJMYQ-WN6HG-YJ2YX-82JDB-CWFCW" />
|
||||
|
@ -984,6 +1006,37 @@
|
|||
<SkuItem DisplayName="Office Word 2019" Id="059834fe-a8ea-4bff-b67b-4d006b5447d3" Gvlk="PBX3G-NWMT6-Q7XBW-PYJGG-WXD33" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Office 2021" Id="86d50b16-4808-41af-b83b-b338274318b2" IsPreview="false" CanMapToDefaultCsvlk="false" DefaultKmsProtocol="6.0" NCountPolicy="5">
|
||||
<SkuItem DisplayName="Office Access LTSC 2021" Id="1fe429d8-3fa7-4a39-b6f0-03dded42fe14" Gvlk="WM8YG-YNGDD-4JHDC-PG3F4-FC4T4" />
|
||||
<SkuItem DisplayName="Office Excel LTSC 2021" Id="ea71effc-69f1-4925-9991-2f5e319bbc24" Gvlk="NWG3X-87C9K-TC7YY-BC2G7-G6RVC" />
|
||||
<SkuItem DisplayName="Office Outlook LTSC 2021" Id="a5799e4c-f83c-4c6e-9516-dfe9b696150b" Gvlk="C9FM6-3N72F-HFJXB-TM3V9-T86R9" />
|
||||
<SkuItem DisplayName="Office Powerpoint LTSC 2021" Id="6e166cc3-495d-438a-89e7-d7c9e6fd4dea" Gvlk="TY7XF-NFRBR-KJ44C-G83KF-GX27K" />
|
||||
<SkuItem DisplayName="Office LTSC Professional Plus 2021" Id="fbdb3e18-a8ef-4fb3-9183-dffd60bd0984" Gvlk="FXYTK-NJJ8C-GB6DW-3DYQT-6F7TH" />
|
||||
<SkuItem DisplayName="Office Project Pro 2021" Id="76881159-155c-43e0-9db7-2d70a9a3a4ca" Gvlk="FTNWT-C6WBT-8HMGF-K9PRX-QV9H8" />
|
||||
<SkuItem DisplayName="Office Project Standard 2021" Id="6dd72704-f752-4b71-94c7-11cec6bfc355" Gvlk="J2JDC-NJCYY-9RGQ4-YXWMH-T3D4T" />
|
||||
<SkuItem DisplayName="Office Publisher LTSC 2021" Id="aa66521f-2370-4ad8-a2bb-c095e3e4338f" Gvlk="2MW9D-N4BXM-9VBPG-Q7W6M-KFBGQ" />
|
||||
<SkuItem DisplayName="Office Skype for Business LTSC 2021" Id="1f32a9af-1274-48bd-ba1e-1ab7508a23e8" Gvlk="HWCXN-K3WBT-WJBKY-R8BD9-XK29P" />
|
||||
<SkuItem DisplayName="Office LTSC Standard 2021" Id="080a45c5-9f9f-49eb-b4b0-c3c610a5ebd3" Gvlk="KDX7X-BNVR8-TXXGX-4Q7Y8-78VT3" />
|
||||
<SkuItem DisplayName="Office Visio LTSC Pro 2021" Id="fb61ac9a-1688-45d2-8f6b-0674dbffa33c" Gvlk="KNH8D-FGHT4-T8RK3-CTDYJ-K2HT4" />
|
||||
<SkuItem DisplayName="Office Visio LTSC Standard 2021" Id="72fce797-1884-48dd-a860-b2f6a5efd3ca" Gvlk="MJVNY-BYWPY-CWV6J-2RKRT-4M8QG" />
|
||||
<SkuItem DisplayName="Office Word LTSC 2021" Id="abe28aea-625a-43b1-8e30-225eb8fbd9e5" Gvlk="TN8H9-M34D3-Y64V9-TR72V-X79KV" />
|
||||
</KmsItem>
|
||||
|
||||
<KmsItem DisplayName="Office 2024" Id="8d368fc1-9470-4be2-8d66-90e836cbb051" CanMapToDefaultCsvlk="false" DefaultKmsProtocol="6.0" NCountPolicy="5">
|
||||
<SkuItem DisplayName="Office Access 2024" Id="9e9bceeb-e736-4f26-88de-763f87dcc485" Gvlk="82FTR-NCHR7-W3944-MGRHM-JMCWD" />
|
||||
<SkuItem DisplayName="Office Excel 2024" Id="237854e9-79fc-4497-a0c1-a70969691c6b" Gvlk="F4DYN-89BP2-WQTWJ-GR8YC-CKGJG" />
|
||||
<SkuItem DisplayName="Office Outlook 2024" Id="c8f8a301-19f5-4132-96ce-2de9d4adbd33" Gvlk="D2F8D-N3Q3B-J28PV-X27HD-RJWB9" />
|
||||
<SkuItem DisplayName="Office Powerpoint 2024" Id="3131fd61-5e4f-4308-8d6d-62be1987c92c" Gvlk="TY7XF-NFRBR-KJ44C-G83KF-GX27K" />
|
||||
<SkuItem DisplayName="Office Professional Plus 2024" Id="8d368fc1-9470-4be2-8d66-90e836cbb051" Gvlk="CW94N-K6GJH-9CTXY-MG2VC-FYCWP" />
|
||||
<SkuItem DisplayName="Office Project Professional 2024" Id="f510af75-8ab7-4426-a236-1bfb95c34ff8" Gvlk="FQQ23-N4YCY-73HQ3-FM9WC-76HF4" />
|
||||
<SkuItem DisplayName="Office Project Standard 2024" Id="1777f0e3-7392-4198-97ea-8ae4de6f6381" Gvlk="PD3TT-NTHQQ-VC7CY-MFXK3-G87F8" />
|
||||
<SkuItem DisplayName="Office Publisher 2024" Id="9d3e4cca-e172-46f1-a2f4-1d2107051444" Gvlk="2MW9D-N4BXM-9VBPG-Q7W6M-KFBGQ" />
|
||||
<SkuItem DisplayName="Office Skype for Business 2024" Id="734c6c6e-b0ba-4298-a891-671772b2bd1b" Gvlk="4NKHF-9HBQF-Q3B6C-7YV34-F64P3" />
|
||||
<SkuItem DisplayName="Office Standard 2024" Id="6912a74b-a5fb-401a-bfdb-2e3ab46f4b02" Gvlk="V28N4-JG22K-W66P8-VTMGK-H6HGR" />
|
||||
<SkuItem DisplayName="Office Visio Professional 2024" Id="fa187091-8246-47b1-964f-80a0b1e5d69a" Gvlk="B7TN8-FJ8V3-7QYCP-HQPMV-YY89G" />
|
||||
<SkuItem DisplayName="Office Visio Standard 2024" Id="e06d7df3-aad0-419d-8dfb-0ac37e2bdf39" Gvlk="JMMVY-XFNQC-KK4HK-9H7R3-WQQTV" />
|
||||
<SkuItem DisplayName="Office Word 2024" Id="059834fe-a8ea-4bff-b67b-4d006b5447d3" Gvlk="MQ84N-7VYDM-FXV7C-6K7CC-VFW9J" />
|
||||
</KmsItem>
|
||||
</AppItem>
|
||||
|
||||
</AppItems>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 524 KiB |
|
@ -4,13 +4,12 @@ import binascii
|
|||
import logging
|
||||
import time
|
||||
import uuid
|
||||
import socket
|
||||
|
||||
from pykms_Structure import Structure
|
||||
from pykms_DB2Dict import kmsDB2Dict
|
||||
from pykms_PidGenerator import epidGenerator
|
||||
from pykms_Filetimes import filetime_to_dt
|
||||
from pykms_Sql import sql_initialize, sql_update, sql_update_epid
|
||||
from pykms_Sql import sql_update, sql_update_epid
|
||||
from pykms_Format import justify, byterize, enco, deco, pretty_printer
|
||||
|
||||
#--------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
@ -119,19 +118,24 @@ class kmsBase:
|
|||
|
||||
# Localize the request time, if module "tzlocal" is available.
|
||||
try:
|
||||
from datetime import datetime
|
||||
from tzlocal import get_localzone
|
||||
from pytz.exceptions import UnknownTimeZoneError
|
||||
try:
|
||||
tz = get_localzone()
|
||||
local_dt = tz.localize(requestDatetime)
|
||||
local_dt = datetime.fromisoformat(str(requestDatetime)).astimezone(get_localzone())
|
||||
except UnknownTimeZoneError:
|
||||
pretty_printer(log_obj = loggersrv.warning,
|
||||
put_text = "{reverse}{yellow}{bold}Unknown time zone ! Request time not localized.{end}")
|
||||
local_dt = requestDatetime
|
||||
except ImportError:
|
||||
pretty_printer(log_obj = loggersrv.warning,
|
||||
put_text = "{reverse}{yellow}{bold}Module 'tzlocal' not available ! Request time not localized.{end}")
|
||||
put_text = "{reverse}{yellow}{bold}Module 'tzlocal' or 'pytz' not available ! Request time not localized.{end}")
|
||||
local_dt = requestDatetime
|
||||
except Exception as e:
|
||||
# Just in case something else goes wrong
|
||||
loggersrv.warning('Okay, something went horribly wrong while localizing the request time (proceeding anyways): ' + str(e))
|
||||
local_dt = requestDatetime
|
||||
pass
|
||||
|
||||
# Activation threshold.
|
||||
# https://docs.microsoft.com/en-us/windows/deployment/volume-activation/activate-windows-10-clients-vamt
|
||||
|
@ -161,6 +165,7 @@ could be detected as not genuine !{end}" %currentClientCount)
|
|||
|
||||
# Get a name for SkuId, AppId.
|
||||
kmsdb = kmsDB2Dict()
|
||||
appName, skuName = str(applicationId), str(skuId)
|
||||
|
||||
appitems = kmsdb[2]
|
||||
for appitem in appitems:
|
||||
|
@ -208,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)
|
||||
|
|
|
@ -13,6 +13,12 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
|
||||
import dns.message
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.query
|
||||
import dns.resolver
|
||||
|
||||
import pykms_RpcBind, pykms_RpcRequest
|
||||
from pykms_Filetimes import dt_to_filetime
|
||||
from pykms_Dcerpc import MSRPCHeader, MSRPCBindNak, MSRPCRequestHeader, MSRPCRespHeader
|
||||
|
@ -39,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()
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
@ -50,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",
|
||||
|
@ -72,6 +77,7 @@ Type \"STDOUT\" to view log info on stdout. Type \"FILESTDOUT\" to combine previ
|
|||
Use \"STDOUTOFF\" to disable stdout messages. Use \"FILEOFF\" if you not want to create logfile.',
|
||||
'def' : os.path.join('.', 'pykms_logclient.log'), 'des' : "logfile"},
|
||||
'lsize' : {'help' : 'Use this flag to set a maximum size (in MB) to the output log file. Deactivated by default.', 'def' : 0, 'des': "logsize"},
|
||||
'discovery' : {'help': 'ask the client to perform a _vlmcs._tcp.domain.tld DNS request to set KMS server.', 'def': None , 'des': 'discovery' },
|
||||
}
|
||||
|
||||
def client_options():
|
||||
|
@ -99,6 +105,8 @@ def client_options():
|
|||
default = clt_options['lfile']['def'], help = clt_options['lfile']['help'], type = str)
|
||||
client_parser.add_argument("-S", "--logsize", dest = clt_options['lsize']['des'], action = "store",
|
||||
default = clt_options['lsize']['def'], help = clt_options['lsize']['help'], type = float)
|
||||
client_parser.add_argument("-D", "--discovery", dest = clt_options['discovery']['des'], action = "store",
|
||||
default = clt_options['discovery']['def'], help = clt_options['discovery']['help'], type = str)
|
||||
|
||||
client_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit")
|
||||
|
||||
|
@ -156,18 +164,25 @@ def client_check():
|
|||
def client_update():
|
||||
kmsdb = kmsDB2Dict()
|
||||
|
||||
loggerclt.debug(f'Searching in kms database for machine "{clt_config["mode"]}"...')
|
||||
|
||||
appitems = kmsdb[2]
|
||||
for appitem in appitems:
|
||||
kmsitems = appitem['KmsItems']
|
||||
for kmsitem in kmsitems:
|
||||
name = re.sub('\(.*\)', '', kmsitem['DisplayName']).replace('2015', '').replace(' ', '')
|
||||
name = re.sub('\(.*\)', '', kmsitem['DisplayName']) # Remove bracets
|
||||
name = name.replace('2015', '') # Remove specific years
|
||||
name = name.replace(' ', '') # Ignore whitespaces
|
||||
name = name.replace('/11', '', 1) # Cut out Windows 11, as it is basically Windows 10
|
||||
if name == clt_config['mode']:
|
||||
skuitems = kmsitem['SkuItems']
|
||||
# Select 'Enterprise' for Windows or 'Professional Plus' for Office.
|
||||
for skuitem in skuitems:
|
||||
if skuitem['DisplayName'].replace(' ','') == name + 'Enterprise' or \
|
||||
skuitem['DisplayName'].replace(' ','') == name[:6] + 'ProfessionalPlus' + name[6:]:
|
||||
|
||||
sName = skuitem['DisplayName']
|
||||
sName = sName.replace(' ', '') # Ignore whitespaces
|
||||
sName = sName.replace('/11', '', 1) # Cut out Windows 11, as it is basically Windows 10
|
||||
if sName == name + 'Enterprise' or \
|
||||
sName == name[:6] + 'ProfessionalPlus' + name[6:]:
|
||||
clt_config['KMSClientSkuID'] = skuitem['Id']
|
||||
clt_config['RequiredClientCount'] = int(kmsitem['NCountPolicy'])
|
||||
clt_config['KMSProtocolMajorVersion'] = int(float(kmsitem['DefaultKmsProtocol']))
|
||||
|
@ -175,9 +190,25 @@ def client_update():
|
|||
clt_config['KMSClientLicenseStatus'] = 2
|
||||
clt_config['KMSClientAppID'] = appitem['Id']
|
||||
clt_config['KMSClientKMSCountedID'] = kmsitem['Id']
|
||||
break
|
||||
return
|
||||
raise RuntimeError(f'Client failed to find machine configuration in kms database - make sure it contains an entry for "{clt_config["mode"]}"')
|
||||
|
||||
def client_connect():
|
||||
|
||||
if clt_config['discovery'] is not None:
|
||||
loggerclt.info(f'Using Domain: {clt_config["discovery"]}')
|
||||
r= None
|
||||
try:
|
||||
r = dns.resolver.resolve('_vlmcs._tcp.' + clt_config['discovery'], dns.rdatatype.SRV)
|
||||
for a in r:
|
||||
loggerclt.debug(f'answer KMS server: {a.target} , port: {a.port}')
|
||||
clt_config['ip'] = socket.gethostbyname(r[0].target.to_text())
|
||||
clt_config['port'] = r[0].port
|
||||
except (dns.exception.Timeout, dns.resolver.NXDOMAIN) as e:
|
||||
pretty_printer(log_obj = loggerclt.warning,
|
||||
put_text = "{reverse}{red}{bold}Cannot resolve '%s'. Error: '%s'...{end}" %(clt_config['discovery'],
|
||||
str(e)))
|
||||
|
||||
loggerclt.info("Connecting to %s on port %d" % (clt_config['ip'], clt_config['port']))
|
||||
try:
|
||||
clt_sock = socket.create_connection((clt_config['ip'], clt_config['port']), timeout = clt_config['timeoutidle'])
|
||||
|
@ -265,9 +296,8 @@ def client_create(clt_sock):
|
|||
pretty_printer(log_obj = loggerclt.warning, to_exit = True, where = "clt",
|
||||
put_text = "{reverse}{magenta}{bold}Something went wrong. Exiting...{end}")
|
||||
|
||||
def clt_main(with_gui = False):
|
||||
def clt_main():
|
||||
try:
|
||||
if not with_gui:
|
||||
# Parse options.
|
||||
client_options()
|
||||
|
||||
|
@ -361,4 +391,4 @@ def readKmsResponseV6(data):
|
|||
return message
|
||||
|
||||
if __name__ == "__main__":
|
||||
clt_main(with_gui = False)
|
||||
clt_main()
|
||||
|
|
|
@ -4,6 +4,9 @@ import os
|
|||
import socket
|
||||
import selectors
|
||||
import ipaddress
|
||||
import logging
|
||||
from pykms_Format import pretty_printer
|
||||
loggersrv = logging.getLogger('logsrv')
|
||||
|
||||
# https://github.com/python/cpython/blob/master/Lib/socket.py
|
||||
def has_dualstack_ipv6():
|
||||
|
@ -27,12 +30,13 @@ def create_server_sock(address, *, family = socket.AF_INET, backlog = None, reus
|
|||
|
||||
*family* should be either AF_INET or AF_INET6.
|
||||
*backlog* is the queue size passed to socket.listen().
|
||||
*reuse_port* dictates whether to use the SO_REUSEPORT socket option.
|
||||
*reuse_port* if True and the platform supports it, we will use the SO_REUSEPORT socket option.
|
||||
*dualstack_ipv6* if True and the platform supports it, it will create an AF_INET6 socket able to accept both IPv4 or IPv6 connections;
|
||||
when False it will explicitly disable this option on platforms that enable it by default (e.g. Linux).
|
||||
"""
|
||||
if reuse_port and not hasattr(socket._socket, "SO_REUSEPORT"):
|
||||
raise ValueError("SO_REUSEPORT not supported on this platform")
|
||||
pretty_printer(log_obj = loggersrv.warning, put_text = "{reverse}{yellow}{bold}SO_REUSEPORT not supported on this platform - ignoring socket option.{end}")
|
||||
reuse_port = False
|
||||
|
||||
if dualstack_ipv6:
|
||||
if not has_dualstack_ipv6():
|
||||
|
|
|
@ -274,9 +274,7 @@ class ShellMessage(object):
|
|||
ShellMessage.indx += 1
|
||||
|
||||
def print_logging_setup(self, logger, async_flag, formatter = logging.Formatter('%(name)s %(message)s')):
|
||||
from pykms_GuiBase import gui_redirector
|
||||
stream = gui_redirector(StringIO())
|
||||
handler = logging.StreamHandler(stream)
|
||||
handler = logging.StreamHandler(StringIO())
|
||||
handler.name = 'LogStream'
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(formatter)
|
||||
|
@ -293,9 +291,6 @@ class ShellMessage(object):
|
|||
|
||||
def print_logging(self, toprint):
|
||||
if (self.nshell and ((0 in self.nshell) or (2 in self.nshell and not ShellMessage.viewclt))) or ShellMessage.indx == 0:
|
||||
from pykms_GuiBase import gui_redirector_setup, gui_redirector_clear
|
||||
gui_redirector_setup()
|
||||
gui_redirector_clear()
|
||||
self.print_logging_setup(ShellMessage.loggersrv_pty, ShellMessage.asyncmsgsrv)
|
||||
self.print_logging_setup(ShellMessage.loggerclt_pty, ShellMessage.asyncmsgclt)
|
||||
|
||||
|
@ -405,7 +400,6 @@ def pretty_printer(**kwargs):
|
|||
if None `put_text` must be defined for printing process.
|
||||
`to_exit ` --> if True system exit is called.
|
||||
`where` --> specifies if message is server-side or client-side
|
||||
(useful for GUI redirect).
|
||||
"""
|
||||
# Set defaults for not defined options.
|
||||
options = {'log_obj' : None,
|
||||
|
|
|
@ -1,948 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from time import sleep
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
from tkinter import filedialog
|
||||
import tkinter.font as tkFont
|
||||
|
||||
from pykms_Server import srv_options, srv_version, srv_config, server_terminate, serverqueue, serverthread
|
||||
from pykms_GuiMisc import ToolTip, TextDoubleScroll, TextRedirect, ListboxOfRadiobuttons
|
||||
from pykms_GuiMisc import custom_background, custom_pages
|
||||
from pykms_Client import clt_options, clt_version, clt_config, client_thread
|
||||
|
||||
gui_version = "py-kms_gui_v3.0"
|
||||
__license__ = "MIT License"
|
||||
__author__ = u"Matteo ℱan <SystemRage@protonmail.com>"
|
||||
__copyright__ = "© Copyright 2020"
|
||||
__url__ = "https://github.com/SystemRage/py-kms"
|
||||
gui_description = "A GUI for py-kms."
|
||||
|
||||
##---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
def get_ip_address():
|
||||
if os.name == 'posix':
|
||||
import subprocess
|
||||
ip = subprocess.getoutput("hostname -I")
|
||||
elif os.name == 'nt':
|
||||
import socket
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
else:
|
||||
ip = 'Unknown'
|
||||
return ip
|
||||
|
||||
def gui_redirector(stream, redirect_to = TextRedirect.Pretty, redirect_conditio = True, stderr_side = "srv"):
|
||||
global txsrv, txclt, txcol
|
||||
if redirect_conditio:
|
||||
if stream == 'stdout':
|
||||
sys.stdout = redirect_to(txsrv, txclt, txcol)
|
||||
elif stream == 'stderr':
|
||||
sys.stderr = redirect_to(txsrv, txclt, txcol, stderr_side)
|
||||
else:
|
||||
stream = redirect_to(txsrv, txclt, txcol)
|
||||
return stream
|
||||
|
||||
def gui_redirector_setup():
|
||||
TextRedirect.Pretty.tag_num = 0
|
||||
TextRedirect.Pretty.newlinecut = [-1, -2, -4, -5]
|
||||
|
||||
def gui_redirector_clear():
|
||||
global txsrv, oysrv
|
||||
try:
|
||||
if oysrv:
|
||||
txsrv.configure(state = 'normal')
|
||||
txsrv.delete('1.0', 'end')
|
||||
txsrv.configure(state = 'disabled')
|
||||
except:
|
||||
# self.onlysrv not defined (menu not used)
|
||||
pass
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class KmsGui(tk.Tk):
|
||||
def __init__(self, *args, **kwargs):
|
||||
tk.Tk.__init__(self, *args, **kwargs)
|
||||
self.wraplength = 200
|
||||
serverthread.with_gui = True
|
||||
self.validation_int = (self.register(self.validate_int), "%S")
|
||||
self.validation_float = (self.register(self.validate_float), "%P")
|
||||
|
||||
## Define fonts and colors.
|
||||
self.customfonts = {'btn' : tkFont.Font(family = 'Fixedsys', size = 11, weight = 'bold'),
|
||||
'oth' : tkFont.Font(family = 'Times', size = 9, weight = 'bold'),
|
||||
'opt' : tkFont.Font(family = 'Fixedsys', size = 9, weight = 'bold'),
|
||||
'lst' : tkFont.Font(family = 'Fixedsys', size = 8, weight = 'bold', slant = 'italic'),
|
||||
'msg' : tkFont.Font(family = 'Monospace', size = 6), # need a monospaced type (like courier, etc..).
|
||||
}
|
||||
|
||||
self.customcolors = { 'black' : '#000000',
|
||||
'white' : '#FFFFFF',
|
||||
'green' : '#00EE76',
|
||||
'yellow' : '#FFFF00',
|
||||
'magenta' : '#CD00CD',
|
||||
'orange' : '#FFA500',
|
||||
'red' : '#FF4500',
|
||||
'blue' : '#1E90FF',
|
||||
'cyan' : '#AFEEEE',
|
||||
'lavender': '#E6E6FA',
|
||||
'brown' : '#A52A2A',
|
||||
}
|
||||
|
||||
self.option_add('*TCombobox*Listbox.font', self.customfonts['lst'])
|
||||
|
||||
self.gui_create()
|
||||
|
||||
def invert(self, widgets = []):
|
||||
for widget in widgets:
|
||||
if widget['state'] == 'normal':
|
||||
widget.configure(state = 'disabled')
|
||||
elif widget['state'] == 'disabled':
|
||||
widget.configure(state = 'normal')
|
||||
|
||||
def gui_menu(self):
|
||||
self.onlysrv, self.onlyclt = (False for _ in range(2))
|
||||
menubar = tk.Menu(self)
|
||||
prefmenu = tk.Menu(menubar, tearoff = 0, font = ("Noto Sans Regular", 10), borderwidth = 3, relief = 'ridge')
|
||||
menubar.add_cascade(label = 'Preferences', menu = prefmenu)
|
||||
prefmenu.add_command(label = 'Enable server-side mode', command = lambda: self.pref_onlysrv(prefmenu))
|
||||
prefmenu.add_command(label = 'Enable client-side mode', command = lambda: self.pref_onlyclt(prefmenu))
|
||||
self.config(menu = menubar)
|
||||
|
||||
def pref_onlysrv(self, menu):
|
||||
global oysrv
|
||||
|
||||
if self.onlyclt or serverthread.is_running_server:
|
||||
return
|
||||
self.onlysrv = not self.onlysrv
|
||||
if self.onlysrv:
|
||||
menu.entryconfigure(0, label = 'Disable server-side mode')
|
||||
self.clt_on_show(force_remove = True)
|
||||
else:
|
||||
menu.entryconfigure(0, label = 'Enable server-side mode')
|
||||
self.invert(widgets = [self.shbtnclt])
|
||||
oysrv = self.onlysrv
|
||||
|
||||
def pref_onlyclt(self, menu):
|
||||
if self.onlysrv or serverthread.is_running_server:
|
||||
return
|
||||
self.onlyclt = not self.onlyclt
|
||||
if self.onlyclt:
|
||||
menu.entryconfigure(1, label = 'Disable client-side mode')
|
||||
if self.shbtnclt['text'] == 'SHOW\nCLIENT':
|
||||
self.clt_on_show(force_view = True)
|
||||
self.optsrvwin.grid_remove()
|
||||
self.msgsrvwin.grid_remove()
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr, stderr_side = "clt")
|
||||
else:
|
||||
menu.entryconfigure(1, label = 'Enable client-side mode')
|
||||
self.optsrvwin.grid()
|
||||
self.msgsrvwin.grid()
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr)
|
||||
|
||||
self.invert(widgets = [self.runbtnsrv, self.shbtnclt, self.runbtnclt])
|
||||
|
||||
def gui_create(self):
|
||||
## Create server gui
|
||||
self.gui_srv()
|
||||
## Create client gui + other operations.
|
||||
self.gui_complete()
|
||||
## Create menu.
|
||||
self.gui_menu()
|
||||
## Create globals for printing process (redirect stdout).
|
||||
global txsrv, txclt, txcol
|
||||
txsrv = self.textboxsrv.get()
|
||||
txclt = self.textboxclt.get()
|
||||
txcol = self.customcolors
|
||||
## Redirect stderr.
|
||||
gui_redirector('stderr', redirect_to = TextRedirect.Stderr)
|
||||
|
||||
def gui_pages_show(self, pagename, side):
|
||||
# https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter
|
||||
# https://www.reddit.com/r/learnpython/comments/7xxtsy/trying_to_understand_tkinter_and_how_to_switch/
|
||||
pageside = self.pagewidgets[side]
|
||||
tk.Misc.lift(pageside["PageWin"][pagename], aboveThis = None)
|
||||
keylist = list(pageside["PageWin"].keys())
|
||||
|
||||
for elem in [pageside["BtnAni"], pageside["LblAni"]]:
|
||||
if pagename == "PageStart":
|
||||
elem["Left"].config(state = "disabled")
|
||||
if len(keylist) == 2:
|
||||
elem["Right"].config(state = "normal")
|
||||
elif pagename == "PageEnd":
|
||||
elem["Right"].config(state = "disabled")
|
||||
if len(keylist) == 2:
|
||||
elem["Left"].config(state = "normal")
|
||||
else:
|
||||
for where in ["Left", "Right"]:
|
||||
elem[where].config(state = "normal")
|
||||
|
||||
if pagename != "PageStart":
|
||||
page_l = keylist[keylist.index(pagename) - 1]
|
||||
pageside["BtnAni"]["Left"]['command'] = lambda pag=page_l, pos=side: self.gui_pages_show(pag, pos)
|
||||
if pagename != "PageEnd":
|
||||
page_r = keylist[keylist.index(pagename) + 1]
|
||||
pageside["BtnAni"]["Right"]['command'] = lambda pag=page_r, pos=side: self.gui_pages_show(pag, pos)
|
||||
|
||||
def gui_pages_buttons(self, parent, side):
|
||||
btnwin = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
btnwin.grid(row = 14, column = 2, padx = 2, pady = 2, sticky = 'nsew')
|
||||
btnwin.grid_columnconfigure(1, weight = 1)
|
||||
self.pagewidgets[side]["BtnWin"] = btnwin
|
||||
|
||||
for position in ["Left", "Right"]:
|
||||
if position == "Left":
|
||||
col = [0, 0, 1]
|
||||
stick = 'e'
|
||||
elif position == "Right":
|
||||
col = [2, 1, 0]
|
||||
stick = 'w'
|
||||
|
||||
aniwin = tk.Canvas(btnwin, background = self.customcolors['white'], borderwidth = 0, relief = 'ridge')
|
||||
aniwin.grid(row = 0, column = col[0], padx = 5, pady = 5, sticky = 'nsew')
|
||||
self.pagewidgets[side]["AniWin"][position] = aniwin
|
||||
|
||||
lblani = tk.Label(aniwin, width = 1, height = 1)
|
||||
lblani.grid(row = 0, column = col[1], padx = 2, pady = 2, sticky = stick)
|
||||
self.pagewidgets[side]["LblAni"][position] = lblani
|
||||
|
||||
btnani = tk.Button(aniwin)
|
||||
btnani.grid(row = 0, column = col[2], padx = 2, pady = 2, sticky = stick)
|
||||
self.pagewidgets[side]["BtnAni"][position] = btnani
|
||||
## Customize buttons.
|
||||
custom_pages(self, side)
|
||||
|
||||
def gui_pages_create(self, parent, side, create = {}):
|
||||
self.pagewidgets.update({side : {"PageWin" : create,
|
||||
"BtnWin" : None,
|
||||
"BtnAni" : {"Left" : None,
|
||||
"Right" : None},
|
||||
"AniWin" : {"Left" : None,
|
||||
"Right" : None},
|
||||
"LblAni" : {"Left" : None,
|
||||
"Right" : None},
|
||||
}
|
||||
})
|
||||
|
||||
for pagename in self.pagewidgets[side]["PageWin"].keys():
|
||||
page = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.pagewidgets[side]["PageWin"][pagename] = page
|
||||
page.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = "nsew")
|
||||
page.grid_columnconfigure(1, weight = 1)
|
||||
self.gui_pages_buttons(parent = parent, side = side)
|
||||
self.gui_pages_show("PageStart", side = side)
|
||||
|
||||
def gui_store(self, side, typewidgets):
|
||||
stored = []
|
||||
for pagename in self.pagewidgets[side]["PageWin"].keys():
|
||||
for widget in self.pagewidgets[side]["PageWin"][pagename].winfo_children():
|
||||
if widget.winfo_class() in typewidgets:
|
||||
stored.append(widget)
|
||||
return stored
|
||||
|
||||
def gui_srv(self):
|
||||
## Create main containers. ------------------------------------------------------------------------------------------------------------------
|
||||
self.masterwin = tk.Canvas(self, borderwidth = 3, relief = tk.RIDGE)
|
||||
self.btnsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.optsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.msgsrvwin = tk.Frame(self.masterwin, background = self.customcolors['black'], relief = 'ridge', width = 300, height = 200)
|
||||
|
||||
## Layout main containers.
|
||||
self.masterwin.grid(row = 0, column = 0, sticky = 'nsew')
|
||||
self.btnsrvwin.grid(row = 0, column = 1, padx = 2, pady = 2, sticky = 'nw')
|
||||
self.optsrvwin.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = 'nsew')
|
||||
self.optsrvwin.grid_rowconfigure(0, weight = 1)
|
||||
self.optsrvwin.grid_columnconfigure(1, weight = 1)
|
||||
|
||||
self.pagewidgets = {}
|
||||
|
||||
## Subpages of "optsrvwin".
|
||||
self.gui_pages_create(parent = self.optsrvwin, side = "Srv", create = {"PageStart": None,
|
||||
"PageEnd": None})
|
||||
|
||||
## Continue to grid.
|
||||
self.msgsrvwin.grid(row = 1, column = 2, padx = 1, pady = 1, sticky = 'nsew')
|
||||
self.msgsrvwin.grid_propagate(False)
|
||||
self.msgsrvwin.grid_columnconfigure(0, weight = 1)
|
||||
self.msgsrvwin.grid_rowconfigure(0, weight = 1)
|
||||
|
||||
## Create widgets (btnsrvwin) ---------------------------------------------------------------------------------------------------------------
|
||||
self.statesrv = tk.Label(self.btnsrvwin, text = 'Server\nState:\nStopped', font = self.customfonts['oth'],
|
||||
foreground = self.customcolors['red'])
|
||||
self.runbtnsrv = tk.Button(self.btnsrvwin, text = 'START\nSERVER', background = self.customcolors['green'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.srv_on_start)
|
||||
self.shbtnclt = tk.Button(self.btnsrvwin, text = 'SHOW\nCLIENT', background = self.customcolors['magenta'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.clt_on_show)
|
||||
self.defaubtnsrv = tk.Button(self.btnsrvwin, text = 'DEFAULTS', background = self.customcolors['brown'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.on_defaults)
|
||||
self.clearbtnsrv = tk.Button(self.btnsrvwin, text = 'CLEAR', background = self.customcolors['orange'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = lambda: self.on_clear([txsrv, txclt]))
|
||||
self.exitbtnsrv = tk.Button(self.btnsrvwin, text = 'EXIT', background = self.customcolors['black'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
command = self.on_exit)
|
||||
|
||||
## Layout widgets (btnsrvwin)
|
||||
self.statesrv.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.runbtnsrv.grid(row = 1, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.shbtnclt.grid(row = 2, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.defaubtnsrv.grid(row = 3, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.clearbtnsrv.grid(row = 4, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
self.exitbtnsrv.grid(row = 5, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
|
||||
## Create widgets (optsrvwin:Srv:PageWin:PageStart) -----------------------------------------------------------------------------------------
|
||||
# Version.
|
||||
ver = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"],
|
||||
text = 'You are running server version: ' + srv_version, font = self.customfonts['oth'],
|
||||
foreground = self.customcolors['red'])
|
||||
# Ip Address.
|
||||
srvipaddlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.customfonts['opt'])
|
||||
self.srvipadd = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'ip')
|
||||
self.srvipadd.insert('end', srv_options['ip']['def'])
|
||||
ToolTip(self.srvipadd, text = srv_options['ip']['help'], wraplength = self.wraplength)
|
||||
myipadd = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Your IP address is: {}'.format(get_ip_address()),
|
||||
font = self.customfonts['oth'], foreground = self.customcolors['red'])
|
||||
# Port.
|
||||
srvportlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Port: ', font = self.customfonts['opt'])
|
||||
self.srvport = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'port',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.srvport.insert('end', str(srv_options['port']['def']))
|
||||
ToolTip(self.srvport, text = srv_options['port']['help'], wraplength = self.wraplength)
|
||||
# EPID.
|
||||
epidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'EPID: ', font = self.customfonts['opt'])
|
||||
self.epid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'epid')
|
||||
self.epid.insert('end', str(srv_options['epid']['def']))
|
||||
ToolTip(self.epid, text = srv_options['epid']['help'], wraplength = self.wraplength)
|
||||
# LCID.
|
||||
lcidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'LCID: ', font = self.customfonts['opt'])
|
||||
self.lcid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lcid',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.lcid.insert('end', str(srv_options['lcid']['def']))
|
||||
ToolTip(self.lcid, text = srv_options['lcid']['help'], wraplength = self.wraplength)
|
||||
# HWID.
|
||||
hwidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'HWID: ', font = self.customfonts['opt'])
|
||||
self.hwid = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = (str(srv_options['hwid']['def']), 'RANDOM'),
|
||||
width = 17, height = 10, font = self.customfonts['lst'], name = 'hwid')
|
||||
self.hwid.set(str(srv_options['hwid']['def']))
|
||||
ToolTip(self.hwid, text = srv_options['hwid']['help'], wraplength = self.wraplength)
|
||||
# Client Count
|
||||
countlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Client Count: ', font = self.customfonts['opt'])
|
||||
self.count = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'count')
|
||||
self.count.insert('end', str(srv_options['count']['def']))
|
||||
ToolTip(self.count, text = srv_options['count']['help'], wraplength = self.wraplength)
|
||||
# Activation Interval.
|
||||
activlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Activation Interval: ', font = self.customfonts['opt'])
|
||||
self.activ = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'activation',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.activ.insert('end', str(srv_options['activation']['def']))
|
||||
ToolTip(self.activ, text = srv_options['activation']['help'], wraplength = self.wraplength)
|
||||
# Renewal Interval.
|
||||
renewlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Renewal Interval: ', font = self.customfonts['opt'])
|
||||
self.renew = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'renewal',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.renew.insert('end', str(srv_options['renewal']['def']))
|
||||
ToolTip(self.renew, text = srv_options['renewal']['help'], wraplength = self.wraplength)
|
||||
# Logfile.
|
||||
srvfilelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.customfonts['opt'])
|
||||
self.srvfile = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lfile')
|
||||
self.srvfile.insert('end', srv_options['lfile']['def'])
|
||||
self.srvfile.xview_moveto(1)
|
||||
ToolTip(self.srvfile, text = srv_options['lfile']['help'], wraplength = self.wraplength)
|
||||
srvfilebtnwin = tk.Button(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Browse', font = self.customfonts['opt'],
|
||||
command = lambda: self.on_browse(self.srvfile, srv_options))
|
||||
# Loglevel.
|
||||
srvlevellbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.customfonts['opt'])
|
||||
self.srvlevel = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = tuple(srv_options['llevel']['choi']),
|
||||
width = 10, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'llevel')
|
||||
self.srvlevel.set(srv_options['llevel']['def'])
|
||||
ToolTip(self.srvlevel, text = srv_options['llevel']['help'], wraplength = self.wraplength)
|
||||
# Logsize.
|
||||
srvsizelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.customfonts['opt'])
|
||||
self.srvsize = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lsize',
|
||||
validate = "key", validatecommand = self.validation_float)
|
||||
self.srvsize.insert('end', srv_options['lsize']['def'])
|
||||
ToolTip(self.srvsize, text = srv_options['lsize']['help'], wraplength = self.wraplength)
|
||||
# Asynchronous messages.
|
||||
self.chkvalsrvasy = tk.BooleanVar()
|
||||
self.chkvalsrvasy.set(srv_options['asyncmsg']['def'])
|
||||
chksrvasy = tk.Checkbutton(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Async\nMsg',
|
||||
font = self.customfonts['opt'], var = self.chkvalsrvasy, relief = 'groove', name = 'asyncmsg')
|
||||
ToolTip(chksrvasy, text = srv_options['asyncmsg']['help'], wraplength = self.wraplength)
|
||||
|
||||
# Listbox radiobuttons server.
|
||||
self.chksrvfile = ListboxOfRadiobuttons(self.pagewidgets["Srv"]["PageWin"]["PageStart"],
|
||||
['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'],
|
||||
self.customfonts['lst'],
|
||||
changed = [(self.srvfile, srv_options['lfile']['def']),
|
||||
(srvfilebtnwin, ''),
|
||||
(self.srvsize, srv_options['lsize']['def']),
|
||||
(self.srvlevel, srv_options['llevel']['def'])],
|
||||
width = 10, height = 1, borderwidth = 2, relief = 'ridge')
|
||||
|
||||
## Layout widgets (optsrvwin:Srv:PageWin:PageStart)
|
||||
ver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
myipadd.grid(row = 2, column = 1, columnspan = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvportlbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvport.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
epidlbl.grid(row = 4, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.epid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
lcidlbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.lcid.grid(row = 5, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
hwidlbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.hwid.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
countlbl.grid(row = 7, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.count.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
activlbl.grid(row = 8, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.activ.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
renewlbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.renew.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvfilelbl.grid(row = 10, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvfile.grid(row = 10, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvfilebtnwin.grid(row = 10, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
self.chksrvfile.grid(row = 11, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
chksrvasy.grid(row = 11, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvlevellbl.grid(row = 12, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvlevel.grid(row = 12, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
srvsizelbl.grid(row = 13, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvsize.grid(row = 13, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
|
||||
## Create widgets (optsrvwin:Srv:PageWin:PageEnd)-------------------------------------------------------------------------------------------
|
||||
# Timeout connection.
|
||||
srvtimeout0lbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Timeout connection: ', font = self.customfonts['opt'])
|
||||
self.srvtimeout0 = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time0')
|
||||
self.srvtimeout0.insert('end', str(srv_options['time0']['def']))
|
||||
ToolTip(self.srvtimeout0, text = srv_options['time0']['help'], wraplength = self.wraplength)
|
||||
# Timeout send/recv.
|
||||
srvtimeout1lbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Timeout send-recv: ', font = self.customfonts['opt'])
|
||||
self.srvtimeout1 = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time1')
|
||||
self.srvtimeout1.insert('end', str(srv_options['time1']['def']))
|
||||
ToolTip(self.srvtimeout1, text = srv_options['time1']['help'], wraplength = self.wraplength)
|
||||
# Sqlite database.
|
||||
self.chkvalsql = tk.BooleanVar()
|
||||
self.chkvalsql.set(srv_options['sql']['def'])
|
||||
self.chkfilesql = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'sql')
|
||||
self.chkfilesql.insert('end', srv_options['sql']['file'])
|
||||
self.chkfilesql.xview_moveto(1)
|
||||
self.chkfilesql.configure(state = 'disabled')
|
||||
|
||||
chksql = tk.Checkbutton(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Create Sqlite\nDatabase',
|
||||
font = self.customfonts['opt'], var = self.chkvalsql, relief = 'groove',
|
||||
command = lambda: self.sql_status())
|
||||
ToolTip(chksql, text = srv_options['sql']['help'], wraplength = self.wraplength)
|
||||
|
||||
## Layout widgets (optsrvwin:Srv:PageWin:PageEnd)
|
||||
# a label for vertical aligning with PageStart
|
||||
tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 0,
|
||||
height = 0, bg = self.customcolors['lavender']).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw')
|
||||
srvtimeout0lbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvtimeout0.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
srvtimeout1lbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.srvtimeout1.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
chksql.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.chkfilesql.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
|
||||
# Store server-side widgets.
|
||||
self.storewidgets_srv = self.gui_store(side = "Srv", typewidgets = ['Button', 'Entry', 'TCombobox', 'Checkbutton'])
|
||||
self.storewidgets_srv.append(self.chksrvfile)
|
||||
|
||||
## Create widgets and layout (msgsrvwin) ---------------------------------------------------------------------------------------------------
|
||||
self.textboxsrv = TextDoubleScroll(self.msgsrvwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled',
|
||||
relief = 'ridge', font = self.customfonts['msg'])
|
||||
self.textboxsrv.put()
|
||||
|
||||
def sql_status(self):
|
||||
if self.chkvalsql.get():
|
||||
self.chkfilesql.configure(state = 'normal')
|
||||
else:
|
||||
self.chkfilesql.insert('end', srv_options['sql']['file'])
|
||||
self.chkfilesql.xview_moveto(1)
|
||||
self.chkfilesql.configure(state = 'disabled')
|
||||
|
||||
def always_centered(self, geo, centered, refs):
|
||||
x = (self.winfo_screenwidth() // 2) - (self.winfo_width() // 2)
|
||||
y = (self.winfo_screenheight() // 2) - (self.winfo_height() // 2)
|
||||
w, h, dx, dy = geo.split('+')[0].split('x') + geo.split('+')[1:]
|
||||
|
||||
if w == refs[1]:
|
||||
if centered:
|
||||
self.geometry('+%d+%d' %(x, y))
|
||||
centered = False
|
||||
elif w == refs[0]:
|
||||
if not centered:
|
||||
self.geometry('+%d+%d' %(x, y))
|
||||
centered = True
|
||||
|
||||
if dx != str(x) or dy != str(y):
|
||||
self.geometry('+%d+%d' %(x, 0))
|
||||
|
||||
self.after(200, self.always_centered, self.geometry(), centered, refs)
|
||||
|
||||
def gui_complete(self):
|
||||
## Create client widgets (optcltwin, msgcltwin, btncltwin)
|
||||
self.update_idletasks() # update Gui to get btnsrvwin values --> btncltwin.
|
||||
minw, minh = self.winfo_width(), self.winfo_height()
|
||||
self.iconify()
|
||||
self.gui_clt()
|
||||
maxw, minh = self.winfo_width(), self.winfo_height()
|
||||
## Main window custom background.
|
||||
self.update_idletasks() # update Gui for custom background
|
||||
self.iconify()
|
||||
custom_background(self)
|
||||
## Main window other modifications.
|
||||
self.eval('tk::PlaceWindow %s center' %self.winfo_pathname(self.winfo_id()))
|
||||
self.wm_attributes("-topmost", True)
|
||||
self.protocol("WM_DELETE_WINDOW", lambda: 0)
|
||||
## Disable maximize button.
|
||||
self.resizable(False, False)
|
||||
## Centered window.
|
||||
self.always_centered(self.geometry(), False, [minw, maxw])
|
||||
|
||||
def get_position(self, widget):
|
||||
x, y = (widget.winfo_x(), widget.winfo_y())
|
||||
w, h = (widget.winfo_width(), widget.winfo_height())
|
||||
return x, y, w, h
|
||||
|
||||
def gui_clt(self):
|
||||
self.count_clear, self.keep_clear = (0, '0.0')
|
||||
self.optcltwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
self.msgcltwin = tk.Frame(self.masterwin, background = self.customcolors['black'], relief = 'ridge', width = 300, height = 200)
|
||||
self.btncltwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge')
|
||||
|
||||
xb, yb, wb, hb = self.get_position(self.btnsrvwin)
|
||||
self.btncltwin_X = xb
|
||||
self.btncltwin_Y = yb + hb + 6
|
||||
self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'outside', anchor = 'center')
|
||||
|
||||
self.optcltwin.grid(row = 0, column = 4, padx = 2, pady = 2, sticky = 'nsew')
|
||||
self.optcltwin.grid_rowconfigure(0, weight = 1)
|
||||
self.optcltwin.grid_columnconfigure(1, weight = 1)
|
||||
|
||||
## Subpages of "optcltwin".
|
||||
self.gui_pages_create(parent = self.optcltwin, side = "Clt", create = {"PageStart": None,
|
||||
"PageEnd": None})
|
||||
|
||||
## Continue to grid.
|
||||
self.msgcltwin.grid(row = 1, column = 4, padx = 1, pady = 1, sticky = 'nsew')
|
||||
self.msgcltwin.grid_propagate(False)
|
||||
self.msgcltwin.grid_columnconfigure(0, weight = 1)
|
||||
self.msgcltwin.grid_rowconfigure(0, weight = 1)
|
||||
|
||||
## Create widgets (btncltwin) ----------------------------------------------------------------------------------------------------------------
|
||||
self.runbtnclt = tk.Button(self.btncltwin, text = 'START\nCLIENT', background = self.customcolors['blue'],
|
||||
foreground = self.customcolors['white'], relief = 'raised', font = self.customfonts['btn'],
|
||||
state = 'disabled', command = self.clt_on_start, width = 8, height = 2)
|
||||
|
||||
## Layout widgets (btncltwin)
|
||||
self.runbtnclt.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew')
|
||||
|
||||
## Create widgets (optcltwin:Clt:PageWin:PageStart) ------------------------------------------------------------------------------------------
|
||||
# Version.
|
||||
cltver = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'You are running client version: ' + clt_version,
|
||||
font = self.customfonts['oth'], foreground = self.customcolors['red'])
|
||||
# Ip Address.
|
||||
cltipaddlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.customfonts['opt'])
|
||||
self.cltipadd = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'ip')
|
||||
self.cltipadd.insert('end', clt_options['ip']['def'])
|
||||
ToolTip(self.cltipadd, text = clt_options['ip']['help'], wraplength = self.wraplength)
|
||||
# Port.
|
||||
cltportlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Port: ', font = self.customfonts['opt'])
|
||||
self.cltport = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'port',
|
||||
validate = "key", validatecommand = self.validation_int)
|
||||
self.cltport.insert('end', str(clt_options['port']['def']))
|
||||
ToolTip(self.cltport, text = clt_options['port']['help'], wraplength = self.wraplength)
|
||||
# Mode.
|
||||
cltmodelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Mode: ', font = self.customfonts['opt'])
|
||||
self.cltmode = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['mode']['choi']),
|
||||
width = 17, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'mode')
|
||||
self.cltmode.set(clt_options['mode']['def'])
|
||||
ToolTip(self.cltmode, text = clt_options['mode']['help'], wraplength = self.wraplength)
|
||||
# CMID.
|
||||
cltcmidlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'CMID: ', font = self.customfonts['opt'])
|
||||
self.cltcmid = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'cmid')
|
||||
self.cltcmid.insert('end', str(clt_options['cmid']['def']))
|
||||
ToolTip(self.cltcmid, text = clt_options['cmid']['help'], wraplength = self.wraplength)
|
||||
# Machine Name.
|
||||
cltnamelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Machine Name: ', font = self.customfonts['opt'])
|
||||
self.cltname = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'name')
|
||||
self.cltname.insert('end', str(clt_options['name']['def']))
|
||||
ToolTip(self.cltname, text = clt_options['name']['help'], wraplength = self.wraplength)
|
||||
# Logfile.
|
||||
cltfilelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.customfonts['opt'])
|
||||
self.cltfile = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lfile')
|
||||
self.cltfile.insert('end', clt_options['lfile']['def'])
|
||||
self.cltfile.xview_moveto(1)
|
||||
ToolTip(self.cltfile, text = clt_options['lfile']['help'], wraplength = self.wraplength)
|
||||
cltfilebtnwin = tk.Button(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Browse', font = self.customfonts['opt'],
|
||||
command = lambda: self.on_browse(self.cltfile, clt_options))
|
||||
# Loglevel.
|
||||
cltlevellbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.customfonts['opt'])
|
||||
self.cltlevel = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['llevel']['choi']),
|
||||
width = 10, height = 10, font = self.customfonts['lst'], state = "readonly", name = 'llevel')
|
||||
self.cltlevel.set(clt_options['llevel']['def'])
|
||||
ToolTip(self.cltlevel, text = clt_options['llevel']['help'], wraplength = self.wraplength)
|
||||
# Logsize.
|
||||
cltsizelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.customfonts['opt'])
|
||||
self.cltsize = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.customfonts['opt'], name = 'lsize',
|
||||
validate = "key", validatecommand = self.validation_float)
|
||||
self.cltsize.insert('end', clt_options['lsize']['def'])
|
||||
ToolTip(self.cltsize, text = clt_options['lsize']['help'], wraplength = self.wraplength)
|
||||
# Asynchronous messages.
|
||||
self.chkvalcltasy = tk.BooleanVar()
|
||||
self.chkvalcltasy.set(clt_options['asyncmsg']['def'])
|
||||
chkcltasy = tk.Checkbutton(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Async\nMsg',
|
||||
font = self.customfonts['opt'], var = self.chkvalcltasy, relief = 'groove', name = 'asyncmsg')
|
||||
ToolTip(chkcltasy, text = clt_options['asyncmsg']['help'], wraplength = self.wraplength)
|
||||
|
||||
# Listbox radiobuttons client.
|
||||
self.chkcltfile = ListboxOfRadiobuttons(self.pagewidgets["Clt"]["PageWin"]["PageStart"],
|
||||
['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'],
|
||||
self.customfonts['lst'],
|
||||
changed = [(self.cltfile, clt_options['lfile']['def']),
|
||||
(cltfilebtnwin, ''),
|
||||
(self.cltsize, clt_options['lsize']['def']),
|
||||
(self.cltlevel, clt_options['llevel']['def'])],
|
||||
width = 10, height = 1, borderwidth = 2, relief = 'ridge')
|
||||
|
||||
## Layout widgets (optcltwin:Clt:PageWin:PageStart)
|
||||
cltver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltportlbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltport.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltmodelbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltmode.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltcmidlbl.grid(row = 4, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltcmid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltnamelbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltname.grid(row = 5, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltfilelbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltfile.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltfilebtnwin.grid(row = 6, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
self.chkcltfile.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
chkcltasy.grid(row = 7, column = 2, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltlevellbl.grid(row = 8, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltlevel.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
cltsizelbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.cltsize.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew')
|
||||
|
||||
# ugly fix when client-side mode is activated.
|
||||
templbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"],
|
||||
bg = self.customcolors['lavender']).grid(row = 10, column = 0,
|
||||
padx = 35, pady = 54, sticky = 'e')
|
||||
|
||||
## Create widgets (optcltwin:Clt:PageWin:PageEnd) -------------------------------------------------------------------------------------------
|
||||
# Timeout connection.
|
||||
clttimeout0lbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], text = 'Timeout connection: ', font = self.customfonts['opt'])
|
||||
self.clttimeout0 = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time0')
|
||||
self.clttimeout0.insert('end', str(clt_options['time0']['def']))
|
||||
ToolTip(self.clttimeout0, text = clt_options['time0']['help'], wraplength = self.wraplength)
|
||||
# Timeout send/recv.
|
||||
clttimeout1lbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], text = 'Timeout send-recv: ', font = self.customfonts['opt'])
|
||||
self.clttimeout1 = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 16, font = self.customfonts['opt'], name = 'time1')
|
||||
self.clttimeout1.insert('end', str(clt_options['time1']['def']))
|
||||
ToolTip(self.clttimeout1, text = clt_options['time1']['help'], wraplength = self.wraplength)
|
||||
|
||||
## Layout widgets (optcltwin:Clt:PageWin:PageEnd)
|
||||
# a label for vertical aligning with PageStart
|
||||
tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 0,
|
||||
height = 0, bg = self.customcolors['lavender']).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw')
|
||||
clttimeout0lbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.clttimeout0.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
clttimeout1lbl.grid(row = 2, column = 0, padx = 5, pady = 5, sticky = 'e')
|
||||
self.clttimeout1.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'w')
|
||||
|
||||
## Store client-side widgets.
|
||||
self.storewidgets_clt = self.gui_store(side = "Clt", typewidgets = ['Button', 'Entry', 'TCombobox', 'Checkbutton'])
|
||||
self.storewidgets_clt.append(self.chkcltfile)
|
||||
|
||||
## Create widgets and layout (msgcltwin) -----------------------------------------------------------------------------------------------------
|
||||
self.textboxclt = TextDoubleScroll(self.msgcltwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled',
|
||||
relief = 'ridge', font = self.customfonts['msg'])
|
||||
self.textboxclt.put()
|
||||
|
||||
def prep_option(self, value):
|
||||
try:
|
||||
# is an INT
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
# is a FLOAT
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
# is a STRING.
|
||||
return value
|
||||
|
||||
def prep_logfile(self, filepath, status):
|
||||
# FILE (pretty on, log view off, logfile yes)
|
||||
# FILEOFF (pretty on, log view off, no logfile)
|
||||
# STDOUT (pretty off, log view on, no logfile)
|
||||
# STDOUTOFF (pretty off, log view off, logfile yes)
|
||||
# FILESTDOUT (pretty off, log view on, logfile yes)
|
||||
|
||||
if status == 'FILE':
|
||||
return filepath
|
||||
elif status in ['FILESTDOUT', 'STDOUTOFF']:
|
||||
return [status, filepath]
|
||||
elif status in ['STDOUT', 'FILEOFF']:
|
||||
return status
|
||||
|
||||
def validate_int(self, value):
|
||||
return value == "" or value.isdigit()
|
||||
|
||||
def validate_float(self, value):
|
||||
if value == "":
|
||||
return True
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def clt_on_show(self, force_remove = False, force_view = False):
|
||||
if self.optcltwin.winfo_ismapped() or force_remove:
|
||||
self.shbtnclt.configure(text = 'SHOW\nCLIENT', relief = 'raised')
|
||||
self.optcltwin.grid_remove()
|
||||
self.msgcltwin.grid_remove()
|
||||
self.btncltwin.place_forget()
|
||||
elif not self.optcltwin.winfo_ismapped() or force_view:
|
||||
self.shbtnclt.configure(text = 'HIDE\nCLIENT', relief = 'sunken')
|
||||
self.optcltwin.grid()
|
||||
self.msgcltwin.grid()
|
||||
self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'inside', anchor = 'nw')
|
||||
|
||||
def srv_on_start(self):
|
||||
if self.runbtnsrv['text'] == 'START\nSERVER':
|
||||
self.on_clear([txsrv, txclt])
|
||||
self.srv_actions_start()
|
||||
# wait for switch.
|
||||
while not serverthread.is_running_server:
|
||||
pass
|
||||
|
||||
self.srv_toggle_all(on_start = True)
|
||||
# run thread for interrupting server when an error happens.
|
||||
self.srv_eject_thread = threading.Thread(target = self.srv_eject, name = "Thread-SrvEjt")
|
||||
self.srv_eject_thread.setDaemon(True)
|
||||
self.srv_eject_thread.start()
|
||||
|
||||
elif self.runbtnsrv['text'] == 'STOP\nSERVER':
|
||||
serverthread.terminate_eject()
|
||||
|
||||
def srv_eject(self):
|
||||
while not serverthread.eject:
|
||||
sleep(0.1)
|
||||
self.srv_actions_stop()
|
||||
|
||||
def srv_actions_start(self):
|
||||
srv_config[srv_options['ip']['des']] = self.srvipadd.get()
|
||||
srv_config[srv_options['port']['des']] = self.prep_option(self.srvport.get())
|
||||
srv_config[srv_options['epid']['des']] = self.epid.get()
|
||||
srv_config[srv_options['lcid']['des']] = self.prep_option(self.lcid.get())
|
||||
srv_config[srv_options['hwid']['des']] = self.hwid.get()
|
||||
srv_config[srv_options['count']['des']] = self.prep_option(self.count.get())
|
||||
srv_config[srv_options['activation']['des']] = self.prep_option(self.activ.get())
|
||||
srv_config[srv_options['renewal']['des']] = self.prep_option(self.renew.get())
|
||||
srv_config[srv_options['lfile']['des']] = self.prep_logfile(self.srvfile.get(), self.chksrvfile.state())
|
||||
srv_config[srv_options['asyncmsg']['des']] = self.chkvalsrvasy.get()
|
||||
srv_config[srv_options['llevel']['des']] = self.srvlevel.get()
|
||||
srv_config[srv_options['lsize']['des']] = self.prep_option(self.srvsize.get())
|
||||
|
||||
srv_config[srv_options['time0']['des']] = self.prep_option(self.srvtimeout0.get())
|
||||
srv_config[srv_options['time1']['des']] = self.prep_option(self.srvtimeout1.get())
|
||||
srv_config[srv_options['sql']['des']] = (self.chkfilesql.get() if self.chkvalsql.get() else self.chkvalsql.get())
|
||||
|
||||
## Redirect stdout.
|
||||
gui_redirector('stdout', redirect_to = TextRedirect.Log,
|
||||
redirect_conditio = (srv_config[srv_options['lfile']['des']] in ['STDOUT', 'FILESTDOUT']))
|
||||
serverqueue.put('start')
|
||||
|
||||
def srv_actions_stop(self):
|
||||
if serverthread.is_running_server:
|
||||
if serverthread.server is not None:
|
||||
server_terminate(serverthread, exit_server = True)
|
||||
# wait for switch.
|
||||
while serverthread.is_running_server:
|
||||
pass
|
||||
else:
|
||||
serverthread.is_running_server = False
|
||||
self.srv_toggle_all(on_start = False)
|
||||
self.count_clear, self.keep_clear = (0, '0.0')
|
||||
|
||||
def srv_toggle_all(self, on_start = True):
|
||||
self.srv_toggle_state()
|
||||
if on_start:
|
||||
self.runbtnsrv.configure(text = 'STOP\nSERVER', background = self.customcolors['red'],
|
||||
foreground = self.customcolors['white'], relief = 'sunken')
|
||||
for widget in self.storewidgets_srv:
|
||||
widget.configure(state = 'disabled')
|
||||
self.runbtnclt.configure(state = 'normal')
|
||||
else:
|
||||
self.runbtnsrv.configure(text = 'START\nSERVER', background = self.customcolors['green'],
|
||||
foreground = self.customcolors['white'], relief = 'raised')
|
||||
for widget in self.storewidgets_srv:
|
||||
widget.configure(state = 'normal')
|
||||
if isinstance(widget, ListboxOfRadiobuttons):
|
||||
widget.change()
|
||||
self.runbtnclt.configure(state = 'disabled')
|
||||
|
||||
def srv_toggle_state(self):
|
||||
if serverthread.is_running_server:
|
||||
txt, color = ('Server\nState:\nServing', self.customcolors['green'])
|
||||
else:
|
||||
txt, color = ('Server\nState:\nStopped', self.customcolors['red'])
|
||||
|
||||
self.statesrv.configure(text = txt, foreground = color)
|
||||
|
||||
def clt_on_start(self):
|
||||
if self.onlyclt:
|
||||
self.on_clear([txclt])
|
||||
else:
|
||||
rng, add_newline = self.on_clear_setup()
|
||||
self.on_clear([txsrv, txclt], clear_range = [rng, None], newline_list = [add_newline, False])
|
||||
|
||||
self.runbtnclt.configure(relief = 'sunken')
|
||||
self.clt_actions_start()
|
||||
# run thread for disabling interrupt server and client, when client running.
|
||||
self.clt_eject_thread = threading.Thread(target = self.clt_eject, name = "Thread-CltEjt")
|
||||
self.clt_eject_thread.setDaemon(True)
|
||||
self.clt_eject_thread.start()
|
||||
|
||||
for widget in self.storewidgets_clt + [self.runbtnsrv, self.runbtnclt, self.defaubtnsrv]:
|
||||
widget.configure(state = 'disabled')
|
||||
self.runbtnclt.configure(relief = 'raised')
|
||||
|
||||
def clt_actions_start(self):
|
||||
clt_config[clt_options['ip']['des']] = self.cltipadd.get()
|
||||
clt_config[clt_options['port']['des']] = self.prep_option(self.cltport.get())
|
||||
clt_config[clt_options['mode']['des']] = self.cltmode.get()
|
||||
clt_config[clt_options['cmid']['des']] = self.cltcmid.get()
|
||||
clt_config[clt_options['name']['des']] = self.cltname.get()
|
||||
clt_config[clt_options['lfile']['des']] = self.prep_logfile(self.cltfile.get(), self.chkcltfile.state())
|
||||
clt_config[clt_options['asyncmsg']['des']] = self.chkvalcltasy.get()
|
||||
clt_config[clt_options['llevel']['des']] = self.cltlevel.get()
|
||||
clt_config[clt_options['lsize']['des']] = self.prep_option(self.cltsize.get())
|
||||
|
||||
clt_config[clt_options['time0']['des']] = self.prep_option(self.clttimeout0.get())
|
||||
clt_config[clt_options['time1']['des']] = self.prep_option(self.clttimeout1.get())
|
||||
|
||||
## Redirect stdout.
|
||||
gui_redirector('stdout', redirect_to = TextRedirect.Log,
|
||||
redirect_conditio = (clt_config[clt_options['lfile']['des']] in ['STDOUT', 'FILESTDOUT']))
|
||||
|
||||
# run client (in a thread).
|
||||
self.clientthread = client_thread(name = "Thread-Clt")
|
||||
self.clientthread.setDaemon(True)
|
||||
self.clientthread.with_gui = True
|
||||
self.clientthread.start()
|
||||
|
||||
def clt_eject(self):
|
||||
while self.clientthread.is_alive():
|
||||
sleep(0.1)
|
||||
|
||||
widgets = self.storewidgets_clt + [self.runbtnclt] + [self.defaubtnsrv]
|
||||
if not self.onlyclt:
|
||||
widgets += [self.runbtnsrv]
|
||||
|
||||
for widget in widgets:
|
||||
if isinstance(widget, ttk.Combobox):
|
||||
widget.configure(state = 'readonly')
|
||||
else:
|
||||
widget.configure(state = 'normal')
|
||||
if isinstance(widget, ListboxOfRadiobuttons):
|
||||
widget.change()
|
||||
|
||||
def on_browse(self, entrywidget, options):
|
||||
path = filedialog.askdirectory()
|
||||
if os.path.isdir(path):
|
||||
entrywidget.delete('0', 'end')
|
||||
entrywidget.insert('end', path + os.sep + os.path.basename(options['lfile']['def']))
|
||||
|
||||
def on_exit(self):
|
||||
if serverthread.is_running_server:
|
||||
if serverthread.server is not None:
|
||||
server_terminate(serverthread, exit_server = True)
|
||||
else:
|
||||
serverthread.is_running_server = False
|
||||
server_terminate(serverthread, exit_thread = True)
|
||||
self.destroy()
|
||||
|
||||
def on_clear_setup(self):
|
||||
if any(opt in ['STDOUT', 'FILESTDOUT'] for opt in srv_config[srv_options['lfile']['des']]):
|
||||
add_newline = True
|
||||
if self.count_clear == 0:
|
||||
self.keep_clear = txsrv.index('end-1c')
|
||||
else:
|
||||
add_newline = False
|
||||
if self.count_clear == 0:
|
||||
self.keep_clear = txsrv.index('end')
|
||||
|
||||
rng = [self.keep_clear, 'end']
|
||||
self.count_clear += 1
|
||||
|
||||
return rng, add_newline
|
||||
|
||||
def on_clear(self, widget_list, clear_range = None, newline_list = []):
|
||||
if newline_list == []:
|
||||
newline_list = len(widget_list) * [False]
|
||||
|
||||
for num, couple in enumerate(zip(widget_list, newline_list)):
|
||||
widget, add_n = couple
|
||||
try:
|
||||
ini, fin = clear_range[num]
|
||||
except TypeError:
|
||||
ini, fin = '1.0', 'end'
|
||||
|
||||
widget.configure(state = 'normal')
|
||||
widget.delete(ini, fin)
|
||||
if add_n:
|
||||
widget.insert('end', '\n')
|
||||
widget.configure(state = 'disabled')
|
||||
|
||||
def on_defaults(self):
|
||||
|
||||
def put_defaults(widgets, chkasy, listofradio, options):
|
||||
for widget in widgets:
|
||||
wclass, wname = widget.winfo_class(), widget.winfo_name()
|
||||
if wname == '!checkbutton':
|
||||
continue
|
||||
|
||||
opt = options[wname]['def']
|
||||
if wclass == 'Entry':
|
||||
widget.delete(0, 'end')
|
||||
if wname == 'sql':
|
||||
self.chkvalsql.set(opt)
|
||||
self.sql_status()
|
||||
else:
|
||||
widget.insert('end', (opt if isinstance(opt, str) else str(opt)))
|
||||
elif wclass == 'Checkbutton':
|
||||
if wname == 'asyncmsg':
|
||||
chkasy.set(opt)
|
||||
elif wclass == 'TCombobox':
|
||||
widget.set(str(opt))
|
||||
|
||||
# ListboxOfRadiobuttons default.
|
||||
listofradio.radiovar.set('FILE')
|
||||
listofradio.textbox.yview_moveto(0)
|
||||
listofradio.change()
|
||||
|
||||
if self.runbtnsrv['text'] == 'START\nSERVER':
|
||||
apply_default = zip(["Srv", "Clt"],
|
||||
[self.chkvalsrvasy, self.chkvalcltasy],
|
||||
[self.chksrvfile, self.chkcltfile],
|
||||
[srv_options, clt_options])
|
||||
elif self.runbtnsrv['text'] == 'STOP\nSERVER':
|
||||
apply_default = zip(*[("Clt",),
|
||||
(self.chkvalcltasy,),
|
||||
(self.chkcltfile,),
|
||||
(clt_options,)])
|
||||
|
||||
for side, chkasy, listofradio, options in apply_default:
|
||||
widgets = self.gui_store(side = side, typewidgets = ['Entry', 'TCombobox', 'Checkbutton'])
|
||||
put_defaults(widgets, chkasy, listofradio, options)
|
|
@ -1,517 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from time import sleep
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import tkinter.font as tkFont
|
||||
|
||||
from pykms_Format import MsgMap, unshell_message, unformat_message
|
||||
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter
|
||||
class ToolTip(object):
|
||||
""" Create a tooltip for a given widget """
|
||||
def __init__(self, widget, bg = '#FFFFEA', pad = (5, 3, 5, 3), text = 'widget info', waittime = 400, wraplength = 250):
|
||||
self.waittime = waittime # ms
|
||||
self.wraplength = wraplength # pixels
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.widget.bind("<Enter>", self.onEnter)
|
||||
self.widget.bind("<Leave>", self.onLeave)
|
||||
self.widget.bind("<ButtonPress>", self.onLeave)
|
||||
self.bg = bg
|
||||
self.pad = pad
|
||||
self.id = None
|
||||
self.tw = None
|
||||
|
||||
def onEnter(self, event = None):
|
||||
self.schedule()
|
||||
|
||||
def onLeave(self, event = None):
|
||||
self.unschedule()
|
||||
self.hide()
|
||||
|
||||
def schedule(self):
|
||||
self.unschedule()
|
||||
self.id = self.widget.after(self.waittime, self.show)
|
||||
|
||||
def unschedule(self):
|
||||
id_ = self.id
|
||||
self.id = None
|
||||
if id_:
|
||||
self.widget.after_cancel(id_)
|
||||
|
||||
def show(self):
|
||||
def tip_pos_calculator(widget, label, tip_delta = (10, 5), pad = (5, 3, 5, 3)):
|
||||
w = widget
|
||||
s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight()
|
||||
width, height = (pad[0] + label.winfo_reqwidth() + pad[2],
|
||||
pad[1] + label.winfo_reqheight() + pad[3])
|
||||
mouse_x, mouse_y = w.winfo_pointerxy()
|
||||
x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
|
||||
x2, y2 = x1 + width, y1 + height
|
||||
|
||||
x_delta = x2 - s_width
|
||||
if x_delta < 0:
|
||||
x_delta = 0
|
||||
y_delta = y2 - s_height
|
||||
if y_delta < 0:
|
||||
y_delta = 0
|
||||
|
||||
offscreen = (x_delta, y_delta) != (0, 0)
|
||||
|
||||
if offscreen:
|
||||
if x_delta:
|
||||
x1 = mouse_x - tip_delta[0] - width
|
||||
if y_delta:
|
||||
y1 = mouse_y - tip_delta[1] - height
|
||||
|
||||
offscreen_again = y1 < 0 # out on the top
|
||||
|
||||
if offscreen_again:
|
||||
# No further checks will be done.
|
||||
|
||||
# TIP:
|
||||
# A further mod might automagically augment the
|
||||
# wraplength when the tooltip is too high to be
|
||||
# kept inside the screen.
|
||||
y1 = 0
|
||||
|
||||
return x1, y1
|
||||
|
||||
bg = self.bg
|
||||
pad = self.pad
|
||||
widget = self.widget
|
||||
|
||||
# creates a toplevel window
|
||||
self.tw = tk.Toplevel(widget)
|
||||
|
||||
# leaves only the label and removes the app window
|
||||
self.tw.wm_overrideredirect(True)
|
||||
|
||||
win = tk.Frame(self.tw, background = bg, borderwidth = 0)
|
||||
label = ttk.Label(win, text = self.text, justify = tk.LEFT, background = bg, relief = tk.SOLID, borderwidth = 0,
|
||||
wraplength = self.wraplength)
|
||||
label.grid(padx = (pad[0], pad[2]), pady = (pad[1], pad[3]), sticky=tk.NSEW)
|
||||
win.grid()
|
||||
|
||||
x, y = tip_pos_calculator(widget, label)
|
||||
|
||||
self.tw.wm_geometry("+%d+%d" % (x, y))
|
||||
|
||||
def hide(self):
|
||||
tw = self.tw
|
||||
if tw:
|
||||
tw.destroy()
|
||||
self.tw = None
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class TextRedirect(object):
|
||||
class Pretty(object):
|
||||
grpmsg = unformat_message([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]])
|
||||
arrows = [ item[0] for item in grpmsg ]
|
||||
clt_msg_nonewline = [ item[1] for item in grpmsg ]
|
||||
arrows = list(set(arrows))
|
||||
lenarrow = len(arrows[0])
|
||||
srv_msg_nonewline = [ item[0] for item in unformat_message([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) ]
|
||||
msg_align = [ msg[0].replace('\t', '').replace('\n', '') for msg in unformat_message([MsgMap[-2], MsgMap[-4]]) ]
|
||||
|
||||
def __init__(self, srv_text_space, clt_text_space, customcolors):
|
||||
self.srv_text_space = srv_text_space
|
||||
self.clt_text_space = clt_text_space
|
||||
self.customcolors = customcolors
|
||||
|
||||
def textbox_write(self, tag, message, color, extras):
|
||||
widget = self.textbox_choose(message)
|
||||
self.w_maxpix, self.h_maxpix = widget.winfo_width(), widget.winfo_height()
|
||||
self.xfont = tkFont.Font(font = widget['font'])
|
||||
widget.configure(state = 'normal')
|
||||
widget.insert('end', self.textbox_format(message), tag)
|
||||
self.textbox_color(tag, widget, color, self.customcolors['black'], extras)
|
||||
widget.after(100, widget.see('end'))
|
||||
widget.configure(state = 'disabled')
|
||||
|
||||
def textbox_choose(self, message):
|
||||
if any(item.startswith('logsrv') for item in [message, self.str_to_print]):
|
||||
self.srv_text_space.focus_set()
|
||||
self.where = "srv"
|
||||
return self.srv_text_space
|
||||
elif any(item.startswith('logclt') for item in [message, self.str_to_print]):
|
||||
self.clt_text_space.focus_set()
|
||||
self.where = "clt"
|
||||
return self.clt_text_space
|
||||
|
||||
def textbox_color(self, tag, widget, forecolor = 'white', backcolor = 'black', extras = []):
|
||||
for extra in extras:
|
||||
if extra == 'bold':
|
||||
self.xfont.configure(weight = "bold")
|
||||
elif extra == 'italic':
|
||||
self.xfont.configure(slant = "italic")
|
||||
elif extra == 'underlined':
|
||||
self.xfont.text_font.configure(underline = True)
|
||||
elif extra == 'strike':
|
||||
self.xfont.configure(overstrike = True)
|
||||
elif extra == 'reverse':
|
||||
forecolor, backcolor = backcolor, forecolor
|
||||
|
||||
widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = self.xfont)
|
||||
widget.tag_add(tag, "insert linestart", "insert lineend")
|
||||
|
||||
def textbox_newline(self, message):
|
||||
if not message.endswith('\n'):
|
||||
return message + '\n'
|
||||
else:
|
||||
return message
|
||||
|
||||
def textbox_format(self, message):
|
||||
# vertical align.
|
||||
self.w_maxpix = self.w_maxpix - 5 # pixel reduction for distance from border.
|
||||
w_fontpix, h_fontpix = (self.xfont.measure('0'), self.xfont.metrics('linespace'))
|
||||
msg_unformat = message.replace('\t', '').replace('\n', '')
|
||||
lenfixed_chars = int((self.w_maxpix / w_fontpix) - len(msg_unformat))
|
||||
|
||||
if message in self.srv_msg_nonewline + self.clt_msg_nonewline:
|
||||
lung = lenfixed_chars - self.lenarrow
|
||||
if message in self.clt_msg_nonewline:
|
||||
message = self.textbox_newline(message)
|
||||
else:
|
||||
lung = lenfixed_chars
|
||||
if (self.where == "srv") or (self.where == "clt" and message not in self.arrows):
|
||||
message = self.textbox_newline(message)
|
||||
# horizontal align.
|
||||
if msg_unformat in self.msg_align:
|
||||
msg_strip = message.lstrip('\n')
|
||||
message = '\n' * (len(message) - len(msg_strip) + TextRedirect.Pretty.newlinecut[0]) + msg_strip
|
||||
TextRedirect.Pretty.newlinecut.pop(0)
|
||||
|
||||
count = Counter(message)
|
||||
countab = (count['\t'] if count['\t'] != 0 else 1)
|
||||
message = message.replace('\t' * countab, ' ' * lung)
|
||||
return message
|
||||
|
||||
def textbox_do(self):
|
||||
msgs, TextRedirect.Pretty.tag_num = unshell_message(self.str_to_print, TextRedirect.Pretty.tag_num)
|
||||
for tag in msgs:
|
||||
self.textbox_write(tag, msgs[tag]['text'], self.customcolors[msgs[tag]['color']], msgs[tag]['extra'])
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def write(self, string):
|
||||
if string != '\n':
|
||||
self.str_to_print = string
|
||||
self.textbox_do()
|
||||
|
||||
class Stderr(Pretty):
|
||||
def __init__(self, srv_text_space, clt_text_space, customcolors, side):
|
||||
self.srv_text_space = srv_text_space
|
||||
self.clt_text_space = clt_text_space
|
||||
self.customcolors = customcolors
|
||||
self.side = side
|
||||
self.tag_err = 'STDERR'
|
||||
self.xfont = tkFont.Font(font = self.srv_text_space['font'])
|
||||
|
||||
def textbox_choose(self, message):
|
||||
if self.side == "srv":
|
||||
return self.srv_text_space
|
||||
elif self.side == "clt":
|
||||
return self.clt_text_space
|
||||
|
||||
def write(self, string):
|
||||
widget = self.textbox_choose(string)
|
||||
self.textbox_color(self.tag_err, widget, self.customcolors['red'], self.customcolors['black'])
|
||||
self.srv_text_space.configure(state = 'normal')
|
||||
self.srv_text_space.insert('end', string, self.tag_err)
|
||||
self.srv_text_space.see('end')
|
||||
self.srv_text_space.configure(state = 'disabled')
|
||||
|
||||
class Log(Pretty):
|
||||
def textbox_format(self, message):
|
||||
if message.startswith('logsrv'):
|
||||
message = message.replace('logsrv ', '')
|
||||
if message.startswith('logclt'):
|
||||
message = message.replace('logclt ', '')
|
||||
return message + '\n'
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class TextDoubleScroll(tk.Frame):
|
||||
def __init__(self, master, **kwargs):
|
||||
""" Initialize.
|
||||
- horizontal scrollbar
|
||||
- vertical scrollbar
|
||||
- text widget
|
||||
"""
|
||||
tk.Frame.__init__(self, master)
|
||||
self.master = master
|
||||
|
||||
self.textbox = tk.Text(self.master, **kwargs)
|
||||
self.sizegrip = ttk.Sizegrip(self.master)
|
||||
self.hs = ttk.Scrollbar(self.master, orient = "horizontal", command = self.on_scrollbar_x)
|
||||
self.vs = ttk.Scrollbar(self.master, orient = "vertical", command = self.on_scrollbar_y)
|
||||
self.textbox.configure(yscrollcommand = self.on_textscroll, xscrollcommand = self.hs.set)
|
||||
|
||||
def on_scrollbar_x(self, *args):
|
||||
""" Horizontally scrolls text widget. """
|
||||
self.textbox.xview(*args)
|
||||
|
||||
def on_scrollbar_y(self, *args):
|
||||
""" Vertically scrolls text widget. """
|
||||
self.textbox.yview(*args)
|
||||
|
||||
def on_textscroll(self, *args):
|
||||
""" Moves the scrollbar and scrolls text widget when the mousewheel is moved on a text widget. """
|
||||
self.vs.set(*args)
|
||||
self.on_scrollbar_y('moveto', args[0])
|
||||
|
||||
def put(self, **kwargs):
|
||||
""" Grid the scrollbars and textbox correctly. """
|
||||
self.textbox.grid(row = 0, column = 0, padx = 3, pady = 3, sticky = "nsew")
|
||||
self.vs.grid(row = 0, column = 1, sticky = "ns")
|
||||
self.hs.grid(row = 1, column = 0, sticky = "we")
|
||||
self.sizegrip.grid(row = 1, column = 1, sticky = "news")
|
||||
|
||||
def get(self):
|
||||
""" Return the "frame" useful to place inner controls. """
|
||||
return self.textbox
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
def custom_background(window):
|
||||
# first level canvas.
|
||||
allwidgets = window.grid_slaves(0,0)[0].grid_slaves() + window.grid_slaves(0,0)[0].place_slaves()
|
||||
widgets_alphalow = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas']
|
||||
widgets_alphahigh = []
|
||||
# sub-level canvas.
|
||||
for side in ["Srv", "Clt"]:
|
||||
widgets_alphahigh.append(window.pagewidgets[side]["BtnWin"])
|
||||
for position in ["Left", "Right"]:
|
||||
widgets_alphahigh.append(window.pagewidgets[side]["AniWin"][position])
|
||||
for pagename in window.pagewidgets[side]["PageWin"].keys():
|
||||
widgets_alphalow.append(window.pagewidgets[side]["PageWin"][pagename])
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# Open Image.
|
||||
img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keys.gif")
|
||||
img = img.convert('RGBA')
|
||||
# Resize image.
|
||||
img.resize((window.winfo_width(), window.winfo_height()), Image.ANTIALIAS)
|
||||
# Put semi-transparent background chunks.
|
||||
window.backcrops_alphalow, window.backcrops_alphahigh = ([] for _ in range(2))
|
||||
|
||||
def cutter(master, image, widgets, crops, alpha):
|
||||
for widget in widgets:
|
||||
x, y, w, h = master.get_position(widget)
|
||||
cropped = image.crop((x, y, x + w, y + h))
|
||||
cropped.putalpha(alpha)
|
||||
crops.append(ImageTk.PhotoImage(cropped))
|
||||
# Not in same loop to prevent reference garbage.
|
||||
for crop, widget in zip(crops, widgets):
|
||||
widget.create_image(1, 1, image = crop, anchor = 'nw')
|
||||
|
||||
cutter(window, img, widgets_alphalow, window.backcrops_alphalow, 36)
|
||||
cutter(window, img, widgets_alphahigh, window.backcrops_alphahigh, 96)
|
||||
|
||||
# Put semi-transparent background overall.
|
||||
img.putalpha(128)
|
||||
window.backimg = ImageTk.PhotoImage(img)
|
||||
window.masterwin.create_image(1, 1, image = window.backimg, anchor = 'nw')
|
||||
|
||||
except ImportError:
|
||||
for widget in widgets_alphalow + widgets_alphahigh:
|
||||
widget.configure(background = window.customcolors['lavender'])
|
||||
|
||||
# Hide client.
|
||||
window.clt_on_show(force_remove = True)
|
||||
# Show Gui.
|
||||
window.deiconify()
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class Animation(object):
|
||||
def __init__(self, gifpath, master, widget, loop = False):
|
||||
from PIL import Image, ImageTk, ImageSequence
|
||||
|
||||
self.master = master
|
||||
self.widget = widget
|
||||
self.loop = loop
|
||||
self.cancelid = None
|
||||
self.flagstop = False
|
||||
self.index = 0
|
||||
self.frames = []
|
||||
|
||||
img = Image.open(gifpath)
|
||||
size = img.size
|
||||
for frame in ImageSequence.Iterator(img):
|
||||
static_img = ImageTk.PhotoImage(frame.convert('RGBA'))
|
||||
try:
|
||||
static_img.delay = int(frame.info['duration'])
|
||||
except KeyError:
|
||||
static_img.delay = 100
|
||||
self.frames.append(static_img)
|
||||
|
||||
self.widget.configure(width = size[0], height = size[1])
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
self.widget.configure(image = self.frames[0])
|
||||
self.widget.image = self.frames[0]
|
||||
|
||||
def deanimate(self):
|
||||
while not self.flagstop:
|
||||
pass
|
||||
self.flagstop = False
|
||||
self.index = 0
|
||||
self.widget.configure(relief = "raised")
|
||||
|
||||
def animate(self):
|
||||
frame = self.frames[self.index]
|
||||
self.widget.configure(image = frame, relief = "sunken")
|
||||
self.index += 1
|
||||
self.cancelid = self.master.after(frame.delay, self.animate)
|
||||
if self.index == len(self.frames):
|
||||
if self.loop:
|
||||
self.index = 0
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def start(self, event = None):
|
||||
if str(self.widget['state']) != 'disabled':
|
||||
if self.cancelid is None:
|
||||
if not self.loop:
|
||||
self.btnani_thread = threading.Thread(target = self.deanimate, name = "Thread-BtnAni")
|
||||
self.btnani_thread.setDaemon(True)
|
||||
self.btnani_thread.start()
|
||||
self.cancelid = self.master.after(self.frames[0].delay, self.animate)
|
||||
|
||||
def stop(self, event = None):
|
||||
if self.cancelid:
|
||||
self.master.after_cancel(self.cancelid)
|
||||
self.cancelid = None
|
||||
self.flagstop = True
|
||||
self.initialize()
|
||||
|
||||
|
||||
def custom_pages(window, side):
|
||||
buttons = window.pagewidgets[side]["BtnAni"]
|
||||
labels = window.pagewidgets[side]["LblAni"]
|
||||
|
||||
for position in buttons.keys():
|
||||
buttons[position].config(anchor = "center",
|
||||
font = window.customfonts['btn'],
|
||||
background = window.customcolors['white'],
|
||||
activebackground = window.customcolors['white'],
|
||||
borderwidth = 2)
|
||||
|
||||
try:
|
||||
anibtn = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keyhole_%s.gif" %position,
|
||||
window, buttons[position], loop = False)
|
||||
anilbl = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Arrow_%s.gif" %position,
|
||||
window, labels[position], loop = True)
|
||||
|
||||
def animationwait(master, button, btn_animation, lbl_animation):
|
||||
while btn_animation.cancelid:
|
||||
pass
|
||||
sleep(1)
|
||||
x, y = master.winfo_pointerxy()
|
||||
if master.winfo_containing(x, y) == button:
|
||||
lbl_animation.start()
|
||||
|
||||
def animationcombo(master, button, btn_animation, lbl_animation):
|
||||
wait_thread = threading.Thread(target = animationwait,
|
||||
args = (master, button, btn_animation, lbl_animation),
|
||||
name = "Thread-WaitAni")
|
||||
wait_thread.setDaemon(True)
|
||||
wait_thread.start()
|
||||
lbl_animation.stop()
|
||||
btn_animation.start()
|
||||
|
||||
buttons[position].bind("<ButtonPress>", lambda event, anim1 = anibtn, anim2 = anilbl,
|
||||
bt = buttons[position], win = window:
|
||||
animationcombo(win, bt, anim1, anim2))
|
||||
buttons[position].bind("<Enter>", anilbl.start)
|
||||
buttons[position].bind("<Leave>", anilbl.stop)
|
||||
|
||||
except ImportError:
|
||||
buttons[position].config(activebackground = window.customcolors['blue'],
|
||||
foreground = window.customcolors['blue'])
|
||||
labels[position].config(background = window.customcolors['lavender'])
|
||||
|
||||
if position == "Left":
|
||||
buttons[position].config(text = '<<')
|
||||
elif position == "Right":
|
||||
buttons[position].config(text = '>>')
|
||||
|
||||
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
class ListboxOfRadiobuttons(tk.Frame):
|
||||
def __init__(self, master, radios, font, changed, **kwargs):
|
||||
tk.Frame.__init__(self, master)
|
||||
|
||||
self.master = master
|
||||
self.radios = radios
|
||||
self.font = font
|
||||
self.changed = changed
|
||||
|
||||
self.scrollv = tk.Scrollbar(self, orient = "vertical")
|
||||
self.textbox = tk.Text(self, yscrollcommand = self.scrollv.set, **kwargs)
|
||||
self.scrollv.config(command = self.textbox.yview)
|
||||
# layout.
|
||||
self.scrollv.pack(side = "right", fill = "y")
|
||||
self.textbox.pack(side = "left", fill = "both", expand = True)
|
||||
# create radiobuttons.
|
||||
self.radiovar = tk.StringVar()
|
||||
self.radiovar.set('FILE')
|
||||
self.create()
|
||||
|
||||
def create(self):
|
||||
self.rdbtns = []
|
||||
for n, nameradio in enumerate(self.radios):
|
||||
rdbtn = tk.Radiobutton(self, text = nameradio, value = nameradio, variable = self.radiovar,
|
||||
font = self.font, indicatoron = 0, width = 15,
|
||||
borderwidth = 3, selectcolor = 'yellow', command = self.change)
|
||||
self.textbox.window_create("end", window = rdbtn)
|
||||
# to force one checkbox per line
|
||||
if n != len(self.radios) - 1:
|
||||
self.textbox.insert("end", "\n")
|
||||
self.rdbtns.append(rdbtn)
|
||||
self.textbox.configure(state = "disabled")
|
||||
|
||||
def change(self):
|
||||
st = self.state()
|
||||
for widget, default in self.changed:
|
||||
wclass = widget.winfo_class()
|
||||
if st in ['STDOUT', 'FILEOFF']:
|
||||
if wclass == 'Entry':
|
||||
widget.delete(0, 'end')
|
||||
widget.configure(state = "disabled")
|
||||
elif wclass == 'TCombobox':
|
||||
if st == 'STDOUT':
|
||||
widget.set(default)
|
||||
widget.configure(state = "readonly")
|
||||
elif st == 'FILEOFF':
|
||||
widget.set('')
|
||||
widget.configure(state = "disabled")
|
||||
elif st in ['FILE', 'FILESTDOUT', 'STDOUTOFF']:
|
||||
if wclass == 'Entry':
|
||||
widget.configure(state = "normal")
|
||||
widget.delete(0, 'end')
|
||||
widget.insert('end', default)
|
||||
widget.xview_moveto(1)
|
||||
elif wclass == 'TCombobox':
|
||||
widget.configure(state = "readonly")
|
||||
widget.set(default)
|
||||
elif wclass == 'Button':
|
||||
widget.configure(state = "normal")
|
||||
|
||||
def configure(self, state):
|
||||
for rb in self.rdbtns:
|
||||
rb.configure(state = state)
|
||||
|
||||
def state(self):
|
||||
return self.radiovar.get()
|
|
@ -194,9 +194,6 @@ def logger_create(log_obj, config, mode = 'a'):
|
|||
frmt_name = '%(name)s '
|
||||
|
||||
from pykms_Server import serverthread
|
||||
if serverthread.with_gui:
|
||||
frmt_std = frmt_name + frmt_std
|
||||
frmt_min = frmt_name + frmt_min
|
||||
|
||||
def apply_formatter(levelnum, formats, handler, color = False):
|
||||
levelformdict = {}
|
||||
|
@ -227,7 +224,7 @@ def logger_create(log_obj, config, mode = 'a'):
|
|||
log_obj.setLevel(config['loglevel'])
|
||||
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
def check_dir(path, where, log_obj = None, argument = '-F/--logfile', typefile = '.log'):
|
||||
def check_dir(path, where, log_obj = None, argument = '-F/--logfile'):
|
||||
filename = os.path.basename(path)
|
||||
pathname = os.path.dirname(path)
|
||||
extension = os.path.splitext(filename)[1]
|
||||
|
@ -243,9 +240,6 @@ def check_dir(path, where, log_obj = None, argument = '-F/--logfile', typefile =
|
|||
pathname = filename
|
||||
pretty_printer(log_obj = log_obj, where = where, to_exit = True,
|
||||
put_text = msg_dir %(argument, pathname))
|
||||
elif not extension.lower() == typefile:
|
||||
pretty_printer(log_obj = log_obj, where = where, to_exit = True,
|
||||
put_text = msg_fil %(argument, typefile, extension))
|
||||
|
||||
def check_logfile(optionlog, defaultlog, where):
|
||||
if not isinstance(optionlog, list):
|
||||
|
@ -524,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.
|
||||
|
@ -533,7 +527,7 @@ def check_setup(config, options, logger, where):
|
|||
pretty_printer(put_text = "{reverse}{red}{bold}argument `-S/--logsize`: invalid with: '%s'. Exiting...{end}" %config['logsize'],
|
||||
where = where, to_exit = True)
|
||||
|
||||
# Check loglevel (py-kms Gui).
|
||||
# Check loglevel
|
||||
if config['loglevel'] == "":
|
||||
# set a recognized level never used.
|
||||
config['loglevel'] = 'ERROR'
|
||||
|
|
|
@ -9,23 +9,20 @@ import uuid
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import pickle
|
||||
import socketserver
|
||||
import queue as Queue
|
||||
import selectors
|
||||
from getpass import getuser
|
||||
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"
|
||||
|
@ -136,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):
|
||||
|
@ -171,21 +169,25 @@ 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
|
||||
|
||||
##---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
loggersrv = logging.getLogger('logsrv')
|
||||
|
||||
def _str2bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
||||
return True
|
||||
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
|
||||
return False
|
||||
else:
|
||||
raise ValueError('Boolean value expected.')
|
||||
|
||||
# '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"},
|
||||
|
@ -197,19 +199,18 @@ 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.',
|
||||
'def' : "364F463A8863D35F", 'des' : "hwid"},
|
||||
Type \"RANDOM\" to auto-generate the HWID.',
|
||||
'def' : "RANDOM", 'des' : "hwid"},
|
||||
'time0' : {'help' : 'Maximum inactivity time (in seconds) after which the connection with the client is closed. If \"None\" (default) serve forever.',
|
||||
'def' : None, 'des' : "timeoutidle"},
|
||||
'time1' : {'help' : 'Set the maximum time to wait for sending / receiving a request / response. Default is no timeout.',
|
||||
'def' : None, 'des' : "timeoutsndrcv"},
|
||||
'asyncmsg' : {'help' : 'Prints pretty / logging messages asynchronously. Deactivated by default.',
|
||||
'def' : False, 'des' : "asyncmsg"},
|
||||
'llevel' : {'help' : 'Use this option to set a log level. The default is \"ERROR\".', 'def' : "ERROR", 'des' : "loglevel",
|
||||
'llevel' : {'help' : 'Use this option to set a log level. The default is \"WARNING\".', 'def' : "WARNING", 'des' : "loglevel",
|
||||
'choi' : ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "MININFO"]},
|
||||
'lfile' : {'help' : 'Use this option to set an output log file. The default is \"pykms_logserver.log\". \
|
||||
Type \"STDOUT\" to view log info on stdout. Type \"FILESTDOUT\" to combine previous actions. \
|
||||
|
@ -220,8 +221,8 @@ Use \"STDOUTOFF\" to disable stdout messages. Use \"FILEOFF\" if you not want to
|
|||
'backlog' : {'help' : 'Specifies the maximum length of the queue of pending connections. Default is \"5\".', 'def' : 5, 'des': "backlog"},
|
||||
'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"}
|
||||
'dual' : {'help' : 'Allows listening to an IPv6 address while also accepting connections via IPv4. If used, it refers to all addresses (main and additional). Activated by default. Pass in "false" or "true" to disable or enable.',
|
||||
'def' : True, 'des': "dual"}
|
||||
}
|
||||
|
||||
def server_options():
|
||||
|
@ -257,15 +258,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")
|
||||
|
@ -277,7 +269,7 @@ def server_options():
|
|||
help = srv_options['backlog']['help'], type = int)
|
||||
connect_parser.add_argument("-u", "--no-reuse", action = "append_const", dest = srv_options['reuse']['des'], const = False, default = [],
|
||||
help = srv_options['reuse']['help'])
|
||||
connect_parser.add_argument("-d", "--dual", action = "store_true", dest = srv_options['dual']['des'], default = srv_options['dual']['def'],
|
||||
connect_parser.add_argument("-d", "--dual", type = _str2bool, dest = srv_options['dual']['des'], default = srv_options['dual']['def'],
|
||||
help = srv_options['dual']['help'])
|
||||
|
||||
try:
|
||||
|
@ -285,14 +277,12 @@ def server_options():
|
|||
|
||||
# Run help.
|
||||
if any(arg in ["-h", "--help"] for arg in userarg):
|
||||
KmsParserHelp().printer(parsers = [server_parser, (daemon_parser, etrigan_parser),
|
||||
(connection_parser, connect_parser)])
|
||||
KmsParserHelp().printer(parsers = [server_parser, (connection_parser, connect_parser)])
|
||||
|
||||
# Get stored arguments.
|
||||
pykmssrv_zeroarg, pykmssrv_onearg = kms_parser_get(server_parser)
|
||||
etrigan_zeroarg, etrigan_onearg = kms_parser_get(etrigan_parser)
|
||||
connect_zeroarg, connect_onearg = kms_parser_get(connect_parser)
|
||||
subdict = {'etrigan' : (etrigan_zeroarg, etrigan_onearg, daemon_parser.parse_args),
|
||||
subdict = {
|
||||
'connect' : (connect_zeroarg, connect_onearg, connection_parser.parse_args)
|
||||
}
|
||||
subpars = list(subdict.keys())
|
||||
|
@ -310,14 +300,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)
|
||||
|
@ -339,7 +322,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)
|
||||
|
||||
|
@ -348,63 +331,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")
|
||||
|
@ -446,13 +372,16 @@ def server_check():
|
|||
|
||||
# Check sqlite.
|
||||
if srv_config['sqlite']:
|
||||
if isinstance(srv_config['sqlite'], str):
|
||||
check_dir(srv_config['sqlite'], 'srv', log_obj = loggersrv.error, argument = '-s/--sqlite', typefile = '.db')
|
||||
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}")
|
||||
|
@ -462,9 +391,6 @@ def server_check():
|
|||
opts = [('clientcount', '-c/--client-count'),
|
||||
('timeoutidle', '-t0/--timeout-idle'),
|
||||
('timeoutsndrcv', '-t1/--timeout-sndrcv')]
|
||||
if serverthread.with_gui:
|
||||
opts += [('activation', '-a/--activation-interval'),
|
||||
('renewal', '-r/--renewal-interval')]
|
||||
check_other(srv_config, opts, loggersrv, where = 'srv')
|
||||
|
||||
# Check further addresses / ports.
|
||||
|
@ -495,14 +421,12 @@ def server_create():
|
|||
all_address = [(
|
||||
srv_config['ip'], srv_config['port'],
|
||||
(srv_config['backlog_main'] if 'backlog_main' in srv_config else srv_options['backlog']['def']),
|
||||
(srv_config['reuse_main'] if 'reuse_main' in srv_config else False if getuser() == 'WDAGUtilityAccount' \
|
||||
else srv_options['reuse']['def'])
|
||||
(srv_config['reuse_main'] if 'reuse_main' in srv_config else srv_options['reuse']['def'])
|
||||
)]
|
||||
log_address = "TCP server listening at %s on port %d" %(srv_config['ip'], srv_config['port'])
|
||||
|
||||
if 'listen' in srv_config:
|
||||
for l, b, r in zip(srv_config['listen'], srv_config['backlog'], srv_config['reuse']):
|
||||
r = (False if getuser() == 'WDAGUtilityAccount' else r)
|
||||
all_address.append(l + (b,) + (r,))
|
||||
log_address += justify("at %s on port %d" %(l[0], l[1]), indent = 56)
|
||||
|
||||
|
@ -546,8 +470,6 @@ def server_main_terminal():
|
|||
server_check()
|
||||
serverthread.checked = True
|
||||
|
||||
if 'etrigan' not in srv_config.values():
|
||||
# (without GUI) and (without daemon).
|
||||
# Run threaded server.
|
||||
serverqueue.put('start')
|
||||
# Wait to finish.
|
||||
|
@ -556,22 +478,6 @@ def server_main_terminal():
|
|||
serverthread.join(timeout = 0.5)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
server_terminate(serverthread, exit_server = True, exit_thread = True)
|
||||
else:
|
||||
# (with or without GUI) and (with daemon)
|
||||
# Setup daemon (eventually).
|
||||
server_daemon()
|
||||
|
||||
def server_with_gui():
|
||||
import pykms_GuiBase
|
||||
|
||||
root = pykms_GuiBase.KmsGui()
|
||||
root.title(pykms_GuiBase.gui_description + ' (' + pykms_GuiBase.gui_version + ')')
|
||||
root.mainloop()
|
||||
|
||||
def server_main_no_terminal():
|
||||
# Run tkinter GUI.
|
||||
# (with GUI) and (without daemon).
|
||||
server_with_gui()
|
||||
|
||||
class kmsServerHandler(socketserver.BaseRequestHandler):
|
||||
def setup(self):
|
||||
|
@ -585,7 +491,7 @@ class kmsServerHandler(socketserver.BaseRequestHandler):
|
|||
try:
|
||||
self.data = self.request.recv(1024)
|
||||
if self.data == '' or not self.data:
|
||||
pretty_printer(log_obj = loggersrv.warning,
|
||||
pretty_printer(log_obj = loggersrv.debug, # use debug, as the healthcheck will spam this
|
||||
put_text = "{reverse}{yellow}{bold}No data received.{end}")
|
||||
break
|
||||
except socket.error as e:
|
||||
|
@ -632,14 +538,8 @@ class kmsServerHandler(socketserver.BaseRequestHandler):
|
|||
|
||||
serverqueue = Queue.Queue(maxsize = 0)
|
||||
serverthread = server_thread(serverqueue, name = "Thread-Srv")
|
||||
serverthread.setDaemon(True)
|
||||
serverthread.daemon = True
|
||||
serverthread.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.stdout.isatty():
|
||||
server_main_terminal()
|
||||
else:
|
||||
try:
|
||||
server_main_no_terminal()
|
||||
except:
|
||||
server_main_terminal()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
@ -18,21 +19,40 @@ loggersrv = logging.getLogger('logsrv')
|
|||
def sql_initialize(dbName):
|
||||
if not os.path.isfile(dbName):
|
||||
# Initialize the database.
|
||||
loggersrv.debug(f'Initializing database file "{dbName}"...')
|
||||
con = None
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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 %}
|
|
@ -0,0 +1,9 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}license{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<pre>{{ license }}</pre>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -0,0 +1 @@
|
|||
docker/docker-py3-kms/requirements.txt
|
Loading…
Reference in New Issue