60 Commits

Author SHA1 Message Date
jpattWPC
e848a938df Add window sizing option 2023-09-14 20:35:27 -05:00
jpattWPC
e8d936b297 Version Upgrade
- Add current VM state to VM list
- Add option for reset button to be enabled in INI file
- Disable Connect button when VM is in a transition state
2023-09-07 15:52:18 -05:00
joshpatten
a80805d69a Fixed virt-manager link 2023-04-19 17:12:16 -05:00
jpattWPC
b382d69126 Fix #51 2023-03-10 09:22:10 -06:00
jpattWPC
715dfef72d Add python3-tk to apt install 2023-03-08 09:14:43 -06:00
jpattWPC
fbe00ec782 Bump release version 2023-02-15 08:54:29 -06:00
joshpatten
7bc5966cd6 Merge pull request #47 from bekema/main
Prevent exceptions when a node is offline
2023-02-15 08:50:14 -06:00
Hessel Bekema
9942361877 only list vms for nodes that are online; prevents KeyError crashes 2023-02-15 23:12:51 +11:00
Hessel Bekema
7148fc85ed bind enter to the log in button 2023-02-15 23:10:09 +11:00
jpattWPC
c2221da4c2 Fix typo 2023-02-03 15:57:12 -06:00
jpattWPC
de4088773b Update install instructions 2023-02-03 15:54:49 -06:00
jpattWPC
de3ab9ea68 Enhance auto-refresh logic 2023-02-03 15:47:10 -06:00
jpattWPC
57ffa48257 Add VM list refresh
- Add ability to query new VMs every 10 seconds, and redraw the window if a different list is found.
- Remove vestigial DPI checking code.
2023-02-03 15:13:27 -06:00
jpattWPC
a7f15ac8b9 Require 64 bit support 2023-02-01 11:10:50 -06:00
jpattWPC
696b8c7cd2 Remove PySimpleGuiQT support
- Remove PySimpleGUIQt support, as it requires breaking changes for Python 3.11
- Build MSI based off Python 3.11 (significantly smaller due to removing Qt requirement)
- Bump version to 1.1.0
2023-02-01 11:06:05 -06:00
jpattWPC
ef95a822c5 Merge pull request #36 from aacater/main
check if template key exists
2022-10-24 10:40:51 -05:00
Alex Cater
26c6ca6bc8 check if template key exists 2022-10-23 22:09:04 -08:00
jpattWPC
9518f65c05 Fix build environment, version bump 2022-10-19 11:32:23 -05:00
jpattWPC
02df87523d Merge pull request #30 from aacater/main
Option to filter guests by type
2022-10-19 11:19:58 -05:00
aacater
09a8d11c96 option to filter guest types 2022-09-09 17:45:51 -08:00
aacater
d643cf85f4 skip templates 2022-09-09 17:44:28 -08:00
jpattWPC
f9be014ea2 API Token Login
Add full support for passwordless token-only login.
2022-08-19 22:16:08 -05:00
jpattWPC
ad31d37364 Update MSI version 2022-08-19 21:28:27 -05:00
jpattWPC
b11817a997 Custom parameter support
Add support for custom remote-viewer paramters: https://www.mankier.com/1/remote-viewer
2022-08-19 21:12:03 -05:00
jpattWPC
3f3323710e Merge pull request #27 from rayksland/patch-1
Update vdiclient.ini.example
2022-08-16 11:57:13 -05:00
rayksland
e2f0b26e40 Update vdiclient.ini.example
grammatical edit. 'it's' is a contraction of 'it is' whereas 'its' is the possessive form of 'it.'
2022-08-15 12:06:42 -07:00
jpattWPC
c78ef81994 Update release number 2022-07-19 10:39:58 -05:00
jpattWPC
44b0bfae59 Work around http redireect issue 2022-07-19 10:37:26 -05:00
jpattWPC
5db945dced Add fullscreen toggle 2022-07-11 14:07:32 -05:00
jpattWPC
34b4d010f5 Add last ditch config read 2022-07-11 13:54:10 -05:00
jpattWPC
dc2d476202 Update vdiclient.json
Update release version
2022-07-11 13:47:03 -05:00
jpattWPC
78f90a61b7 Work around MS inconsistency 2022-07-11 13:44:43 -05:00
jpattWPC
ab792f2a2a Add requests requirement 2022-06-30 11:42:46 -05:00
joshpatten
d4d6809527 Merge pull request #16 from incoggnito/main
Uppercase hostname and proxy documentation
2022-05-13 10:26:26 -05:00
incoggnito
2efe5cd0e3 More documentation on proxy 2022-05-13 15:53:37 +02:00
incoggnito
1df4f4dd13 handle uppercase hostname 2022-05-13 15:50:18 +02:00
jpattWPC
ec1edfbebc Add fallback to PySimpleGUI 2022-04-28 09:15:38 -05:00
jpattWPC
fe11d33359 Update vdiclient.py
Fix TKinter integer requirement
2022-04-21 11:27:23 -05:00
jpattWPC
2368e7c4a5 Update vdiclient.py
Fallback to Tkinter if QT not available.
2022-04-20 08:56:30 -05:00
joshpatten
21c061ebfc Merge pull request #10 from domfi/patch-1
fix loading configfile from ~/.config/
2022-04-15 10:33:24 -05:00
domfi
fc312a095c fix loading configfile from ~/.config/ 2022-04-15 16:11:21 +02:00
jpattWPC
61edff3e87 Merge pull request #6 from richardcgingrasjr/main
Moved theme set location
2022-04-11 15:48:31 -05:00
Richard C Gingras Jr
163c83d3b2 Moved theme set location 2022-03-17 22:37:58 -04:00
jpattWPC
e29796bba5 DPI Scaling Issue Fix
Fixed DPI scaling issue that was causing the window to have text cut off. A known limitation is the scaling is based off the first monitor, and does not dynamically rescale (that I'm aware of).
2022-03-14 21:24:36 -05:00
jpattWPC
fc60d99a65 Add support for INI debug
In order to troubleshoot spice connection issues, add ini debug option
2022-03-10 16:58:24 -06:00
joshpatten
d3df0c4df6 Fix command reference 2022-03-09 22:41:56 -06:00
jpattWPC
b8a1be7a51 Minor UI bugfix 2022-03-09 21:48:50 -06:00
jpattWPC
6065985159 Add scrolling after 5 VMs 2022-03-09 21:46:52 -06:00
joshpatten
4e251c31cb Merge pull request #3 from completion/main
API Tokens
2022-03-09 21:14:57 -06:00
completion
c3a5233b87 remove list length hack 2022-03-09 16:29:00 -09:00
completion
ce137819ab default fill blank user instead 2022-03-09 16:20:42 -09:00
completion
cf3f8e2cc2 only fill if user is defined 2022-03-09 16:14:47 -09:00
completion
8ff5002579 fill user from config 2022-03-09 15:31:52 -09:00
completion
f048505000 add ability to use API tokens 2022-03-09 15:14:16 -09:00
completion
f161129b9d typo? G.cmd -> G.vvcmd 2022-03-09 15:11:26 -09:00
joshpatten
4fc20eea71 Incorrect image used on README 2022-03-08 10:10:25 -06:00
jpattWPC
a53acb7cd8 Update README.md
Formatting issue.
2022-03-07 17:06:29 -06:00
jpattWPC
cd7e27a531 Add permission information 2022-03-07 17:05:06 -06:00
jpattWPC
5c038c70fe Merge branch 'main' of https://github.com/joshpatten/PVE-VDIClient 2022-03-07 17:01:49 -06:00
jpattWPC
3d4fbc7ba7 Initial Commit 2022-03-07 16:56:54 -06:00
15 changed files with 1513 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
**/build/
**/dist/vdiclient
/dist/*.msi
/dist/*.wixobj
/dist/*.wxs
/dist/*.wixpdb
**/__pycache__
vdiclient.spec

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# PVE VDI Client
This project's focus is to create a simple VDI client intended for mass deployment. This VDI client connects directly to Proxmox VE and allows users to connect (via Spice) to any VMs they have permission to access.
![Login Screen](screenshots/login.png)
![Login Screen with OTP](screenshots/login-totp.png)
![VDI View](screenshots/vdiview.png)
## Windows Installation
You **MUST** install virt-viewer prior to using PVE VDI client, you may download it from the [official Virtual Machine Manager](https://virt-manager.org/download) site.
Please visit the [releases](https://github.com/joshpatten/PVE-VDIClient/releases) section to download a prebuilt MSI package
If you need to customize the installation, such as to sign the executable and MSI, you may download and install the [WIX toolset](https://wixtoolset.org/releases/) and use the build_vdiclient.bat file to build a new MSI.
you will need to download the latest 3.10 python release, and run the following commands to install the necessary packages:
requirements.bat
## Linux Installation
Run the following commands on a Debian/Ubuntu Linux system to install the appropriate prerequisites
apt install python3-pip python3-tk virt-viewer git
git clone https://github.com/joshpatten/PVE-VDIClient.git
cd ./PVE-VDIClient/
chmod +x requirements.sh
./requirements.sh
cp vdiclient.py /usr/local/bin
chmod +x /usr/local/bin/vdiclient.py
## Configuration File
PVE VDI Client **REQUIRES** a configuration file to function. The client searches for this file in the following locations unless **--config** is specified on the commmand line:
* Windows
* %APPDATA%\VDIClient\vdiclient.ini
* %PROGRAMFILES%\VDIClient\vdiclient.ini
* Linux
* ~/.config/VDIClient/vdiclient.ini
* /etc/vdiclient/vdiclient.ini
* /usr/local/etc/vdiclient/vdiclient.ini
Please refer to **vdiclient.ini.example** for all available config file options
If you encounter any issues feel free to submit an issue report.
## Proxmox Permission Requirements
Users that are accessing VDI instances need to have the following permissions assigned for each VM they access:
* VM.PowerMgmt
* VM.Console
* VM.Audit

24
build_vdiclient.bat Normal file
View File

@@ -0,0 +1,24 @@
@echo off
pyinstaller --noconsole --noconfirm --hidden-import proxmoxer.backends --hidden-import proxmoxer.backends.https --hidden-import proxmoxer.backends.https.AuthenticationError --hidden-import proxmoxer.core --hidden-import proxmoxer.core.ResourceException --hidden-import subprocess.TimeoutExpired --hidden-import subprocess.CalledProcessError --hidden-import requests.exceptions --hidden-import requests.exceptions.ReadTimeout --hidden-import requests.exceptions.ConnectTimeout --hidden-import requests.exceptions.ConnectionError -i vdiicon.ico vdiclient.py
copy vdiclient.png dist\vdiclient
copy vdiicon.ico dist\vdiclient
del dist\vdiclient\opengl32sw.dll
del dist\vdiclient\libGLESv2.dll
del dist\vdiclient\d3dcompiler_47.dll
del dist\vdiclient\Qt5Pdf.dll
del dist\vdiclient\Qt5VirtualKeyboard.dll
del dist\vdiclient\Qt5WebSockets.dll
del dist\vdiclient\Qt5Quick.dll
del dist\vdiclient\PySide2\plugins\imageformats\qgif.dll
del dist\vdiclient\PySide2\plugins\imageformats\qjpeg.dll
del dist\vdiclient\PySide2\plugins\imageformats\qpdf.dll
del dist\vdiclient\PySide2\plugins\imageformats\qsvg.dll
del dist\vdiclient\PySide2\plugins\imageformats\qtga.dll
del dist\vdiclient\PySide2\plugins\imageformats\qtiff.dll
del dist\vdiclient\PySide2\plugins\imageformats\qwbmp.dll
del dist\vdiclient\PySide2\plugins\imageformats\qwebp.dll
del dist\vdiclient\PySide2\plugins\platforminputcontexts\qtvirtualkeyboardplugin.dll
del /Q dist\vdiclient\PySide2\translations\*
cd dist
python createmsi.py vdiclient.json
cd ..

335
dist/License.rtf vendored Normal file
View File

@@ -0,0 +1,335 @@
{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}
{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}
{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}
{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f43\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f44\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\f46\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f47\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f48\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f49\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\f50\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f51\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f53\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f54\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;}
{\f56\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f57\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f58\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f59\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);}
{\f60\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f61\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}
{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}
{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}
{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}
{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}
{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}
{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;
\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;
\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;}{\*\defchp \fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }
\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0
\fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\*
\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1
\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused
Normal Table;}}{\*\rsidtbl \rsid9327448\rsid11103545}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\operator Josh M. Patten}{\creatim\yr2022\mo3\dy7\hr16\min32}
{\revtim\yr2022\mo3\dy7\hr16\min33}{\version2}{\edmins1}{\nofpages3}{\nofwords1544}{\nofchars8807}{\nofcharsws10331}{\vern43}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}
\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect
\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701
\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot9327448 \fet0{\*\wgrffmtfilter 2450}\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2
\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6
\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang
{\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\qc \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {
\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 Apache License
\par \hich\af1\dbch\af31505\loch\f1 Version 2.0, January 2004
\par }{\field{\*\fldinst {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 HYPERLINK "http://www.apache.org/licenses/"}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\f1\fs18\insrsid9327448 {\*\datafield
00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b5800000068007400740070003a002f002f007700770077002e006100700061006300680065002e006f00720067002f006c006900630065006e007300650073002f000000795881f43b1d7f48af2c825dc48527630000
0000a5ab0003}}}{\fldrslt {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\f1\fs18\ul\cf2\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 http://www.apache.org/licenses/}}}\sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0
\b\f1\fs18\insrsid11103545
\par }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
\par
\par }\pard \ltrpar\ql \fi-180\li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 1. Definitions.
\par
\par }\pard \ltrpar\ql \li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
\par
\par \hich\af1\dbch\af31505\loch\f1 "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
\par
\par \hich\af1\dbch\af31505\loch\f1
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause
\hich\af1\dbch\af31505\loch\f1 the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
\par
\par \hich\af1\dbch\af31505\loch\f1 "You" (or "Your") shall mean an individual or Legal En\hich\af1\dbch\af31505\loch\f1 tity exercising permissions granted by this License.
\par
\par \hich\af1\dbch\af31505\loch\f1 "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
\par
\par \hich\af1\dbch\af31505\loch\f1 "Object" form shall mean any form\hich\af1\dbch\af31505\loch\f1
resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
\par
\par \hich\af1\dbch\af31505\loch\f1 "Work" shall mean the work of authorship, whether in Source or Obj\hich\af1\dbch\af31505\loch\f1
ect form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
\par
\par \hich\af1\dbch\af31505\loch\f1 "Derivati\hich\af1\dbch\af31505\loch\f1
ve Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. Fo
\hich\af1\dbch\af31505\loch\f1 r\hich\af1\dbch\af31505\loch\f1
the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
\par
\par \hich\af1\dbch\af31505\loch\f1 "Contribution" shall mean any work of authorship, including t\hich\af1\dbch\af31505\loch\f1
he original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to sub
\hich\af1\dbch\af31505\loch\f1 m\hich\af1\dbch\af31505\loch\f1
it on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mail
\hich\af1\dbch\af31505\loch\f1 i\hich\af1\dbch\af31505\loch\f1
ng lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in
\hich\af1\dbch\af31505\loch\f1 w\hich\af1\dbch\af31505\loch\f1 riting by the copyright owner as "Not a Contribution."
\par
\par \hich\af1\dbch\af31505\loch\f1 "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \fi-180\li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 2. Grant of Copyrigh\hich\af1\dbch\af31505\loch\f1
t License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display
,\hich\af1\dbch\af31505\loch\f1 publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
\par
\par \hich\af1\dbch\af31505\loch\f1 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-e\hich\af1\dbch\af31505\loch\f1
xclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by su
\hich\af1\dbch\af31505\loch\f1 c\hich\af1\dbch\af31505\loch\f1
h Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-clai
\hich\af1\dbch\af31505\loch\f1 m\hich\af1\dbch\af31505\loch\f1
or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the
\hich\af1\dbch\af31505\loch\f1 \hich\af1\dbch\af31505\loch\f1 date such litigation is filed.
\par
\par \hich\af1\dbch\af31505\loch\f1 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \fi-270\li450\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin450\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
\par
\par \hich\af1\dbch\af31505\loch\f1 (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
\par
\par \hich\af1\dbch\af31505\loch\f1 (c) You must retain, in the Source form of any De\hich\af1\dbch\af31505\loch\f1
rivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
\par
\par \hich\af1\dbch\af31505\loch\f1 (d) If the Work includes a "NOTICE" text f\hich\af1\dbch\af31505\loch\f1
ile as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in a
\hich\af1\dbch\af31505\loch\f1 t\hich\af1\dbch\af31505\loch\f1
least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and
\hich\af1\dbch\af31505\loch\f1 \hich\af1\dbch\af31505\loch\f1
wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an
\hich\af1\dbch\af31505\loch\f1 a\hich\af1\dbch\af31505\loch\f1 ddendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1
You may add Your own copyright statement to Your modifications and may provide additional or different license terms \hich\af1\dbch\af31505\loch\f1
and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \fi-180\li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or cond
\hich\af1\dbch\af31505\loch\f1 i\hich\af1\dbch\af31505\loch\f1 tions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
\par
\par \hich\af1\dbch\af31505\loch\f1 6. Trademarks. This License does not grant permission to use the trade\hich\af1\dbch\af31505\loch\f1
names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
\par
\par \hich\af1\dbch\af31505\loch\f1 7. Disclaimer of Warranty. Unless required by app\hich\af1\dbch\af31505\loch\f1
licable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties o
\hich\af1\dbch\af31505\loch\f1 r\hich\af1\dbch\af31505\loch\f1
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissi
\hich\af1\dbch\af31505\loch\f1 o\hich\af1\dbch\af31505\loch\f1 ns under this License.
\par
\par \hich\af1\dbch\af31505\loch\f1
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in wri
\hich\af1\dbch\af31505\loch\f1
ting, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not
\hich\af1\dbch\af31505\loch\f1 l\hich\af1\dbch\af31505\loch\f1
imited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
\par
\par \hich\af1\dbch\af31505\loch\f1 9. Accepting Warranty or Additional L\hich\af1\dbch\af31505\loch\f1
iability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in acceptin
\hich\af1\dbch\af31505\loch\f1 g\hich\af1\dbch\af31505\loch\f1
such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserte
\hich\af1\dbch\af31505\loch\f1 d\hich\af1\dbch\af31505\loch\f1 against, such Contributor by reason of your accepting any such warranty or additional liability.
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par \hich\af1\dbch\af31505\loch\f1 END OF TERMS AND CONDITIONS
\par
\par \hich\af1\dbch\af31505\loch\f1 APPENDIX: How to apply the Apache License to your work.
\par
\par \hich\af1\dbch\af31505\loch\f1 To apply the Apache License to your work, attach the following boilerpl\hich\af1\dbch\af31505\loch\f1
ate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class
\hich\af1\dbch\af31505\loch\f1 n\hich\af1\dbch\af31505\loch\f1 ame and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
\par
\par }\pard \ltrpar\ql \li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 Copyright }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid9327448
\hich\af1\dbch\af31505\loch\f1 2022}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid9327448 \hich\af1\dbch\af31505\loch\f1 Joshua
\hich\af1\dbch\af31505\loch\f1 Patten}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par
\par \hich\af1\dbch\af31505\loch\f1 Licensed under the Apache License, Version 2.0 (the "License")\hich\af1\dbch\af31505\loch\f1 ; you may not use this file except in compliance with the License. You may obtain a copy of the License at
\par
\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\field{\*\fldinst {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 HYPERLINK "http://www.apache.org/licenses/LICENSE-2.0"}{
\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid9327448 {\*\datafield
00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b6e00000068007400740070003a002f002f007700770077002e006100700061006300680065002e006f00720067002f006c006900630065006e007300650073002f004c004900430045004e00530045002d0032002e00
30000000795881f43b1d7f48af2c825dc485276300000000a5ab0003}}}{\fldrslt {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\ul\cf2\insrsid11103545 \hich\af1\dbch\af31505\loch\f1 http://www.apache.org/licenses/LICENSE-2.0}}}\sectd \ltrsect
\linex0\sectdefaultcl\sftnbj {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }\pard \ltrpar\ql \li180\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin180\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par \hich\af1\dbch\af31505\loch\f1
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language govern
\hich\af1\dbch\af31505\loch\f1 i\hich\af1\dbch\af31505\loch\f1 ng permissions and limitations under the License.
\par }\pard \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \f1\fs18\insrsid11103545
\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a
9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad
5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6
b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0
0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6
a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f
c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512
0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462
a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865
6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b
4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b
4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b6f4679893070000c9200000160000007468656d652f7468656d652f
7468656d65312e786d6cec59cd8b1bc915bf07f23f347d97f5d5ad8fc1f2a24fcfda33b6b164873dd648a5eef2547789aad28cc56208de532e81c026e49085bd
ed21842cecc22eb9e48f31d8249b3f22afaa5bdd5552c99e191c3061463074977eefd5afde7bf5de53d5ddcf5e26d4bbc05c1096f6fcfa9d9aefe174ce16248d
7afeb3d9a4d2f13d2151ba4094a5b8e76fb0f03fbbf7eb5fdd454732c609f6403e1547a8e7c752ae8eaa5531876124eeb0154ee1bb25e30992f0caa3ea82a34b
d09bd06aa3566b55134452df4b51026a1f2f97648ebd9952e9dfdb2a1f53784da5500373caa74a35b6243476715e5708b11143cabd0b447b3eccb3609733fc52
fa1e4542c2173dbfa6fffceabdbb5574940b517940d6909be8bf5c2e17589c37f49c3c3a2b260d823068f50bfd1a40e53e6edc1eb7c6ad429f06a0f91c569a71
b175b61bc320c71aa0ecd1a17bd41e35eb16ded0dfdce3dc0fd5c7c26b50a63fd8c34f2643b0a285d7a00c1feee1c3417730b2f56b50866fede1dbb5fe28685b
fa3528a6243ddf43d7c25673b85d6d0159327aec8477c360d26ee4ca4b144443115d6a8a254be5a1584bd00bc6270050408a24493db959e1259a43140f112567
9c7827248a21f056286502866b8ddaa4d684ffea13e827ed5174849121ad780113b137a4f87862cec94af6fc07a0d537206f7ffef9cdeb1fdfbcfee9cd575fbd
79fdf77c6eadca923b466964cafdf2dd1ffef3cd6fbd7ffff0ed2f5fff319b7a172f4cfcbbbffdeedd3ffef93ef5b0e2d2146ffff4fdbb1fbf7ffbe7dfffebaf
5f3bb4f7393a33e1339260e13dc297de5396c0021dfcf119bf9ec42c46c494e8a791402952b338f48f656ca11f6d10450edc00db767cce21d5b880f7d72f2cc2
d398af2571687c182716f094313a60dc6985876a2ec3ccb3751ab927e76b13f714a10bd7dc43945a5e1eaf579063894be530c616cd2714a5124538c5d253dfb1
738c1dabfb8210cbaea764ce99604be97d41bc01224e93ccc899154da5d03149c02f1b1741f0b7659bd3e7de8051d7aa47f8c246c2de40d4417e86a965c6fb68
2d51e252394309350d7e8264ec2239ddf0b9891b0b099e8e3065de78818570c93ce6b05ec3e90f21cdb8dd7e4a37898de4929cbb749e20c64ce4889d0f6394ac
5cd829496313fbb938871045de13265df05366ef10f50e7e40e941773f27d872f787b3c133c8b026a53240d4376beef0e57dccacf89d6ee8126157aae9f3c44a
b17d4e9cd131584756689f604cd1255a60ec3dfbdcc160c05696cd4bd20f62c82ac7d815580f901dabea3dc5027a25d5dcece7c91322ac909de2881de073bad9
493c1b9426881fd2fc08bc6eda7c0ca52e7105c0633a3f37818f08f480102f4ea33c16a0c308ee835a9fc4c82a60ea5db8e375c32dff5d658fc1be7c61d1b8c2
be04197c6d1948eca6cc7b6d3343d49aa00c9819822ec3956e41c4727f29a28aab165b3be596f6a62ddd00dd91d5f42424fd6007b4d3fb84ffbbde073a8cb77f
f9c6b10f3e4ebfe3566c25ab6b763a8792c9f14e7f7308b7dbd50c195f904fbfa919a175fa04431dd9cf58b73dcd6d4fe3ffdff73487f6f36d2773a8dfb8ed64
7ce8306e3b99fc70e5e3743265f3027d8d3af0c80e7af4b14f72f0d46749289dca0dc527421ffc08f83db398c0a092d3279eb838055cc5f0a8ca1c4c60e1228e
b48cc799fc0d91f134462b381daafb4a492472d591f0564cc0a1911e76ea5678ba4e4ed9223becacd7d5c16656590592e5782d2cc6e1a04a66e856bb3cc02bd4
6bb6913e68dd1250b2d721614c6693683a48b4b783ca48fa58178ce620a157f65158741d2c3a4afdd6557b2c805ae115f8c1edc1cff49e1f06200242701e07cd
f942f92973f5d6bbda991fd3d3878c69450034d8db08283ddd555c0f2e4fad2e0bb52b78da2261849b4d425b46377822869fc17974aad1abd0b8aeafbba54b2d
7aca147a3e08ad9246bbf33e1637f535c8ede6069a9a9982a6de65cf6f35430899395af5fc251c1ac363b282d811ea3717a211dcbccc25cf36fc4d32cb8a0b39
4222ce0cae934e960d122231f728497abe5a7ee1069aea1ca2b9d51b90103e59725d482b9f1a3970baed64bc5ce2b934dd6e8c284b67af90e1b35ce1fc568bdf
1cac24d91adc3d8d1797de195df3a708422c6cd795011744c0dd413db3e682c0655891c8caf8db294c79da356fa3740c65e388ae62945714339967709dca0b3a
faadb081f196af190c6a98242f8467912ab0a651ad6a5a548d8cc3c1aafb6121653923699635d3ca2aaa6abab39835c3b60cecd8f26645de60b53531e434b3c2
67a97b37e576b7b96ea74f28aa0418bcb09fa3ea5ea12018d4cac92c6a8af17e1a56393b1fb56bc776811fa07695226164fdd656ed8edd8a1ae19c0e066f54f9
416e376a6168b9ed2bb5a5f5adb979b1cdce5e40f2184197bba6526857c2c92e47d0104d754f92a50dd8222f65be35e0c95b73d2f3bfac85fd60d80887955a27
1c57826650ab74c27eb3d20fc3667d1cd66ba341e31514161927f530bbb19fc00506dde4f7f67a7cefee3ed9ded1dc99b3a4caf4dd7c5513d777f7f5c6e1bb7b
8f40d2f9b2d598749bdd41abd26df627956034e854bac3d6a0326a0ddba3c9681876ba9357be77a1c141bf390c5ae34ea5551f0e2b41aba6e877ba9576d068f4
8376bf330efaaff23606569ea58fdc16605ecdebde7f010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d65
2f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d36
3f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e
3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d985
0528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c020000130000000000000000000000
0000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000
000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c0000000000000000000000000019020000
7468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b6f4679893070000c92000001600000000000000
000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b01000027000000
000000000000000000009d0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000980b00000000}
{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d
617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169
6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363
656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}
{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;
\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text;
\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;
\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;
\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1;
\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision;
\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;
\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1;
\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;
\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;
\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;
\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;
\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;
\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;
\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;
\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;
\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;
\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;
\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;
\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;
\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;
\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;
\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;
\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;
\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;
\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;
\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;
\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;
\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000
02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000
d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000d012
e8557332d801feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000105000000000000}}

