Paint.NET and the resurrection of DLL hell

Sono da diversi anni un utilizzatore, assai soddisfatto per altro, di Paint.NET. L’ho conosciuto nel 2007 grazie alla segnalazione di Jeff Atwood (la mente e il braccio dietro Stack Overflow e Discourse) e mi sono affezionato alla sua UI da allora. È user-friendly, snello e ha tutte quelle funzioni che mediamente mi occorrono per quel poco di image editing che faccio nel quotidiano.

GUI di Paint.NET

Fino alla versione 4.3.12 non ho mai avuto problemi con nessuno dei rilasci di Paint.NET: poi è arrivata la versione 5.0, ricompilata contro il framework .NET 7. Su nessuna delle mie workstation l’aggiornamento automatico, presente all’interno del software, ha funzionato correttamente, lasciandomi in un caso anche completamente “a piedi” (versione 4.x disinstallata, installazione della versione 5.x fallita, setup della versione funzionante non più disponibile sui server del progetto). Cercando sul loro forum di supporto, ho scoperto di non essere il solo.

Molti altri sono i post sul loro forum che documentano problemi di installazione con la nuova versione: le soluzioni proposte vanno dall’inutile al ridicolo (seriamente: usare Revo Uninstaller? Davvero, nel 2023?), mentre la “colpa” viene attribuita di volta in volta a Smart App Control, Windows in S mode, installazioni corrotte in precedenza, ecc.

Per fortuna, a partire dalla versione 5.0, gli sviluppatori hanno messo a disposizione un repository ufficiale contenente setup standard in formato MSI. Scarico quindi il file paint.net.5.0.1.winmsi.x64.msi (fe0c62ea6ff600ef316f31088eb11760) e lo lancio, ottenendo un laconico messaggio d’errore – e ovviamente nessuna installazione funzionante.

Errore durante l'installazione dell'MSI

Decido quindi di eseguire il pacchetto MSI in modalità di debug: msiexec /i paint.net.5.0.1.winmsi.x64.msi /l*v logfile.txt.

Il log non è di grandissimo aiuto, ma si coglie che l’errore deriva dal binario principale di Paint.NET, contenuto nel pacchetto, che dovrebbe essere lanciato durante l’installazione con la sintassi /setupActions /install DESKTOPSHORTCUT=1 PDNUPDATING= SKIPCLEANUP= "PROGRAMSGROUP=" /disablePGO /skipEstablishNVProfile /skipRepairAttempt, e la cui esecuzione termina con errore:

MSI (s) (08:D0): System Environment : Client has accepted UAC prompt
CustomAction _...si_x64_scd_r2r_paintdotnet_exe returned actual error code -2147450740 (note this may not be 100% accurate if translation happened inside sandbox)
MSI (s) (08:AC): Note: 1: 1722 2: _...si_x64_scd_r2r_paintdotnet_exe 3: C:\Program Files\paint.net\paintdotnet.exe 4: /setupActions /install DESKTOPSHORTCUT=1 PDNUPDATING= SKIPCLEANUP= "PROGRAMSGROUP=" /disablePGO /skipEstablishNVProfile /skipRepairAttempt 
MSI (s) (08:AC): Note: 1: 2205 2: 3: Error 
MSI (s) (08:AC): Note: 1: 2228 2: 3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 1722 
MSI (c) (80:D4): Font created. Charset: Req=0, Ret=0, Font: Req=MS Shell Dlg, Ret=MS Shell Dlg

Error 1722. There is a problem with this Windows Installer package. A program run as part of the setup did not finish as expected. Contact your support personnel or package vendor. Action _...si_x64_scd_r2r_paintdotnet_exe, location: C:\Program Files\paint.net\paintdotnet.exe, command: /setupActions /install DESKTOPSHORTCUT=1 PDNUPDATING= SKIPCLEANUP= "PROGRAMSGROUP=" /disablePGO /skipEstablishNVProfile /skipRepairAttempt 
MSI (s) (08:AC): Note: 1: 2205 2: 3: Error 
MSI (s) (08:AC): Note: 1: 2228 2: 3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 1709 
MSI (s) (08:AC): Product: paint.net -- Error 1722. There is a problem with this Windows Installer package. A program run as part of the setup did not finish as expected. Contact your support personnel or package vendor. Action _...si_x64_scd_r2r_paintdotnet_exe, location: C:\Program Files\paint.net\paintdotnet.exe, command: /setupActions /install DESKTOPSHORTCUT=1 PDNUPDATING= SKIPCLEANUP= "PROGRAMSGROUP=" /disablePGO /skipEstablishNVProfile /skipRepairAttempt

Ecco quindi spiegata la situazione di stallo: l’MSI richiede la disinstallazione della versone precedente, ma poi fallisce nell’installazione di quella nuova.

Scarico quindi, sempre dal repository ufficiale, la versione portable paint.net.5.0.1.portable.x64.zip  (7b75248e0487e60db75b7f9d5b400aed) e lancio l’eseguibile principale. Anche in questo caso resto deluso: nessuna esecuzione, nessun messaggio d’errore, il processo termina pochi millisecondi dopo il lancio.

