Microsoft Installer (MSI)

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Microsoft Installer (MSI)

Beitrag von Krishty »

(Ich benutze das hier einfach mal als Sammelthread, damit ich die ganzen Nachschlagwerke wiederfinde. Die Information ist unglaublich weit verstreut.)

„Warum nicht einfach eine Batch-Datei?“ – Einführung Roaming Profiles

In Firmennetzwerken ist es üblich, dass sich jeder Benutzer an jedem beliebigen Computer einloggen kann. Ihr geht also ins Nebenbüro, loggt euch am Rechner eures Kollegen an (mit eurem eigenen Namen und Passwort natürlich) und wie von Zauberhand sind eure gewohnten Anwendungen da und eure Dateien in den gewohnten Ordnern. Habt ihr euch schonmal gefragt, wie das geht? Dafür sind Roaming Profiles da.

Im Grunde synchronisiert Windows euer Benutzerprofil bei jedem Abmelden mit eurem Domain Server. Diese Synchronisierung beinhaltet auch die Registry, Startmenüs, usw. Bei der Anmeldung werden diese Informationen wieder heruntergeladen.

Das ist ein Grund, warum man sich beim Speichern von Benutzerdaten entscheiden muss, ob sie nun nach %APPDATA% (C:\Users\Krishty\AppData\Roaming) oder nach %LOCALAPPDATA% (C:\Users\Krishty\AppData\Local) gehen: Ersteres wird mit dem Domain Server synchronisiert, dort sollten also rein benutzerspezifische Einstellungen landen. Letzteres wird nicht synchronisiert; dort sollten also maschinenabhängige Benutzereinstellungen landen. Am Beispiel eines Spieles, weil wir ja Spiele entwickeln: Nach %APPDATA% schreibt ihr den Schwierigkeitsgrad und die aktuellen Spielstände / Achievements (sollen auf jedem Rechner, auf dem der Benutzer zockt, identisch sein). Nach %LOCALAPPDATA% schreibt ihr die Grafikeinstellungen, Lautstärke, und die Lizenz (falls sie von der Maschine abhängt), denn auf dem langsamen Laptop möchte der Benutzer sicher andere Grafikeinstellungen haben als auf dem starken Desktoprechner und ffffuuuu falls die automatisch synchronisiert würden.

Roaming User Profiles haben jede Menge Probleme und Details, und sie werden langsam durch Cloud Computing verdrängt. Aber das ist hier egal. Kann man in der MSDN nachlesen.

Bis hier hin können mir die meisten wahrscheinlich noch folgen. Die viel interessantere Frage ist aber: Wenn nur ausgewählte Benutzerordner und die Registry synchronisiert werden, sind dann die Shortcuts im Startmenü nicht kaputt? Das Spiel ist doch sicher nicht installiert, wenn ich mich auf einem anderen Rechner einlogge? Oder werden die Gigabyte-großen Programmdateien beim Login ebenfalls synchronisiert?

Die Antwort ist das eigentliche Kernthema: Advertising.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Einführung in Advertising und on-Demand-Setup

Wie kann es sein, dass auf jedem Rechner im Firmennetzwerk jede Software verfügbar ist?

Microsoft hat sich überlegt, dass es wohl nicht so schlau wäre, jede Software komplett auf jeden Computer zu kopieren. Die langsamen Rechner in der Buchhaltung könnten SAP nicht starten, weil die Festplatte vom Visual Studio der Entwickler zugemüllt ist, das sie aber gar nicht benutzen.

Stattdessen hat Microsoft sich für Advertising und on Demand-Setups entschieden. Hier wird es abgefahren. Das ist eine Möglichkeit, wie das System vorgaukeln kann, eine Anwendung wäre installiert – aber tatsächlich findet das Setup erst statt, sobald man auf die Anwendung zugreift.

Es funktioniert folgendermaßen: Wenn Office auf einen Rechner advertised wird, installiert das Setup alle Registry-Schlüssel. Es legt außerdem alle Verknüpfungen im Startmenü an und trägt Word als Standardprogramm für .docx-Dateien ein. Es werden aber keine Programmdateien von Office auf das Zielsystem kopiert. Stattdessen wird dort nur vermerkt, wo das Office-Setup zu finden ist (typischerweise auf dem Domain Server).

Erst, wenn ein Benutzer tatsächlich einen der Einsprungspunkte – Verknüpfung im Startmenü, Doppelklick auf .docx, usw – auswählt, springt das Setup an und installiert die benötigte Komponente. Programme, die nicht ausgeführt werden, verbrauchen also kaum Platz auf dem Rechner. Und wenn mein Kollege aus der Buchhaltung an meinen Rechner kommt, sich auf sein Konto einloggt und mir eine Word-Datei zeigt, müssen wir halt erst einmal ein Bisschen warten, bis es startet – dafür ist es aber exakt gleich eingestellt wie auf seinem eigenen Rechner (denn Registry & Co. sind synchronisiert). (Beim nächsten Start ist es sofort da, denn die neue Installation wird auch nach dem Logout auf meinem Rechner behalten.)

Beachtet auch, dass hier keine Admin-Rechte nötig waren, denn theoretisch war Office ja schon die ganze Zeit auf dem Rechner installiert!

Ebenso werden Features on Demand nachinstalliert, die man beim Setup von Office abgewählt hat. Wer kein Excel wollte, aber auf eine .xlsx-Datei klickt, bei dem startet dann automatisch das Excel-Setup.

Genau da kommt auch viel Hass auf das Feature her: Man öffnet irgendwas, und plötzlich springt ein Setup an und installiert irgendwas. Oder – noch schlimmer – fragt nach einem Datenträger, von dem man nichts weiß. Wo diese Fehler herkommen und wie man sie vermeidet, das schreibe ich später.

Trotz dieser Unannehmlichkeiten ist das Feature für große Unternehmensnetzwerke unbezahlbar.

Nun sollte auch jedem klar sein: Mit Batch-Dateien geht das nicht. Die Komplexität, mit der z.B. Icons für Verknüpfungen verwaltet werden müssen, deren Ziele noch gar nicht auf dem System existieren, ist in dieser Kategorie nicht meisterbar. Hier kommt der Microsoft Installer ins Spiel.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Microsoft Installer: Eierlegende Wollmilchsau

Um Roaming, Advertising, und on Demand-Setups zu realisieren, hat Microsoft eine neue Systemebene geschaffen: Den Microsoft Installer, kurz MSI. Das erste Produkt, das damit installiert wurde, war Office 2000.

Weitere große Ziele, die damit verfolgt wurden:
  • Transaktionales Verhalten. Eine Installation soll ein einziger, atomarer Vorgang sein. Schlägt sie fehl, muss alles wie vorher sein. Wird das Produkt deinstalliert, muss alles wie vorher sein. Keine ungültigen Systemzustände; keine Dateileichen.

    Self Repair. Wird eine Anwendung beschädigt, kann das Setup sie automatisch reparieren (aber nicht immer; Details später).

    Weiterverwertbarkeit. Admins sollen alle Anwendungen auf ihre Firma zuschneiden können (indem sie z.B. Setups mit Patches kombinieren, Logos ersetzen, Sprachen übersetzen). Das war für mich die größte Überraschung: Ihr könnt tatsächlich jede MSI-Datei jedes Programms öffnen und nach eurem Willen verändern. Wie das geht, dazu später mehr.
Alle diese Punkte übernimmt MSI ohne Zutun des Packagers (Berufsbezeichnung für die Person, die ein Setup für ein Programm entwickelt oder weiterverwertet). Man muss sich bei MSI nicht um Rollback kümmern (vollautomatisch erzeugt). Man muss sich nicht darum kümmern, ob die Deinstallation Komponenten entfernen soll, die mit anderen Programmen geteilt sind (automatisches Reference Counting). Man muss keine Dateien testen (MSI berechnet Prüfsummen und vergleicht sie mit dem Sollzustand). Advertising und on Demand-Setup sind nicht viel mehr als Flags beim Erstellen des Setups.

MSI läuft als Hintergrunddienst auf jeder Windows-Version seit der Jahrtausendwende. Klickt der Anwender auf eine Verknüpfung, prüft MSI, ob das Ziel advertised ist, und startet die Installation, falls benötigt. (Das ist die Art von Verknüpfungen, für die in den Eigenschaften kein Pfad eingetragen ist!) Fragt eine Anwendung via CoCreateInstance() nach einer COM-Komponente, die auf dem System vorhanden sein sollte, prüft MSI die Integrität und startet bei Bedarf automatisch die Reparatur, bevor sie geladen wird. Und, nicht zuletzt: Klickt der Anwender auf ein Setup, führt MSI es aus.

Wie müssen Setups aufgebaut sein, damit das funktioniert?
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Grundlegende Konzepte

MSI ist deklarativ, also komplett data-driven. Alles wird beschrieben. Es wird nichts programmiert. Man sagt gewissermaßen aus, wie das System aussehen soll, wenn die Anwendung installiert ist – wo welche Datei liegt, welchen Wert welcher Registry-Schlüssel hat – und wie dieses Ziel erreicht wird, darum kümmert MSI sich dann. Dadurch fällt es MSI auch so einfach, Rollbacks durchzuführen. Übrigens ist auch die Benutzeroberfläche, die während des Setups angezeigt wird, deklarativ beschrieben.

Ein offensichtlicher Vorteil ist Transparenz. Man kann einem MSI-Paket ansehen, welche Dateien auf der Platte landen – und es ist unmöglich, dass es die Festplatte formatiert oder meine Dateien auf einen Server hochlädt. (Abgesehen von einer Einschränkung: Custom Actions. Die sind aber geächtet. So sehr, dass das Installer-Team elf Punkte aufzählt, warum man sie niemals einsetzen sollte: Windows Installer Team Blog, Integration Hurdles for EXE Custom Actions (scrollt über die leere Seite hinaus).)

MSI speichert diese Daten in einer SQL-ähnlichen Datenbank. Zu meinem Bedauern, denn das war nicht die beste Wahl. Das Datenbankschema (also, wie die Tabellen aufgebaut sind) ist, wie wir später sehen werden, leider eher maschinen- als menschenlesbar. Aber man muss eingestehen, dass es so zumindest eine standardisierte Schnittstelle garantiert. Tatsächlich kann man MSIs fröhlich mergen und alles ist immer wohldefiniert.

Was wir Anwendung nennen, ist im MSI-Slang gewissermaßen ein Package.

Packages bestehen aus Features. Ein Spiel könnte beispielsweise aus den Features Spiel, Online-Client und Desktop-Verknüpfung bestehen. Features können optional sein, oder auch nicht.

Das war einfach. Aber Features bestehen aus Components. Die Definitionen gehen weit auseinander, aber zumindest hat Microsoft einen Leitfaden veröffentlicht, in welche Components man seine Features unterteilen sollte (Microsoft Docs, Organizing Applications into Components und Defining Installer Components):
• Define a new component for every .exe, .dll, and .ocx file. […]
• Define a new component for every file that serves as a target of a shortcut. […]
• Group all of the remaining resources into folders. All resources in each folder must ship together. […] Define a new component for every folder. […]
• […] Any registry key that points to a file should be included in that file's component.
… diese Components bestehen aus einzelnen Files, Registry Keys, und Shortcuts.

Jede Component wird durch eine GUID identifiziert. Bei dieser Aufteilung müssen wir aber noch etwas extrem wichtiges im Hinterkopf behalten: Updates und das Teilen von Components zwischen verschiedenen Packages.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Versionierung und Shared Components

Besprechen wir, dass MSI alle Components mit GUID adressiert.