377
dist/createmsi.py vendored Normal file
View File

@@ -0,0 +1,377 @@
#!/usr/bin/env python3
# Copyright 2017-2018 Jussi Pakkanen et al
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys, os, subprocess, shutil, uuid, json
from glob import glob
import platform
import xml.etree.ElementTree as ET
sys.path.append(os.getcwd())
def gen_guid():
return str(uuid.uuid4()).upper()
class Node:
def __init__(self, dirs, files):
assert(isinstance(dirs, list))
assert(isinstance(files, list))
self.dirs = dirs
self.files = files
class PackageGenerator:
def __init__(self, jsonfile):
jsondata = json.load(open(jsonfile, 'rb'))
self.product_name = jsondata['product_name']
self.manufacturer = jsondata['manufacturer']
self.version = jsondata['version']
self.comments = jsondata['comments']
self.installdir = jsondata['installdir']
self.license_file = jsondata['license_file']
self.name = jsondata['name']
self.guid = '*'
self.upgrade_guid = jsondata['upgrade_guid']
self.basename = jsondata['name_base']
self.need_msvcrt = jsondata.get('need_msvcrt', False)
self.addremove_icon = jsondata.get('addremove_icon', None)
self.startmenu_shortcut = jsondata.get('startmenu_shortcut', None)
self.desktop_shortcut = jsondata.get('desktop_shortcut', None)
self.main_xml = self.basename + '.wxs'
self.main_o = self.basename + '.wixobj'
if 'installscope' in jsondata:
self.installscope = jsondata['installscope']
else:
self.installscope = 'perUser'
assert self.installscope in ('perUser', 'perMachine')
if 'arch' in jsondata:
self.arch = jsondata['arch']
else:
# rely on the environment variable since python architecture may not be the same as system architecture
if 'PROGRAMFILES(X86)' in os.environ:
self.arch = 64
else:
self.arch = 32 if '32' in platform.architecture()[0] else 64
self.final_output = '%s-%s-%d.msi' % (self.basename, self.version, self.arch)
if self.arch == 64:
self.progfile_dir = 'ProgramFiles64Folder'
if platform.system() == "Windows":
redist_glob = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC141_CRT_x64.msm'
else:
redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x64.msm'
else:
self.progfile_dir = 'ProgramFilesFolder'
if platform.system() == "Windows":
redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\2017\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC141_CRT_x86.msm'
else:
redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x86.msm'
trials = glob(redist_glob)
if self.need_msvcrt:
if len(trials) != 1:
sys.exit('There are more than one potential redist dirs.')
self.redist_path = trials[0]
self.component_num = 0
self.registry_entries = jsondata.get('registry_entries', None)
self.major_upgrade = jsondata.get('major_upgrade', None)
self.parts = jsondata['parts']
self.feature_components = {}
self.feature_properties = {}
def generate_files(self):
self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'})
product = ET.SubElement(self.root, 'Product', {
'Name': self.product_name,
'Manufacturer': self.manufacturer,
'Id': self.guid,
'UpgradeCode': self.upgrade_guid,
'Language': '1033',
'Codepage': '1252',
'Version': self.version,
})
package = ET.SubElement(product, 'Package', {
'Id': '*',
'Keywords': 'Installer',
'Description': '%s %s installer' % (self.name, self.version),
'Comments': self.comments,
'Manufacturer': self.manufacturer,
'InstallerVersion': '500',
'Languages': '1033',
'Compressed': 'yes',
'SummaryCodepage': '1252',
'InstallScope': self.installscope,
})
if self.major_upgrade is not None:
majorupgrade = ET.SubElement(product, 'MajorUpgrade', {})
for mkey in self.major_upgrade.keys():
majorupgrade.set(mkey, self.major_upgrade[mkey])
else:
ET.SubElement(product, 'MajorUpgrade', {'DowngradeErrorMessage': 'A newer version of %s is already installed.' % self.name})
if self.arch == 64:
package.set('Platform', 'x64')
ET.SubElement(product, 'Media', {
'Id': '1',
'Cabinet': self.basename + '.cab',
'CompressionLevel': 'high',
'EmbedCab': 'yes',
})
targetdir = ET.SubElement(product, 'Directory', {
'Id': 'TARGETDIR',
'Name': 'SourceDir',
})
progfiledir = ET.SubElement(targetdir, 'Directory', {
'Id': self.progfile_dir,
})
pmf = ET.SubElement(targetdir, 'Directory', {'Id': 'ProgramMenuFolder'},)
if self.startmenu_shortcut is not None:
ET.SubElement(pmf, 'Directory', {
'Id': 'ApplicationProgramsFolder',
'Name': self.product_name,
})
if self.desktop_shortcut is not None:
ET.SubElement(pmf, 'Directory', {'Id': 'DesktopFolder',
'Name': 'Desktop',
})
installdir = ET.SubElement(progfiledir, 'Directory', {
'Id': 'INSTALLDIR',
'Name': self.installdir,
})
if self.need_msvcrt:
ET.SubElement(installdir, 'Merge', {
'Id': 'VCRedist',
'SourceFile': self.redist_path,
'DiskId': '1',
'Language': '0',
})
if self.startmenu_shortcut is not None:
ap = ET.SubElement(product, 'DirectoryRef', {'Id': 'ApplicationProgramsFolder'})
comp = ET.SubElement(ap, 'Component', {'Id': 'ApplicationShortcut',
'Guid': gen_guid(),
})
ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationStartMenuShortcut',
'Name': self.product_name,
'Description': self.comments,
'Target': '[INSTALLDIR]' + self.startmenu_shortcut,
'WorkingDirectory': 'INSTALLDIR',
})
ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveApplicationProgramsFolder',
'Directory': 'ApplicationProgramsFolder',
'On': 'uninstall',
})
ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
'Key': 'Software\\Microsoft\\' + self.name,
'Name': 'Installed',
'Type': 'integer',
'Value': '1',
'KeyPath': 'yes',
})
if self.desktop_shortcut is not None:
desk = ET.SubElement(product, 'DirectoryRef', {'Id': 'DesktopFolder'})
comp = ET.SubElement(desk, 'Component', {'Id':'ApplicationShortcutDesktop',
'Guid': gen_guid(),
})
ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationDesktopShortcut',
'Name': self.product_name,
'Description': self.comments,
'Target': '[INSTALLDIR]' + self.desktop_shortcut,
'WorkingDirectory': 'INSTALLDIR',
})
ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveDesktopFolder',
'Directory': 'DesktopFolder',
'On': 'uninstall',
})
ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
'Key': 'Software\\Microsoft\\' + self.name,
'Name': 'Installed',
'Type': 'integer',
'Value': '1',
'KeyPath': 'yes',
})
ET.SubElement(product, 'Property', {
'Id': 'WIXUI_INSTALLDIR',
'Value': 'INSTALLDIR',
})
if platform.system() == "Windows":
ET.SubElement(product, 'UIRef', {
'Id': 'WixUI_FeatureTree',
})
top_feature = ET.SubElement(product, 'Feature', {
'Id': 'Complete',
'Title': self.name + ' ' + self.version,
'Description': 'The complete package',
'Display': 'expand',
'Level': '1',
'ConfigurableDirectory': 'INSTALLDIR',
})
for f in self.parts:
self.scan_feature(top_feature, installdir, 1, f)
if self.need_msvcrt:
vcredist_feature = ET.SubElement(top_feature, 'Feature', {
'Id': 'VCRedist',
'Title': 'Visual C++ runtime',
'AllowAdvertise': 'no',
'Display': 'hidden',
'Level': '1',
})
ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'})
if self.startmenu_shortcut is not None:
ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcut'})
if self.desktop_shortcut is not None:
ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcutDesktop'})
if self.addremove_icon is not None:
icoid = 'addremoveicon.ico'
ET.SubElement(product, 'Icon', {'Id': icoid,
'SourceFile': self.addremove_icon,
})
ET.SubElement(product, 'Property', {'Id': 'ARPPRODUCTICON',
'Value': icoid,
})
if self.registry_entries is not None:
registry_entries_directory = ET.SubElement(product, 'DirectoryRef', {'Id': 'TARGETDIR'})
registry_entries_component = ET.SubElement(registry_entries_directory, 'Component', {'Id': 'RegistryEntries', 'Guid': gen_guid()})
if self.arch == 64:
registry_entries_component.set('Win64', 'yes')
ET.SubElement(top_feature, 'ComponentRef', {'Id': 'RegistryEntries'})
for r in self.registry_entries:
self.create_registry_entries(registry_entries_component, r)
ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True)
# ElementTree can not do prettyprinting so do it manually
import xml.dom.minidom
doc = xml.dom.minidom.parse(self.main_xml)
with open(self.main_xml, 'w') as of:
of.write(doc.toprettyxml(indent=' '))
def create_registry_entries(self, comp, reg):
reg_key = ET.SubElement(comp, 'RegistryKey', {
'Root': reg['root'],
'Key': reg['key'],
'Action': reg['action'],
})
ET.SubElement(reg_key, 'RegistryValue', {
'Name': reg['name'],
'Type': reg['type'],
'Value': reg['value'],
'KeyPath': reg['key_path'],
})
def scan_feature(self, top_feature, installdir, depth, feature):
for sd in [feature['staged_dir']]:
if '/' in sd or '\\' in sd:
sys.exit('Staged_dir %s must not have a path segment.' % sd)
nodes = {}
for root, dirs, files in os.walk(sd):
cur_node = Node(dirs, files)
nodes[root] = cur_node
fdict = {
'Id': feature['id'],
'Title': feature['title'],
'Description': feature['description'],
'Level': '1'
}
if feature.get('absent', 'ab') == 'disallow':
fdict['Absent'] = 'disallow'
self.feature_properties[sd] = fdict
self.feature_components[sd] = []
self.create_xml(nodes, sd, installdir, sd)
self.build_features(nodes, top_feature, sd)
def build_features(self, nodes, top_feature, staging_dir):
feature = ET.SubElement(top_feature, 'Feature', self.feature_properties[staging_dir])
for component_id in self.feature_components[staging_dir]:
ET.SubElement(feature, 'ComponentRef', {
'Id': component_id,
})
def path_to_id(self, pathname):
return pathname.replace('\\', '_').replace('/', '_').replace('#', '_').replace('-', '_').replace("+", "__")
def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir):
cur_node = nodes[current_dir]
if cur_node.files:
component_id = 'ApplicationFiles%d' % self.component_num
comp_xml_node = ET.SubElement(parent_xml_node, 'Component', {
'Id': component_id,
'Guid': gen_guid(),
})
self.feature_components[staging_dir].append(component_id)
if self.arch == 64:
comp_xml_node.set('Win64', 'yes')
if platform.system() == "Windows" and self.component_num == 0:
ET.SubElement(comp_xml_node, 'Environment', {
'Id': 'Environment',
'Name': 'PATH',
'Part': 'last',
'System': 'yes',
'Action': 'set',
'Value': '[INSTALLDIR]',
})
self.component_num += 1
for f in cur_node.files:
file_id = self.path_to_id(os.path.join(current_dir, f))
ET.SubElement(comp_xml_node, 'File', {
'Id': file_id,
'Name': f,
'Source': os.path.join(current_dir, f),
})
for dirname in cur_node.dirs:
dir_id = self.path_to_id(os.path.join(current_dir, dirname))
dir_node = ET.SubElement(parent_xml_node, 'Directory', {
'Id': dir_id,
'Name': dirname,
})
self.create_xml(nodes, os.path.join(current_dir, dirname), dir_node, staging_dir)
def build_package(self):
wixdir = 'c:\\Program Files\\Wix Toolset v3.11\\bin'
if platform.system() != "Windows":
wixdir = '/usr/bin'
if not os.path.isdir(wixdir):
wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
if not os.path.isdir(wixdir):
print("ERROR: This script requires WIX")
sys.exit(1)
if platform.system() == "Windows":
subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml])
subprocess.check_call([os.path.join(wixdir, 'light'),
'-ext', 'WixUIExtension',
'-cultures:en-us',
'-dWixUILicenseRtf=' + self.license_file,
'-out', self.final_output,
self.main_o])
else:
subprocess.check_call([os.path.join(wixdir, 'wixl'), '-o', self.final_output, self.main_xml])
def run(args):
if len(args) != 1:
sys.exit('createmsi.py <msi definition json>')
jsonfile = args[0]
if '/' in jsonfile or '\\' in jsonfile:
sys.exit('Input file %s must not contain a path segment.' % jsonfile)
p = PackageGenerator(jsonfile)
p.generate_files()
p.build_package()
if __name__ == '__main__':
run(sys.argv[1:])