Provo quindi a diagnosticare il problema usando Process Monitor di Sysinternals: si nota come l’eseguibile paintdotnet.exe carichi una manciata di file di configurazione JSON e subito dopo provi a cercare in più path quella che sembra essere una dipendenza insoddisfatta verso Microsoft.Windows.SDK.NET.dll (ma non era una portable?).

Trace di paintdotnet.exe eseguita tramite Process Monitor

All’interno dell’installazione portable, noto il file paintdotnet.deps.json, visibile anche nel trace di Process Monitor. A riga 78 si legge:

"runtimepack.Microsoft.Windows.SDK.NET.Ref/10.0.17763.28": {
   "runtime": {
      "Microsoft.Windows.SDK.NET.dll": {
         "assemblyVersion": "10.0.17763.24",
         "fileVersion": "10.0.17763.28"
      },
      "WinRT.Runtime.dll": {
         "assemblyVersion": "2.0.0.0",
         "fileVersion": "2.0.1.40881"
      }
   }
}

Cerco quindi Microsoft.Windows.SDK.NET.dll all’interno della mia workstation: il file esiste, ma non nelle path esplorate in automatico durante il lancio. Ma se è necessaria all’esecuzione, come mai non viene distribuita all’interno del pacchetto portable come tutte le altre librerie? Nello zip c’è persino Microsoft.CSharp.dll

Sulla mia macchina di test, Microsoft.Windows.SDK.NET.dll è in versione 10.0.19041.26 (c6af942d694f38e0a2be4de1bc0899cd), più recente della 10.0.17763.28 richiesta.

Provo a copiarla all’interno della directory della installazione portable: nuovo fallimento. Ora Process Monitor indica che il problema si ripete esattamente come prima, ma a causa di WinRT.Runtime.dll. Il copione dettato da paintdotnet.deps.json è quindi rispettato alla lettera.

Anche in questo caso, la versione non combacia: sulla mia macchina, WinRT.Runtime.dll (9b5a0545764f7cbdc362fa5fe9d104a1) è presente nella versione 1.6.4.39141, mentre Paint.NET richiederebbe la più recente 2.0.1.40881.

Copiando anche questa seconda DLL direttamente nella directory della portable, Paint.NET si avvia. Tuttavia, il trace di Process Monitor mostra anche una particolarità: nessuna delle due librerie viene in realtà caricata dinamicamente all’interno del processo. Che sia un errore di publishing del progetto? Ciò spiegherebbe, almeno in parte, perchè l’applicazione non incontri errori pur dipendendo da DLL la cui versione è diversa da quella dichiarata.

Decido quindi di ricominciare da capo: creo una nuova istanza portable eliminando la precedente ed invece di integrare le due DLL, elimino semplicemente la porzione di codice relativa alle due librerie da paintdotnet.deps.json: Paint.NET si avvia senza errori. Riprovo rimuovendo totalmente il file paintdotnet.deps.json: anche in questo caso, il software parte senza alcun errore.

Cercando in giro, mi rendo conto che il problema potrebbe essere il risultato della migration che è stata effettuata sulla codebase, quando il progetto è stato aggiornato da .NET 5/6 a .NET 7. Senza i sorgenti è però impossibile dimostrarlo: in ogni caso, appare evidente che si tratti di un errore di publishing, in quanto si da come requisito una versione estremamente specifica del pacchetto Microsoft.Windows.SDK.NET.Ref, per di più obsoleta (la corrente mentre scrivo è la 10.0.22621.28, perchè dovrei avere a bordo la 10.0.17763.24?) o, in alternativa, ci si trova davanti ad una violazione dell’indipendenza del pacchetto “portable” (per la cronaca: WinRT.Runtime.dll è invece parte del pacchetto Microsoft.Windows.CsWinRT).

Risultato: abbiamo appena resuscitato un gremlin, sepolto da almeno quindici anni, noto come DLL Hell, e che .NET aveva il preciso compito di eliminare.

Ora il problema diventa: come correggere il problema senza avere a disposizione i sorgenti e senza essere costretti ad adoperare la versione portable? A questo scopo torna utile Orca, un editor di database MSI. L’idea è quella di patchare il setup MSI paint.net.5.0.1.winmsi.x64.msi (fe0c62ea6ff600ef316f31088eb11760) rimuovendo i riferimenti al file paintdotnet.deps.json al suo interno.

Setup MSI caricato in Orca

Utilizzando Orca, si possono identificare quattro riferimenti al file JSON, per ciascuno dei quali si può eseguire una drop row.

Tabella Component
Component.msi_x64_scd_r2r_paintdotnet_deps_json

Tabella FeatureComponents
DefaultFeature, Component.msi_x64_scd_r2r_paintdotnet_deps_json

Tabella File
msi_x64_scd_r2r_paintdotnet_deps_json

Tabella MsiFileHash
msi_x64_scd_r2r_paintdotnet_deps_json

Salvata la transform sul file MSI, si ottiene finalmente un setup MSI funzionante e redistribuibile che non genera a disco, durante l’installazione, il file paintdotnet.deps.json e dunque evita che al lancio paintdotnet.exe vada in errore.

Tutto risolto, per lo meno finché non si risveglia il prossimo gremlin.


Aggiornamenti:

Mentre elaboravo questo post, Paint.NET è stato nuovamente aggiornato ed è ora in versione 5.0.2. Il problema persiste ed è tutt’ora presente nel pacchetto MSI, nel setup automatico e nella versione portable.