Thumbnail: nsis

Ich Packe meinen Installer mit NSIS

von am unter blog
18 Minuten TTR (Time To Read)

Einführung

NSIS is ein Tool/Toolset von Nullsoft zum erstellen von Installer Paketen, dieses Tool wird gerne auch zum erstellen von “Portable”-Runnables verwendet, im groben ist es nichts anderes wie ein selbst entpackendes zip was danach eine Anwendung öffnet…
Im TANlockManager wird das ganze genutzt um die Software (einmal zentral) zu Installieren, die Plugins zu Kopieren, sowie zum Anlegen einer Firewall-Regel und des REST Dienstes.

Welcome to WIKI-Hell

Die NSIS Doku ist auf den ersten Blick super verwirrend. Wie geht wo was? Hä, wie sind die Abläufe? Kann ich da eingreifen? Der NSIS Lifecycle ist eigentlich nicht so schlimm, zumindest für das “wichtige” Zeug. Zunächst vorne weg, den “alten” Installer habe ich nie ausprobiert, ich hatte gleich “MUI” genommen, da dort ein vernünftiges Script gegeben war… Ich meine aber mit den paar basics, und einem Skript im folgenden, kann man (wenn man nebenbei die Doku offen hat :man_technologist:) einiges sehr leicht verstehen.

Pages

Pages sind quasi die Sichtbaren Installations “Seiten” (Explizit nicht Schritte, sondern Seiten).
Der Standard (ohne MUI) umfasst die Seiten:

  • license
  • components
  • directory
  • instfiles
  • uninstConfirm

Alternativ gibt es noch die custom page mit der man seinen eigenen stepps basteln kann. So wie ich das verstanden habe ist diese reihenfolge irgenwie so halb vorgeschrieben, ich würde aber mal behaubten, dass nur components und directory vor install kommen muss, der rest, könnte egal sein^^ Einfach mal testen… :black_joker:
Wenns nicht geht auch gut, weil macht kaum Sinn. Custom Pages kann man jedoch immer dazu quetschen.

Nun zum MUI, das Modern UI erweitert das standard Set um die Pages welcome, startmenu und finish, die Doku ist recht gut.

Sections

Eine Section ist ein Installations Schritt, man kann mehrere Sektions aneinander Ketten/komponenten basiertes koppeln erlauben.
Bei älteren PC Spielen war das gern eine Option, z.B. minimal Installation (50MB) oder alles Installieren (200MB), der Anfang ist also gleich, der unterschied war meist nur das fehlen der Video Dateien, welche von der CD geladen wurden. Leider war diese Option in meinem Fall nicht ganz ausreichend.

Sections kann man dan gliedern indem man die Option SectionIn mitgiebt, dort werden Indizes hinterlegt um sie zu einem InstType zuzuweisen.
Die Install Types sind wenn ich alles richtig verstanden habe die Optionen für die Components Page, und werden einfach definiert (vmtl vor den Pages, da der “compiler” allem anschein nach von oben nach unten durch die Datei geht).

Function

Eine Function kann verwendet werden um mit Pages zu komunizieren, eine custom Page braucht zudem eine “draw” function, welche die UI erstellt.

CustomPage

Custom Pages sind Seiten welche z.B. mit dem nsDialogs Plugin befüttert werden können.
Dies ist einfach nur eine Page mit eigenen Funktionen (je nachdem, welche funktionen man dem Page custom Kommando zu mapt).

Als Designtool existiert der NSIS Dialog Designer, ist ein Drag & Drop Baukasten, welcher einerseits eine XML aufbaut und eine .nsinc ausspuckt. Die .nsinc kann dann in das Installations-Skript included werden und bei einer custom Page die Anzeige-Funktion aufgerufen werden.

Installfiles Page

Die Installfiles Page ist die vorgefertigte Seite, die die Sections ausführt. Die Reihenfolge müsste also von Oben nach unten sein, wobei der SectionIn einfach nur ein Flag darstellt, ob ich diesen Step jetzt machen will oder nicht.