29
dist/vdiclient.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8",
"version" : "1.2.02",
"product_name" : "VDI Client",
"manufacturer" : "Josh Patten",
"name" : "VDI Client",
"name_base" : "vdiclient",
"arch": 64,
"comments" : "This is the Proxmox VDI client. This client interfaces with Proxmox requires that virt-viewer be installed.",
"installdir" : "VDIClient",
"installscope" : "perMachine",
"license_file" : "License.rtf",
"startmenu_shortcut": "vdiclient.exe",
"desktop_shortcut": "vdiclient.exe",
"parts" : [
{
"id" : "MainProgram",
"title" : "VDI Client",
"description" : "Proxmox VDI Client",
"absent" : "disallow",
"staged_dir" : "vdiclient"
}
],
"major_upgrade": {
"Schedule": "afterInstallInitialize",
"DowngradeErrorMessage": "A later version of [ProductName] is already installed",
"AllowSameVersionUpgrades": "yes"
}
}

6
requirements.bat Normal file
View File

@@ -0,0 +1,6 @@
@echo off
pip install pyinstaller
pip install proxmoxer
pip install PySimpleGUI
pip install requests
pip install pywin32

4
requirements.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
pip3 install proxmoxer
pip3 install PySimpleGUI
pip3 install requests

BIN
screenshots/login-totp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
screenshots/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
screenshots/vdiview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

