first upload
This commit is contained in:
1 parent
ea2928aac6
commit
424509aac8
5 files changed
+827
-55
No files matched your search
@@ -0,0 +1,78 @@
|
||||
# comi-cleanmgr - sources de paquet WAPT
|
||||
|
||||
Paquet de configuration de **N profils cleanmgr Windows**, destine a etre personnalise
|
||||
avec **Lancer update-package** depuis la console WAPT. Pas de suppression de fichiers lors
|
||||
de l'installation : seuls les parametres des profils sont deposes dans le registre.
|
||||
|
||||
## Update-package : approche native WAPT
|
||||
|
||||
Le code est adapte des conventions utilisees dans le paquet public officiel
|
||||
[`tis-waptagent-logo-template` 1.1-15](https://wapt.tranquil.it/store/en/details/tis-waptagent-logo-template_1.1-15_windows_5.1_PROD.wapt) :
|
||||
|
||||
- `waptguihelper.message_dialog(...)` : introduction et validation.
|
||||
- `waptguihelper.input_dialog(...)` : nombre de profils, categories additionnelles.
|
||||
- `waptguihelper.grid_dialog(..., waptguihelper.GRT_ALL, MetaData)` : tableau editable des IDs/noms et grilles de categories (booleens).
|
||||
- `control.inc_build()` puis `control.save_control_to_wapt()` : incrementation native du build WAPT lors d'un changement.
|
||||
- Garde pour `params['install_with_luti']` ou `params['running_as_luti']` : pas d'interface sur les builds automatiques LUTI.
|
||||
|
||||
Les helpers `ask_control_package` et `ask_control_version` de l'exemple de Tranquil IT
|
||||
sont **definis dans le fichier de l'exemple** ; ce ne sont pas des API standard.
|
||||
Ici, comme le nom du paquet ne change pas et que seule sa configuration change,
|
||||
`control.inc_build()` est suffisant.
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Extraire l'archive comme sources WAPT, puis ouvrir le paquet dans la **console WAPT sur Windows**.
|
||||
2. Depuis le depot prive, cliquer **Lancer update-package**.
|
||||
3. Saisir N (1 a 50), puis renseigner **dans une seule grille** les IDs `0..9999`
|
||||
et les noms des N profils.
|
||||
4. Pour chacun, cocher les categories dans la grille interactive. Les `VolumeCaches`
|
||||
personnalises sont disponibles dans un champ facultatif.
|
||||
5. Confirmer le recapitulatif : cela reecrit `profiles.json`, incremente le numero
|
||||
de build de `WAPT/control` et enregistre le controle.
|
||||
6. Construire, **signer** et publier le paquet selon le flux de travail habituel,
|
||||
puis l'installer sur les postes.
|
||||
7. Exemples d'execution (locale, admin ou action distante autorisee) :
|
||||
|
||||
```cmd
|
||||
cleanmgr.exe /sagerun:4101
|
||||
cleanmgr.exe /sagerun:4102
|
||||
```
|
||||
|
||||
`update_package()` s'execute **sur le poste de l'administrateur**, pas sur les endpoints.
|
||||
Les postes ne voient jamais de fenetres de configuration : `setup.py` lit seulement
|
||||
`profiles.json` a l'installation.
|
||||
|
||||
## Fonctionnement agent
|
||||
|
||||
- Configuration en `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches`.
|
||||
- `StateFlagsNNNN` REG_DWORD a `2` (categorie activee) ou `0` (categorie desactivee).
|
||||
- Trace des IDs et de la configuration dans `HKLM\SOFTWARE\Comitari\CleanmgrProfiles`.
|
||||
- `audit()` verifie les flags : `OK`, `WARNING` si categorie selectionnee absente de ce Windows,
|
||||
`ERROR` si registre non conforme.
|
||||
- `uninstall()` efface les valeurs de profil gerees sans toucher aux autres IDs.
|
||||
- Les profils supprimes lors d'une evolution du paquet voient leurs flags nettoyes.
|
||||
|
||||
## Precautions
|
||||
|
||||
- Ne **jamais** reutiliser un ID attribue a un autre administrateur : le paquet prend
|
||||
le controle des `StateFlagsNNNN` de l'ID utilise.
|
||||
- Certaines categories n'existent pas sous toutes les versions de Windows.
|
||||
- `Previous Installations` et `Update Cleanup` peuvent limiter le retour en arriere ;
|
||||
`Recycle Bin` supprime le contenu de la corbeille.
|
||||
- Le comportement sous SYSTEM peut differer de celui dans une session utilisateur.
|
||||
- L'action integree **Lancer CleanMgr** de WAPT Enterprise n'est pas prouvee comme
|
||||
prenant en charge les profils `4101/4102` personnalises.
|
||||
- Aucune tache planifiee ou nettoyage n'est lance automatiquement par ce paquet.
|
||||
- L'interface graphique n'a pas ete testee sur une console WAPT Windows reelle.
|
||||
La logique a ete testee via des doubles Python des helpers, de la console et du registre.
|
||||
|
||||
## Contenu
|
||||
|
||||
- `WAPT/control` : metadonnees WAPT (minimum WAPT 2.4).
|
||||
- `update_package.py` : assistant interactif de configuration, utilisant les helpers WAPT.
|
||||
- `profiles.json` : donnees des profils embarquees.
|
||||
- `setup.py` : application des profils sur les machines Windows, `audit`, `uninstall`.
|
||||
|
||||
Sources uniquement ; archive non signee, non compilable directement en `.wapt`
|
||||
sans la console et la chaine de signature WAPT.
|
||||
@@ -0,0 +1,43 @@
|
||||
package : comi-conf-clean-manager-template
|
||||
version : 0-0
|
||||
architecture : all
|
||||
section : base
|
||||
priority : optional
|
||||
name :
|
||||
categories :
|
||||
maintainer : root
|
||||
description : Package for comi-conf-clean-manager-template
|
||||
depends :
|
||||
conflicts :
|
||||
maturity :
|
||||
locale :
|
||||
target_os : all
|
||||
min_wapt_version :
|
||||
sources :
|
||||
installed_size :
|
||||
impacted_process :
|
||||
description_fr :
|
||||
description_pl :
|
||||
description_de :
|
||||
description_es :
|
||||
description_pt :
|
||||
description_it :
|
||||
description_nl :
|
||||
description_ru :
|
||||
audit_schedule :
|
||||
editor :
|
||||
keywords :
|
||||
licence :
|
||||
homepage :
|
||||
package_uuid :
|
||||
valid_from :
|
||||
valid_until :
|
||||
forced_install_on :
|
||||
changelog :
|
||||
min_os_version :
|
||||
max_os_version :
|
||||
icon_sha256sum :
|
||||
signer :
|
||||
signer_fingerprint:
|
||||
signature_date :
|
||||
signed_attributes :
|
||||
@@ -0,0 +1,239 @@
|
||||
[PyScripter]
|
||||
Version=3.6.4.0
|
||||
|
||||
[Project]
|
||||
ClassName=TProjectRootNode
|
||||
StoreRelativePaths=TRUE
|
||||
ShowFileExtensions=FALSE
|
||||
|
||||
[Project\ChildNodes\Node0]
|
||||
ClassName=TProjectFilesNode
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0]
|
||||
ClassName=TProjectFolderNode
|
||||
Name=wapt
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node0]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\common.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node1]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\setuphelpers.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node2]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\setuphelpers_linux.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node3]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\setuphelpers_macos.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node4]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\setuphelpers_unix.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node5]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\setuphelpers_windows.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node6]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\wapt-get.ini
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node7]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\wapt-get.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes\Node8]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=/opt/wapt\waptpackage.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node0\ChildNodes]
|
||||
Count=9
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node1]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=$[Project-Path]changelog.txt
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node2]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=$[Project-Path]control
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node3]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=$[Project-Path]..\setup.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes\Node4]
|
||||
ClassName=TProjectFileNode
|
||||
FileName=$[Project-Path]..\update_package.py
|
||||
|
||||
[Project\ChildNodes\Node0\ChildNodes]
|
||||
Count=5
|
||||
|
||||
[Project\ChildNodes\Node1]
|
||||
ClassName=TProjectRunConfiguationsNode
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node0]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: install
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node0\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch package installation.
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=install "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node0\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node1]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: remove
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node1\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch uninstallation (the uninstallation code found in local wapt database, remember to launch after an installation).
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=remove "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node1\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node2]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: uninstall
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node2\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch uninstallation code (for debugging purpose, it only runs the code of uninstall function).
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=uninstall "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node2\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[Project-Path]..
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node3]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: session-setup
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node3\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch session-setup (it runs directly as current user).
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=session-setup "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node3\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node4]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: audit
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node4\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch package audit.
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=audit -f "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node4\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node5]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: update-package
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node5\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch update_package (it usually serve to update binaries of the package).
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=update-package-sources "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node5\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node6]
|
||||
ClassName=TProjectRunConfiguationNode
|
||||
Name=WAPT: cleanup-session
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node6\RunConfig]
|
||||
ScriptName=/opt/wapt\wapt-get.py
|
||||
Description=Launch cleanup_session for the current package (it must have been installed, had session-setup and then removed).
|
||||
EngineType=peRemote
|
||||
ReinitializeBeforeRun=TRUE
|
||||
Parameters=cleanup-session "$[Project-Path].."
|
||||
WorkingDir=$[Project-Path]..
|
||||
WriteOutputToFile=FALSE
|
||||
OutputFileName=$[ActiveScript-NoExt].log
|
||||
AppendToFile=FALSE
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes\Node6\RunConfig\ExternalRun]
|
||||
Caption=External Run
|
||||
Description=Run script using an external Python Interpreter
|
||||
ApplicationName=$[PythonExe-Short]
|
||||
Parameters=$[ActiveScript-Short]
|
||||
WorkingDirectory=$[ActiveScript-Dir]
|
||||
|
||||
[Project\ChildNodes\Node1\ChildNodes]
|
||||
Count=7
|
||||
|
||||
[Project\ChildNodes]
|
||||
Count=2
|
||||
|
||||
[Project\ExtraPythonPath]
|
||||
Count=0
|
||||
|
||||
@@ -1,78 +1,193 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""WAPT agent-side configuration of cleanmgr.exe profiles.
|
||||
|
||||
The WAPT update_package.py wizard generates profiles.json in the package sources.
|
||||
Installing this package configures VolumeCaches only: it DOES NOT run cleanup.
|
||||
"""
|
||||
|
||||
from setuphelpers import *
|
||||
import json
|
||||
import re
|
||||
import winreg
|
||||
|
||||
uninstallkey = []
|
||||
|
||||
PROFILE = 100
|
||||
REG_PATH = (
|
||||
r'SOFTWARE\Microsoft\Windows\CurrentVersion'
|
||||
r'\Explorer\VolumeCaches'
|
||||
)
|
||||
|
||||
CATEGORIES = {
|
||||
'Temporary Files',
|
||||
'Thumbnail Cache',
|
||||
'Delivery Optimization Files',
|
||||
}
|
||||
|
||||
FLAG = 'StateFlags{:04d}'.format(PROFILE)
|
||||
VOLUME_CACHES = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
|
||||
TRACK_KEY = r'SOFTWARE\Comitari\CleanmgrProfiles'
|
||||
TRACK_VALUE = 'ProfilesJson'
|
||||
PROFILE_LIMIT = 50
|
||||
|
||||
|
||||
def get_categories():
|
||||
with reg_openkey_noredir(
|
||||
winreg.HKEY_LOCAL_MACHINE, REG_PATH
|
||||
) as root:
|
||||
categories = []
|
||||
index = 0
|
||||
def _validate_config(config):
|
||||
if not isinstance(config, dict) or config.get('schema_version') != 1:
|
||||
raise ValueError('Invalid profiles.json schema_version')
|
||||
profiles = config.get('profiles')
|
||||
if not isinstance(profiles, list) or not 1 <= len(profiles) <= PROFILE_LIMIT:
|
||||
raise ValueError('Expected 1..%d profiles' % PROFILE_LIMIT)
|
||||
|
||||
seen = set()
|
||||
for profile in profiles:
|
||||
if not isinstance(profile, dict):
|
||||
raise ValueError('Invalid profile')
|
||||
profile_id = profile.get('id')
|
||||
if type(profile_id) is not int or not 0 <= profile_id <= 9999:
|
||||
raise ValueError('Profile id must be in 0..9999')
|
||||
if profile_id in seen:
|
||||
raise ValueError('Duplicate profile id: %d' % profile_id)
|
||||
seen.add(profile_id)
|
||||
|
||||
name = profile.get('name')
|
||||
if not isinstance(name, str) or not 1 <= len(name.strip()) <= 80:
|
||||
raise ValueError('Profile name missing or too long')
|
||||
categories = profile.get('categories')
|
||||
if not isinstance(categories, list) or not categories:
|
||||
raise ValueError('Profile %d has no categories' % profile_id)
|
||||
for category in categories:
|
||||
if not isinstance(category, str) or not re.fullmatch(
|
||||
r'[A-Za-z0-9][A-Za-z0-9 ()_.-]{0,127}', category):
|
||||
raise ValueError('Unsafe or invalid VolumeCaches key: %r' % category)
|
||||
if len(set(categories)) != len(categories):
|
||||
raise ValueError('Duplicate categories in profile %d' % profile_id)
|
||||
return config
|
||||
|
||||
|
||||
def _read_deployed_config():
|
||||
try:
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, TRACK_KEY,
|
||||
sam=winreg.KEY_READ) as key:
|
||||
raw = winreg.QueryValueEx(key, TRACK_VALUE)[0]
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return _validate_config(json.loads(raw))
|
||||
|
||||
|
||||
def _store_deployed_config(config):
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, TRACK_KEY,
|
||||
sam=winreg.KEY_WRITE, create_if_missing=True) as key:
|
||||
reg_setvalue(key, TRACK_VALUE,
|
||||
json.dumps(config, ensure_ascii=False, sort_keys=True),
|
||||
type=winreg.REG_SZ)
|
||||
|
||||
|
||||
def _list_volume_categories():
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, VOLUME_CACHES,
|
||||
sam=winreg.KEY_READ) as root:
|
||||
result = []
|
||||
position = 0
|
||||
while True:
|
||||
try:
|
||||
categories.append(winreg.EnumKey(root, index))
|
||||
index += 1
|
||||
result.append(winreg.EnumKey(root, position))
|
||||
except OSError:
|
||||
break
|
||||
return categories
|
||||
position += 1
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def _flag(profile_id):
|
||||
return 'StateFlags%04d' % profile_id
|
||||
|
||||
|
||||
def _delete_profile_flags(profile_id, categories):
|
||||
for category in categories:
|
||||
path = VOLUME_CACHES + '\\' + category
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, path,
|
||||
sam=winreg.KEY_SET_VALUE) as key:
|
||||
try:
|
||||
winreg.DeleteValue(key, _flag(profile_id))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def _apply_profile(profile, available):
|
||||
wanted = set(profile['categories'])
|
||||
flag = _flag(profile['id'])
|
||||
for category in available:
|
||||
path = VOLUME_CACHES + '\\' + category
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, path,
|
||||
sam=winreg.KEY_SET_VALUE) as key:
|
||||
reg_setvalue(key, flag, 2 if category in wanted else 0,
|
||||
type=winreg.REG_DWORD)
|
||||
unavailable = wanted - set(available)
|
||||
if unavailable:
|
||||
print('Profile %d (%s), missing on this Windows: %s' % (
|
||||
profile['id'], profile['name'], ', '.join(sorted(unavailable))))
|
||||
print('Configured profile %d (%s): %d available selected categories' % (
|
||||
profile['id'], profile['name'], len(wanted & set(available))))
|
||||
|
||||
|
||||
def install():
|
||||
available = get_categories()
|
||||
path = makepath(basedir, 'profiles.json')
|
||||
with open(path, 'r', encoding='utf-8') as stream:
|
||||
config = _validate_config(json.load(stream))
|
||||
|
||||
available = _list_volume_categories()
|
||||
if not available:
|
||||
raise RuntimeError('No VolumeCaches categories available on this Windows')
|
||||
|
||||
previous = _read_deployed_config()
|
||||
previous_ids = {p['id'] for p in previous['profiles']} if previous else set()
|
||||
new_ids = {p['id'] for p in config['profiles']}
|
||||
for removed_id in sorted(previous_ids - new_ids):
|
||||
_delete_profile_flags(removed_id, available)
|
||||
print('Removed former profile %d' % removed_id)
|
||||
|
||||
for profile in config['profiles']:
|
||||
_apply_profile(profile, available)
|
||||
|
||||
_store_deployed_config(config)
|
||||
print('Cleanmgr profiles configured. No cleaning was executed.')
|
||||
|
||||
|
||||
def audit():
|
||||
try:
|
||||
config = _read_deployed_config()
|
||||
if not config:
|
||||
print('Cleanmgr profile configuration missing')
|
||||
return 'ERROR'
|
||||
available = _list_volume_categories()
|
||||
if not available:
|
||||
return 'ERROR'
|
||||
missing = False
|
||||
errors = []
|
||||
for profile in config['profiles']:
|
||||
wanted = set(profile['categories'])
|
||||
missing_categories = wanted - set(available)
|
||||
if missing_categories:
|
||||
print('Profile %d unavailable categories: %s' % (
|
||||
profile['id'], ', '.join(sorted(missing_categories))))
|
||||
missing = True
|
||||
for category in available:
|
||||
path = REG_PATH + '\\' + category
|
||||
|
||||
with reg_openkey_noredir(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
path,
|
||||
sam=winreg.KEY_SET_VALUE
|
||||
) as key:
|
||||
value = 2 if category in CATEGORIES else 0
|
||||
reg_setvalue(
|
||||
key, FLAG, value, type=winreg.REG_DWORD
|
||||
)
|
||||
|
||||
if category in CATEGORIES:
|
||||
print('Enabled: {}'.format(category))
|
||||
|
||||
missing = CATEGORIES - set(available)
|
||||
if missing:
|
||||
print('Unavailable categories: {}'.format(missing))
|
||||
|
||||
# Execution silencieuse du profil
|
||||
run('cleanmgr.exe /sagerun:{}'.format(PROFILE),
|
||||
timeout=3600)
|
||||
path = VOLUME_CACHES + '\\' + category
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, path,
|
||||
sam=winreg.KEY_READ) as key:
|
||||
try:
|
||||
current, kind = winreg.QueryValueEx(key, _flag(profile['id']))
|
||||
except FileNotFoundError:
|
||||
current, kind = None, None
|
||||
expected = 2 if category in wanted else 0
|
||||
if current != expected or kind != winreg.REG_DWORD:
|
||||
errors.append('%d / %s: expected %d, got %r' % (
|
||||
profile['id'], category, expected, current))
|
||||
if errors:
|
||||
for item in errors[:25]:
|
||||
print(item)
|
||||
return 'ERROR'
|
||||
return 'WARNING' if missing else 'OK'
|
||||
except Exception as exc:
|
||||
print('Cleanmgr audit failed: %s' % exc)
|
||||
return 'ERROR'
|
||||
|
||||
|
||||
def uninstall():
|
||||
for category in get_categories():
|
||||
path = REG_PATH + '\\' + category
|
||||
|
||||
with reg_openkey_noredir(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
path,
|
||||
sam=winreg.KEY_SET_VALUE
|
||||
) as key:
|
||||
previous = _read_deployed_config()
|
||||
if previous:
|
||||
available = _list_volume_categories()
|
||||
for profile in previous['profiles']:
|
||||
_delete_profile_flags(profile['id'], available)
|
||||
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, TRACK_KEY,
|
||||
sam=winreg.KEY_SET_VALUE) as key:
|
||||
try:
|
||||
winreg.DeleteValue(key, FLAG)
|
||||
winreg.DeleteValue(key, TRACK_VALUE)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
print('Managed Cleanmgr profile flags removed. No cleaning was executed.')
|
||||
@@ -0,0 +1,297 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Interactive WAPT update_package for multiple cleanmgr profiles.
|
||||
|
||||
Uses the same WAPT Console conventions as Tranquil IT's
|
||||
``tis-waptagent-logo-template``: waptguihelper dialogs, control.inc_build(),
|
||||
and control.save_control_to_wapt(). The names ask_control_version() and
|
||||
ask_control_package() in that template are local helpers, not WAPT APIs.
|
||||
"""
|
||||
|
||||
from setuphelpers import *
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
MAX_PROFILES = 50
|
||||
|
||||
# Stable VolumeCaches registry subkey names, NOT translated Windows labels.
|
||||
# Windows builds expose different subsets of this list.
|
||||
CATALOG = [
|
||||
('Temporary Files', 'Fichiers temporaires', 'Standard'),
|
||||
('Thumbnail Cache', 'Miniatures', 'Standard'),
|
||||
('D3D Shader Cache', 'Cache des shaders Direct3D', 'Standard'),
|
||||
('Delivery Optimization Files', 'Optimisation de la distribution', 'Standard'),
|
||||
('Downloaded Program Files', 'Programmes telecharges', 'Standard'),
|
||||
('Internet Cache Files', 'Cache Internet de Windows', 'Standard'),
|
||||
('Windows Defender', 'Fichiers non essentiels Defender', 'Maintenance'),
|
||||
('Windows Error Reporting Files', 'Rapports erreurs Windows', 'Maintenance'),
|
||||
('Setup Log Files', 'Journaux installation Windows', 'Maintenance'),
|
||||
('Windows Upgrade Log Files', 'Journaux mises a niveau Windows', 'Maintenance'),
|
||||
('Temporary Setup Files', 'Fichiers installation temporaires', 'Maintenance'),
|
||||
('Old ChkDsk Files', 'Fragments CHKDSK anciens', 'Maintenance'),
|
||||
('System error memory dump files', 'Dump memoire systeme', 'Diagnostic'),
|
||||
('System error minidump files', 'Mini dumps systeme', 'Diagnostic'),
|
||||
('Memory Dump Files', 'Autres dumps memoire', 'Diagnostic'),
|
||||
('Update Cleanup', 'Nettoyage Windows Update / rollback', 'Sensible'),
|
||||
('Previous Installations', 'Anciennes installations Windows / rollback', 'Sensible'),
|
||||
('Recycle Bin', 'Corbeille', 'Sensible'),
|
||||
('Windows ESD installation files', 'Fichiers de reinitialisation ESD', 'Sensible'),
|
||||
('Device Driver Packages', 'Anciens packages pilotes', 'Sensible'),
|
||||
]
|
||||
|
||||
PROFILE_META = json.dumps({'columns': [
|
||||
{'propertyname': 'id', 'datatype': 'Integer'},
|
||||
{'propertyname': 'name', 'datatype': 'String'},
|
||||
]})
|
||||
CATEGORY_META = json.dumps({'columns': [
|
||||
{'propertyname': 'key', 'datatype': 'String'},
|
||||
{'propertyname': 'description', 'datatype': 'String'},
|
||||
{'propertyname': 'risk', 'datatype': 'String'},
|
||||
{'propertyname': 'enabled', 'datatype': 'Boolean'},
|
||||
]})
|
||||
|
||||
DEFAULT_SAFE_CATEGORIES = [
|
||||
'Temporary Files', 'Thumbnail Cache',
|
||||
'D3D Shader Cache', 'Delivery Optimization Files',
|
||||
]
|
||||
|
||||
|
||||
def _load_config(path):
|
||||
if not os.path.isfile(path):
|
||||
return {'schema_version': 1, 'profiles': []}
|
||||
with open(path, 'r', encoding='utf-8') as stream:
|
||||
content = json.load(stream)
|
||||
if not isinstance(content, dict) or content.get('schema_version') != 1:
|
||||
raise ValueError('profiles.json : schema_version invalide.')
|
||||
if not isinstance(content.get('profiles'), list):
|
||||
raise ValueError('profiles.json : profiles doit etre une liste.')
|
||||
return content
|
||||
|
||||
|
||||
def _as_rows(result, dialog_name):
|
||||
if result is None:
|
||||
return None
|
||||
if isinstance(result, (str, bytes)):
|
||||
result = json.loads(result)
|
||||
if not isinstance(result, list) or any(not isinstance(row, dict) for row in result):
|
||||
raise ValueError('%s : resultat de grille invalide.' % dialog_name)
|
||||
return result
|
||||
|
||||
|
||||
def _valid_key(key):
|
||||
return isinstance(key, str) and bool(re.fullmatch(
|
||||
r'[A-Za-z0-9][A-Za-z0-9 ()_.-]{0,127}', key))
|
||||
|
||||
|
||||
def _normalize_id(value):
|
||||
if isinstance(value, bool):
|
||||
raise ValueError('ID Cleanmgr invalide : %r' % value)
|
||||
if isinstance(value, int):
|
||||
profile_id = value
|
||||
elif isinstance(value, str) and value.strip().isdigit():
|
||||
profile_id = int(value.strip())
|
||||
else:
|
||||
raise ValueError('ID Cleanmgr invalide : %r (nombre entier attendu)' % value)
|
||||
if not 0 <= profile_id <= 9999:
|
||||
raise ValueError('ID Cleanmgr hors plage 0..9999 : %s' % profile_id)
|
||||
return profile_id
|
||||
|
||||
|
||||
def _enabled(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return value != 0
|
||||
return isinstance(value, str) and value.strip().lower() in (
|
||||
'true', 'yes', 'oui', '1')
|
||||
|
||||
|
||||
def _next_id(used):
|
||||
# Keeps IDs together without overwriting existing IDs already in use.
|
||||
for candidate in list(range(4101, 10000)) + list(range(0, 4101)):
|
||||
if candidate not in used:
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
raise ValueError('Plus aucun identifiant Cleanmgr disponible.')
|
||||
|
||||
|
||||
def _profile_rows(old_profiles, count):
|
||||
rows = [{'id': profile['id'], 'name': profile['name']}
|
||||
for profile in old_profiles[:count]]
|
||||
used = {row['id'] for row in rows}
|
||||
while len(rows) < count:
|
||||
profile_id = _next_id(used)
|
||||
rows.append({'id': profile_id, 'name': 'Profil %s' % profile_id})
|
||||
return rows
|
||||
|
||||
|
||||
def _validate_profile_rows(rows, count):
|
||||
if len(rows) != count:
|
||||
raise ValueError('La grille doit contenir exactement %d profils.' % count)
|
||||
profiles = []
|
||||
used = set()
|
||||
for row in rows:
|
||||
profile_id = _normalize_id(row.get('id'))
|
||||
if profile_id in used:
|
||||
raise ValueError('Identifiant Cleanmgr duplique : %d.' % profile_id)
|
||||
used.add(profile_id)
|
||||
name = row.get('name')
|
||||
if not isinstance(name, str) or not 1 <= len(name.strip()) <= 80:
|
||||
raise ValueError('Le profil %d doit avoir un nom (1 a 80 caracteres).' % profile_id)
|
||||
profiles.append({'id': profile_id, 'name': name.strip()})
|
||||
return profiles
|
||||
|
||||
|
||||
def _category_rows(selected):
|
||||
keys = {key for key, _, _ in CATALOG}
|
||||
rows = [
|
||||
{'key': key, 'description': label, 'risk': risk, 'enabled': key in selected}
|
||||
for key, label, risk in CATALOG
|
||||
]
|
||||
for key in sorted(selected - keys):
|
||||
rows.append({'key': key, 'description': 'Categorie personnalisee existante',
|
||||
'risk': 'A verifier', 'enabled': True})
|
||||
return rows
|
||||
|
||||
|
||||
def _parse_categories(rows, allowed_keys):
|
||||
if rows is None:
|
||||
return None
|
||||
if len(rows) != len(allowed_keys):
|
||||
raise ValueError('Le nombre de categories de la grille a ete modifie.')
|
||||
result = []
|
||||
seen = set()
|
||||
for row in rows:
|
||||
key = row.get('key')
|
||||
if key not in allowed_keys or key in seen:
|
||||
raise ValueError('Cle VolumeCaches inconnue, modifiee ou dupliquee : %r' % key)
|
||||
seen.add(key)
|
||||
if _enabled(row.get('enabled')):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
def _write_config(path, config):
|
||||
temporary = path + '.tmp'
|
||||
try:
|
||||
with open(temporary, 'w', encoding='utf-8') as stream:
|
||||
json.dump(config, stream, indent=2, ensure_ascii=False)
|
||||
stream.write('\n')
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.remove(temporary)
|
||||
|
||||
|
||||
def update_package():
|
||||
import waptguihelper
|
||||
|
||||
# Based on official Store template: LUTI/headless builds must avoid GUI prompts.
|
||||
current_params = globals().get('params', {}) or {}
|
||||
if current_params.get('install_with_luti') or current_params.get('running_as_luti'):
|
||||
print('Build LUTI : conservation de profiles.json sans intervention graphique.')
|
||||
return 'OK'
|
||||
|
||||
config_path = makepath(basedir, 'profiles.json')
|
||||
existing_config = _load_config(config_path)
|
||||
old_profiles = existing_config['profiles']
|
||||
|
||||
waptguihelper.message_dialog(
|
||||
'Comitari - Profils Cleanmgr',
|
||||
'Cette action permet de definir plusieurs profils de nettoyage Windows.\n'
|
||||
'Pour chaque profil, choisissez un identifiant, un nom et les categories.\n'
|
||||
'L installation configure les profils mais NE LANCE PAS le nettoyage.\n'
|
||||
'Les categories disponibles dependent de la version de Windows.',
|
||||
0 + 64 # MB_OK + MB_ICONINFORMATION (comme le template officiel)
|
||||
)
|
||||
|
||||
count_str = waptguihelper.input_dialog(
|
||||
'Comitari - Profils Cleanmgr',
|
||||
'Nombre de profils a configurer (1 a %d) :' % MAX_PROFILES,
|
||||
str(len(old_profiles) or 2))
|
||||
if count_str is None:
|
||||
print('Configuration annulee (aucun fichier modifie).')
|
||||
return False
|
||||
try:
|
||||
count = int(count_str.strip())
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('Veuillez saisir un nombre entier de profils.')
|
||||
if not 1 <= count <= MAX_PROFILES:
|
||||
raise ValueError('Le nombre de profils doit etre entre 1 et %d.' % MAX_PROFILES)
|
||||
|
||||
# WAPT native editable grid: one dialog for N names and IDs instead of 2*N dialogs.
|
||||
rows = _as_rows(waptguihelper.grid_dialog(
|
||||
'Profils Cleanmgr : modifiez les identifiants et noms',
|
||||
_profile_rows(old_profiles, count),
|
||||
waptguihelper.GRT_ALL,
|
||||
PROFILE_META), 'Profils Cleanmgr')
|
||||
if rows is None:
|
||||
return False
|
||||
new_profiles = _validate_profile_rows(rows, count)
|
||||
|
||||
by_id = {p['id']: p for p in old_profiles}
|
||||
for index, profile in enumerate(new_profiles):
|
||||
# When an ID is changed, retain selections from its previous row.
|
||||
prior = by_id.get(profile['id'])
|
||||
if prior is None and index < len(old_profiles):
|
||||
prior = old_profiles[index]
|
||||
previous = set(prior.get('categories', [])) if prior else set(DEFAULT_SAFE_CATEGORIES)
|
||||
possible = _category_rows(previous)
|
||||
possible_keys = {row['key'] for row in possible}
|
||||
title = 'Profil %d/%d : %d - %s' % (
|
||||
index + 1, count, profile['id'], profile['name'])
|
||||
|
||||
returned = _as_rows(waptguihelper.grid_dialog(
|
||||
title + ' - cochez les categories',
|
||||
possible,
|
||||
waptguihelper.GRT_ALL,
|
||||
CATEGORY_META), 'Categories Cleanmgr')
|
||||
if returned is None:
|
||||
return False
|
||||
categories = _parse_categories(returned, possible_keys)
|
||||
|
||||
# Add optional custom registry subkeys, when needed on specific Windows versions.
|
||||
custom = waptguihelper.input_dialog(
|
||||
title, 'Autres sous-cles VolumeCaches a ajouter (virgules, optionnel) :', '')
|
||||
if custom is None:
|
||||
return False
|
||||
for candidate in custom.split(','):
|
||||
category = candidate.strip()
|
||||
if category and category not in categories:
|
||||
categories.append(category)
|
||||
if not categories:
|
||||
raise ValueError('Le profil %d doit contenir au moins une categorie.' % profile['id'])
|
||||
if len(categories) > 200 or any(not _valid_key(key) for key in categories):
|
||||
raise ValueError('Categorie VolumeCaches invalide ou liste trop longue.')
|
||||
profile['categories'] = categories
|
||||
|
||||
overview = '\n'.join('%d - %s : %d categories' % (
|
||||
p['id'], p['name'], len(p['categories'])) for p in new_profiles)
|
||||
risky = [p['name'] for p in new_profiles if
|
||||
set(p['categories']) & {'Previous Installations', 'Recycle Bin',
|
||||
'Windows ESD installation files', 'Update Cleanup'}]
|
||||
if risky:
|
||||
overview += ('\n\nATTENTION : categories sensibles dans %s.\n'
|
||||
'Certaines suppressions empechent un retour a la version '
|
||||
'precedente de Windows ou vident la corbeille.' % ', '.join(risky))
|
||||
overview += '\n\nEnregistrer les profils dans ce paquet ?'
|
||||
confirmed = waptguihelper.message_dialog(
|
||||
'Validation des profils', overview,
|
||||
4 + 32) # MB_YESNO + MB_ICONQUESTION
|
||||
if confirmed != waptguihelper.ID_YES:
|
||||
print('Configuration non validee (aucun fichier modifie).')
|
||||
return False
|
||||
|
||||
new_config = {'schema_version': 1, 'profiles': new_profiles}
|
||||
if new_config == existing_config:
|
||||
waptguihelper.message_dialog(
|
||||
'Profils inchanges', 'Aucun changement detecte dans profiles.json.', 0 + 64)
|
||||
return False
|
||||
|
||||
# WAPT-native control API instead of manually parsing the version string.
|
||||
_write_config(config_path, new_config)
|
||||
control.inc_build()
|
||||
control.save_control_to_wapt()
|
||||
print('Profils enregistres : %s (%d profils).' % (config_path, len(new_profiles)))
|
||||
print('Version du paquet WAPT : %s' % control.version)
|
||||
return True
|
||||
Reference in new issue
Block a user