Meine Ergänzung

Ich habe mir für meinen Installer nach der Installfiles Page noch eine custom Page angelegt, diese fragt nach paar Optionen:

Meine NSIS Post Install Options (Screenshot mit WINE :wine_glass:, also echt gebaute EXE, nur eben ned in der Windows VM)

Problematisch ist hierbei, die teilweise längere Verarbeitungszeit beim Kopieren der Daten, ich hoffe dass ich hierfür noch was finde… Eine Weitere Page mit Ladebalke, wie eine InstallPage wäre schön, aber ich habe bis jetzt nochnichts hierfür gefunden.

Mein Script

; Namen Setzen (da ich kein Leerzeichen habe, benutze ich den fast überall)
; Leerzeichen sind tötlich 💀
!define MUI_PRODUCT "TANlockManager"
CRCCheck On

; Titel für den Installer
Name "${MUI_PRODUCT}"
OutFile "${MUI_PRODUCT}-Setup.exe"

; Ich benötige Admin Rechte
RequestExecutionLevel admin


; Standard Installations Pfad
InstallDir "$PROGRAMFILES64\${MUI_PRODUCT}"


Var StartMenuFolder

!include MUI2.nsh
!include LogicLib.nsh ; If Blöcke
!include ".\TANlockManagerBaseConfig.nsdinc" ; eiener Dialog
!include StrRep.nsh ; von https://github.com/postgrespro/pgwininstall/blob/master/nsis/StrRep.nsh

!define MUI_ABORTWARNING
!define MUI_WELCOMEPAGE_TITLE "${MUI_PRODUCT} Installer"
!define MUI_FINISHPAGE_NOAUTOCLOSE
!define MUI_UNFINISHPAGE_NOAUTOCLOSE

; Pageablauf 1/2
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "License.txt" ; Lizenz Datei
; Text kann auch in einer RTF sein 📄
!insertmacro MUI_PAGE_DIRECTORY

;Start Menu Folder Page Configuration
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${MUI_PRODUCT}"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder

!insertmacro MUI_PAGE_INSTFILES

; My Custom Page Is Here
Page custom fnc_TANlockManagerBaseConfig_Show doneBaseConfig
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES

;Dektop Shortcut
; Ich Missbrauche hier den eigentlichen Show Readme als Create Desktop Shortcut (vgl FinishPage MUI)
!define MUI_FINISHPAGE_SHOWREADME ""
!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut"
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION finishpageaction
!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_LANGUAGE "English"

Section "install" "${MUI_PRODUCT}"
    ; Somit gilt das $SMPROGRAMS für alle USER
    SetShellVarContext all
    SetOutPath "$INSTDIR"
    WriteUninstaller "$INSTDIR\uninstall.exe"

    SetOutPath "$INSTDIR\bin"
    ; Die zu packenden dateien sind in ../dist/win-unpacked (electronjs anwendung)
    File /r "..\dist\win-unpacked\"

    ; Ich mudd meine Plugins erst installieren, um sie dann in meiner eigenen Page zu benutzen
    SetOutPath "$INSTDIR\availablePlugins"
    File "..\plugins\*.js"

    !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
    CreateDirectory "$SMPROGRAMS\$StartMenuFolder"
    CreateShortcut "$SMPROGRAMS\$StartMenuFolder\${MUI_PRODUCT} - Uninstall.lnk" "$INSTDIR\uninstall.exe"
    CreateShortcut "$SMPROGRAMS\$StartMenuFolder\${MUI_PRODUCT}.lnk" "$INSTDIR\bin\tanlockmanager.exe" "--no-server"
    !insertmacro MUI_STARTMENU_WRITE_END
SectionEnd

Function finishpageaction
    ; Wieder All um ein globales Shortcut zu erstellen
    SetShellVarContext all
    CreateShortcut "$DESKTOP\${MUI_PRODUCT}.lnk" "$INSTDIR\bin\tanlockmanager.exe" "--no-server"
FunctionEnd

Section "Uninstall"
  SetShellVarContext all

  ; Stoppen der Services und entfernen der Firewall Regel
  SimpleSC::StopService "${MUI_PRODUCT}" 1 30
  SimpleSC::RemoveService "${MUI_PRODUCT}"
  SimpleFC::AdvRemoveRule "$INSTDIR\bin\tanlockmanager.exe"

  RMDir /r "$INSTDIR\availablePlugins"
  RMDir /r "$INSTDIR\bin"

  Delete "$DESKTOP\${MUI_PRODUCT}.lnk"

  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder
  RMDir /r "$SMPROGRAMS\$StartMenuFolder"
  DeleteRegKey /ifempty HKLM "Software\${MUI_PRODUCT}"

  Delete "$INSTDIR\uninstal.exe"
SectionEnd


Var InstallAsService
Var AddFirewallException
Var SelectedCameraPlugin
Var SelectedSensorPlugin
Var DataBasePath
Function "doneBaseConfig"
  ; Holen der auswahlen aus meiner eigenen Page
  ${NSD_GetState} $hCtl_TANlockManagerBaseConfig_InstallServiceCheckBox $InstallAsService
  ${NSD_GetState} $hCtl_TANlockManagerBaseConfig_AddFirewallCheckBox $AddFirewallException
  ${NSD_GetText} $hCtl_TANlockManagerBaseConfig_CameraPluginDropList $SelectedCameraPlugin
  ${NSD_GetText} $hCtl_TANlockManagerBaseConfig_SensorPluginDropList $SelectedSensorPlugin
  ${NSD_GetText} $hCtl_TANlockManagerBaseConfig_BasepathDirRequest_Txt $DataBasePath

  ; Backslashe sind ebenso tötlich 💀
  ; va wenn man einen Windows Pfad in JS braucht
  ${StrRep} '$0' '$DataBasePath' '\' '\\'
  FileOpen $9 "$INSTDIR\bin\config.json" w
  FileWrite $9 "{$\"basePath$\":$\"$0$\"}"
  FileClose $9

  CreateDirectory "$DataBasePath\plugins"
  ${If} $SelectedCameraPlugin != "None"
    SetOutPath "$DataBasePath\plugins"
    File /r "..\plugins\node_modules"
    CopyFiles "$INSTDIR\availablePlugins\$SelectedCameraPlugin" "$DataBasePath\plugins"
  ${EndIf}

  ${If} $SelectedSensorPlugin != "None"
    SetOutPath "$DataBasePath\plugins"
    CopyFiles "$INSTDIR\availablePlugins\$SelectedSensorPlugin" "$DataBasePath\plugins"
  ${EndIf}

  ; Firewall 🔥🧱 Anlegen/Aktivieren
  ${If} $AddFirewallException == 1
      SimpleFC::AdvExistsRule "${MUI_PRODUCT}"
      Pop $0 ; return error(1)/success(0)
      Pop $1 ; return 1=Added/0=Not added
      ${If} $0 == 1
        MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to check current Firewall Status"
      ${Else}
        ${If} $1 == 0 ;;NOTADDED
            ;SimpleFC::AddApplication "${MUI_PRODUCT}" "$INSTDIR\bin\tanlockmanager.exe" 0 2 "" 1
            SimpleFC::AdvAddRule "${MUI_PRODUCT}" "${MUI_PRODUCT} REST Server (TCP)" "6" "1" "1" "7" "1" "$INSTDIR\bin\tanlockmanager.exe" \
              "" "" "@$INSTDIR\bin\tanlockmanager.exe,-10000" "" ""
            Pop $0
            ${If} $0 == 1
              MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to create Firewall Rule"
            ${EndIf}
        ${Else} ;; ADDED
            SimpleFC::IsApplicationEnabled "$INSTDIR\bin\tanlockmanager.exe"
            Pop $0 ; err(1) succ(0)
            Pop $1 ; enabled(1) disabled (0)
            ${If} $0 == 0
              ${If} $1 == 0
                SimpleFC::EnableDisableApplication "$INSTDIR\bin\tanlockmanager.exe" 1
                Pop $0
                ${If} $0 == 1
                  MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to enable Firewall Rule"
                ${EndIf}
              ${EndIf}
            ${Else}
              MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to check state of Firewall Rule"
            ${EndIf}
        ${EndIf}
      ${EndIf}
  ${EndIf}

  ; Service Anlegen/Starten
  ${If} $InstallAsService == 1
    SetOutPath "$INSTDIR\bin"
    File ".\swfe.exe"
    SimpleSC::ExistsService "${MUI_PRODUCT}"
    Pop $0
    ${If} $0 != 0
        SimpleSC::InstallService "${MUI_PRODUCT}" "${MUI_PRODUCT} REST Service" "16" "2" "$\"$INSTDIR\bin\swfe.exe$\" $\"$INSTDIR\bin\tanlockmanager.exe$\" --no-ui" "" "" ""
        Pop $0 ; returns an errorcode (<>0) otherwise success (0)
        ${If} $0 != 0
          MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to create Service"
        ${EndIf}
    ${EndIf}

    SimpleSC::GetServiceStatus "${MUI_PRODUCT}"
    Pop $0 ; returns an errorcode (<>0) otherwise success (0)
    Pop $1 ; return the status of the service (See "service_status" in the parameters)
    ${If} $0 == 0
      ${If} $1 == 1
        SimpleSC::StartService "${MUI_PRODUCT}" "" 5
        Pop $0
        ${If} $0 != 0
          MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to start Service"
        ${EndIf}
      ${EndIf}
    ${EndIf}
  ${EndIf}

FunctionEnd

Wenn man vor allem den Schluss betrachtet erkennt man, dass das ganze Skript eher wie eine Register Maschine aufgebaut ist und viel mit einem Stack gearbeitet wird, erst beim Kompilieren scheinen die Funktionen aufgelöst zu werden, und Makros werden laut Anleitung einfach nur eingefügt. Vermutlich deshalb müssen Variablen erst mal definiert werden und dies auch nur Global.

Um schnell noch das Auswählen des Plugins zu zeigen:

  ; Hier Merkt man diesen Stack artigen aufbau der "sparache"
  FindFirst $SensorHandle $SensorFileName $INSTDIR\availablePlugins\*SensorPlugin.js
  loopSensor:
    StrCmp $SensorFileName "" doneSensor
    ${NSD_CB_AddString} $hCtl_TANlockManagerBaseConfig_SensorPluginDropList $SensorFileName
    FindNext $SensorHandle $SensorFileName
    Goto loopSensor
  doneSensor:
  FindClose $SensorHandle
  ;${NSD_CB_AddString} $hCtl_TANlockManagerBaseConfig_SensorPluginDropList "PanduitSmartzoneG5Sensor.js"
  ${NSD_CB_SelectString} $hCtl_TANlockManagerBaseConfig_SensorPluginDropList "None"

Hierbei wird der Ordner durchsucht, inden ich zuvor alle Plugins geschmissen habe, die Sensoren müssen im installer das Suffix “SensorPlugin” haben. In der Anwendung selbst suche ich mir einfach nur ein Plugin, welches die Methode Implementiert…

SWFE - NSFW?!

SWFE hä what?
Start Wait For Exit, klingt komisch isch aber so…
Windows will mal wieder nicht wie ich, eigentlich gibts genau für sowas NSSM ein Wrapper um eine Exe um sie als Dienst lauffähig zu machen, anscheinend muss ein Windows Dienst irgendwie seinen zustan komunizieren, ein einfaches “running” reicht wohl nicht.
Da ich aber für ein anderes Projekt auch einen Service Launcher in C# brauche, habe ich mir einfach erlaubt in die Installation knappe 80MB on top zu geben (sind ja bloß knappe 40%) :rofl:.
.NET is halt einfach fett, dafür kann ichs auf Linux bauen und dann is das für mich ok :penguin:, und da die TANlockManager anwendung noch keine Requirements hatte, gibt es nun folgende.

OS Version Architectures Notes
Windows Client 7 SP1+, 8.1 x64, x86  
Windows 10 Client Version 1607+ x64, x86  
Nano Server Version 1803+ x64, ARM32  
Windows Server 2012 R2 SP1+ x64, x86  

Kopiert von GitHub/dotnet/core

Was Electron selbst hat habe ich gerade nicht im Kopf, aber allem anschein nach gibt es da auch Probleme, mit Windows 10 Build 19xx scheint es zu funktionieren, egal ob ich aus Linux gebaut habe oder aus Windows. Beim Ersten test des TANlockManagers ging die geile Zertifikat verarbeitung nicht… Free 4 All, war die lösung kurzfristig. :v:

Implementierung

Als Code habe ich die Klasse Service genommen

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
public class Service : ServiceBase
{
    private Process process;
    private WorkerThread wt;

    public Service(string[] args)
    {
        this.process = new Process();
        this.process.StartInfo.UseShellExecute = false;
        this.process.StartInfo.FileName = args[0];
        this.process.StartInfo.CreateNoWindow = true;
        this.process.StartInfo.Arguments = String.Join(" ", args, 1);
    }
    protected override void OnStart(string[] args)
    {
        this.wt = new WorkerThread(process, this);
        Thread t = new Thread(new ThreadStart(wt.run));
        t.Start();
        base.OnStart(args);
    }
    protected override void OnStop()
    {
        wt.stop();
        base.OnStop();
    }

    class WorkerThread
    {
        private Process process;
        private Service parent;
        public WorkerThread(Process process, Service parent)
        {
            this.process = process;
            this.parent = parent;
        }
        public void run()
        {
            this.process.Start();
            this.process.WaitForExit();
        }

        public void stop()
        {
            this.process.Kill();
        }
    }
}

Unschwer zu erkennen mache ich einfach einen Thread der den Prozess ausführt… Und den Spass knalle ich einfach in einen Service.

Die Main ist dmnach folgende

using System;
using System.ServiceProcess;
class Program
{
    static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Start And Wait For Exit - Geek in Business GmbH (c) 2019");
            Console.WriteLine("");
            Console.WriteLine("./swfe.exe [pathToExecutable] [opts]");
        }
        else
        {
            ServiceBase[] services = new ServiceBase[] { new Service(args)};
            ServiceBase.Run(services);
        }
    }
}

Es gab nur einen haken ich musste die Bib Microsoft.Windows.Compatibility nachladen.

Aber der richtig geile :shit: an C# bzw .NET core ist, dass man es als single Exe packen kann.

dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true

Isch schon nice :sunglasses:, zwar knappe 80MB aber immerhin besser als in .NET Core 2.2 wo man einen haufen DLLs daneben hat. (Für meinen Quarkus service muss die verbuchungs Anwendung auf Server 2008 R2 laufen, weshalb ich ursprünglich mit nsis angefangen habe um eine Zip File auszuweichen)

Bauen

Bauen ist recht billig von den NSIS Dingern, NSI Skript scheiben und dann ab durch den makensis.

Und auf dem Firmen Jenkins ab durch Docker (ok lokal bei mir auch durch Docker) :whale2:, is halt chilliger als Software zu installieren, so liegen halt nur Images, Netzwerke und Logs verwaist auf der Platte rum, aber das aufräumen könnte theoretisch einfacher sein :woman_facepalming:. Hab letzens knappe 20Gig an Images weggeworfen, also lohnt sich :broom:.

Die Docker file sollte ich mal wo hochladen, aber prinzipiell basiert sie auf hp41/nsis nur dass die Plugins SimpleServicePlugin und SimpleFirewallPlugin direkt eingebunden werden, mit der letzten experimental Version von NSIS. Das Image basiert bei mir auf debian:experimental.

nsis, windows, blog, tanlock