57
vdiclient.ini.example Normal file
View File

@@ -0,0 +1,57 @@
[General]
# This is the title that is diplayed to the user
title = VDI Login
# This is the PySimpleGui Theme that is used. Run vdiclient.py with flag `--list_themes` for a list of themes
theme = LightBlue
# Program Icon
icon = vdiicon.ico
# Logo displayed on all windows
logo = vdiclient.png
# Enable Kiosk mode, which does not allow the user to close anything
kiosk = False
# Enable/Disable Fullscreen mode (not applicable in Kiosk mode)
fullscreen = True
# Enable displaying SPICE ini file before opening virt-viewer
inidebug = False
# Select which guest types to display. Acceptable values: both, lxc, qemu
guest_type = both
# Show VM option for resetting VM
#show_reset = True
# Set Window Dimensions. Only use if window isn't sizing properly
#window_width = 800
#window_height = 600
[Authentication]
# This is the authentication backend that will be used to authenticate
auth_backend = pve
# If enabled, 2FA TOTP entry dialog will show
auth_totp = false
# If disabled, TLS certificate will not be checked
tls_verify = false
# User name (if using token)
#user = user
# API Token Name
#token_name = dvi
# API Token Value
#token_value = xxx-x-x-x-xxx
[Hosts]
# Hosts are entered as `IP/FQDN = Port`
10.10.10.100 = 8006
pve1.example.com = 8006
[SpiceProxyRedirect]
# The Spice Proxy provided by the Proxmox API may need to have its host/port rewritten
# These rewrite rules are written `IP:port = IP:port`
# 1. Use the inidebug and read the current proxy=pve1.example.com:3128
# 2. Add your proxmox ip to the right side e.g. 123.123.123.123:6000
pve1.example.com:3128 = 123.123.123.123:6000
#[AdditionalParameters]
# If you wish to define additional parameters to pass to virt-viewer you may define them here
# More parameter definitions here: https://www.mankier.com/1/remote-viewer
# Some Examples:
# Enable USB passthrough
#enable-usbredir = true
# Enable auto USB device sharing
#enable-usb-autoshare = true