Ihr führt eine Firma ContosoGames, die eine Batterie erfolgreicher Videospiele entwickelt hat. Dabei nutzt ihr den Ogg Vorbis-Codec für Klangdateien.

Wie legt ihr vorbis.dll den Spielen bei?
  • Ins Hauptverzeichnis und ab damit! – Ist eigentlich nur eine Option für Billigspiele, die nicht gewartet werden müssen. Sollte ein Patch für eine der Bibliotheken nötig werden, müsst ihr für jedes veröffentlichte Spiel ein eigenes Patch-Paket bauen. Schließlich hat jedes Spiel eine private Kopie der Bibliothek!
  • Wir liefern die in einem eigenen Setup (also als gesonderte Packages) aus! – Gute Idee, denn so fallen Patches wirklich minimal aus. Was passiert aber, wenn der Anwender Platz auf der Platte schaffen möchte, durch die Programmliste stöbert, und Ogg Vorbis deinstalliert (er weiß ja nicht, dass es von euren Spielen benötigt wird)? Nun haben eure Spiele keinen Klang mehr!
  • Wir installieren sie in ein gesondertes Verzeichnis (Program Files\ContosoGames\soundlibs), und wenn ein Spiel aktualisiert wird, ist die Sache erledigt! – Das wäre die MSI-konforme Lösung, aber dort gibt es einiges zu beachten.
Components, die von unterschiedlichen Packages geteilt werden, nennt man Shared Components. Der große Fallstrick hierbei ist, die Identifikation zu vergeigen und sich eine Reparaturschleife einzuhandeln. Man muss folgendes unbedingt einhalten:
  1. Wenn man GUIDs an Components verteilt, müssen Shared Components in jedem Package die selbe GUID haben. Alle eure Spiele müssen also vorbis.dll mit den selben GUIDs ansprechen.
  2. Alle Updates müssen die selben GUIDs für die Components verwenden. Ausnahme: Wenn eine der Bibliotheken durch ein Update inkompatibel zu früheren Versionen wird, müsst ihr sie als neue Component mit neuem Namen an einem anderen Ort installieren (z. B. als vorbis2.dll).
  3. Shared Components müssen versioniert sein. Beim Bauen des Setups müsst ihr also eine Version angeben, von Anfang an!
Was passiert, wenn ihr das vergesst?
  1. Der Anwender installiert Space Contoso 1. Program Files\ContosoGames\soundlibs\vorbis.dll hat Version 1.0 und die GUID {AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}.
  2. Der Anwender spielt Space Contoso 1. Alles funktioniert gut.
  3. Der Anwender installiert Space Contoso 2. Program Files\ContosoGames\soundlibs\vorbis.dll ist identisch (auch Version 1.0), ihr habt aber vergessen, die GUID zu synchronisieren – sie ist nun {BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}.
  4. Der Anwender spielt Space Contoso 2. Alles funktioniert gut.
  5. Hey, hatte Captain Conto auch im ersten Teil schon so eine komische Frisur? Der Anwender startet Space Contoso 1, um nachzusehen.
  6. Wie wir wissen, prüft MSI vor dem Laden jeder Komponente, ob sie korrekt installiert ist. Für Program Files\ContosoGames\soundlibs\vorbis.dll ist aber nun eine GUID {BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB} von Teil 2 vermerkt!
  7. Das Setup von Space Contoso 1 startet im Hintergrund, um eine Reparatur durchzuführen. Da die CD noch im Laufwerk liegt, wird der Anwender nicht nach dem Datenträger gefragt. Aber irgendwie dauert das Starten länger als sonst!
  8. Program Files\ContosoGames\soundlibs\vorbis.dll hat nun wieder die GUID von Teil 1: {AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}.
  9. Beim Start von Space Contoso 2 wird wieder eine Reparatur angestoßen werden, weil die GUID nicht {BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB} ist!
Das war das GUID-Beispiel. Ein Versionsbeispiel lässt sich analog herbeiführen, wenn eine Version von vorbis.dll ohne Version ausgeliefert wurde.

Falls die GUIDs identisch sind und die Components korrekt versioniert wurden, löst MSI kein Repair aus, wenn es eine neue Version vorfindet. Nach Installation eures Sound-Patches hat vorbis.dll die Version 1.1 und beide Spiele starten damit normal.

Solche Repair Loops sind berüchtigt. Unter anderem hatte Visual Studio 2010 Beta 1 eine, weil eine Visual Basic-Komponente mit Office 2007 geteilt war, und beim Advertising auf dem falschen Laufwerk landete: Setup & Install by Heath Stewart, How to work around the issue when opening Office applications repairs Visual Studio

Quintessenz: Komponenten teilen ist gut für die Wartbarkeit. Es sei denn, man weist ihnen unterschiedliche GUIDs zu. Oder vergisst, sie zu versionieren. Oder vergisst, für inkompatible Änderungen eine neue Komponente mit neuer GUID anzulegen.

Die Signifikanz der GUID für Updates sollte damit auch klar sein: Verlegt man die GUID, können Komponenten nicht mehr sauber aktualisiert werden.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Ausflug: Warum ist C:\Windows\Installer so groß?! Kann ich das löschen?!

Wir wissen nun, dass MSI höllisch viele Aufgaben übernimmt:
  1. Aus der Deklaration eines Packages berechnet es, was am System zu ändern ist, um den Zielzustand herzustellen (Installation).
  2. Während dieser Installation führt es Buch über ein mögliches Rollback, um bei Fehlschlag den Ausgangszustand wiederherzustellen.
  3. Es überwacht Components auf Veränderungen und repariert sie nötigenfalls.
  4. Es überwacht Entry Points für Advertisement und installiert die angeforderte Anwendung nötigenfalls.
  5. Bisher nicht erwähnt: Falls während einer Installation Dateien ersetzt werden, legt MSI Sicherheitskopien davon an. Nach der Deinstallation werden sie wiederhergestellt (das Rollback aus 1. wird also dauerhaft behalten).
Alle diese Dinge müssen irgendwo gespeichert werden. Dieses Irgendwo ist … *Trommelwirbel* … C:\Windows\Installer.

Wenn ihr euch also fragt, warum das Verzeichnis so riesig ist: Weil jedes Setup, das ihr jemals ausgeführt habt, dort beschrieben ist, samt Rollback-Informationen. Falls ihr das löscht, könnt ihr nicht einmal mehr eine Deinstallation der betroffenen Packages durchführen.

Pikantes Detail: Bis Windows 7 wurden dort nur reduzierte Packages gespeichert – also nur die Deklarationen ohne tatsächliche Dateidaten. Dummerweise lassen sich die Packages dann nicht mehr digital zertifizieren, denn beim Entfernen der Dateidaten ändert sich ja die Prüfsumme! Bedeutet: Wenn ihr eine 1-GiB-MSI installiert, dann ja, wird euer C:\Windows\Installer-Verzeichnis tatsächlich um einen Gigabyte größer! Dass diese Änderung nicht auf signierte Packages eingeschränkt wurde, und dass MSI diese Packages nicht einmal vollständig zur Reparatur einsetzen kann, wurde breit kritisiert. Microsoft begründete es damit, dass wahrscheinlich nicht genügend Ressourcen zur Verfügung standen, diese Komplexität breit zu testen. Um ein funktionsfähiges System zu garantieren, hat man eben den einfachen Algorithmus gewählt und massig Plattenspeicher geopfert (Setup & Install by Heath Stewart, Changes to Package Caching in Windows Installer 5.0).

Wenn ich mehr über MSI erzählt habe, spendiere ich eine Tour durch die verschiedenen Tools, mit denen man das Verzeichnis säubern kann. Nun haben wir aber erstmal Wichtigeres zu tun – es wird Zeit für einen Blick in die Datenbanken selber, also in MSI-Dateien. Gebt euch damit zufrieden, dass die Festplattenbereinigung ab Windows 8.1 auch dieses Verzeichnis mit bereinigt.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von xq »

Sehr awesome, danke für die Ausführungen!
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Schrompf »

Tolle Übersicht! Vielen Dank.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Freut mich, wenn es nützt!

Das .msi-Dateiformat

Wie gesagt arbeitet der MSI deklarativ. Dabei können wir direkt ein häufiges Missverständnis aus dem Weg räumen: MSI-Packages sind nicht ausführbar. Es befindet sich üblicherweise kein Code in .msi-Dateien. Sie werden vom MSI ausgeführt.

Das ausführende Tool ist msiexec.exe; es prüft kurz die Integrität des Packages und dirigiert dann einen Großteil der Arbeit an einen Systemdienst. MSI muss aus zwei Hauptgründen als Systemdienst arbeiten:
  1. Nebenläufige Installationen wären nicht transaktional. Wenn zwei Packages parallel die selbe Datei installieren würden, würde das die Rollback-Logik schwer verkomplizieren. MSI darf deshalb nur als eine globale Instanz laufen.
  2. on-Demand-Installationen dürfen keine Admin Prompts auslösen, weil sie sonst nicht mehr transparent für den Anwender wären. (Der Admin müsste jedes Mal zum Anwender rennen, wenn der ein Programm zum ersten Mal benutzt.) Dafür braucht MSI ständige Admin-Rechte.
Ein unschätzbarer Vorteil des Weges über msiexec.exe ist die einheitliche Schnittstelle. Alle MSIs aller Produkte aller Hersteller können über exakt die selben Parameter stummgeschaltet, advertised, installiert, repariert, oder deinstalliert werden. Das Logging erfolgt auf identische Weise mit identischen Steuerparametern.

MSI nutzt eine SQL-ähnliche Datenbank. Neben dieser Datenbank braucht ein Package aber noch weitere Daten:
  • Die eigentlichen Dateidaten, die ans Ziel kopiert werden sollen. MSI nutzt Windows’ traditionelle .cab-Archive und gruppiert die Dateien in Media.

    Diese Gruppierung kann beliebig erfolgen – heutzutage gruppiert man normalerweise logisch (32-Bit-Dateien in ein Medium; 64-Bit-Dateien in ein Medium; plattformunabhängige Dateien in ein drittes). Früher hat man meist physisch gruppiert – die ersten 600 (komprimierten) MiB auf das Medium der ersten CD, die nächsten 600 auf das Medium der zweiten CD, usw (im Ergebnis mit je einer .cab pro CD). MSI sucht im Ausführungsverzeichnis der .msi nach den .cabs; falls es sie nicht findet, fragt es den Anwender.

    .cab-Archive können auch in .msi eingebettet werden. So bekommt der Anwender nur eine Datei statt eines ganzen Satzes. In der Datenbank wird dem Archivnamen dafür eine Raute # vorangestellt.

    Die maximale Größe eines .cab-Archives liegt bei 2 GiB. Ich glaube, auch einzelne Dateien dürfen nicht größer werden. Mehr muss man halt auf mehrere Medien verteilen. Einzelne Dateien dürfen größer als 2 GiB sein, und können bei Bedarf mehrere Archive übergreifen: Microsoft Docs, Cabinet Files.

    Die Kompression ist ganz gut – es gibt einen völlig veralteten MSzip-Algorithmus, der gerade mal DEFLATE-Kompression erreicht. Es gibt aber auch LZX, das Microsoft in den 90ern eingekauft hat, und das „nur“ 15–25 % hinter modernen Algorithmen liegt.
  • Für Advertising und on-Demand-Installationen werden Symbole benötigt, die den Dateitypen und Verknüpfungen mitgegeben werden können. Sie werden als binärer Datenstrom direkt in der Datenbank abgelegt.
  • Die Benutzeroberfläche der Installation kann mit Bildern und Logos angepasst werden. Auch sie landen als Binärströme in der Datenbank.
  • .msi-Dateien können digital signiert werden. Die Signatur landet als Pseudo-Tabelle in der Datenbank.
  • Der Explorer zeigt Package-Eigenschaften an, etwa Version und Namen. Zu dieser Summary Information mit ihren ganz besonderen Eigenschaften werden wir später noch kommen.
Das Ganze kann man sich in 7-Zip recht gut ansehen; hier z. B. mit dem Setup des Application Verifiers:
AppVerifMSI7z.png
Die Einträge mit den Ausrufzeichen sind Datenbanktabellen; appverifier.cab ist das Haupt-Medium; Binärdatenströme und das Icon kommen danach. Die Datei endet mit der digitalen Signatur und der Summary Information.

Nun können wir uns die Tabellen genauer ansehen.
Zuletzt geändert von Krishty am 02.09.2018, 12:05, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Die MSI-Datenbank

Die MSI-Datenbank unterstützt eingeschränkt SQL-Befehle (Microsoft Docs, SQL Syntax).

Diese Syntax ist wichtig, wenn man mit der Win32-API auf Packages zugreifen möchte (Microsoft Docs, Installer Function Reference). Die Funktionsfülle ist überwältigend, aber sie deckt zwei große Anwendungsfälle ab: Das Verwalten von Packages genau so wie Laufzeitveränderungen der Packages von den geächteten Custom Actions aus. Im Augenblick lassen wir die API beiseite und merken uns schlicht, dass man MSIs von Win32 aus erzeugen und verändern kann.

Um sich die eigentliche Datenbank anzusehen, gibt es eine Vielzahl Tools:
  • Der „offizielle“ Weg ist Orca, Microsofts .msi-Editor. Es lag von Anfang an dem Microsoft Installer SDK bei, das es nun schon seit einem Jahrzehnt nicht mehr gibt. Obwohl es wie aus der Steinzeit aussieht, wird es ständig weiterentwickelt. Ihr findet die aktuelle Version im Windows 10 SDK. Falls euch der Download zu groß für das winzige Tool ist, habe ich eine Kopie auf meinem Webspace.
  • Pantaray SuperOrca ist eine Neuentwicklung mit viel größerem Funktionsumfang. Sie passt zum Beispiel Verknüpfungen zwischen Tabellen an, die man bei Orca händisch synchronisieren muss. Ich habe sie nicht getestet.
  • InstEd It! ist eine weitere Drittentwicklung. Auch die habe ich nicht getestet, aber viele Profis in den Packaging-Foren nutzen sie.
Ich werde hier alles mit Orca illustrieren.

Sehen wir uns Application Verifiers MSI-Datenbank an. Ich werde ungefähr die gleiche Reihenfolge einhalten wie in der Erklärung grundlegender Konzepte.

Jede Datenbank besteht aus Tabellen. Tabellen enthalten Werte, die in Spalten sortiert sind. Diese Organisation wird durch ein Schema vorgegeben. Ob Null-Werte (leere Werte) erlaubt sind oder nicht, hängt bei MSI vom Spaltentyp ab.

Die Features, die ein MSI installieren kann, stehen in der Feature-Tabelle:
1 Feature.png
Das Package besteht also nur aus einem Feature – dem Application Verifier selber. Der Name in der ersten Spalte sollte eigentlich menschenlesbar sein – dieses Package wurde aber augenscheinlich maschinell generiert.

Um zu wissen, welche Components für ein Feature installiert werden, kann man sich die FeatureComponents-Tabelle ansehen:
2 FeatureComponents.png
Ein einfaches Mapping von Name zu Name: Alle Komponenten hängen am selben Feature (es gibt ja nur eins).

In der Component-Tabelle wird es interessanter:
3 Component.png
  • Der Name einer Komponente wurde eindeutig aus dessen GUID erzeugt. In eurem Setup solltet ihr natürlich lesbare Namen verwenden.
  • Directory_ gibt an, in welches Verzeichnis die Komponente erzeugt wird. Dabei ist TARGETDIR symbolisch und wird bei der Installation durch den tatsächlichen Ort ersetzt. Falls die Komponente keine Datei ist, sondern ein Registry-Schlüssel, ist der Pfad logischerweise ein Registry-Pfad. Alle Pfade kann man sich in der Directory-Tabelle ansehen.
  • Die Komponente im ProgramMenuFolder hat die Condition, dass sie nur unter der NT-Linie (also nicht unter Windows 95/98/ME) installiert wird. Auf die Syntax solcher Einschränkungen kommen wir später zurück; da sie auch selbstdefinierte Variablen nutzen kann, ist sie recht mächtig.
  • KeyPath gibt eine Datei oder einen Registry-Schlüssel an, den MSI prüfen soll, wenn die entsprechende Komponente angefordert wird. Damit wird Self Repair (im Fall einer beschädigten oder fehlenden Datei) oder das Setup (im Fall von Advertising) ausgelöst.
  • Attributes ist eine hässliche Sammlung kryptischer Flags für Sonderfälle – etwa, ob die Datei nach der Deinstallation doch auf dem System verbleiben soll.
Hier ist die File-Tabelle mit der Zuordnung einzelner Dateien zu Komponenten:
4 File.png
  • Die erste Spalte ist mal wieder der symbolische Name; in diesem Fall maschinell erzeugt.
  • Die Werte der Component_-Spalte kennen wir aus der vorherigen Tabelle.
  • Dahinter der tatsächliche Name, mit dem die Datei im Dateisystem angelegt wird.
  • Die Dateigröße in FileSize ist nötig, damit MSI bei der Installation prüfen kann, ob ausreichend Platz auf der Festplatte zur Verfügung steht.
  • Wir haben gelernt, dass Komponenten möglichst immer versioniert sein sollten, um Updates zu erleichtern. Dafür die Version- und Language-Spalte.
  • Die Sequence Number ist gewissermaßen der eindeutige Index der Datei. Er wirkt sich auf die Installationsreihenfolge aus – weil Zugriff auf komprimierte Dateien in .cab-Archiven nicht immer wahlfrei ist. Der Installer wird sich bemühen, die zu installierenden Dateien nach dieser Zahl zu ordnen.
Eine fast identische Zuordnung der Registry-Schlüssel findet sich in der Registry-Tabelle.

Wo die Dateien dann tatsächlich zu finden sind, bestimmt die Media-Tabelle:
5 Media.png
Dabei ordnet LastSequence die Dateien einem Medium zu: Die Dateien 1–13 stammen aus 838060235bcd28bf40ef7532c50ee032.cab; die Datei 13 aus a35cd6c9233b6ba3da66eecaa9190436.cab, Nummer 14 aus fe38b2fd0d440e3c6740b626f51a22fc.cab. Warum sie so verstreut sind? Wahrscheinlich sind die Komponenten mit anderen Packages aus dem Windows SDK geteilt, und das Überspringen von Dateien in einem Archiv ist mangels wahlfreiem Zugriff teuer. Die DiskId am Anfang ist eher kosmetisch: Wenn der Benutzer aufgefordert wird, einen Datenträger einzulegen, wird diese Id angezeigt.

Mit diesen Informationen könntet ihr exakt rekonstruieren, wohin welche Datei installiert wird, aus welchem Archiv sie stammt, und welcher Registry-Schlüssel mit welchem Wert angelegt wird. Die Dateidaten könntet ihr, wie oben gezeigt, aus den .cab-Dateien extrahieren.

Es gibt noch viel mehr Tabellen – vor allem für die UI. Die sind aber erstmal nicht so wichtig.

Glückwunsch – ihr könnt nun beliebige MSIs extrahieren! Gönnt euch zum Stöbern ruhig einen Blick in LibreOffice & Co …
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

MSI und Unicode

Wegen der Lokalisierung ist das ein wichtiges Thema.

Machen wir es kurz: MSI-Datenbanken arbeiten leider mit Byte-orientierten Strings. Sie erfordern eine Codepage, die für das gesamte MSI identisch sein muss – anders als die Win32-API, die auf UTF-16 aufbaut. Grund ist die Kompatibilität zu Windows 95/98/ME, auf dem MSI ursprünglich eingeführt wurde.

Aber es gibt Hoffnung: Die Codepage 65001 – allgemein bekannt als UTF-8 – wird akzeptiert. Das ist nicht offiziell dokumentiert, aber der Trick ist mittlerweile zu weit verbreitet, als dass Windows irgendwann inkompatibel dazu werden könnte. Auch das MSI-Team hat das 2005 in die Welt hinaus gebloggt (Setup & Install by Heath Stewart, MSI Databases and Code Pages).

Schauen wir uns das Hauptproblem mit Unicode an:
Encoded strings will store correctly and will be converted correctly when the W functions are called, but they may not display properly because the correct font for wide characters is not chosen.
Tatsächlich gibt es Berichte, wonach z.B. eine japanische Übersetzung an der Schriftart gescheitert ist (microsoft.public.platformsdk.msi, Japanese MSI with UTF8 65001 codepage issues?).

Andererseits hat Microsoft durch den rasanten Aufstieg von Emojis viel investiert, um alle Windows-Schriftarten komplett Unicode-tauglich zu kriegen. Ich habe jedenfalls schnell ein UTF-8-Setup auf Windows 10 getestet, und ASCII-Buchstaben plus traditionelles Chinesisch und Japanisch zeigten makellos an. Ich werde es einfach drauf ankommen lassen. Wenn ich was feststelle, schreibe ich es hier.

Vorläufiges Fazit: Mit dem inoffiziellen Trick, die Codepage auf 65001 zu setzen, dürfte MSI allerspätestens ab Windows 10 Unicode-kompatibel sein.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
mrz
Beiträge: 79
Registriert: 07.08.2008, 14:34

Re: Microsoft Installer (MSI)

Beitrag von mrz »

Auch von mit erstmal Danke für die Infos.
Krishty hat geschrieben: Hier ist die File-Tabelle mit der Zuordnung einzelner Dateien zu Komponenten:
[...]
[*]Dahinter der tatsächliche Name, mit dem die Datei im Dateisystem angelegt wird.
Bei deinem Screen sieht man aber dass die unterersten beiden Einträge ein | drin haben, entsprechend kann das nicht der finale Dateiname sein.
Denke mal das ist ein "Sondefall" für irgend einen OR Bastel?
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Sehr gut beobachtet! Das Zeichen | trennt den langen vom kurzen 8.3-Dateinamen. Aus Kompatibilitätsgründen legen viele Setups beides an (zumindest tut’s WiX bei mir standardmäßig).

Ich denke nicht, dass wir uns 2018 noch damit beschäftigen sollten, deshalb werde ich das hier ignorieren. Davon ab ist die Syntax nicht sehr menschenfreundlich (Microsoft Docs, Filename):
No space is allowed preceding the vertical bar (|) separator for the short file name/long file name syntax. Short file names may not include a space, although a long file name may. A space can exist after the separator only if the long file name of the file name begins with the space. No full-path syntax is allowed.
… und wie die Logik damit umgeht, dass Short und Long Name stark unterschiedlich sind, habe ich selber noch nicht verstanden. Wie gesagt, einfach ignorieren.

Nachtrag zur Klärung: Die MSDN gibt an, dass man einen Short Name eintragen muss. Tatsächlich wird der aber nur verwendet, wenn man die Setups via SHORTFILENAMES-Property oder durch ein Registry-Setting in Windows erzwingt. Wer sowas tut, ist IMHO selber schuld, wenn nichts mehr funktioniert.

Nachtrag 2: Für Short Filenames gelten andere Regeln als für Long Filenames – bswp. ist das Pluszeichen + nicht erlaubt. Short Filenames sind immer der erste Fall, erst danach kommt das Trennzeichen und dann der Long Filename. Es könnte also sein, dass MSI einen Dateinamen mit Pluszeichen nicht akzeptiert, wenn nicht explizit auch ein Short Filename angegeben wurde. Das muss ich nachher testen; wäre ein ziemlicher Kracher …

Nachtrag 3: Wenn man .cab-Dateien mit dem Windows-Standard-Utility makecab erzeugt, unterstützt es nur 8.3-Dateinamen. Ich werde euch aber später zeigen, wie man das direkt mit der Win32-API und langen Namen hinkriegt.

Nachtrag 4: Dateiname mit Pluszeichen wirft einen Fehler in Orcas Überprüfung des Pakets; Installation trotzdem problemlos (sogar ohne Warnungen) auf Windows 10 x64.

(Ihr seht, es ist ein ziemliches Rabbit Hole. Ich muss hier auch alles aufschreiben, damit ich es nicht selber sofort wieder vergesse.)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Ein Setup auspacken

MSI ist standardisiert und datengesteuert. Packages liefern also keinen eigenen Code für die Installation, und jedes Package lässt sich auf die selbe Art entpacken. (Ausnahmen am Ende des Beitrags.)

Das Auspacken eines Setups nennt sich in MSI Administrative Installation. Fragt mich nicht, wo der Name herkommt oder was er bedeutet – ist halt so.

Die Befehlszeile

  msiexec /a foo.msi TARGETDIR=C:\foo

entpackt das Package foo.msi in das Verzeichnis C:\foo. Davon abgesehen wird nichts am Package oder an eurem System geändert. Den Parameter TARGETDIR=C:\foo könnt ihr bei vernünftig gebauten Packages weglassen; sie extrahieren dann ins aktuelle Verzeichnis. Jedoch haben gerade mit WiX gebaute Packages die Unart, zu C:\ zu extrahieren, wenn man kein TARGETDIR angibt. Sicher ist sicher.

Nach dem Aufruf sollte das Zielverzeichnis eine (geschrumpfte) .msi und ein neues Unterverzeichnis enthalten. Die geschrumpfte .msi speichert weiterhin alle Registry-Einträge, UI, usw.; die eingebetteten Binärdaten und Archive sind jedoch alle in das neue Unterverzeichnis extrahiert. Wenn ihr darin rumstöbert, findet ihr alle Dateien, die das Package auf dem System installieren kann. Wenn ihr das mit Word o. ä. macht, könnt ihr euch z. B. alle Schriftarten ansehen oder in Ruhe welche rauspicken.


Dieses entpackte Package ist vollständig und kann problemlos zur Installation benutzt werden. Ihr könnt das ursprüngliche Package löschen, wenn euch das entpackte besser gefällt. Diese Eigenschaft ist enorm nützlich – einerseits für die Archivierung, aber zu einem viel wichtigeren Anwendungsfalle komme ich im nächsten Beitrag.


Die versprochene Ausnahme von der Regel: Office 2003 lässt sich nicht extrahieren, weil Microsoft dort die entsprechenden Tabellen aus der Datenbank gelöscht hat. Ich weiß nicht, was hinter der Entscheidung stand. Mit etwas Eifer kann man sie wiederherstellen, aber das spare ich mir für einen späteren Beitrag.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Setups, Patches, und Service Packs zusammenfassen

Falls ihr euch für Office & Co. die Patches und Service Packs selber herunterladet statt durch einen Web Installer, habt ihr ein ursprüngliches Setup und eine Sammlung von .msp-Dateien (MSI-Patches). Hier seht ihr meinen Ordner für den PowerPoint Viewer (im Gegensatz zu Libre Office & Co. 100 % kompatibel; dabei viel leichter als eine echte Office-Installation):
PPV folder.png
Ihr merkt vielleicht: Möchte ich den PowerPoint Viewer installieren, muss ich 35 Installationen starten. Alternativ installiere ich ein Mal und Windows Update installiert 34 Updates nach.

Beides kein Zustand.

Nun zu dem irren Vorteil der entpackten Setups, die ich im Beitrag zuvor erwähnt habe: Man kann Patches direkt auf sie anwenden.

Legen wir los. Das Grund-Setup ist kein MSI, sondern eine EXE. Sowas nennt man Bootstrapper und oft ist es bloß ein selbstextrahierendes Archiv, um einen netten Splash Screen anzuzeigen und die Dateigröße weiter zu drücken. Wir kommen an das eigentliche Setup, indem wir das .msi-Package via 7-Zip aus der EXE extrahieren (und sein zugehöriges .cab-Archiv, denn das ist – aus welchem Grund auch immer – nicht ins MSI eingebettet). Rechtsklick auf die EXE7-ZipOpen Archive:
PPV actual MSI.png
Dann extrahieren wir das MSI mit der Methode, die wir im vorherigen Beitrag gelernt haben:
PPV MSI extracted.png
PPV MSI extracted.png (2.22 KiB) 9266 mal betrachtet
Nun wenden wir via /p-Parameter die gesammelten Patches auf das extrahierte Package an:

Code: Alles auswählen

msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2553347 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2589298 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2589318 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2589352 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2597087 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2880522 Update for PowerPoint Viewer 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2881030 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2889841 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2920812 Security Update for PowerPoint 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB2956076 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3054886 Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3085528 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3101520 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3115197 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3118380 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3118382 Security Update for PowerPoint Viewer 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3128030 Security Update for PowerPoint Viewer 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3141538 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3178688 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3191844 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3191899 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3203460 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3213624 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB3213631 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4011055 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4011191 Update for PowerPoint Viewer 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4011611 Security Update for Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4011707 Security Update for Microsoft Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4018311 Security Update for Microsoft Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4018312 Update for PowerPoint Viewer 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4022136 Update for Microsoft PowerPoint 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4022137 Security Update for Microsoft Office 2010.msp" TARGETDIR=C:\foo
msiexec /a "%~dp0ppviewer.msi" /p "%~dp0KB4022198 Security Update for Microsoft Office 2010.msp" TARGETDIR=C:\foo
Die Liste zu erzeugen sieht nach PITA aus, aber der geneigte Windows-User weiß, dass er via dir >> list.txt alle Dateinamen in eine Textdatei ausspucken lassen kann, und von da an ist die Sache quasi erledigt :)

Nun befinden sich unter C:\foo eine .msi-Datei und ein Ordner, die ein vollständig gepatchtes Setup speichern. Die könnt ihr wegspeichern, archivieren, oder sonstwas – und wenn die .msi aufgerufen wird, installiert sie in einem Rutsch den PowerPoint Viewer mitsamt allen Patches. Wenn neue Patches erscheinen, könnt ihr das Verzeichnis auf die selbe Art erweitern.

Protip für die Archivierung: Setup und Patches als gesonderte .exe und .msp waren 266 MiB groß, denn wenn eine Datei durch zehn Updates ersetzt wurde, lag sie auch elf Mal komprimiert vor. Das extrahierte, gepatchte Setup ist nach 7-Zip nur noch 42 MiB groß. Man kann also auch richtig, richtig viel Platz sparen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Installation Context

Hiermit habe ich mir ziemlich in den Fuß geschossen, darum ein extra Post.

Wenn ihr Setups mit WiX erstellt, braucht ihr Admin-Rechte zur Installation. Andererseits haben einige Setups aber auch den klassischen Auswahldialog, ob man für alle Benutzer oder nur für sich selber installieren möchte. Und letztlich strebt Microsoft eine pro-User-Installation an. Hier ist, wie das Ganze zusammenpasst:

Der Windows Installer unterscheidet zwei Installation Contexts: per-User und per-Machine. Eine per-User-Installation erfordert keine Admin-Rechte; eine per-Machine-Installation schon.

Ursprünglich gab es eine einzige Property-Variable, die das Ganze steuerte: ALLUSERS. Wenn dieser Wert auf 1 gesetzt ist, wird per-Machine installiert; sonst per-User. Der Wert kann an UI-Elemente gebunden werden (ControlEvent-Tabelle) um dem Anwender eine Auswahl zu ermöglichen. Entwickler müssen basierend auf diesem Wert z. B. unterscheiden, ob sie nach Program Files installieren oder nach AppData\Local\Programs.

Um es dem Anwender (und den Entwicklern) noch bequemer zu machen, wurde mit Windows 7 und dem Windows Installer 5.0 der Wert 2 eingeführt. Sobald der gesetzt ist, erfolgt die Steuerung durch eine neue Property-Variable MSIINSTALLPERUSER, und die wird von Windows automatisch initialisiert – startet ein Administrator das Package, hält sie 0 und die Installation wird per-Machine durchgeführt. Startet ein Standardbenutzer das Package, wird die Installation per-User durchgeführt. Die Pfade werden automatisch umgeleitet; der Entwickler muss keine Unterscheidung mehr einbauen – das nennt sich Single Package Authoring. Die Variable lässt sich ebenso durch die UI ändern, um dem Anwender eine Wahl zu ermöglichen (das Single Package Authoring Example führt es sehr gut vor). Beim Reparieren und Deinstallieren startet das Package automatisch wieder im richtigen Kontext.

Übrigens dürft ihr das UAC-Bit im Summary nur setzen, wenn das Package ausschließlich per-User installiert werden kann. (Die Namen sind irreführend!) Sonst fragt Windows nicht nach Admin-Rechten bei einer per-Machine-Installation, und sie schlägt fehl.

Damit ist es sehr einfach geworden, Packages zu entwickeln, die sich ohne UAC-Prompts bedienen lassen. So weit, so gut. Kommen wir nun zu dem Punkt, an dem sie alles verkackt haben: Upgrades.

Die Dokumentation sagt zwar „Applications that have been installed per-user therefore receive all updates or repairs on a per-user basis and applications installed per-machine receive updates or repairs on a per-machine basis“, und wörtlich stimmt das auch. Allerdings versteht Windows Installer unter Update nur minimale Patches; nicht komplette Packages – die heißen Upgrades. Und man kommt fast gar nicht um sie herum.

Wenn nun ein Upgrade startet, indem man eine neue Version über die alte installiert, erfolgt die Suche nach älteren Packages ausschließlich im Installation Context, in dem das neue Package gestartet wird. Mit per-User-Installationen klappt das perfekt:
  1. User X startet Package 1.0. Er ist Standardbenutzer, darum startet es per-User.
  2. Er installiert das Package 1.0 ohne UAC-Prompts; Windows Installer leitet alles nach C:\Users\X\AppData\Local\Programs um.
  3. Installation erfolgreich; X benutzt das Programm.
  4. Package 1.1 mit kritischen Sicherheitsupdates wird verteilt. Es ist als vollständiges Package mit identischem Upgrade-Code und unterschiedlichem Product Code realisiert (Upgrade).
  5. Beim Start erkennt Windows-Installer, dass bereits eine frühere Version installiert ist. Der Lizenzdialog wird übersprungen; die alten Einstellungen werden übernommen; die Installation wird aktualisiert.
  6. Alles ist perfekt.
Nun spielen wir das mit einem Auswahlbildschirm für den Kontext durch – Wollen Sie das Programm für alle Benutzer dieses Computers installieren? – und der Benutzer sagt immer Ja:
  1. User X startet Package 1.0. Er ist Standardbenutzer, darum startet es per-User.
  2. Im Auswahlbildschirm wählt er per-Machine-Kontext aus.
  3. Die Installation erfordert einen UAC-Prompt; Windows Installer leitet alles nach C:\Program Files um.
  4. Installation erfolgreich; X benutzt das Programm.
  5. Package 1.1 mit kritischen Sicherheitsupdates wird verteilt.
  6. X startet das neue Package. Er ist Standardbenutzer, darum startet es per-User.
  7. Die Suche nach vorherigen Installationen schlägt fehl, weil die vorherige Installation per-Machine durchgeführt wurde. Der Lizenzdialog taucht erneut auf. Die alten Einstellungen werden nicht übernommen.
  8. Im Auswahlbildschirm wählt X wieder per-Machine-Kontext aus.
  9. Die Installation erfordert einen UAC-Prompt; Windows Installer leitet alles nach C:\Program Files um.
  10. Installation erfolgreich … und alles ist Schrott.
Nun hat Windows Installer nämlich nicht das alte Package deinstalliert (es wurde beim Start der UI ja nicht gefunden). Nun sind zwei Packages mit unterschiedlichen Versionen am selben Ort installiert. Das Programm taucht unter Programme und Features doppelt mit unterschiedlichen Versionsnummern auf. Falls noch irgendwo eine Verknüpfung zur alten Version besteht, erkennt Windows Installer beim Start, dass die Zieldatei geändert wurde und startet eine automatische Reparatur auf Version 1.0. Die beiden Installationen bekämpfen sich.

Ich habe für dieses Dilemma keine andere Lösung gefunden, als …
  1. … entweder den Auswahlbildschirm für den Kontext zu vermeiden
  2. … oder das Paket von vornherein auf einen bestimmten Kontext festzulegen.
Keine freie Wahl also. Ich finde, dass das eine üble Fehlplanung ist, zumal die direkte API solche Einschränkungen nicht hat.

Das dürfte erklären, warum WiX standardmäßig als per-Machine-Installation startet.

Hier ist übrigens noch eine gute Zusammenfassung der per-User-Nachteile:
  • Installing to locations the user has the ability to alter might reduce the confidence the package producer has for the integrity of the install. This can affect support costs as well as computational correctness under a regulatory environment (lawyers, accounts, food and drug companies, government agencies, etc)
  • Multiple instances of an install means there is duplicate copies of binaries on the machine which wastes disk space. A “Per-Machine” install creates a single copy of common binaries for all users thus saving space.
  • Software is less secure because updating behavior has to be done for each user on the machine. In other words, the occasional user on the machine can made the machine vulnerable because they are not on the machine often enough to keep the software they use up to date.
  • IT departments want programs in locations users can’t tamper with. User tampering is a major source of support costs.
  • Centralized install, servicing, and uninstall from a central IT department are all more challenging when the apps are just in the users profile. There are numerous conditions where it is known not to work at all
Andererseits weiß jeder, der auf der Arbeit nur einen eingeschränkten Benutzer-Account zur Verfügung hat, die Vorteile von per-User-Installationen zu schätzen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
mrz
Beiträge: 79
Registriert: 07.08.2008, 14:34

Re: Microsoft Installer (MSI)

Beitrag von mrz »

Wieder mal sehr informativ, Danke!
Fühle richtig mit, erinnert mich an die Zeit wo ich das Vergnügen mit Sysprep hatte
und mit WAIK ("Windows Automated Installation Kit") resp. WADK ("Windows Assessment and Deployment Kit").
Bis vor deinen letzten Post hat für mich MSI, abgesehen von der Unschönheit mit dem Deinstaller vonwegen Diskspace,
einen brauchbaren Eindruck gemacht. Aber die Problematik mit per-User und per-Machine ist ja ein übler Killer.
Weil die freie Wahl dieser Option im Setup finde ich eine gute Sache.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Die Namen sagen mir was :D

Das mit per-User und per-Machine ist ein derart übler Killer, dass ich fast eine Woche alles auf den Kopf gestellt habe um zu prüfen, ob ich nicht irgendwo was falsch mache – aber das MS-Sample zeigt gleiches Verhalten. Ich liefere das Programm nun mit dem Hinweis aus, dass man das Setup händisch per-User starten kann (msiexec /i setup.msi ALLUSERS=2 MSIINSTALLPERUSER=1) und dass man dann darauf achten muss, alle zukünftigen Updates auch auf gleichem Wege zu starten.

Was ich nicht ausprobiert habe, ist: ALLUSERS=1, FindRelatedProducts-Action; falls nichts gefunden, das selbe nochmal mit ALLUSERS=0. Nach einer Woche Herumprobieren wäre mir das zu viel Aufwand gewesen und ich weiß nicht, ob man Standard-Aktionen doppelt einreihen kann.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Ich bin bei diesem Thema in eine weitere Falle getappt.

Ich kann mein altes, mit WiX erstelltes Setup nicht mit meinem neuen, selber erzeugten Setup upgraden: Obwohl das WiX-Setup während der Installation Admin-Rechte einfordert, und nach C:\Program Files installiert, ist das Produkt nur per-User registriert! Irgendwas ist da total abgefuckt.

Sobald ich mein Setup zu per-User ändere, erkennt er es als Upgrade des alten WiX-Setups. Aber dann installiert es natürlich zu LocalAppData statt nach Program Files, weil … es ja per-User ist. Total kirre. Aber ändern kann ich nichts mehr; ist halt schon lange ausgeliefert.

Also, falls ihr bestehende Setups mit WiX habt: Testet den Upgrade-Pfad gut und überlegt euch den Installation Context ganz genau. Irgendwas ist da richtig, richtig faul.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

.CAB-Archive packen

Cabinet-Archive sind ja quasi der Nukleus von Installationen unter Windows (werden schon seit Urzeiten für Treiber benutzt), aber sie zu erzeugen ist nicht so einfach.
  • Windows bringt makecab mit. Über die Befehlszeile könnt ihr damit nur eine einzelne Datei packen, also ist es quasi nutzlos. Für mehrere Dateien müsst ihr eine kryptische Direktivendatei anlegen, deren Syntax in einem SDK beschrieben ist, das ihr im Internet Archive suchen müsst.
  • cabarc war der Vorgänger von makecab, ist schwer aufzutreiben, und unterstützt gemäß Doku nicht einmal die begehrte LZX-Kompression. Unbrauchbar.
  • cabcomp, auch bekannt als HeleCAB, ist eine schmale GUI über Microsofts Cabinet.dll. Steinalt, aber ganz brauchbar, falls man mal was packen muss.
  • CabPack soll gemäß encode.ru „a couple bytes better“ sein, aber bei mir stürzt es immer nur ab. Unbrauchbar.
  • wimlib hat die einzige nicht-Microsoft-Implementierung der LZX-Kompression, die ich auftreiben konnte. Es ist aber halt blankes LZX ohne CAB-Container, ohne Sliding Windows. Und es soll der Kompression in Windows’ Cabinet.dll unterlegen sein.
  • Das Windows SDK enthält in fci.h die komplette API der Cabinet.dll. Obwohl das irre kryptisch aussieht, gibt es in der MSDN sogar quasi-fertigen Quelltext, den man nur zusammenkopieren muss! (Einfach durch die Liste der Makros klicken, jedes Code-Beispiel abkopieren.) Benutzen lässt er sich folgendermaßen:

    Code: Alles auswählen

    // TODO: _A_NAME_IS_UTF!!!!!
    
    ::CCAB cab = { };
    cab.cb             = 0x7FFF'FFFF; // allow up to 2 GiB per CAB
    cab.cbFolderThresh = 0x7FFF'FFFF; // allow up to 2 GiB per compression block
    cab.setID          = 555;         // TODO: ??!? Cabinet set ID
    cab.iCab           = 0;           // Number of this cabinet in a set
    cab.iDisk          = 0;           // Disk number
    cab.szDisk[0]      = '\0';
    strcpy_s(cab.szCabPath, (char const*)path);
    cab.szCab[0]       = '\0';
    
    ::ERF da_staet;
    auto builder = ::FCICreate(
    	&da_staet,
    	&onFilePlaced,
    	&onReserveMemory, &onReleaseMemory,
    	&openFile, &readFile, &writeFile, &closeFile, &seekFile, &deleteFile,
    	&generateTempFileName,
    	&cab,
    	nullptr
    );
    if(builder) {
    
    	auto       toJob           = jobs;
    	auto const toEndOfJobs     = toJob + numberOfJobs;
    	auto       lastCompression = compression(jobs[0].type, jobs[0].compressionLevel, jobs[0].memoryLevel);
    	do {
    
    		// The memory window can span multiple files, which improves compression drastically (for the sake of random access
    		//  latency). This is controlled for via “folders” (each folder being a solid block of compressed data, if not
    		//  interrupted by CAB size limit).
    		// Start a new folder when the compression settings change.
    		// TODO: Doensn’t the CAB API do that automatically?! Test it!
    		auto const newCompression = compression(toJob->type, toJob->compressionLevel, toJob->memoryLevel);
    		if(newCompression != lastCompression) {
    			if(0 == ::FCIFlushFolder(builder, &onNeedNextCAB, &onStatus)) {
    				__debugbreak(); // error
    			}
    			lastCompression = newCompression;
    		}
    
    		if(0 == ::FCIAddFile(
    			builder, (char *)toJob->sourcePath, (char *)toJob->nameInArchive,
    			0, // don’t execute
    			&onNeedNextCAB, &onStatus, &fnGetOpenInfo,
    			newCompression
    		)) {
    			__debugbreak();
    		}
    
    	} while(++toJob < toEndOfJobs);
    
    	if(0 == ::FCIFlushCabinet(builder, 0, onNeedNextCAB, onStatus)) { // TODO: necessary?!
    		__debugbreak();
    	}
    	::FCIDestroy(builder);
    }
    (Dabei sind Compression Type die Werte tcompTYPE_NONE, tcompTYPE_MSZIP, tcompTYPE_QUANTUM, tcompTYPE_LZX aus den Windows-Headern und Compression Level / Memory Level sind in den Headern direkt daneben erklärt. Beste Kompression ist immer tcompTYPE_LZX mit Level 21.)

    Nachtrag: Microsoft hat auch ein Aufrufbeispiel; nutzt besser das: https://docs.microsoft.com/en-us/window ... -a-cabinet
… und das ist dann auch schon alles, was mein Setup-Generator braucht.

Abschließende Trivia: Für Mitte der 90er war LZX wirklich bahnbrechend. Der Typ, den MS damals dafür eingestellt hat, hatte sogar extra-Filter für ausführbare Dateien entwickelt. Heute ist es zwar abgeschlagen hinter LZMA, Oodle, und Konsorten – Microsoft setzt es aber nichtsdestotrotz für komprimierte Windows 10-Installationen (nicht -Setups!) ein, bei denen das Betriebssystem komprimiert installiert wird. Raymond Chen hatte letztens was dazu.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Tiles
Establishment
Beiträge: 1990
Registriert: 11.01.2003, 13:21
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Tiles »

Ich finde es Top wie du dich hier durchwühlst :)

Hast du eigentlich geplant mit dem Fachwissen unterm Arm einen eigenen Installmaker zu bauen? Mir scheint das Nutzen/ Aufwandsverhältnis ein wenig gross um nur eine einzige Setupdatei zu bauen ...
Free Gamegraphics, Freeware Games https://www.reinerstilesets.de
Die deutsche 3D Community: https://www.3d-ring.de
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Ich habe schon einen gebaut, aber da sind noch nicht alle Schwächen ausgebügelt. Insbesondere ist die EULA noch hard-coded, weil sie für alle meine Projekte identisch ist … mit der Zeit wird sich das schrittweise verbessern, und dann kann ich den Generator auch veröffentlichen.

Tatsächlich sollte man das meiste hier aber auch wissen, wenn man nur ein einziges Setup bauen will. Leider. Das hätte Microsoft echt viel, viel einfacher entwerfen können.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

.CAB-Archive in MSI einbetten

.CAB-Archive können in MSIs eingebettet werden oder nicht; Windows Installer kann mit beiden Fällen umgehen.

Externe CABs sind besser, wenn:
  • Sich mehrere Installer die selben Daten teilen. Etwa zwei MSIs – einmal die 32-Bit-Version, einmal die 64-Bit-Version – die sich gemeinsame Programmdaten teilen.
  • Wenn man aus irgendeinem Grund mehrere Installationsmedien braucht (CD 1, CD 2, …) – stirbt aber heute aus.
Eingebettete Tabs haben die Vorteile:
  • Nur eine Datei für den Kunden – also die beste Option für Downloads.
  • Beim Kunden ist die Hürde, versehentlich was kaputtzumachen, viel höher (er kann nicht versehentlich das CAB löschen).
Wenn man das CAB im MSI haben möchte, muss es als separater Stream hinzugefügt werden. (MSIs sind intern die selben Stream-Container wie sie auch von frühen Office-Versionen genutzt wurden.) Nativ in Win32 geht das so (Quelle):
  • Dem echten CAB-Namen muss im MSI eine Raute vorangestellt werden – das bedeutet, dass Windows Installer es sich aus einem Stream holen soll statt aus einer Datei. Gilt vor allem für die Cabinet-Spalte in der Media-Tabelle.
  • Neues MSI erzeugen via

      MSIHANDLE msi;
      if(ERROR_SUCCESS == MsiOpenDatabaseW(L"foo.msi", MSIDBOPEN_CREATEDIRECT, &msi)) {
        // foo.msi
    erfolgreich erzeugt
  • Eine Ansicht aller Streams im MSI öffnen via

      MSIHANDLE hView;
      if(ERROR_SUCCESS == ::MsiDatabaseOpenViewA(msi, "select * from `_Streams`", &hView)) {
        // hView
    ist nun eine Ansicht aller Streams
  • Einen neuen Eintrag für den Stream erzeugen – erste Spalte ist der Zielname, zweite Spalte ist der Pfad zum CAB, das hineinkopiert werden soll:

      if(auto hRec = ::MsiCreateRecord(2)) {
        ::MsiRecordSetStringA(hRec, 1, "#data.cab");
        ::MsiRecordSetStreamA(hRec, 2, "C:\\Users\\Krishty\\AppData\\Local\\Temp\\data.cab");
        ::MsiViewModify(hView, MSIMODIFY_INSERT, hRec);
      }
… und so kommt das CAB ins MSI. Nun haben wir fast alle Zutaten, um eigene (sehr primitive) Packages mit der Win32-API zu erzeugen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Ein mehrsprachiges Setup ausliefern

Optimalerweise möchte man eine .msi-Datei haben, die beim Anwender automatisch in der richtigen Sprache startet. Das ist aber leider schwierig und setzt einiges voraus; darunter zwei inoffizielle Features.


1. Transformationen

Lokalisierung ist bei MSI nur als nachträgliche Transformation möglich: Man ersetzt dabei Datenbankspalten, die Strings enthalten, durch lokalisierte Versionen der Strings.

(Theoretisch könnte man Properties und Bedingungen nutzen; praktisch werden aber auch einige Dialoge – bspw. das Laden des Setups – vom MSI verwaltet, und nutzen deshalb keine Properties für ihre Texte, sondern die Sprache des aufgerufenen Paketes. Von der exponentiellen Code-Explosion mal ganz abgesehen!)


2. UTF-8

MSI-Datenbanken sind Codepage-basiert – weil das 1997 eben so gemacht wurde. Die Transformationen müssen also nicht nur die Strings ersetzen, sondern auch die Codepage der Datenbank ändern! Das kann eigentlich nur in zwei Fällen gelingen:
  1. Die ursprüngliche Datenbank enthält nur ASCII-Zeichen. Das könnt ihr euch abschmieren, sobald ihr einen Ordner, Registry-Schlüssel, oder eine Datei mit Sonderzeichen drin habt. Mit Sonderzeichen im Produktnamen sowieso.
  2. Die Codepage der ursprünglichen Datenbank ist identisch mit der Codepage eurer Übersetzung.
Der letzte Punkt ist eigentlich kontraintuitiv: Die Codepages können doch nicht bei allen Übersetzungen gleich sein; außer vielleicht für die British English-Übersetzung eines American English-Programms! Protip: MSI unterstützt inoffiziell, aber stabil die Codepage 65001 UTF-8.

Zweite Voraussetzung für eine praktikable Übersetzung eines Packages ist also, dass es komplett in UTF-8 vorliegt. Die Transformationen der Übersetzungen können ebenfalls in UTF-8 vorliegen. Dann kann nichts kaputtgemacht werden.


3. Namen eingebetteter Transformationen

Auf die dritte Voraussetzung kommt man nicht alleine, denn sie ist ein Implementierungsdetail: Damit MSI die passende Transformation für die Sprache des Benutzers auswählt, muss sie mit einem bestimmten Namen in der MSI eingebettet sein, nämlich unter der Language/Region Decimal ID.

Wenn ihr also eine Transformation habt, die euer Package ins Chinesische (Volksrepublik) übersetzt, müsst ihr eure Transformation unter dem Namen 2052 (ohne .mst-Erweiterung!) ins MSI einbetten.

Auch dieses Feature ist undokumentiert. Allerdings ist es seit über fünfzehn Jahren bekannt, und mittlerweile basieren derart viele Packages darauf (u. a. einige Versionen von Microsoft Office und Libre Office), dass Microsoft es nicht mehr entfernen wird. Unter Windows 7 und Windows 10 funktioniert es garantiert; unter älteren Versionen habe ich es nicht getestet.

Glaubt nicht den Gerüchten, Windows würde die Sprache anhand des Tastaturschemas aussuchen. Unter Windows 10 ist das garantiert falsch.


4. Languages in Summary Page

Die letzte Voraussetzung ist, dass in der Summary Page des Packages alle eingebetteten Language/Region Decimal IDs aufgelistet werden. Für Deutsch, Englisch, und Chinesisch sollte dort z. B. unter Languages eingetragen sein: 1031,1033,2052.

––––

Was bedeutet das praktisch? Falls ihr WiX nutzt, gibt es eine schöne Anleitung, wie ihr Transformationen für Sprachen erzeugen könnt:
  1. Nutzt <WixLocalization>, um Strings zu lokalisieren.
  2. Stellt dabei sicher, dass ihr immer in UTF-8 arbeitet.
  3. Kompiliert dann für jede Sprache ein komplettes Setup. (Ja ich weiß – aber WiX kann es wohl einfach nicht anders.)
  4. Nutzt Torch.exe, um ein Diff der einzelnen Versionen zu machen. Im Resultat erhaltet ihr pro Lokalisierung eine .mst-Transformation.
  5. Benennt die Transformationen zu ihren entsprechenden Language/Region Decimal IDs um (ohne Dateierweiterung!).
  6. Ladet euch die WiX-Ergänzung EmbedTransform herunter.
  7. Nutzt sie, um die Transformationen in die .msi einzubetten.
  8. Achtung: Hierfür habe ich noch keine saubere Lösung gefunden: Ihr müsstet nun eigentlich die IDs ins Summary eures Packages eintragen, etwa

    <Package
    Languages='1033,1031'


    aber dann meckert der Compiler, dass diese Sprachen deklariert, aber nicht unterstützt sind. Klar – die Sprachen landen ja auch erst nach der Kompilierung im Package! Ihr müsst also entweder die Validierung ausschalten (-sval-Parameter bei Light.exe) oder die Languages undefiniert lassen und im fertigen Paket händisch nachtragen, etwa via Orca.

    Ich habe aber auch nicht wirklich lange nach einer Lösung gesucht, weil ich eh meinen eigenen MSI-Compiler gebaut habe.
Bedenkt, dass sich das alles automatisieren lässt (vielleicht 20 Zeilen Batch). Es ist also nicht so schlimm, wie es klingt.

––––

Eine Frage, die mich beschäftigt: Sollte das sprachneutrale Setup überhaupt keine Strings enthalten? Im Augenblick ist das sprachneutrale Setup bei mir Englisch; dafür gibt es dann aber keine explizite englische Transformation. Hätte es Vorteile, das anders zu machen? Im Hinblick auf Merge Modules? Mal ausprobieren.


Setup in einer bestimmten Sprache starten

Falls ihr den Erfolg eurer Arbeit prüfen wollt: msiexec /i foo.msi TRANSFORMS=":2052" ruft die chinesische Version von foo.msi auf. (Der Doppelpunkt markiert, wie wir wissen, eingebettete Dateien statt externer.)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Advertising mit COM

Möchte man ein Programm in die Windows-Oberfläche – die Shell – integrieren, führt kein Weg an COM vorbei:
  • Shell Extension Handlers – also zusätzliche Menüpunkte beim Rechtsklick auf bestimmte Dateitypen – werden via COM realisiert.
  • Preview Handlers – die kleinen Vorschaubilder für einige Dateitypen – werden via COM realisiert.
Dabei geht es fast immer um Classes (COM-Implementierungen), ProgIds (Arten von Programmen), Verbs (Befehle, was mit einer Datei gemacht werden kann – etwa Öffnen, Drucken, und Vorschau), und Extensions (Dateierweiterungen). Die Informationen darüber landen ultimativ in der Registry. Sie werden von Explorer.exe ausgewertet, sobald Benutzer Dateien anklicken. Auf Basis dieser Informationen wird dann der COM-Handler mit den gewünschten Shell-Erweiterungen gesucht.

Windows Installer stellt Tabellen bereit, um eben diese Informationen in MSI-Pakete einzubetten: Diese Tabellen bilden das COM-Ökosystem um Shell-Erweiterungen gut ab. Dennoch: Erzeugt man ein Setup mit WiX, und benutzt dabei extra die passenden Elemente wie <ProgId>, löst WiX die Daten zu normalen Registry-Schlüsseln auf und schreibt sie in die Registry-Tabelle. Die oben genannten Tabellen werden nie genutzt. Und das löst dann zahllose ICE33-Warnungen beim Validieren des Setups aus. Warum eigentlich?! Und warum sind diese Warnungen mittlerweile veraltet?!

Die Antwort dafür ist ziemlich weit in den Tiefen des Internets verborgen. Rob Mensching (der WiX-Maintainer) hat 2007 einen Blog-Artikel darüber geschrieben. Angespornt wurde er 2004 durch Michael Sanford (von Zero G InstallAnywhere) in WiX’ Bug Tracker. Mensching redete viel und sagte wenig (die Tabellen lösten obskure Fehler in der Spracherkennung auf Windows XP aus, wenn Office XP installiert war), Sanfords Antwort ist schwer auffindbar, und die Beispiele aus Menschings Blog sind heute für immer verloren.

Ich denke aber, dass ich den ausschlaggebenden Punkt gefunden habe:
https://sourceforge.net/p/wix/mailman/message/8012944/ hat geschrieben:The problem comes down to the fact that the Windows Installer registers the COM advertisement with the last product installed.
Advertising installiert Features, sobald sie benötigt werden. Der Zugriff auf eine COM-Schnittstelle ist so ein Zeitpunkt: Wurde die Schnittstelle advertised installiert, löst der erste Zugriff – auch durch die Shell – eine tatsächliche Installation aus. Was aber, wenn die Schnittstelle von zwei unterschiedlichen Produkten installiert wird? Dann installiert Windows Installer die Schnittstelle aus dem Setup, das zuletzt ausgeführt wurde.

Die Folge ist, dass manchmal ein Self Repair für Programme startet, die scheinbar nichts mit dem zu tun haben, was gerade auf dem Computer vor sich geht. Wenn Windows XP und Office XP ihre Spracherkennung teilen, und sie von beiden via Advertising vorbereitet wurde, dann wird ein erster Start der Spracherkennungs-App immer nach dem Office XP-Installationsmedium fragen – selbst, wenn man die App über die Windows-Systemsteuerung startet. Eben weil Office XP später installiert wurde, und Windows Installer immer nur das letzte Setup heranzieht. Mensching führt weiter aus, wie sehr Anwender davon verwirrt werden können.

WiX verzichtet auf die expliziten Tabellen und schreibt also direkt in die Registry-Tabelle, um ein Advertising solcher Schnittstellen zu verhindern.

Ich habe dazu noch keine Position bezogen. Die expliziten Tabellen haben einige andere Nachteile:
  • Sobald eine explizite ProgId vorliegt, muss auch eine Class-Tabelle vorliegen – sogar dann, wenn sie leer ist (InstallShield ist 2006 in diese Falle getappt).
  • Die ProgId-Tabellen haben nicht ausreichend Spalten, um alle Anwendungsfälle abzudecken – Preview Handlers können zum Beispiel nicht eingetragen werden; ebenso wie spezielle COM-Server-Optionen. Mit rohen Registry-Schlüsseln geht das problemlos.
Für den Augenblick bin ich in allen meinen Setups den WiX-Weg gegangen. Ich muss noch austesten, ob explizite Tabellen besser wären.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Versionen dürfen sich nur nach vorn entwickeln

Übler Fallstrick: Nichts spricht dagegen, ein altes MSI über ein neues zu installieren, also ein Programm downzugraden. Jedoch dürfen neue MSIs immer nur Komponenten neuerer oder gleicher Versionsnummer enthalten!

(Dabei bezieht sich die Versionsnummer vor allem auf die Versionsinformation, die als Ressource in einer EXE oder DLL hinterlegt ist. Ist die nicht vorhanden, gilt das Datum.)

Der Grund dafür ist, dass man sonst ratzfatz in Situationen kommt, in denen die Dateien beim Downgrade entfernt werden:
https://blogs.msdn.microsoft.com/astebn ... n-numbers/
https://stackoverflow.com/questions/422 ... ad-of-down

Das Problem kann man durch Verschieben der Action-Reihenfolge abmildern, aber das öffnet nur die Büchse der Pandora für ganz andere Abnormalitäten … also lieber die Versionsnummern monoton steigend halten!
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Verknüpfungen direkt ins Startmenü, ohne Ordner

(Hat nicht direkt mit MSI zu tun, eher generell mit Deployment …)

Ich musste ewig googeln, um endlich eine offizielle Stellungnahme zu finden, aber hier haben wir sie: MSDN – Chapter 3. User Interface Fundamentals (2006)

Programme sollten ihre Verknüpfungen direkt in der Programmliste anlegen, ohne Unterordner:
The following behaviors, though not required, are recommended:
  • Place your icon to launch your application directly under Start -> Programs. Avoid placing it in a folder under programs. In particular, do not create a folder in the Start menu in which you only put one item. Often, applications will create a folder, based on Company name, and then put a single shortcut to launch the application inside that folder. Instead, consider renaming the shortcut to include the company name and dropping the use of the folder.

          Programs       My Company       My App       (Avoid this)

          Programs       My Company My App             (Recommended)
     
  • Do not put anything in the top of the Start menu, as users consider this their own personal space.
     
  • If you have support applications, tools, or utilities associated with your application, and you wish to publish these in the Start menu, create a single folder in the Start menu as a peer of the icon to launch your application and place them there.
Visual Studio macht das also richtig vor: Eine einzige Verknüpfung Visual Studio 2017 direkt im Startmenü, und dann noch ein gleichnamiger Ordner mit dem seltener genutzten Zeug wie den Developer Command Prompts, Blend, usw.

Mit Windows 8/10/Cortana hat sich das ja eh erledigt, weil das Startmenü quasi komplett virtuell ist.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

Gemischte 32- und 64-Bit-Setups

MSI unterstützt in gewissem Maße das Ausliefern von 32-Bit-Dateien in 64-Bit-Setups (und umgekehrt). Leider kann kein Setup sowohl die Installation einer kompletten 64-Bit-Anwendung als auch die Installation einer kompletten 32-Bit-Anwendung unterstützen – dafür braucht man immer zwei getrennte MSIs.


Das Setup muss sich für eine Plattform entscheiden

In der Summary Page der MSI-Datei muss eine Architektur angegeben werden – entweder Intel (für 32-Bit-x86) oder x64, aber niemals beide zugleich. Darauf weist auch der 64-Bit-MSI-Leitfaden ziemlich weit oben hin.


Warum kein 32-Bit-Paket, das optional die 64-Bit-Version mitbringt?

Wie eingangs gesagt, unterstützt MSI das Ausliefern von 64-Bit-Komponenten in 32-Bit-MSIs. Dann müsste es eigentlich möglich sein, das MSI zur Laufzeit eine 32- oder 64-Bit-Installation durchführen zu lassen:
  1. Ein 32-Bit-MSI erzeugen.
  2. Die 64-Bit-Programmkomponenten hinzufügen, mit msidbComponentAttributes64bit.
  3. Die 64-Bit-Komponenten in ProgramFiles64Folder schreiben lassen statt ProgramFilesFolder.
  4. Für die 64-Bit-Komponenten die Bedingung VersionNT64 angeben, und für 32-Bit-Komponenten NOT VersionNT64.
Augenscheinlich funktioniert das auch, aber praktisch laufen wir ins Problem, dass ProgramFiles64Folder in 32-Bit-Setups auf C:\Program Files (x86)\ zeigt – also ins 32-Bit-Programmverzeichnis! Das selbe gilt fürs Systemverzeichnis und alle anderen 64-Bit-spezifischen Verzeichnisse. Damit ist das keine realistische Option.


Warum kein 64-Bit-Paket, das optional die 32-Bit-Version mitbringt?

Das wäre das gleiche Spiel wie oben, nur mit x64 als Architektur in der Summary Page. Leider funktioniert das Setup dann garnicht mehr auf 32-Bit-Systemen: This installation package is not supported by this processor type. Contact your product vendor.


Bootstrapper

Um einen Bootstrapper – also ein Programm, das die Installation anlaufen lässt und Voreinstellungen trifft, indem es z. B. das korrekte MSI für die aktuelle Plattform auswählt – führt kein Weg herum. Das WiX Toolset unterstützt sowas in Form von Burn.

Hier gibt es dann wieder mehrere Möglichkeiten:
  1. Zwei komplett eigenständige Setups ausliefern. Das ist der einfachste Weg, opfert aber jede Menge Speicherplatz – Dinge, die nicht 32- oder 64-Bit-spezifisch sind, liegen doppelt vor.
  2. Zwei MSIs, die auf ein gemeinsames .cab mit den geteilten Daten zugreifen. Die Realisierung ist ein kleines Bisschen schwieriger. Der Platzbedarf ist deutlich geringer, aber noch immer nicht minimal, weil z. B. Übersetzungen doppelt in den MSIs vorliegen.
  3. Ein MSI mit einer Transformation, die die 32-Bit-Version zur 64-Bit-Version patcht. Am schwierigsten zu realisieren, aber geringster Platzbedarf.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

MSI-Overhead abspecken

Wie bereits vorher erwähnt, sind MSI-Dateien Container. Das Containerformat ist dabei OLE Compound File Binary File Format für OLE Structured Storage. Es wurde für frühe Office-Versionen entwickelt, um Illustrationen und Texte eines Dokuments bündeln zu können. In den späten 90ern wurde es für alle möglichen Dinge zweckentfremdet – zum Beispiel auch für thumbs.db-Dateien, die intern eine Vielzahl von Bilddateien speichern.

Außerdem können Datensätze in OLE-CF-Dateien beliebig wachsen. Dafür baut jede Datei ein komplettes, FAT-ähnliches Dateisystem auf, das virtuelle Sektoren in der Datei verwaltet. Die einzelnen Tabellen in einer MSI-Datenbank sind also Dateien in einem virtuellen Dateisystem – Icons, Programmdateien und Skripte ebenfalls. Man nennt sie Streams, und die Containerdatei Storage.

Das kann rekursiv so weitergehen. An Komplexität ist das schwer zu toppen, und ihr könnt ahnen, wohin das führt: Mit zunehmender Bearbeitung einer MSI-Datei (oder eines alten Office-Dokuments) können die enthaltenen Streams fragmentieren.

Um einen Container wieder zu defragmentieren, hat Microsoft ab Windows 2000 Bordmittel zur Verfügung gestellt:
  1. Man öffnet den Container über die IStream-COM-Schnittstelle (seit jeher die Standard-Schnittstelle für Operationen auf dem Format).
  2. Man erzeugt einen neuen, leeren Container.
  3. Man nutzt IStorage::CopyTo(), um die Quelldaten in den leeren Container zu kopieren. Dabei wird das Layout optimiert; verwaiste Sektoren werden verworfen, usw.
  4. Man ruft IStorage::Commit(STGC_CONSOLIDATE) auf. Das schreibt die Streams mit minimaler Sektorgröße in den Zielcontainer.
Nochmal als Code:

Code: Alles auswählen

IStorage * source;
if(FAILED(StgOpenStorageEx(source_path, STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_ANY, 0, NULL, NULL, IID_IStorage, (void * *)&source))) {
	// TODO Fehlerbehandlung
}

IStorage * destination;
if(FAILED(StgCreateStorageEx(destination_path, STGM_DIRECT | STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, STGFMT_STORAGE, 0, NULL, NULL, IID_IStorage, (void * *)&destination))) {
	// TODO Fehlerbehandlung
}

if(FAILED(source->CopyTo(0, NULL, NULL, destination))) {
	// TODO Fehlerbehandlung
}

if(FAILED(destination->Commit(STGC_CONSOLIDATE | STGC_DEFAULT))) {
	// TODO Fehlerbehandlung
}

destination->Release();
Die resultierende Datei ist meist viele KiB kleiner als die Quelldatei .

Da wir hier speziell über Setups sprechen: Das sollte vor dem Veröffentlichen eines Setups als Gratis-Größengewinn mitgenommen werden. WiX scheint den Trick nicht zu kennen – schade. LibreOffice z. B. wird dadurch 315 KiB kleiner.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Microsoft Installer (MSI)

Beitrag von Krishty »

MSI-Overhead abspecken (2)

Beim Ergebnis des vorherigen Beitrags hätte ich es belassen können – habe ich aber nicht. Zum Glück, denn die Situation ist komplizierter!

Der Code im vorherigen Beitrag ist der Standard-Code zum Packen von Compound File Binary Format (CFBF)-Dateien. Er ist *unheimlich* weit verbreitet – schon 1999 hat Igor Pavlov, der 7-Zip-Entwickler, Document Press zur Verfügung gestellt, das die selben Aufrufe durchführt und bspw. auch im File Optimizer eingesetzt wird.

2009 hat sich Tom Jebo aus Microsofts Open Specifications Support Team Blog näher mit dem Thema befasst und Probleme gefunden: Anmerkung: Die Seiten waren zehn Jahre lang online. Vor fünf Wochen hat das Internet Archive seine einzige Sicherheitskopie gezogen. Vorgestern habe ich die Seiten gelesen und den Code kopiert. Und heute morgen wurden sie gelöscht. Das ist Timing! Spendet dem Internet Archive!

Im CFBF werden kleine Dateien (< 4 KiB) in einem eigenen Dateisystem mit kleinerer Sektorgröße abgelegt, damit nicht zu viel Platz verschwendet wird. Tom Jebo ist aufgefallen, dass der Standard-Code zwar die großen Dateien defragmentiert – nicht aber die kleinen! Lest die Artikel für Details.

Ich habe mit dem Code gespielt und weitere Probleme gefunden: Bei IStorage::Commit() wurde nicht STGC_OVERWRITE übergeben. Dadurch blieben einige Sektoren unnötigerweise allokiert.

Außerdem habe ich durch Umsortieren der Befehle noch kleinere Dateien erzeugen können.

Darum nach langer Analyse hier …

Der ultimative Weg, Compound File Binary Format-Dateien zu optimieren
  1. Die Topologie kopieren.
    Ihr geht einmal durch den Quell-Container und öffnet jeden Ordner (Storage) und jede Datei (Stream). Parallel legt ihr den selben Storage und den selben Stream im Zielcontainer an.
    Achtung: Legt die Ziel-Streams leer an! Dies sorgt dafür, dass nur Directory Entries angelegt werden und der Directory Stream nicht fragmentiert.
    Achtung: Lasst die Handles der Ziel-Streams und Ziel-Storages offen! Sobald ihr einen existierenden Stream schließt und erneut öffnet, fragmentiert er, sogar mit den STGM_CREATE | STGM_SHARE_EXCLUSIVE-Flags!
  2. Kleine Streams kopieren.
    Ihr geht durch alle Streams und kopiert via IStream::CopyTo() nur diejenigen ins Ziel, deren Größe geringer als 4096 B ist.
    Dadurch legt ihr einen nicht-fragmentierten Mini Stream an.
  3. Große Streams kopieren.
    Ihr kopiert alle verbleibenden Streams (>= 4096 B) via IStream::CopyTo(). Dadurch legt ihr normale, nicht-fragmentierte Streams an.
  4. Alles freigeben.
    Ihr müsst alle Handles schließen. Ist ein Handle auf Storage oder Stream beim Schließen der Datei noch offen, erzeugt das ungenutzte Sektoren!
  5. Commit(STGC_OVERWRITE | STGC_CONSOLIDATE) auf dem Ziel.
    Benutzt keine anderen Flags, sonst wird der Stream nicht korrekt bereinigt!
Ich habe Tom Jebos Code entsprechend überarbeitet:

Code: Alles auswählen

#include <Shlwapi.h>
#include <cstdio>
#include <vector>



struct Stream {
	UINT64     size;
	IStream *  source;
	IStream *  dest;
	IStorage * sourceParent;
	IStorage * destParent;
};

// Copies topology from one storage to another and generates a list of streams whose content must be copied.
static void copyTopology(
	IStorage &            source,
	IStorage &            dest,
	std::vector<Stream> & streamsToCopy
) {
	IEnumSTATSTG * childs;
	if(FAILED(source.EnumElements(0, nullptr, 0, &childs))) {
		__debugbreak(); // TODO error
	}

	ULONG childrenActuallyFetched;
	STATSTG child;
	while(S_OK == childs->Next(1, &child, &childrenActuallyFetched)) {
		if(STGTY_STREAM == child.type) {

			Stream stream;
			stream.size = child.cbSize.QuadPart;

			// Open the stream for exclusive reading (prevents snapshots):
			if(FAILED(source.OpenStream(child.pwcsName, nullptr, STGM_READ | STGM_SHARE_EXCLUSIVE, 0, &stream.source))) {
				__debugbreak(); // TODO error
			}
			stream.sourceParent = &source; source.AddRef(); // closing the storage would destroy the stream (WTF!)

			// Create a new stream with exclusive access (prevents snapshots):
			if(FAILED(dest.CreateStream(child.pwcsName, STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, 0, 0, &stream.dest))) {
				__debugbreak(); // TODO error
			}
			stream.destParent = &dest; dest.AddRef(); // closing the storage would destroy the stream (WTF!)

			streamsToCopy.push_back(stream);

		} else if(STGTY_STORAGE == child.type) {

			// Open the storage for exclusive reading (prevents snapshots):
			IStorage * sourceStorage;
			if(FAILED(source.OpenStorage(child.pwcsName, nullptr, STGM_READ | STGM_SHARE_EXCLUSIVE, nullptr, 0, &sourceStorage))) {
				__debugbreak(); // TODO error
			}

			// Create new storage with exclusive access (prevents snapshots):
			IStorage * destinationStorage;
			if(FAILED(dest.CreateStorage(child.pwcsName, STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, 0, 0, &destinationStorage))) {
				__debugbreak(); // TODO error
			}

			copyTopology(*sourceStorage, *destinationStorage, streamsToCopy);

			destinationStorage->Release();
			sourceStorage->Release();

		} else {
			__debugbreak(); // TODO error unknown stream type
		}
	}
}

static void optimize(
	WCHAR const sourcePath[],
	WCHAR const destPath[],
	bool const largeSectors
) {

	IStorage * source;
	if(FAILED(StgOpenStorage(sourcePath, nullptr, STGM_DIRECT | STGM_READ | STGM_SHARE_DENY_WRITE, nullptr, 0, &source))) {
		__debugbreak(); // TODO error
	}

	STGOPTIONS opt = { 0 };
	opt.usVersion = 1;
	opt.ulSectorSize = 4096;
	IStorage * destination;
	if(FAILED(StgCreateStorageEx(destPath, STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, STGFMT_DOCFILE, 0, largeSectors ? &opt : nullptr, nullptr, IID_IStorage, (void * *)&destination))) {
		__debugbreak(); // TODO error
	}

	// 0. copy the class ID – why?!
	STATSTG StatStg;
	source->Stat(&StatStg, 0);
	destination->SetClass(StatStg.clsid);

	// 1. copy the topology
	//    • prevents fragmentation of the directory table
	//    • keep handles open, NEVER re-open storage
	//      – opening a second time adds unknown data, making the resulting file larger(!)
	std::vector<Stream> streams;
	copyTopology(*source, *destination, streams);

	// 2. copy data of small streams (less than 4096 B)
	//    • prevents fragmentation of the mini streams
	//    • keeps small streams like Summary Information close to the beginning of the file
	for(auto & stream : streams) {
		if(stream.size < 4096)
		{
			ULARGE_INTEGER nread, nwritten;
			ULARGE_INTEGER size; size.QuadPart = stream.size;
			if(FAILED(stream.source->CopyTo(stream.dest, size, &nread, &nwritten))) {
				__debugbreak(); // TODO error
			}
			stream.source->Release();
			stream.dest->Release();
			stream.sourceParent->Release();
			stream.destParent->Release();
		}
	}

	// 3. copy data of large streams (4096 B or more)
	//    • prevents fragmentation of normal streams
	for(auto & stream : streams) {
		if(stream.size >= 4096)
		{
			ULARGE_INTEGER nread, nwritten;
			ULARGE_INTEGER size; size.QuadPart = stream.size;
			if(FAILED(stream.source->CopyTo(stream.dest, size, &nread, &nwritten))) {
				__debugbreak(); // TODO error
			}
			stream.source->Release();
			stream.dest->Release();
			stream.sourceParent->Release();
			stream.destParent->Release();
		}
	}

	// 4. commit the changes
	//    • STGC_OVERWRITE throws away old versions/snapshots (makes some sectors difference)
	//    • STGC_CONSOLIDATE
    destination->Commit(STGC_OVERWRITE | STGC_CONSOLIDATE);

	destination->Release();
	source->Release();
}



int wmain(int argc, wchar_t* argv[])
{
	int srclen = 0;

	if ((argc < 2)||((srclen = wcslen(argv[1])) > 107)) // give 107 chars total, the path plus 7 for ".defrag" that we'll add
	{
		return -16; //help();
	}

	// save off the extension
	wchar_t* lptszTail = wcsrchr((wchar_t*)argv[1], '.' ); // find the last dot
  	
	// allocate the new path buffer and clear it
	wchar_t* lptszTarget = new wchar_t[108];  // allocate enough to hold the path plus the ".defrag" insertion and a null.
	memset(lptszTarget, 0x00, 108);  // clear out the remainder of the string.

	// copy the path up the the extension, insert ".defrag", then copy the extension.
	int posdot = lptszTail-argv[1];
	wcsncpy_s(lptszTarget, posdot+1, argv[1], posdot); // copy everything before the dot.
	wcsncpy_s(lptszTarget+posdot, 8, L".defrag", 7);   // insert the ".defrag"
	wcscpy_s(lptszTarget+posdot+7, wcslen(lptszTail)+1, lptszTail); // copy the rest
	
	CoInitialize(nullptr);

	optimize(argv[1], lptszTarget, false);

	CoUninitialize();

	return 0;
}
Damit werden meine Dateien nochmals kleiner. Jetzt könnte alles perfekt sein. Außer …

Das funktioniert nur mit kleinen MSIs
… nämlich bis zu einer Größe von rund fünf Megabyte. Danach werden die optimierten CFBF-Dateien sogar größer als vorher!

Ich habe nicht schlecht gestaunt, als ich das zum ersten Mal gesehen habe. Es hat ein Bisschen gedauert, bis ich die Ursache gefunden hatte:
  • Es gibt zwei Versionen des CFBF-Formates: Version 3 hat eine Sektorgröße von 512 Bytes; Version 4 von 4096 Bytes.
  • Version 4 wurde mit Windows 2000 eingeführt, um Engpässe in der Maximalgröße von Version 3 zu umgehen.
  • Bis ungefähr 2011 hat der Microsoft Installer seine MSIs in Version 3 – also mit Sektorgröße 512 – erzeugt.
  • Dann wurde das zu Version 4 geändert. (Ich stelle mir gerade die Gesichter vor, wenn nach einem Windows-Update plötzlich die Setup-Dateien größer oder kleiner werden.) Durch die größeren Sektoren wird zwar mehr Platz pro Datei verschwendet, aber die Gesamtzahl der Sektoren fällt geringer aus, und damit auch die Größe der FAT-Tabellen. Ab 5, 6 MiB rechnet sich das.
  • Mein Code hat die Datei zu Version 3 konvertiert. Kleine MSIs profitierten von der geringeren Sektorgröße. Große MSIs litten.
Ihr müsst also theoretisch zweimal optimieren – einmal als CFBF v3, und einmal als CFBF v4. Das kleinere von beiden solltet ihr dann nehmen. In den Code oben konnte ich das noch nicht einbauen; ihr könnt aber auf v4 umstellen, indem ihr den dritten Parameter von optimize() auf true setzt.

Was für ein Krampf! Dafür haben wir nun aber den besten Compound File-Optimizer des Webs :)

Bleibt nur noch eine Frage: Was passiert, wenn nun das Ergebnis digital signiert wird? Das erreicht signtool bei MSIs durch Hinzufügen eines zusätzlichen Streams. Fragmentiert dann nicht wieder alles? Ich bin nicht zum Testen gekommen, aber ich fürchte: Ja. Aber ich hoffe, dass man das durch Hinzufügen und Löschen eines Dummy-Streams umgehen kann. Wenn ich’s herausgefunden habe, werde ich’s hier ergänzen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten