first upload

This commit is contained in:
PierreCsn committed 2026-10-28 10:11:18 +01:00
1 parent ea2928aac6
commit 424509aac8
5 files changed
+827 -55

No files matched your search

+78
View File
@@ -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.
+43
View File
@@ -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 :
+239
View File
@@ -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
+170 -55
View File
@@ -1,78 +1,193 @@
# -*- coding: utf-8 -*- # -*- 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 * from setuphelpers import *
import json
import re
import winreg import winreg
uninstallkey = [] uninstallkey = []
PROFILE = 100 VOLUME_CACHES = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
REG_PATH = ( TRACK_KEY = r'SOFTWARE\Comitari\CleanmgrProfiles'
r'SOFTWARE\Microsoft\Windows\CurrentVersion' TRACK_VALUE = 'ProfilesJson'
r'\Explorer\VolumeCaches' PROFILE_LIMIT = 50
)
CATEGORIES = {
'Temporary Files',
'Thumbnail Cache',
'Delivery Optimization Files',
}
FLAG = 'StateFlags{:04d}'.format(PROFILE)
def get_categories(): def _validate_config(config):
with reg_openkey_noredir( if not isinstance(config, dict) or config.get('schema_version') != 1:
winreg.HKEY_LOCAL_MACHINE, REG_PATH raise ValueError('Invalid profiles.json schema_version')
) as root: profiles = config.get('profiles')
categories = [] if not isinstance(profiles, list) or not 1 <= len(profiles) <= PROFILE_LIMIT:
index = 0 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: while True:
try: try:
categories.append(winreg.EnumKey(root, index)) result.append(winreg.EnumKey(root, position))
index += 1
except OSError: except OSError:
break 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(): 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: for category in available:
path = REG_PATH + '\\' + category path = VOLUME_CACHES + '\\' + category
with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, path,
with reg_openkey_noredir( sam=winreg.KEY_READ) as key:
winreg.HKEY_LOCAL_MACHINE, try:
path, current, kind = winreg.QueryValueEx(key, _flag(profile['id']))
sam=winreg.KEY_SET_VALUE except FileNotFoundError:
) as key: current, kind = None, None
value = 2 if category in CATEGORIES else 0 expected = 2 if category in wanted else 0
reg_setvalue( if current != expected or kind != winreg.REG_DWORD:
key, FLAG, value, type=winreg.REG_DWORD errors.append('%d / %s: expected %d, got %r' % (
) profile['id'], category, expected, current))
if errors:
if category in CATEGORIES: for item in errors[:25]:
print('Enabled: {}'.format(category)) print(item)
return 'ERROR'
missing = CATEGORIES - set(available) return 'WARNING' if missing else 'OK'
if missing: except Exception as exc:
print('Unavailable categories: {}'.format(missing)) print('Cleanmgr audit failed: %s' % exc)
return 'ERROR'
# Execution silencieuse du profil
run('cleanmgr.exe /sagerun:{}'.format(PROFILE),
timeout=3600)
def uninstall(): def uninstall():
for category in get_categories(): previous = _read_deployed_config()
path = REG_PATH + '\\' + category if previous:
available = _list_volume_categories()
with reg_openkey_noredir( for profile in previous['profiles']:
winreg.HKEY_LOCAL_MACHINE, _delete_profile_flags(profile['id'], available)
path, with reg_openkey_noredir(winreg.HKEY_LOCAL_MACHINE, TRACK_KEY,
sam=winreg.KEY_SET_VALUE sam=winreg.KEY_SET_VALUE) as key:
) as key:
try: try:
winreg.DeleteValue(key, FLAG) winreg.DeleteValue(key, TRACK_VALUE)
except FileNotFoundError: except FileNotFoundError:
pass pass
print('Managed Cleanmgr profile flags removed. No cleaning was executed.')
+297
View File
@@ -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