BIN
vdiclient.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

616
vdiclient.py Normal file
View File

@@ -0,0 +1,616 @@
#!/usr/bin/python3
import proxmoxer # pip install proxmoxer
import PySimpleGUI as sg # pip install PySimpleGUI
gui = 'TK'
import requests
from datetime import datetime
from configparser import ConfigParser
import random
import sys
import copy
import os
import subprocess
from time import sleep
from io import StringIO
class G:
hostpool = []
spiceproxy_conv = {}
proxmox = None
vvcmd = None
scaling = 1
#########
title = 'VDI Login'
backend = 'pve'
user = ""
token_name = None
token_value = None
totp = False
imagefile = None
kiosk = False
fullscreen = True
verify_ssl = True
icon = None
inidebug = False
show_reset = False
show_hibernate = False
addl_params = None
theme = 'LightBlue'
guest_type = 'both'
width = None
height = None
def loadconfig(config_location = None):
if config_location:
config = ConfigParser(delimiters='=')
try:
config.read(config_location)
except Exception as e:
win_popup_button(f'Unable to read supplied configuration:\n{e!r}', 'OK')
config_location = None
if not config_location:
if os.name == 'nt': # Windows
config_location = f'{os.getenv("APPDATA")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
config_location = f'{os.getenv("PROGRAMFILES")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
config_location = f'{os.getenv("PROGRAMFILES(x86)")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
# Last ditch effort
config_location = 'C:\\Program Files\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
return False
elif os.name == 'posix': #Linux
config_location = os.path.expanduser('~/.config/VDIClient/vdiclient.ini')
if not os.path.exists(config_location):
config_location = '/etc/vdiclient/vdiclient.ini'
if not os.path.exists(config_location):
config_location = '/usr/local/etc/vdiclient/vdiclient.ini'
if not os.path.exists(config_location):
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
return False
config = ConfigParser(delimiters='=')
try:
config.read(config_location)
except Exception as e:
win_popup_button(f'Unable to read configuration file:\n{e!r}', 'OK')
config_location = None
if not 'General' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `General` section defined!', 'OK')
return False
else:
if 'title' in config['General']:
G.title = config['General']['title']
if 'theme' in config['General']:
G.theme = config['General']['theme']
if 'icon' in config['General']:
if os.path.exists(config['General']['icon']):
G.icon = config['General']['icon']
if 'logo' in config['General']:
if os.path.exists(config['General']['logo']):
G.imagefile = config['General']['logo']
if 'kiosk' in config['General']:
G.kiosk = config['General'].getboolean('kiosk')
if 'fullscreen' in config['General']:
G.fullscreen = config['General'].getboolean('fullscreen')
if 'inidebug' in config['General']:
G.inidebug = config['General'].getboolean('inidebug')
if 'guest_type' in config['General']:
G.guest_type = config['General']['guest_type']
if 'show_reset' in config['General']:
G.show_reset = config['General'].getboolean('show_reset')
if 'window_width' in config['General']:
G.width = config['General'].getint('window_width')
if 'window_height' in config['General']:
G.height = config['General'].getint('window_height')
if not 'Authentication' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `Authentication` section defined!', 'OK')
return False
else:
if 'auth_backend' in config['Authentication']:
G.backend = config['Authentication']['auth_backend']
if 'auth_totp' in config['Authentication']:
G.totp = config['Authentication'].getboolean('auth_totp')
if 'tls_verify' in config['Authentication']:
G.verify_ssl = config['Authentication'].getboolean('tls_verify')
if 'user' in config['Authentication']:
G.user = config['Authentication']['user']
if 'token_name' in config['Authentication']:
G.token_name = config['Authentication']['token_name']
if 'token_value' in config['Authentication']:
G.token_value = config['Authentication']['token_value']
if not 'Hosts' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `Hosts` section defined!', 'OK')
return False
else:
for key in config['Hosts']:
G.hostpool.append({
'host': key,
'port': int(config['Hosts'][key])
})
if 'SpiceProxyRedirect' in config:
for key in config['SpiceProxyRedirect']:
G.spiceproxy_conv[key] = config['SpiceProxyRedirect'][key]
if 'AdditionalParameters' in config:
G.addl_params = {}
for key in config['AdditionalParameters']:
G.addl_params[key] = config['AdditionalParameters'][key]
return True
def win_popup(message):
layout = [
[sg.Text(message, key='-TXT-')]
]
window = sg.Window('Message', layout, return_keyboard_events=True, no_titlebar=True, keep_on_top=True, finalize=True, )
window.bring_to_front()
_, _ = window.read(timeout=10) # Fixes a black screen bug
window['-TXT-'].update(message)
sleep(.15)
window['-TXT-'].update(message)
return window
def win_popup_button(message, button):
layout = [
[sg.Text(message)],
[sg.Button(button)]
]
window = sg.Window('Message', layout, return_keyboard_events=True, no_titlebar=True, keep_on_top=True, finalize=True)
window.Element(button).SetFocus()
while True:
event, values = window.read()
if event in (button, sg.WIN_CLOSED, 'Log In', '\r', 'special 16777220', 'special 16777221'):
window.close()
return
def setmainlayout():
layout = []
if G.imagefile:
layout.append([sg.Image(G.imagefile), sg.Text(G.title, size =(18*G.scaling, 1*G.scaling), justification='c', font=["Helvetica", 18])])
else:
layout.append([sg.Text(G.title, size =(30*G.scaling, 1*G.scaling), justification='c', font=["Helvetica", 18])])
layout.append([sg.Text("Username", size =(12*G.scaling, 1*G.scaling), font=["Helvetica", 12]), sg.InputText(default_text=G.user,key='-username-', font=["Helvetica", 12])])
layout.append([sg.Text("Password", size =(12*G.scaling, 1*G.scaling),font=["Helvetica", 12]), sg.InputText(key='-password-', password_char='*', font=["Helvetica", 12])])
if G.totp:
layout.append([sg.Text("OTP Key", size =(12*G.scaling, 1), font=["Helvetica", 12]), sg.InputText(key='-totp-', font=["Helvetica", 12])])
if G.kiosk:
layout.append([sg.Button("Log In", font=["Helvetica", 14], bind_return_key=True)])
else:
layout.append([sg.Button("Log In", font=["Helvetica", 14], bind_return_key=True), sg.Button("Cancel", font=["Helvetica", 14])])
return layout
def getvms(listonly = False):
vms = []
try:
nodes = []
for node in G.proxmox.cluster.resources.get(type='node'):
if node['status'] == 'online':
nodes.append(node['node'])
for vm in G.proxmox.cluster.resources.get(type='vm'):
if vm['node'] not in nodes:
continue
if 'template' in vm and vm['template']:
continue
if G.guest_type == 'both' or G.guest_type == vm['type']:
if listonly:
vms.append(
{
'vmid': vm['vmid'],
'name': vm['name'],
'node': vm['node']
}
)
else:
vms.append(vm)
return vms
except proxmoxer.core.ResourceException as e:
win_popup_button(f"Unable to display list of VMs:\n {e!r}", 'OK')
return False
def setvmlayout(vms):
layout = []
if G.imagefile:
layout.append([sg.Image(G.imagefile), sg.Text(G.title, size =(18*G.scaling, 1*G.scaling), justification='c', font=["Helvetica", 18])])
else:
layout.append([sg.Text(G.title, size =(30*G.scaling, 1*G.scaling), justification='c', font=["Helvetica", 18])])
layout.append([sg.Text('Please select a desktop instance to connect to', size =(40*G.scaling, 1*G.scaling), justification='c', font=["Helvetica", 10])])
layoutcolumn = []
for vm in vms:
if not vm["status"] == "unknown":
print(vm)
vmkeyname = f'-VM|{vm["vmid"]}-'
connkeyname = f'-CONN|{vm["vmid"]}-'
resetkeyname = f'-RESET|{vm["vmid"]}-'
hiberkeyname = f'-HIBER|{vm["vmid"]}-'
state = 'stopped'
connbutton = sg.Button('Connect', font=["Helvetica", 14], key=connkeyname)
if vm['status'] == 'running':
if 'lock' in vm:
state = vm['lock']
if state in ('suspending', 'suspended'):
if state == 'suspended':
state = 'starting'
connbutton = sg.Button('Connect', font=["Helvetica", 14], key=connkeyname, disabled=True)
else:
state = vm['status']
tmplayout = [
sg.Text(vm['name'], font=["Helvetica", 14], size=(22*G.scaling, 1*G.scaling)),
sg.Text(f"State: {state}", font=["Helvetica", 0], size=(22*G.scaling, 1*G.scaling), key=vmkeyname),
connbutton
]
if G.show_reset:
tmplayout.append(
sg.Button('Reset', font=["Helvetica", 14], key=resetkeyname)
)
if G.show_hibernate:
tmplayout.append(
sg.Button('Hibernate', font=["Helvetica", 14], key=hiberkeyname)
)
layoutcolumn.append(tmplayout)
layoutcolumn.append([sg.HorizontalSeparator()])
if len(vms) > 5: # We need a scrollbar
layout.append([sg.Column(layoutcolumn, scrollable = True, size = [450*G.scaling, None] )])
else:
for row in layoutcolumn:
layout.append(row)
layout.append([sg.Button('Logout', font=["Helvetica", 14])])
return layout
def iniwin(inistring):
inilayout = [
[sg.Multiline(default_text=inistring, size=(800*G.scaling, 600*G.scaling))]
]
iniwindow = sg.Window('INI debug', inilayout)
while True:
event, values = iniwindow.read()
if event == None:
break
iniwindow.close()
return True
def vmaction(vmnode, vmid, vmtype, action='connect'):
status = False
if vmtype == 'qemu':
vmstatus = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.get('current')
else: # Not sure this is even a thing, but here it is...
vmstatus = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.get('current')
if action == 'reload':
stoppop = win_popup(f'Stopping {vmstatus["name"]}...')
sleep(.1)
try:
if vmtype == 'qemu':
jobid = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.stop.post(timeout=28)
else: # Not sure this is even a thing, but here it is...
jobid = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.stop.post(timeout=28)
except proxmoxer.core.ResourceException as e:
stoppop.close()
win_popup_button(f"Unable to stop VM, please provide your system administrator with the following error:\n {e!r}", 'OK')
return False
running = True
i = 0
while running and i < 30:
try:
jobstatus = G.proxmox.nodes(vmnode).tasks(jobid).status.get()
except Exception:
# We ran into a query issue here, going to skip this round and try again
jobstatus = {}
if 'exitstatus' in jobstatus:
stoppop.close()
stoppop = None
if jobstatus['exitstatus'] != 'OK':
win_popup_button('Unable to stop VM, please contact your system administrator for assistance', 'OK')
return False
else:
running = False
status = True
sleep(1)
i += 1
if not status:
if stoppop:
stoppop.close()
return status
status = False
if vmtype == 'qemu':
vmstatus = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.get('current')
else: # Not sure this is even a thing, but here it is...
vmstatus = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.get('current')
sleep(.2)
if vmstatus['status'] != 'running':
startpop = win_popup(f'Starting {vmstatus["name"]}...')
sleep(.1)
try:
if vmtype == 'qemu':
jobid = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.start.post(timeout=28)
else: # Not sure this is even a thing, but here it is...
jobid = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.start.post(timeout=28)
except proxmoxer.core.ResourceException as e:
startpop.close()
win_popup_button(f"Unable to start VM, please provide your system administrator with the following error:\n {e!r}", 'OK')
return False
running = False
i = 0
while running == False and i < 30:
try:
jobstatus = G.proxmox.nodes(vmnode).tasks(jobid).status.get()
except Exception:
# We ran into a query issue here, going to skip this round and try again
jobstatus = {}
if 'exitstatus' in jobstatus:
startpop.close()
startpop = None
if jobstatus['exitstatus'] != 'OK':
win_popup_button('Unable to start VM, please contact your system administrator for assistance', 'OK')
running = True
else:
running = True
status = True
sleep(1)
i += 1
if not status:
if startpop:
startpop.close()
return status
if action == 'reload':
return
try:
if vmtype == 'qemu':
spiceconfig = G.proxmox.nodes(vmnode).qemu(str(vmid)).spiceproxy.post()
else: # Not sure this is even a thing, but here it is...
spiceconfig = G.proxmox.nodes(vmnode).lxc(str(vmid)).spiceproxy.post()
except proxmoxer.core.ResourceException as e:
win_popup_button(f"Unable to connect to VM {vmid}:\n{e!r}\nIs SPICE display configured for your VM?", 'OK')
return False
confignode = ConfigParser()
confignode['virt-viewer'] = {}
for key, value in spiceconfig.items():
if key == 'proxy':
val = value[7:].lower()
if val in G.spiceproxy_conv:
confignode['virt-viewer'][key] = f'http://{G.spiceproxy_conv[val]}'
else:
confignode['virt-viewer'][key] = f'{value}'
else:
confignode['virt-viewer'][key] = f'{value}'
if G.addl_params:
for key, value in G.addl_params.items():
confignode['virt-viewer'][key] = f'{value}'
inifile = StringIO('')
confignode.write(inifile)
inifile.seek(0)
inistring = inifile.read()
if G.inidebug:
closed = iniwin(inistring)
connpop = win_popup(f'Connecting to {vmstatus["name"]}...')
pcmd = [G.vvcmd]
if G.kiosk:
pcmd.append('--kiosk')
pcmd.append('--kiosk-quit')
pcmd.append('on-disconnect')
elif G.fullscreen:
pcmd.append('--full-screen')
pcmd.append('-') #We need it to listen on stdin
process = subprocess.Popen(pcmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
try:
output = process.communicate(input=inistring.encode('utf-8'), timeout=5)[0]
except subprocess.TimeoutExpired:
pass
status = True
connpop.close()
return status
def setcmd():
try:
if os.name == 'nt': # Windows
import csv
cmd1 = 'ftype VirtViewer.vvfile'
result = subprocess.check_output(cmd1, shell=True)
cmdresult = result.decode('utf-8')
cmdparts = cmdresult.split('=')
for row in csv.reader([cmdparts[1]], delimiter = ' ', quotechar = '"'):
G.vvcmd = row[0]
break
elif os.name == 'posix':
cmd1 = 'which remote-viewer'
result = subprocess.check_output(cmd1, shell=True)
G.vvcmd = 'remote-viewer'
except subprocess.CalledProcessError:
if os.name == 'nt':
win_popup_button('Installation of virt-viewer missing, please install from https://virt-manager.org/download/', 'OK')
elif os.name == 'posix':
win_popup_button('Installation of virt-viewer missing, please install using `apt install virt-viewer`', 'OK')
sys.exit()
def pveauth(username, passwd=None, totp=None):
random.shuffle(G.hostpool)
err = None
for hostinfo in G.hostpool:
host = hostinfo['host']
if 'port' in hostinfo:
port = hostinfo['port']
else:
port = 8006
connected = False
authenticated = False
if not connected and not authenticated:
try:
if G.token_name and G.token_value:
G.proxmox = proxmoxer.ProxmoxAPI(host, user=f'{username}@{G.backend}',token_name=G.token_name,token_value=G.token_value, verify_ssl=G.verify_ssl, port=port)
elif totp:
G.proxmox = proxmoxer.ProxmoxAPI(host, user=f'{username}@{G.backend}', otp=totp, password=passwd, verify_ssl=G.verify_ssl, port=port)
else:
G.proxmox = proxmoxer.ProxmoxAPI(host, user=f'{username}@{G.backend}', password=passwd, verify_ssl=G.verify_ssl, port=port)
connected = True
authenticated = True
return connected, authenticated, err
except proxmoxer.backends.https.AuthenticationError as e:
err = e
connected = True
return connected, authenticated, err
except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError) as e:
err = e
connected = False
return connected, authenticated, err
def loginwindow():
layout = setmainlayout()
if G.user and G.token_name and G.token_value: # We need to skip the login
popwin = win_popup("Please wait, authenticating...")
connected, authenticated, error = pveauth(G.user)
popwin.close()
if not connected:
win_popup_button(f'Unable to connect to any VDI server, are you connected to the Internet?\nError Info: {error}', 'OK')
return False
elif connected and not authenticated:
win_popup_button('Invalid username and/or password, please try again!', 'OK')
return False
elif connected and authenticated:
return True
else:
if G.icon:
window = sg.Window(G.title, layout, return_keyboard_events=True, resizable=False, no_titlebar=G.kiosk, icon=G.icon)
else:
window = sg.Window(G.title, layout, return_keyboard_events=True, resizable=False, no_titlebar=G.kiosk)
while True:
event, values = window.read()
if event == 'Cancel' or event == sg.WIN_CLOSED:
window.close()
return False
else:
if event in ('Log In', '\r', 'special 16777220', 'special 16777221'):
popwin = win_popup("Please wait, authenticating...")
user = values['-username-']
passwd = values['-password-']
totp = None
if '-totp-' in values:
if values['-totp-'] not in (None, ''):
totp = values['-totp-']
connected, authenticated, error = pveauth(user, passwd=passwd, totp=totp)
popwin.close()
if not connected:
win_popup_button(f'Unable to connect to any VDI server, are you connected to the Internet?\nError Info: {error}', 'OK')
elif connected and not authenticated:
win_popup_button('Invalid username and/or password, please try again!', 'OK')
elif connected and authenticated:
window.close()
return True
#break
def showvms():
vms = getvms()
vmlist = getvms(listonly=True)
newvmlist = vmlist.copy()
if vms == False:
return False
if len(vms) < 1:
win_popup_button('No desktop instances found, please consult with your system administrator', 'OK')
return False
layout = setvmlayout(vms)
if G.icon:
window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height), icon=G.icon)
else:
window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, size=(G.width, G.height), no_titlebar=G.kiosk)
timer = datetime.now()
while True:
if (datetime.now() - timer).total_seconds() > 5:
timer = datetime.now()
newvmlist = getvms(listonly = True)
if vmlist != newvmlist:
vmlist = newvmlist.copy()
vms = getvms()
layout = setvmlayout(vms)
window.close()
if G.icon:
window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height), icon=G.icon)
else:
window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height))
window.bring_to_front()
else: # Refresh existing vm status
newvms = getvms()
for vm in newvms:
vmkeyname = f'-VM|{vm["vmid"]}-'
connkeyname = f'-CONN|{vm["vmid"]}-'
state = 'stopped'
if vm['status'] == 'running':
if 'lock' in vm:
state = vm['lock']
if state in ('suspending', 'suspended'):
window[connkeyname].update(disabled=True)
if state == 'suspended':
state = 'starting'
else:
state = vm['status']
window[connkeyname].update(disabled=False)
else:
window[connkeyname].update(disabled=False)
window[vmkeyname].update(f"State: {state}")
event, values = window.read(timeout = 1000)
if event in ('Logout', None):
window.close()
return False
if event.startswith('-CONN'):
eventparams = event.split('|')
vmid = eventparams[1][:-1]
found = False
for vm in vms:
if str(vm['vmid']) == vmid:
found = True
vmaction(vm['node'], vmid, vm['type'])
if not found:
win_popup_button(f'VM {vm["name"]} no longer availble, please contact your system administrator', 'OK')
elif event.startswith('-RESET'):
eventparams = event.split('|')
vmid = eventparams[1][:-1]
found = False
for vm in vms:
if str(vm['vmid']) == vmid:
found = True
vmaction(vm['node'], vmid, vm['type'], action='reload')
if not found:
win_popup_button(f'VM {vm["name"]} no longer availble, please contact your system administrator', 'OK')
return True
def main():
G.scaling = 1 # TKinter requires integers
config_location = None
if len(sys.argv) > 1:
if sys.argv[1] == '--list_themes':
sg.preview_all_look_and_feel_themes()
return
if sys.argv[1] == '--config':
if len(sys.argv) < 3:
win_popup_button('No config file provided with `--config` parameter.\nPlease provide location of config file!', 'OK')
return
else:
config_location = sys.argv[2]
setcmd()
if not loadconfig(config_location):
return False
sg.theme(G.theme)
loggedin = False
while True:
if not loggedin:
loggedin = loginwindow()
if not loggedin:
if G.user and G.token_name and G.token_value: # This means if we don't exit we'll be in an infinite loop
return 1
break
else:
vmstat = showvms()
if not vmstat:
G.proxmox = None
loggedin = False
if G.user and G.token_name and G.token_value: # This means if we don't exit we'll be in an infinite loop
return 0
else:
return
if __name__ == '__main__':
sys.exit(main())

BIN
vdiicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB